@forthtilliath/forth-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -300,29 +300,28 @@ base file overrides the default colors via normal CSS cascade:
300
300
  ```css
301
301
  @import "@forthtilliath/shadcn-ui/styles/globals-static.css";
302
302
  @import "@forthtilliath/shadcn-ui/styles/themes/rose.css";
303
-
304
- @source "../node_modules/@forthtilliath/*/dist/**/*.js";
303
+ @import "@forthtilliath/forth-ui/styles/sources.css";
305
304
  ```
306
305
 
307
306
  Don't add your own `@import "tailwindcss";` — `globals-static.css` already
308
307
  does that internally.
309
308
 
310
- **What that `@source` line does, and why it's needed:** Tailwind only
311
- generates CSS for class names it can actually find, as literal text, in
312
- the files it scans — by default your own project source, honoring
313
- `.gitignore` (which is why `node_modules` is skipped: scanning every
314
- dependency would be slow and mostly pointless). But forth-ui and shadcn-ui
315
- ship pre-built, and their component code — the literal `"bg-primary"`,
316
- `"rounded-md"`, etc. strings — only exists inside their already-compiled
317
- `node_modules/@forthtilliath/*/dist/**/*.js` files. Without pointing
318
- Tailwind at them explicitly, none of those classes would be generated and
319
- every forth-ui/shadcn-ui component would render completely unstyled. This
320
- one line, with a wildcard for the package name, covers both packages (and
321
- any other `@forthtilliath/*` package added later) — confirmed to produce
322
- the exact same generated CSS as listing each package's path separately.
323
- This isn't a workaround specific to forth-ui: it's the
324
- [documented, standard way](https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources)
325
- Tailwind v4 expects any npm-distributed component library to be consumed.
309
+ **What `sources.css` does, and why it's needed:** Tailwind only generates
310
+ CSS for class names it can actually find, as literal text, in the files it
311
+ scans — by default your own project source, honoring `.gitignore` (which is
312
+ why `node_modules` is skipped). But forth-ui and shadcn-ui ship pre-built:
313
+ their component code — the literal `"bg-primary"`, `"rounded-md"`, etc.
314
+ strings — only exists in their compiled `dist/` files. Without registering
315
+ them explicitly, none of those classes would be generated and the
316
+ components would render unstyled. `sources.css` does that with
317
+ [`@source`](https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources)
318
+ for forth-ui's components and, through shadcn-ui's own `sources.css`, for
319
+ the shadcn-ui ones they're built on.
320
+
321
+ Its paths are relative to the package itself, so it works whatever the
322
+ `node_modules` layout. Don't write the `@source` yourself with a glob such as
323
+ `../node_modules/@forthtilliath/*/dist/**/*.js`: under pnpm (packages are
324
+ symlinks — junctions on Windows) that pattern silently matches nothing.
326
325
 
327
326
  ### Available themes
328
327
 
