@forthtilliath/forth-ui 0.4.0 → 0.4.1

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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/forth-ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -28,7 +28,7 @@
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",
31
+ "@forthtilliath/react-kit": "^0.6.0",
32
32
  "@forthtilliath/shadcn-ui": "^0.3.0",
33
33
  "@forthtilliath/ts-types": "^0.3.0"
34
34
  },
@@ -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"