Compare commits

..
2 Commits
Author SHA1 Message Date
tben 9f8527fc4d Updates 2026-03-08 17:03:10 +01:00
tben 358bfed988 Updates 2026-03-05 22:06:38 +01:00
5 changed files with 225 additions and 65 deletions
+106
View File
@@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
Regular → Executable
BIN
View File
Binary file not shown.
+69 -29
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 (
+29 -28
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>
+21 -8
View File
@@ -9,18 +9,31 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"tailwind-merge": "^2.5.4", "gray-matter": "^4.0.3",
"html-to-react": "^1.7.0",
"lodash": "^4.17.23",
"lucide-react": "^0.577.0",
"mdx": "^0.3.1",
"next-mdx-remote": "^6.0.0",
"openai": "^6.25.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.14" "react-code-blocks": "^0.1.6",
"react-responsive-modal": "^7.1.0",
"rehype-prism-plus": "^2.0.2",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"next": "14.2.15", "@next/mdx": "^16.1.6",
"react": "^18", "@types/ace": "^0.0.52",
"react-dom": "^18", "@types/bun": "latest",
"typescript": "^5", "@types/mdx": "^2.0.13",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^19.2.14",
"@types/react-dom": "^18" "@types/react-dom": "^19.2.3",
"typescript": "^5.9.3",
"@types/lodash": "^4.17.23"
}, },
"exports": { "exports": {
".": "./components" ".": "./components"