@@ -1 +1 @@
1
- {"version":3,"file":"color-picker.d.ts","sourceRoot":"","sources":["../../../src/components/color-picker/color-picker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,wBAAwB,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,YAAwB,EACxB,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,SAAS,EACT,MAAM,GACP,EAAE,gBAAgB,qBAsDlB"}
1
+ {"version":3,"file":"color-picker.d.ts","sourceRoot":"","sources":["../../../src/components/color-picker/color-picker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,wBAAwB,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,YAAwB,EACxB,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,SAAS,EACT,MAAM,GACP,EAAE,gBAAgB,qBA6DlB"}
@@ -22,8 +22,14 @@ const HEX_PATTERN = /^#[0-9a-f]{6}$/i;
22
22
  export function ColorPicker({ value, defaultValue = "#000000", onValueChange, disabled = false, className, ariaLabel, locale, }) {
23
23
  const messages = useUiMessages(locale);
24
24
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
25
- const [textValue, setTextValue] = React.useState(defaultValue);
26
25
  const color = value ?? uncontrolledValue;
26
+ // Hex text being typed; resynced when the color changes from outside.
27
+ const [textValue, setTextValue] = React.useState(color);
28
+ const [shownColor, setShownColor] = React.useState(color);
29
+ if (color !== shownColor) {
30
+ setShownColor(color);
31
+ setTextValue(color);
32
+ }
27
33
  function commit(next) {
28
34
  setUncontrolledValue(next);
29
35
  setTextValue(next);
@@ -17,6 +17,10 @@ export type NumberInputProps = Omit<React.ComponentProps<"input">, "type" | "val
17
17
  * A numeric input with increment/decrement stepper buttons, clamped to
18
18
  * `min`/`max`.
19
19
  *
20
+ * Typing stays free: in-range values are reported as you type, and the value
21
+ * is only clamped when the field loses focus or on Enter — so `600` can be
22
+ * typed with `min={128}` without the first `6` jumping to `128`.
23
+ *
20
24
  * _Inspired from multiple sources, to make a consistent and reusable component._
21
25
  * @see https://www.heroui.com/docs/components/number-input
22
26
  *
@@ -24,5 +28,5 @@ export type NumberInputProps = Omit<React.ComponentProps<"input">, "type" | "val
24
28
  * @author Forth
25
29
  * @copyright 2026 Forth
26
30
  */
27
- export declare function NumberInput({ className, value, defaultValue, onValueChange, min, max, step, disabled, locale, ...props }: NumberInputProps): React.JSX.Element;
31
+ export declare function NumberInput({ className, value, defaultValue, onValueChange, min, max, step, disabled, locale, onBlur, onKeyDown, ...props }: NumberInputProps): React.JSX.Element;
28
32
  //# sourceMappingURL=number-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"number-input.d.ts","sourceRoot":"","sources":["../../../src/components/number-input/number-input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,wBAAwB,CAAC;AAGtE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,EAC7B,MAAM,GAAG,OAAO,GAAG,cAAc,GAAG,UAAU,CAC/C,GAAG;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,KAAK,EACL,YAAgB,EAChB,aAAa,EACb,GAAe,EACf,GAAc,EACd,IAAQ,EACR,QAAgB,EAChB,MAAM,EACN,GAAG,KAAK,EACT,EAAE,gBAAgB,qBA8DlB"}
1
+ {"version":3,"file":"number-input.d.ts","sourceRoot":"","sources":["../../../src/components/number-input/number-input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,wBAAwB,CAAC;AAGtE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,EAC7B,MAAM,GAAG,OAAO,GAAG,cAAc,GAAG,UAAU,CAC/C,GAAG;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB,CAAC;AAcF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,KAAK,EACL,YAAgB,EAChB,aAAa,EACb,GAAe,EACf,GAAc,EACd,IAAQ,EACR,QAAgB,EAChB,MAAM,EACN,MAAM,EACN,SAAS,EACT,GAAG,KAAK,EACT,EAAE,gBAAgB,qBAuFlB"}
@@ -6,10 +6,24 @@ import { Input } from "@forthtilliath/shadcn-ui/components/input";
6
6
  import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
7
7
  import { useUiMessages } from "../../locale/locale.js";
8
8
  import { Button } from "../button/index.js";
9
+ /** Number typed by the user, accepting a comma as decimal separator. */
10
+ function parseTyped(text) {
11
+ const normalized = text.trim().replace(",", ".");
12
+ return normalized === "" ? Number.NaN : Number(normalized);
13
+ }
14
+ /** Drops float noise from stepping (0.4 + 0.01) at the step's precision. */
15
+ function roundToStep(value, step) {
16
+ const decimals = String(step).split(".")[1]?.length ?? 0;
17
+ return Number(value.toFixed(decimals));
18
+ }
9
19
  /**
10
20
  * A numeric input with increment/decrement stepper buttons, clamped to
11
21
  * `min`/`max`.
12
22
  *
23
+ * Typing stays free: in-range values are reported as you type, and the value
24
+ * is only clamped when the field loses focus or on Enter — so `600` can be
25
+ * typed with `min={128}` without the first `6` jumping to `128`.
26
+ *
13
27
  * _Inspired from multiple sources, to make a consistent and reusable component._
14
28
  * @see https://www.heroui.com/docs/components/number-input
15
29
  *
@@ -17,22 +31,48 @@ import { Button } from "../button/index.js";
17
31
  * @author Forth
18
32
  * @copyright 2026 Forth
19
33
  */
20
- export function NumberInput({ className, value, defaultValue = 0, onValueChange, min = -Infinity, max = Infinity, step = 1, disabled = false, locale, ...props }) {
34
+ export function NumberInput({ className, value, defaultValue = 0, onValueChange, min = -Infinity, max = Infinity, step = 1, disabled = false, locale, onBlur, onKeyDown, ...props }) {
21
35
  const messages = useUiMessages(locale);
22
36
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
23
37
  const current = value ?? uncontrolledValue;
38
+ // Raw text while typing; resynced when the value changes from outside.
39
+ const [text, setText] = React.useState(String(current));
40
+ const [shownValue, setShownValue] = React.useState(current);
41
+ if (current !== shownValue) {
42
+ setShownValue(current);
43
+ if (parseTyped(text) !== current)
44
+ setText(String(current));
45
+ }
24
46
  function commit(next) {
25
- const clamped = Math.min(max, Math.max(min, next));
47
+ const clamped = roundToStep(Math.min(max, Math.max(min, next)), step);
26
48
  setUncontrolledValue(clamped);
27
- onValueChange?.(clamped);
49
+ setText(String(clamped));
50
+ if (clamped !== current)
51
+ onValueChange?.(clamped);
52
+ }
53
+ function commitText() {
54
+ const parsed = parseTyped(text);
55
+ if (Number.isFinite(parsed))
56
+ commit(parsed);
57
+ else
58
+ setText(String(current));
28
59
  }
29
60
  return (_jsxs("div", { "data-slot": "number-input", className: cn("flex w-fit items-stretch", className), children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled || current <= min, onClick: () => {
30
61
  commit(current - step);
31
- }, "aria-label": messages.numberInput.decrement, className: "rounded-r-none", children: _jsx(MinusIcon, {}) }), _jsx(Input, { type: "number", inputMode: "numeric", value: current, min: min === -Infinity ? undefined : min, max: max === Infinity ? undefined : max, step: step, disabled: disabled, onChange: (e) => {
32
- const parsed = Number(e.target.value);
33
- if (!Number.isNaN(parsed)) {
34
- commit(parsed);
62
+ }, "aria-label": messages.numberInput.decrement, className: "rounded-r-none", children: _jsx(MinusIcon, {}) }), _jsx(Input, { type: "number", inputMode: Number.isInteger(step) ? "numeric" : "decimal", value: text, min: min === -Infinity ? undefined : min, max: max === Infinity ? undefined : max, step: step, disabled: disabled, onChange: (e) => {
63
+ setText(e.target.value);
64
+ const parsed = parseTyped(e.target.value);
65
+ if (Number.isFinite(parsed) && parsed >= min && parsed <= max) {
66
+ setUncontrolledValue(parsed);
67
+ onValueChange?.(parsed);
35
68
  }
69
+ }, onBlur: (e) => {
70
+ commitText();
71
+ onBlur?.(e);
72
+ }, onKeyDown: (e) => {
73
+ if (e.key === "Enter")
74
+ commitText();
75
+ onKeyDown?.(e);
36
76
  }, className: "w-16 rounded-none text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", ...props }), _jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled || current >= max, onClick: () => {
37
77
  commit(current + step);
38
78
  }, "aria-label": messages.numberInput.increment, className: "rounded-l-none", children: _jsx(PlusIcon, {}) })] }));
package/dist/globals.css CHANGED
@@ -2123,6 +2123,10 @@
2123
2123
  outline-style: var(--tw-outline-style);
2124
2124
  outline-width: 1px;
2125
2125
  }
2126
+ .blur {
2127
+ --tw-blur: blur(8px);
2128
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
2129
+ }
2126
2130
  .backdrop-blur-sm {
2127
2131
  --tw-backdrop-blur: blur(var(--blur-sm));
2128
2132
  -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
@@ -4158,6 +4162,59 @@
4158
4162
  inherits: false;
4159
4163
  initial-value: solid;
4160
4164
  }
4165
+ @property --tw-blur {
4166
+ syntax: "*";
4167
+ inherits: false;
4168
+ }
4169
+ @property --tw-brightness {
4170
+ syntax: "*";
4171
+ inherits: false;
4172
+ }
4173
+ @property --tw-contrast {
4174
+ syntax: "*";
4175
+ inherits: false;
4176
+ }
4177
+ @property --tw-grayscale {
4178
+ syntax: "*";
4179
+ inherits: false;
4180
+ }
4181
+ @property --tw-hue-rotate {
4182
+ syntax: "*";
4183
+ inherits: false;
4184
+ }
4185
+ @property --tw-invert {
4186
+ syntax: "*";
4187
+ inherits: false;
4188
+ }
4189
+ @property --tw-opacity {
4190
+ syntax: "*";
4191
+ inherits: false;
4192
+ }
4193
+ @property --tw-saturate {
4194
+ syntax: "*";
4195
+ inherits: false;
4196
+ }
4197
+ @property --tw-sepia {
4198
+ syntax: "*";
4199
+ inherits: false;
4200
+ }
4201
+ @property --tw-drop-shadow {
4202
+ syntax: "*";
4203
+ inherits: false;
4204
+ }
4205
+ @property --tw-drop-shadow-color {
4206
+ syntax: "*";
4207
+ inherits: false;
4208
+ }
4209
+ @property --tw-drop-shadow-alpha {
4210
+ syntax: "<percentage>";
4211
+ inherits: false;
4212
+ initial-value: 100%;
4213
+ }
4214
+ @property --tw-drop-shadow-size {
4215
+ syntax: "*";
4216
+ inherits: false;
4217
+ }
4161
4218
  @property --tw-backdrop-blur {
4162
4219
  syntax: "*";
4163
4220
  inherits: false;
@@ -4355,6 +4412,19 @@
4355
4412
  --tw-ring-offset-color: #fff;
4356
4413
  --tw-ring-offset-shadow: 0 0 #0000;
4357
4414
  --tw-outline-style: solid;
4415
+ --tw-blur: initial;
4416
+ --tw-brightness: initial;
4417
+ --tw-contrast: initial;
4418
+ --tw-grayscale: initial;
4419
+ --tw-hue-rotate: initial;
4420
+ --tw-invert: initial;
4421
+ --tw-opacity: initial;
4422
+ --tw-saturate: initial;
4423
+ --tw-sepia: initial;
4424
+ --tw-drop-shadow: initial;
4425
+ --tw-drop-shadow-color: initial;
4426
+ --tw-drop-shadow-alpha: 100%;
4427
+ --tw-drop-shadow-size: initial;
4358
4428
  --tw-backdrop-blur: initial;
4359
4429
  --tw-backdrop-brightness: initial;
4360
4430
  --tw-backdrop-contrast: initial;
@@ -0,0 +1,8 @@
1
+ /* Registers forth-ui's components, and the shadcn-ui ones they're built on,
2
+ with the consumer's Tailwind build: import this file from your CSS entry
3
+ point. The path is relative to this file, so it holds both in src/ (this
4
+ monorepo) and in the published dist/styles/, whatever the node_modules
5
+ layout (npm, or pnpm symlinks). Copied to dist/ by `build:css`. */
6
+ @import "@forthtilliath/shadcn-ui/styles/sources.css";
7
+
8
+ @source "../components";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/forth-ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -28,8 +28,8 @@
28
28
  "lucide-react": "^1.31.0",
29
29
  "qrcode": "^1.5.4",
30
30
  "shiki": "^4.4.3",
31
- "@forthtilliath/react-kit": "^0.5.0",
32
- "@forthtilliath/shadcn-ui": "^0.3.0",
31
+ "@forthtilliath/shadcn-ui": "^0.4.0",
32
+ "@forthtilliath/react-kit": "^0.6.0",
33
33
  "@forthtilliath/ts-types": "^0.3.0"
34
34
  },
35
35
  "peerDependencies": {
@@ -59,8 +59,8 @@
59
59
  "size-limit": "^14.1.0",
60
60
  "tailwindcss": "4.3.3",
61
61
  "typescript": "^5.9.3",
62
- "@forthtilliath/typescript-config": "0.1.1",
63
- "@forthtilliath/eslint-config": "0.8.0"
62
+ "@forthtilliath/eslint-config": "0.9.0",
63
+ "@forthtilliath/typescript-config": "0.1.1"
64
64
  },
65
65
  "files": [
66
66
  "dist"
@@ -68,7 +68,8 @@
68
68
  "exports": {
69
69
  "./components/*": "./dist/components/*/index.js",
70
70
  "./locale": "./dist/locale/index.js",
71
- "./styles/globals.css": "./dist/globals.css"
71
+ "./styles/globals.css": "./dist/globals.css",
72
+ "./styles/sources.css": "./dist/styles/sources.css"
72
73
  },
73
74
  "size-limit": [
74
75
  {
@@ -89,9 +90,10 @@
89
90
  "dev": "concurrently \"pnpm:dev:ts\" \"pnpm:dev:tw\"",
90
91
  "dev:ts": "tsc --watch",
91
92
  "dev:tw": "pnpm run build:tw --watch",
92
- "build": "pnpm run clean && pnpm run build:ts && pnpm run build:tw",
93
+ "build": "pnpm run clean && pnpm run build:ts && pnpm run build:tw && pnpm run build:css",
93
94
  "build:ts": "tsc",
94
95
  "build:tw": "tailwindcss -i ./src/styles/globals.css -o ./dist/globals.css",
96
+ "build:css": "node -e \"require('node:fs').cpSync('src/styles/sources.css', 'dist/styles/sources.css')\"",
95
97
  "size": "size-limit"
96
98
  }
97
99
  }