This commit is contained in:
Benjamin Toby 2026-03-14 07:15:43 +01:00
parent 57f1ecf5c3
commit 634d72bbf5
6 changed files with 194 additions and 113 deletions

View File

@ -15,3 +15,13 @@ You need a couple of packages and settings to integrate this package
### CSS Base ### CSS Base
This package contains a `base.css` file which has all the base css rules required to run. This css file must be imported in your base project, and it can be update in a separate `.css` file. This package contains a `base.css` file which has all the base css rules required to run. This css file must be imported in your base project, and it can be update in a separate `.css` file.
### Install packages
```sh
bun add lucide-react tailwind-merge html-to-react gray-matter mdx typescript lodash react-code-blocks react-responsive-modal next-mdx-remote remark-gfm rehype-prism-plus openai
```
```sh
bun add -D @types/ace @types/react @types/react-dom tailwindcss @types/mdx @next/mdx
```

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import { Info, Minus, Plus } from "lucide-react"; import { Info, Minus, Plus } from "lucide-react";
import twuiNumberfy from "../../utils/numberfy"; import twuiNumberfy from "../../utils/numberfy";
@ -7,10 +7,10 @@ import { InputProps } from ".";
let pressInterval: any; let pressInterval: any;
let pressTimeout: any; let pressTimeout: any;
type Props = Pick<InputProps<any>, "min" | "max" | "step"> & { type Props = Pick<InputProps<any>, "min" | "max" | "step" | "decimal"> & {
value: string;
setValue: React.Dispatch<React.SetStateAction<string>>; setValue: React.Dispatch<React.SetStateAction<string>>;
getNormalizedValue: (v: string) => void; buttonDownRef: React.RefObject<boolean>;
buttonDownRef: React.MutableRefObject<boolean>;
inputRef: React.RefObject<HTMLInputElement | null>; inputRef: React.RefObject<HTMLInputElement | null>;
}; };
@ -18,21 +18,50 @@ type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
* # Input Number Text Buttons * # Input Number Text Buttons
*/ */
export default function NumberInputButtons({ export default function NumberInputButtons({
getNormalizedValue, value,
setValue, setValue,
min, min,
max, max,
step, step,
buttonDownRef, buttonDownRef,
inputRef, inputRef,
decimal,
}: Props) { }: Props) {
const PRESS_TRIGGER_TIMEOUT = 200; const PRESS_TRIGGER_TIMEOUT = 200;
const DEFAULT_STEP = 1; const DEFAULT_STEP = 1;
const [buttonDown, setButtonDown] = useState(false);
// function getNormalizedValue(value: string) {
// if (numberText) {
// if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max))
// return getFinalValue(props.max);
// if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min))
// return getFinalValue(props.min);
// return getFinalValue(value);
// } else {
// return value;
// }
// }
useEffect(() => {
buttonDownRef.current = buttonDown;
if (buttonDown) {
setValue(inputRef.current?.value || "");
} else {
setTimeout(() => {
setValue(inputRef.current?.value || "");
}, 50);
}
}, [buttonDown]);
function incrementDownPress() { function incrementDownPress() {
window.clearTimeout(pressTimeout); window.clearTimeout(pressTimeout);
setButtonDown(true);
pressTimeout = setTimeout(() => { pressTimeout = setTimeout(() => {
buttonDownRef.current = true;
pressInterval = setInterval(() => { pressInterval = setInterval(() => {
increment(); increment();
}, 50); }, 50);
@ -40,14 +69,15 @@ export default function NumberInputButtons({
} }
function incrementDownCancel() { function incrementDownCancel() {
buttonDownRef.current = false; setButtonDown(false);
window.clearTimeout(pressTimeout); window.clearTimeout(pressTimeout);
window.clearInterval(pressInterval); window.clearInterval(pressInterval);
} }
function decrementDownPress() { function decrementDownPress() {
setButtonDown(true);
pressTimeout = setTimeout(() => { pressTimeout = setTimeout(() => {
buttonDownRef.current = true;
pressInterval = setInterval(() => { pressInterval = setInterval(() => {
decrement(); decrement();
}, 50); }, 50);
@ -55,41 +85,51 @@ export default function NumberInputButtons({
} }
function decrementDownCancel() { function decrementDownCancel() {
buttonDownRef.current = false; setButtonDown(false);
window.clearTimeout(pressTimeout); window.clearTimeout(pressTimeout);
window.clearInterval(pressInterval); window.clearInterval(pressInterval);
} }
function increment() { function increment() {
const existingValue = inputRef.current?.value; if (!inputRef.current) return;
const existingNumberValue = twuiNumberfy(existingValue);
if (max && existingNumberValue >= twuiNumberfy(max)) { const existingValue = inputRef.current.value;
return setValue(String(max)); const existingNumberValue = twuiNumberfy(existingValue, decimal);
} else if (min && existingNumberValue < twuiNumberfy(min)) {
return setValue(String(min)); let new_value = "";
if (max && existingNumberValue >= twuiNumberfy(max, decimal)) {
new_value = twuiNumberfy(max, decimal).toLocaleString();
} else if (min && existingNumberValue < twuiNumberfy(min, decimal)) {
new_value = twuiNumberfy(min, decimal).toLocaleString();
} else { } else {
setValue( new_value = (
String( existingNumberValue +
existingNumberValue + twuiNumberfy(step || DEFAULT_STEP), twuiNumberfy(step || DEFAULT_STEP, decimal)
), ).toLocaleString();
);
} }
inputRef.current.value = new_value;
} }
function decrement() { function decrement() {
const existingValue = inputRef.current?.value; if (!inputRef.current) return;
const existingNumberValue = twuiNumberfy(existingValue);
if (min && existingNumberValue <= twuiNumberfy(min)) { const existingValue = inputRef.current?.value;
setValue(String(min)); const existingNumberValue = twuiNumberfy(existingValue, decimal);
let new_value = "";
if (min && existingNumberValue <= twuiNumberfy(min, decimal)) {
new_value = twuiNumberfy(min, decimal).toLocaleString();
} else { } else {
setValue( new_value = (
String( existingNumberValue -
existingNumberValue - twuiNumberfy(step || DEFAULT_STEP), twuiNumberfy(step || DEFAULT_STEP, decimal)
), ).toLocaleString();
);
} }
inputRef.current.value = new_value;
} }
return ( return (

View File

@ -6,27 +6,21 @@ import React, {
ReactNode, ReactNode,
RefObject, RefObject,
TextareaHTMLAttributes, TextareaHTMLAttributes,
useRef,
} from "react"; } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Span from "../../layout/Span"; import Span from "../../layout/Span";
import Button from "../../layout/Button";
import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react"; import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
import { AutocompleteOptions } from "../../types"; import { AutocompleteOptions } from "../../types";
import twuiNumberfy from "../../utils/numberfy"; import twuiNumberfy from "../../utils/numberfy";
import Dropdown from "../../elements/Dropdown"; import Dropdown from "../../elements/Dropdown";
import Card from "../../elements/Card";
import Stack from "../../layout/Stack"; import Stack from "../../layout/Stack";
import NumberInputButtons from "./NumberInputButtons"; import NumberInputButtons from "./NumberInputButtons";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import twuiUseReady from "../../hooks/useReady";
import Row from "../../layout/Row"; import Row from "../../layout/Row";
import Paper from "../../elements/Paper"; import Paper from "../../elements/Paper";
import { TWUISelectValidityObject } from "../Select"; import { TWUISelectValidityObject } from "../Select";
let timeout: any;
let validationFnTimeout: any;
let externalValueChangeTimeout: any;
export type InputProps<KeyType extends string> = Omit< export type InputProps<KeyType extends string> = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
"prefix" | "suffix" "prefix" | "suffix"
@ -80,6 +74,7 @@ export type InputProps<KeyType extends string> = Omit<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
>; >;
// refreshDefaultValue?: number;
}; };
let refreshes = 0; let refreshes = 0;
@ -121,9 +116,18 @@ export default function Input<KeyType extends string>(
validity: existingValidity, validity: existingValidity,
clearInputProps, clearInputProps,
rawNumber, rawNumber,
// refreshDefaultValue,
...props ...props
} = inputProps; } = inputProps;
const componentRefreshesRef = useRef(0);
let timeoutRef = useRef<any>(null);
let validationFnTimeoutRef = useRef<any>(null);
let externalValueChangeTimeoutRef = useRef<any>(null);
refreshes++;
componentRefreshesRef.current++;
function getFinalValue(v: any) { function getFinalValue(v: any) {
if (rawNumber) return twuiNumberfy(v); if (rawNumber) return twuiNumberfy(v);
if (numberText) { if (numberText) {
@ -167,21 +171,8 @@ export default function Input<KeyType extends string>(
props.placeholder || props.placeholder ||
(props.name ? twuiSlugToNormalText(props.name) : undefined); (props.name ? twuiSlugToNormalText(props.name) : undefined);
function getNormalizedValue(value: string) {
if (numberText) {
if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max))
return getFinalValue(props.max);
if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min))
return getFinalValue(props.min);
return getFinalValue(value);
} else {
return value;
}
}
React.useEffect(() => { React.useEffect(() => {
// if (!existingReady) return;
if (!existingValidity) return; if (!existingValidity) return;
setValidity(existingValidity); setValidity(existingValidity);
}, [existingValidity]); }, [existingValidity]);
@ -190,8 +181,8 @@ export default function Input<KeyType extends string>(
if (buttonDownRef.current) return; if (buttonDownRef.current) return;
if (changeHandler) { if (changeHandler) {
window.clearTimeout(externalValueChangeTimeout); window.clearTimeout(externalValueChangeTimeoutRef.current);
externalValueChangeTimeout = setTimeout(() => { externalValueChangeTimeoutRef.current = setTimeout(() => {
changeHandler(val); changeHandler(val);
}, finalDebounce); }, finalDebounce);
} }
@ -208,10 +199,10 @@ export default function Input<KeyType extends string>(
return; return;
} }
window.clearTimeout(timeout); window.clearTimeout(timeoutRef.current);
if (validationRegex) { if (validationRegex) {
timeout = setTimeout(() => { timeoutRef.current = setTimeout(() => {
setValidity({ setValidity({
isValid: validationRegex.test(val), isValid: validationRegex.test(val),
msg: "Value mismatch", msg: "Value mismatch",
@ -220,9 +211,9 @@ export default function Input<KeyType extends string>(
} }
if (validationFunction) { if (validationFunction) {
window.clearTimeout(validationFnTimeout); window.clearTimeout(validationFnTimeoutRef.current);
validationFnTimeout = setTimeout(() => { validationFnTimeoutRef.current = setTimeout(() => {
if (validationRegex && !validationRegex.test(val)) { if (validationRegex && !validationRegex.test(val)) {
return; return;
} }
@ -235,11 +226,20 @@ export default function Input<KeyType extends string>(
}; };
React.useEffect(() => { React.useEffect(() => {
// if (!existingReady) return;
if (typeof props.value !== "string" || !props.value.match(/./)) return; if (typeof props.value !== "string" || !props.value.match(/./)) return;
setValue(String(props.value)); setValue(String(props.value));
}, [props.value]); }, [props.value]);
// React.useEffect(() => {
// if (!refreshDefaultValue) return;
// console.log("Name:", props.title || props.name);
// console.log("props.defaultValue", props.defaultValue);
// // setValue(String(props.defaultValue || ""));
// }, [refreshDefaultValue]);
React.useEffect(() => { React.useEffect(() => {
// if (!existingReady) return;
if (istextarea && textAreaRef.current) { if (istextarea && textAreaRef.current) {
} else if (inputRef?.current) { } else if (inputRef?.current) {
inputRef.current.value = getFinalValue(value); inputRef.current.value = getFinalValue(value);
@ -452,11 +452,12 @@ export default function Input<KeyType extends string>(
<NumberInputButtons <NumberInputButtons
setValue={setValue} setValue={setValue}
inputRef={inputRef} inputRef={inputRef}
getNormalizedValue={getNormalizedValue} value={value}
max={props.max} max={props.max}
min={props.min} min={props.min}
step={props.step} step={props.step}
buttonDownRef={buttonDownRef} buttonDownRef={buttonDownRef}
decimal={decimal}
/> />
) : null} ) : null}
</div> </div>

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useRef } from "react";
type Param = { type Param = {
elementRef?: React.RefObject<Element | undefined>; elementRef?: React.RefObject<Element | undefined>;
@ -9,8 +9,6 @@ type Param = {
delay?: number; delay?: number;
}; };
let timeout: any;
export default function useIntersectionObserver({ export default function useIntersectionObserver({
elementRef, elementRef,
className, className,
@ -19,6 +17,8 @@ export default function useIntersectionObserver({
delay, delay,
elId, elId,
}: Param) { }: Param) {
let timeoutRef = useRef<any>(null);
const [isIntersecting, setIsIntersecting] = React.useState(false); const [isIntersecting, setIsIntersecting] = React.useState(false);
const [refresh, setRefresh] = React.useState(0); const [refresh, setRefresh] = React.useState(0);
@ -27,10 +27,10 @@ export default function useIntersectionObserver({
const observerCallback: IntersectionObserverCallback = React.useCallback( const observerCallback: IntersectionObserverCallback = React.useCallback(
(entries, observer) => { (entries, observer) => {
const entry = entries[0]; const entry = entries[0];
window.clearTimeout(timeout); window.clearTimeout(timeoutRef.current);
if (entry.isIntersecting) { if (entry.isIntersecting) {
timeout = setTimeout(() => { timeoutRef.current = setTimeout(() => {
setIsIntersecting(true); setIsIntersecting(true);
if (removeIntersected) { if (removeIntersected) {
@ -41,7 +41,7 @@ export default function useIntersectionObserver({
setIsIntersecting(false); setIsIntersecting(false);
} }
}, },
[] [],
); );
React.useEffect(() => { React.useEffect(() => {

View File

@ -6,6 +6,7 @@ export type UseWebsocketHookParams = {
disableReconnect?: boolean; disableReconnect?: boolean;
/** Interval to ping the websocket. So that the connection doesn't go down. Default 30000ms (30 seconds) */ /** Interval to ping the websocket. So that the connection doesn't go down. Default 30000ms (30 seconds) */
keepAliveDuration?: number; keepAliveDuration?: number;
/** Interval in ms to force-refresh the connection */
refreshConnection?: number; refreshConnection?: number;
}; };
@ -16,10 +17,10 @@ export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
* @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events * @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events
* @event wsMessageEvent Listen for event named `wsMessageEvent` on `window` to receive Message events * @event wsMessageEvent Listen for event named `wsMessageEvent` on `window` to receive Message events
* *
* @example window.addEventLiatener("wsDataEvent", (e)=>{ * @example window.addEventListener("wsDataEvent", (e)=>{
* console.log(e.detail.data) // type object * console.log(e.detail.data) // type object
* }) * })
* @example window.addEventLiatener("wsMessageEvent", (e)=>{ * @example window.addEventListener("wsMessageEvent", (e)=>{
* console.log(e.detail.message) // type string * console.log(e.detail.message) // type string
* }) * })
*/ */
@ -34,22 +35,29 @@ export default function useWebSocket<
}: UseWebsocketHookParams) { }: UseWebsocketHookParams) {
const DEBOUNCE = debounce || 500; const DEBOUNCE = debounce || 500;
const KEEP_ALIVE_DURATION = keepAliveDuration || 1000 * 30; const KEEP_ALIVE_DURATION = keepAliveDuration || 1000 * 30;
const KEEP_ALIVE_TIMEOUT = 1000 * 60 * 3;
const KEEP_ALIVE_MESSAGE = "twui::ping"; const KEEP_ALIVE_MESSAGE = "twui::ping";
let uptime = 0; const tries = useRef(0);
let tries = useRef(0);
// const queue: string[] = []; // Refs to avoid stale closures in callbacks
const urlRef = useRef(url);
const disableReconnectRef = useRef(disableReconnect);
const keepAliveDurationRef = useRef(KEEP_ALIVE_DURATION);
React.useEffect(() => {
urlRef.current = url;
disableReconnectRef.current = disableReconnect;
keepAliveDurationRef.current = KEEP_ALIVE_DURATION;
});
const msgInterval = useRef<any>(null); const msgInterval = useRef<any>(null);
const sendInterval = useRef<any>(null); const sendInterval = useRef<any>(null);
const keepAliveInterval = useRef<any>(null); const keepAliveInterval = useRef<any>(null);
const refreshInterval = useRef<any>(null);
const reconnectTimeout = useRef<any>(null);
const [socket, setSocket] = React.useState<WebSocket | undefined>( const [socket, setSocket] = React.useState<WebSocket | undefined>(undefined);
undefined const socketRef = useRef<WebSocket | undefined>(undefined);
);
const messageQueueRef = React.useRef<string[]>([]); const messageQueueRef = React.useRef<string[]>([]);
const sendMessageQueueRef = React.useRef<string[]>([]); const sendMessageQueueRef = React.useRef<string[]>([]);
@ -74,16 +82,17 @@ export default function useWebSocket<
* # Connect to Websocket * # Connect to Websocket
*/ */
const connect = React.useCallback(() => { const connect = React.useCallback(() => {
const currentUrl = urlRef.current;
const domain = window.location.origin; const domain = window.location.origin;
const wsURL = url.startsWith(`ws`) const wsURL = currentUrl.startsWith("ws")
? url ? currentUrl
: domain.replace(/^http/, "ws") + ("/" + url).replace(/\/\//g, "/"); : domain.replace(/^http/, "ws") + ("/" + currentUrl).replace(/\/\//g, "/");
if (!wsURL) return; if (!wsURL) return;
let ws = new WebSocket(wsURL); const ws = new WebSocket(wsURL);
ws.onerror = (ev) => { ws.onerror = () => {
console.log(`Websocket ERROR:`); console.log(`Websocket ERROR:`);
}; };
@ -91,15 +100,17 @@ export default function useWebSocket<
messageQueueRef.current.push(ev.data); messageQueueRef.current.push(ev.data);
}; };
ws.onopen = (ev) => { ws.onopen = () => {
window.clearInterval(keepAliveInterval.current); window.clearInterval(keepAliveInterval.current);
keepAliveInterval.current = window.setInterval(() => { keepAliveInterval.current = window.setInterval(() => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(KEEP_ALIVE_MESSAGE); ws.send(KEEP_ALIVE_MESSAGE);
} }
}, KEEP_ALIVE_DURATION); }, keepAliveDurationRef.current);
tries.current = 0;
socketRef.current = ws;
setSocket(ws); setSocket(ws);
console.log(`Websocket connected to ${wsURL}`); console.log(`Websocket connected to ${wsURL}`);
}; };
@ -111,23 +122,21 @@ export default function useWebSocket<
wasClean: ev.wasClean, wasClean: ev.wasClean,
}); });
if (disableReconnect) return;
console.log("Attempting to reconnect ...");
console.log("URL:", url);
window.clearInterval(keepAliveInterval.current); window.clearInterval(keepAliveInterval.current);
socketRef.current = undefined;
setSocket(undefined);
console.log("tries", tries); if (disableReconnectRef.current) return;
if (tries.current >= 3) { if (tries.current >= 3) {
console.log("Max reconnect attempts reached.");
return; return;
} }
console.log("Attempting to reconnect ...");
tries.current += 1; tries.current += 1;
const backoff = Math.min(1000 * 2 ** tries.current, 30000);
connect(); console.log(`Attempting to reconnect in ${backoff}ms... (attempt ${tries.current})`);
reconnectTimeout.current = window.setTimeout(connect, backoff);
}; };
}, []); }, []);
@ -135,18 +144,40 @@ export default function useWebSocket<
* # Initial Connection * # Initial Connection
*/ */
React.useEffect(() => { React.useEffect(() => {
if (socket) return;
connect(); connect();
return () => {
window.clearTimeout(reconnectTimeout.current);
window.clearInterval(keepAliveInterval.current);
window.clearInterval(refreshInterval.current);
socketRef.current?.close();
};
}, []); }, []);
/**
* # Refresh Connection Interval
*/
React.useEffect(() => {
if (!refreshConnection) return;
refreshInterval.current = window.setInterval(() => {
console.log("Refreshing WebSocket connection...");
window.clearTimeout(reconnectTimeout.current);
socketRef.current?.close();
tries.current = 0;
connect();
}, refreshConnection);
return () => window.clearInterval(refreshInterval.current);
}, [refreshConnection]);
React.useEffect(() => { React.useEffect(() => {
if (!socket) return; if (!socket) return;
sendInterval.current = setInterval(handleSendMessageQueue, DEBOUNCE); sendInterval.current = setInterval(handleSendMessageQueue, DEBOUNCE);
msgInterval.current = setInterval(handleReceivedMessageQueue, DEBOUNCE); msgInterval.current = setInterval(handleReceivedMessageQueue, DEBOUNCE);
return function () { return () => {
window.clearInterval(sendInterval.current); window.clearInterval(sendInterval.current);
window.clearInterval(msgInterval.current); window.clearInterval(msgInterval.current);
}; };
@ -173,7 +204,8 @@ export default function useWebSocket<
* Send Message Queue Handler * Send Message Queue Handler
*/ */
const handleSendMessageQueue = React.useCallback(() => { const handleSendMessageQueue = React.useCallback(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) { const ws = socketRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) {
window.clearInterval(sendInterval.current); window.clearInterval(sendInterval.current);
return; return;
} }
@ -181,19 +213,18 @@ export default function useWebSocket<
const newMessage = sendMessageQueueRef.current.shift(); const newMessage = sendMessageQueueRef.current.shift();
if (!newMessage) return; if (!newMessage) return;
socket.send(newMessage); ws.send(newMessage);
}, [socket]); }, []);
/** /**
* # Send Data Function * # Send Data Function
*/ */
const sendData = React.useCallback( const sendData = React.useCallback((data: T) => {
(data: T) => {
try { try {
const queueItemJSON = JSON.stringify(data); const queueItemJSON = JSON.stringify(data);
const existingQueue = sendMessageQueueRef.current.find( const existingQueue = sendMessageQueueRef.current.find(
(q) => q == queueItemJSON (q) => q === queueItemJSON
); );
if (!existingQueue) { if (!existingQueue) {
sendMessageQueueRef.current.push(queueItemJSON); sendMessageQueueRef.current.push(queueItemJSON);
@ -201,9 +232,7 @@ export default function useWebSocket<
} catch (error: any) { } catch (error: any) {
console.log("Error Sending socket message", error.message); console.log("Error Sending socket message", error.message);
} }
}, }, []);
[socket]
);
return { socket, sendData }; return { socket, sendData };
} }

View File

@ -93,6 +93,7 @@ export default async function fetchApi<
options.headers = _.merge(options.headers, finalHeaders); options.headers = _.merge(options.headers, finalHeaders);
const finalOptions: any = { ...options }; const finalOptions: any = { ...options };
fetchData = await fetch(finalURL, finalOptions); fetchData = await fetch(finalURL, finalOptions);
} else { } else {
const finalOptions = { const finalOptions = {