@dashforge/tw 0.2.0-beta → 0.3.0-beta
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/A11Y.md +130 -0
- package/CHANGELOG.md +339 -82
- package/CONSUMER-VALIDATION.md +130 -0
- package/dist/index.esm.js +351 -47
- package/dist/src/components/AppShell/AppShell.d.ts +14 -0
- package/dist/src/components/AppShell/AppShell.d.ts.map +1 -1
- package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -1
- package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -1
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -1
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -1
- package/dist/src/components/Button/Button.d.ts.map +1 -1
- package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -1
- package/dist/src/components/NumberField/NumberField.d.ts.map +1 -1
- package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
- package/dist/src/components/Switch/switch.variants.d.ts.map +1 -1
- package/dist/src/components/TextField/TextField.d.ts.map +1 -1
- package/dist/src/components/TextField/textField.types.d.ts +29 -3
- package/dist/src/components/TextField/textField.types.d.ts.map +1 -1
- package/dist/src/components/TextField/textField.variants.d.ts +6 -0
- package/dist/src/components/TextField/textField.variants.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/package.json +3 -3
- package/src/components/AppShell/AppShell.tsx +126 -1
- package/src/components/AppShell/appShell.variants.ts +8 -2
- package/src/components/Autocomplete/Autocomplete.tsx +77 -4
- package/src/components/Autocomplete/autocomplete.variants.ts +6 -0
- package/src/components/Breadcrumbs/breadcrumbs.variants.ts +5 -0
- package/src/components/Button/Button.tsx +11 -0
- package/src/components/Checkbox/Checkbox.tsx +63 -5
- package/src/components/LeftNav/leftNav.variants.ts +12 -1
- package/src/components/NumberField/NumberField.tsx +30 -2
- package/src/components/RadioGroup/RadioGroup.tsx +23 -1
- package/src/components/Snackbar/snackbar.variants.ts +4 -2
- package/src/components/Switch/switch.variants.ts +6 -1
- package/src/components/TextField/TextField.tsx +29 -0
- package/src/components/TextField/textField.types.ts +23 -3
- package/src/components/TextField/textField.variants.ts +18 -0
- package/src/index.ts +1 -1
- package/LICENSE +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../../../src/components/Autocomplete/Autocomplete.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../../../src/components/Autocomplete/Autocomplete.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AA8FjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAG,kBAAkB,EACvD,KAAK,EAAE,iBAAiB,CAAC,OAAO,CAAC,kDAy5BlC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autocomplete.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Autocomplete/autocomplete.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"autocomplete.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Autocomplete/autocomplete.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAmH/B,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,YAAY,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breadcrumbs.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Breadcrumbs/breadcrumbs.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"breadcrumbs.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Breadcrumbs/breadcrumbs.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAqC9B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA8BrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA8BrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,MAAM,2GAyEjB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../../../src/components/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../../../src/components/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA0DzD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,kDA8L5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"leftNav.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/LeftNav/leftNav.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"leftNav.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/LeftNav/leftNav.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAmF1B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NumberField.d.ts","sourceRoot":"","sources":["../../../../src/components/NumberField/NumberField.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAuC/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"NumberField.d.ts","sourceRoot":"","sources":["../../../../src/components/NumberField/NumberField.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAuC/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,kDAkQlD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RadioGroup.d.ts","sourceRoot":"","sources":["../../../../src/components/RadioGroup/RadioGroup.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"RadioGroup.d.ts","sourceRoot":"","sources":["../../../../src/components/RadioGroup/RadioGroup.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,kDA+OhD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snackbar.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Snackbar/snackbar.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"snackbar.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Snackbar/snackbar.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CA0D3B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switch.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Switch/switch.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"switch.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/Switch/switch.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAsDzB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/TextField.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/TextField.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,kDAoN9C"}
|
|
@@ -5,9 +5,27 @@ import type { TextFieldVariants } from './textField.variants.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Per-slot overrides for `<TextField>`.
|
|
7
7
|
*
|
|
8
|
-
* Each slot accepts `{ className: string }`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Each slot accepts `{ className: string }` (and, for `prefix` /
|
|
9
|
+
* `suffix`, a `children` node) so the override path is extensible —
|
|
10
|
+
* extra fields like `style`, `aria-*`, `data-*` can be added without
|
|
11
|
+
* a breaking change. Mirrors the MUI-side `slotProps` shape.
|
|
12
|
+
*
|
|
13
|
+
* **`prefix` / `suffix`** are inline adornments rendered INSIDE the
|
|
14
|
+
* `inputWrapper`, before / after the `<input>` element. Typical use:
|
|
15
|
+
* currency symbols, units, status icons. Passing only `className`
|
|
16
|
+
* (no `children`) is allowed but renders an empty slot — usually
|
|
17
|
+
* you'll always pair the two.
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <TextField
|
|
21
|
+
* name="price"
|
|
22
|
+
* type="number"
|
|
23
|
+
* slotProps={{
|
|
24
|
+
* prefix: { children: '$' },
|
|
25
|
+
* suffix: { children: 'USD' },
|
|
26
|
+
* }}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
11
29
|
*/
|
|
12
30
|
export interface TextFieldSlotProps {
|
|
13
31
|
root?: {
|
|
@@ -31,6 +49,14 @@ export interface TextFieldSlotProps {
|
|
|
31
49
|
errorText?: {
|
|
32
50
|
className?: string;
|
|
33
51
|
};
|
|
52
|
+
prefix?: {
|
|
53
|
+
children?: ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
};
|
|
56
|
+
suffix?: {
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
className?: string;
|
|
59
|
+
};
|
|
34
60
|
}
|
|
35
61
|
/**
|
|
36
62
|
* Props for the Dashforge TW `<TextField>`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textField.types.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/textField.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE
|
|
1
|
+
{"version":3,"file":"textField.types.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/textField.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,KAAK,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B,YAAY,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,YAAY,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,KAAK,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B,UAAU,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,SAAS,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cACf,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,EACvE,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;IAC1D,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IAEb,qBAAqB;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAE1C,mCAAmC;IACnC,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,wBAAwB;IACxB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAE3B,2EAA2E;IAC3E,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ,qDAAqD;IACrD,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC"}
|
|
@@ -59,6 +59,8 @@ export declare const textFieldVariants: import("tailwind-variants").TVReturnType
|
|
|
59
59
|
input: string[];
|
|
60
60
|
helperText: string;
|
|
61
61
|
errorText: string;
|
|
62
|
+
prefix: string[];
|
|
63
|
+
suffix: string[];
|
|
62
64
|
}, undefined, {
|
|
63
65
|
size: {
|
|
64
66
|
sm: {
|
|
@@ -107,6 +109,8 @@ export declare const textFieldVariants: import("tailwind-variants").TVReturnType
|
|
|
107
109
|
input: string[];
|
|
108
110
|
helperText: string;
|
|
109
111
|
errorText: string;
|
|
112
|
+
prefix: string[];
|
|
113
|
+
suffix: string[];
|
|
110
114
|
}, import("tailwind-variants").TVReturnType<{
|
|
111
115
|
size: {
|
|
112
116
|
sm: {
|
|
@@ -155,6 +159,8 @@ export declare const textFieldVariants: import("tailwind-variants").TVReturnType
|
|
|
155
159
|
input: string[];
|
|
156
160
|
helperText: string;
|
|
157
161
|
errorText: string;
|
|
162
|
+
prefix: string[];
|
|
163
|
+
suffix: string[];
|
|
158
164
|
}, undefined, unknown, unknown, undefined>>;
|
|
159
165
|
export type TextFieldVariants = VariantProps<typeof textFieldVariants>;
|
|
160
166
|
//# sourceMappingURL=textField.variants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textField.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/textField.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"textField.variants.d.ts","sourceRoot":"","sources":["../../../../src/components/TextField/textField.variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAwF5B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -100,5 +100,5 @@ export type { VariantProps } from 'tailwind-variants';
|
|
|
100
100
|
/**
|
|
101
101
|
* Package version (synced with `package.json` at publish time).
|
|
102
102
|
*/
|
|
103
|
-
export declare const VERSION = "0.
|
|
103
|
+
export declare const VERSION = "0.3.0-beta";
|
|
104
104
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashforge/tw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"tailwind-variants": "^3.1.1",
|
|
27
27
|
"vitest": "*",
|
|
28
28
|
"@dashforge/forms": "0.2.3-beta",
|
|
29
|
-
"@dashforge/
|
|
30
|
-
"@dashforge/
|
|
29
|
+
"@dashforge/ui-core": "0.2.3-beta",
|
|
30
|
+
"@dashforge/rbac": "0.2.3-beta"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { cn } from '../../utils/cn.js';
|
|
3
3
|
import { appShellVariants } from './appShell.variants.js';
|
|
4
4
|
import type { AppShellProps } from './appShell.types.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Query selector for natively-focusable elements + anything with an
|
|
8
|
+
* explicit positive (or implicit `0`) tabindex. Used by the focus
|
|
9
|
+
* trap to enumerate the drawer's interactive content.
|
|
10
|
+
*
|
|
11
|
+
* Notes:
|
|
12
|
+
* - Excludes `[tabindex="-1"]` (intentionally non-tabbable).
|
|
13
|
+
* - Excludes `:disabled` — the browser already skips them in tab
|
|
14
|
+
* order; including them here would route focus to dead elements.
|
|
15
|
+
* - `details > summary` is the standard way to get a focusable
|
|
16
|
+
* `<details>` toggle.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const FOCUSABLE_SELECTOR = [
|
|
21
|
+
'a[href]',
|
|
22
|
+
'button:not(:disabled)',
|
|
23
|
+
'input:not(:disabled):not([type="hidden"])',
|
|
24
|
+
'select:not(:disabled)',
|
|
25
|
+
'textarea:not(:disabled)',
|
|
26
|
+
'[tabindex]:not([tabindex="-1"]):not(:disabled)',
|
|
27
|
+
'details > summary:first-of-type',
|
|
28
|
+
].join(',');
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Dashforge TW AppShell — top-level layout orchestrator.
|
|
8
32
|
*
|
|
@@ -31,6 +55,20 @@ import type { AppShellProps } from './appShell.types.js';
|
|
|
31
55
|
* - The mobile drawer + backdrop participate in the standard
|
|
32
56
|
* "click outside to close" pattern — `Escape` closes the drawer
|
|
33
57
|
* too (added via global keydown).
|
|
58
|
+
* - **Focus trap** (WCAG 2.4.3) — when the mobile drawer opens:
|
|
59
|
+
* 1. The previously-focused element is captured.
|
|
60
|
+
* 2. Focus moves to the first focusable element inside the drawer.
|
|
61
|
+
* 3. Tab / Shift+Tab wrap inside the drawer (cycle from
|
|
62
|
+
* last → first and first → last) — Tab can never escape
|
|
63
|
+
* to the page underneath while the drawer is open.
|
|
64
|
+
* 4. On close, focus returns to the captured element (typically
|
|
65
|
+
* the hamburger toggle the user pressed to open the drawer).
|
|
66
|
+
* This pattern matches WAI-ARIA APG's modal dialog guidance even
|
|
67
|
+
* though the drawer isn't strictly a dialog — same focus
|
|
68
|
+
* management makes keyboard users' experience predictable.
|
|
69
|
+
* - **`role="dialog"` + `aria-modal="true"`** are applied to the
|
|
70
|
+
* drawer when open so screen readers announce it as a modal
|
|
71
|
+
* overlay rather than just an aside.
|
|
34
72
|
*/
|
|
35
73
|
export function AppShell(props: AppShellProps) {
|
|
36
74
|
const {
|
|
@@ -66,6 +104,86 @@ export function AppShell(props: AppShellProps) {
|
|
|
66
104
|
return () => document.removeEventListener('keydown', handler);
|
|
67
105
|
}, [navOpen, onNavOpenChange]);
|
|
68
106
|
|
|
107
|
+
/*
|
|
108
|
+
* Focus trap for the mobile drawer.
|
|
109
|
+
*
|
|
110
|
+
* Implementation:
|
|
111
|
+
* - On open: capture the currently-focused element (`document.activeElement`)
|
|
112
|
+
* so we can restore it on close. Then move focus to the first
|
|
113
|
+
* focusable element inside the drawer.
|
|
114
|
+
* - While open: a Tab/Shift+Tab keydown listener wraps focus inside
|
|
115
|
+
* the drawer subtree (last → first on Tab from end, first → last
|
|
116
|
+
* on Shift+Tab from start).
|
|
117
|
+
* - On close: restore focus to the captured element.
|
|
118
|
+
*
|
|
119
|
+
* Hand-rolled (no `focus-trap-react` dep) — the logic is ~20 LOC and
|
|
120
|
+
* scoped to a single drawer; adding a runtime dep felt like overkill.
|
|
121
|
+
* If we ever need more sophisticated trap semantics (nested traps,
|
|
122
|
+
* sentinel nodes, etc.) the dep is the right call.
|
|
123
|
+
*/
|
|
124
|
+
const drawerRef = useRef<HTMLDivElement | null>(null);
|
|
125
|
+
const restoreFocusRef = useRef<HTMLElement | null>(null);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!navOpen) return;
|
|
129
|
+
// 1) Capture the element that had focus before the drawer opened.
|
|
130
|
+
restoreFocusRef.current = document.activeElement as HTMLElement | null;
|
|
131
|
+
|
|
132
|
+
// 2) Move focus to the first focusable element inside the drawer.
|
|
133
|
+
// Defer via rAF so the drawer DOM (slide-in animation start) has
|
|
134
|
+
// settled and the elements are actually visible/focusable.
|
|
135
|
+
const raf = requestAnimationFrame(() => {
|
|
136
|
+
const drawer = drawerRef.current;
|
|
137
|
+
if (!drawer) return;
|
|
138
|
+
const first = drawer.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
139
|
+
first?.focus();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 3) Trap Tab / Shift+Tab inside the drawer.
|
|
143
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
144
|
+
if (event.key !== 'Tab') return;
|
|
145
|
+
const drawer = drawerRef.current;
|
|
146
|
+
if (!drawer) return;
|
|
147
|
+
const focusables = Array.from(
|
|
148
|
+
drawer.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
|
|
149
|
+
);
|
|
150
|
+
if (focusables.length === 0) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const first = focusables[0];
|
|
155
|
+
const last = focusables[focusables.length - 1];
|
|
156
|
+
const active = document.activeElement as HTMLElement | null;
|
|
157
|
+
if (event.shiftKey) {
|
|
158
|
+
// Shift+Tab on first → wrap to last.
|
|
159
|
+
if (active === first || !drawer.contains(active)) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
last.focus();
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// Tab on last → wrap to first.
|
|
165
|
+
if (active === last || !drawer.contains(active)) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
first.focus();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
document.addEventListener('keydown', onKeyDown);
|
|
172
|
+
|
|
173
|
+
return () => {
|
|
174
|
+
cancelAnimationFrame(raf);
|
|
175
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
176
|
+
// 4) Restore focus to the previously-focused element on close.
|
|
177
|
+
// Guard against the element being removed from the DOM (rare
|
|
178
|
+
// but possible if the consumer re-renders the page while the
|
|
179
|
+
// drawer is open).
|
|
180
|
+
const restore = restoreFocusRef.current;
|
|
181
|
+
if (restore && document.body.contains(restore)) {
|
|
182
|
+
restore.focus();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}, [navOpen]);
|
|
186
|
+
|
|
69
187
|
return (
|
|
70
188
|
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
71
189
|
{header && (
|
|
@@ -81,8 +199,15 @@ export function AppShell(props: AppShellProps) {
|
|
|
81
199
|
{nav}
|
|
82
200
|
</aside>
|
|
83
201
|
<aside
|
|
202
|
+
ref={drawerRef}
|
|
84
203
|
className={cn(v.navMobile(), slotProps?.navMobile?.className)}
|
|
85
204
|
aria-hidden={!navOpen}
|
|
205
|
+
// When open, the drawer is a modal overlay — `dialog` +
|
|
206
|
+
// `aria-modal="true"` so screen readers announce it as
|
|
207
|
+
// such. When closed, drop both attributes (the aria-
|
|
208
|
+
// hidden=true above already removes it from the AT tree).
|
|
209
|
+
role={navOpen ? 'dialog' : undefined}
|
|
210
|
+
aria-modal={navOpen ? true : undefined}
|
|
86
211
|
// The drawer is the SAME nav content rendered twice — once
|
|
87
212
|
// inline (desktop) and once as a slide-in (mobile). We
|
|
88
213
|
// mark the inactive copy hidden from AT to avoid double
|
|
@@ -21,14 +21,20 @@ export const appShellVariants = tv({
|
|
|
21
21
|
nav: 'hidden md:flex shrink-0',
|
|
22
22
|
navMobile: [
|
|
23
23
|
'flex md:hidden fixed inset-y-0 left-0 z-40',
|
|
24
|
-
|
|
24
|
+
// Drawer slide-in is the most prominent motion in AppShell and
|
|
25
|
+
// a clear WCAG 2.3.3 candidate. Gate the transition on
|
|
26
|
+
// `prefers-reduced-motion: no-preference`; the data-driven
|
|
27
|
+
// `-translate-x-full` / `translate-x-0` still applies, just
|
|
28
|
+
// without the smooth slide.
|
|
29
|
+
'transition-transform duration-200 ease-out motion-reduce:transition-none motion-reduce:duration-0',
|
|
25
30
|
'-translate-x-full',
|
|
26
31
|
],
|
|
27
32
|
main: 'flex-1 min-w-0 overflow-y-auto',
|
|
28
33
|
footer: 'shrink-0',
|
|
29
34
|
backdrop: [
|
|
30
35
|
'md:hidden fixed inset-0 z-30 bg-black/40',
|
|
31
|
-
|
|
36
|
+
// Backdrop opacity fade — micro motion, gated for consistency.
|
|
37
|
+
'transition-opacity duration-200 motion-reduce:transition-none motion-reduce:duration-0',
|
|
32
38
|
'opacity-0 pointer-events-none',
|
|
33
39
|
],
|
|
34
40
|
},
|
|
@@ -23,6 +23,61 @@ import type {
|
|
|
23
23
|
AutocompleteValue,
|
|
24
24
|
} from './autocomplete.types.js';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Inline SVG icons used by the Autocomplete chrome (chip remove, clear,
|
|
28
|
+
* dropdown caret). Stroke uses `currentColor` so the parent's `text-*`
|
|
29
|
+
* propagates — same pattern as Checkbox's CheckIcon.
|
|
30
|
+
*
|
|
31
|
+
* Why inline SVG (not lucide / heroicons / unicode glyphs):
|
|
32
|
+
* - Zero icon-library dependency: keeps `@dashforge/tw` self-contained.
|
|
33
|
+
* - Crisp at every size (the unicode `×` / `▾` glyphs we shipped pre-
|
|
34
|
+
* 0.2.2 rendered as chunky font characters that looked unpolished
|
|
35
|
+
* next to the rest of the design system).
|
|
36
|
+
* - SVG scales with parent font-size cleanly via `width="1em" height="1em"`.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function CloseIcon({ className }: { className?: string }) {
|
|
41
|
+
return (
|
|
42
|
+
<svg
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
viewBox="0 0 16 16"
|
|
45
|
+
width="1em"
|
|
46
|
+
height="1em"
|
|
47
|
+
fill="none"
|
|
48
|
+
className={className}
|
|
49
|
+
>
|
|
50
|
+
<path
|
|
51
|
+
d="M4 4l8 8M12 4l-8 8"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
strokeWidth="1.75"
|
|
54
|
+
strokeLinecap="round"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ChevronDownIcon({ className }: { className?: string }) {
|
|
61
|
+
return (
|
|
62
|
+
<svg
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
viewBox="0 0 16 16"
|
|
65
|
+
width="1em"
|
|
66
|
+
height="1em"
|
|
67
|
+
fill="none"
|
|
68
|
+
className={className}
|
|
69
|
+
>
|
|
70
|
+
<path
|
|
71
|
+
d="M4 6l4 4 4-4"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth="1.75"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
/**
|
|
27
82
|
* Coerce a raw value (from bridge / props) into a canonical shape:
|
|
28
83
|
*
|
|
@@ -402,7 +457,17 @@ export function Autocomplete<TOption = AutocompleteOption>(
|
|
|
402
457
|
if (next == null) {
|
|
403
458
|
setInputValue('');
|
|
404
459
|
} else {
|
|
405
|
-
|
|
460
|
+
// Look up the label in the EFFECTIVE option pool (static
|
|
461
|
+
// `options` + async `asyncOptions` when `loadOptions` is
|
|
462
|
+
// configured). Looking only at the static `options` here
|
|
463
|
+
// means async-loaded picks never update the input — the
|
|
464
|
+
// user sees their search query stick instead of the
|
|
465
|
+
// selected label after click. Same lookup logic mirrored in
|
|
466
|
+
// `displayInputValue` so the two paths can't drift.
|
|
467
|
+
const pool = loadOptions && asyncOptions !== null
|
|
468
|
+
? asyncOptions
|
|
469
|
+
: options;
|
|
470
|
+
const found = pool.find((opt) => getOptionValue(opt) === next);
|
|
406
471
|
const found_label = found ? labelAsString(found) : undefined;
|
|
407
472
|
if (found_label !== undefined) {
|
|
408
473
|
setInputValue(found_label);
|
|
@@ -434,6 +499,8 @@ export function Autocomplete<TOption = AutocompleteOption>(
|
|
|
434
499
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
435
500
|
[
|
|
436
501
|
options,
|
|
502
|
+
asyncOptions,
|
|
503
|
+
loadOptions,
|
|
437
504
|
isFormMode,
|
|
438
505
|
bridge,
|
|
439
506
|
name,
|
|
@@ -837,7 +904,7 @@ export function Autocomplete<TOption = AutocompleteOption>(
|
|
|
837
904
|
slotProps?.chipRemove?.className
|
|
838
905
|
)}
|
|
839
906
|
>
|
|
840
|
-
|
|
907
|
+
<CloseIcon />
|
|
841
908
|
</button>
|
|
842
909
|
)}
|
|
843
910
|
</span>
|
|
@@ -893,7 +960,7 @@ export function Autocomplete<TOption = AutocompleteOption>(
|
|
|
893
960
|
tabIndex={-1}
|
|
894
961
|
className={cn(v.clearButton(), slotProps?.clearButton?.className)}
|
|
895
962
|
>
|
|
896
|
-
|
|
963
|
+
<CloseIcon />
|
|
897
964
|
</button>
|
|
898
965
|
)}
|
|
899
966
|
|
|
@@ -908,7 +975,13 @@ export function Autocomplete<TOption = AutocompleteOption>(
|
|
|
908
975
|
disabled={effectiveDisabled}
|
|
909
976
|
className={cn(v.trigger(), slotProps?.trigger?.className)}
|
|
910
977
|
>
|
|
911
|
-
|
|
978
|
+
{/*
|
|
979
|
+
* Chevron flips up when popover is open (CSS-only, driven by
|
|
980
|
+
* the `aria-expanded` attribute selector on the parent
|
|
981
|
+
* `<button>`). See autocomplete.variants.ts → `trigger` slot
|
|
982
|
+
* for the `aria-expanded:rotate-180` rule.
|
|
983
|
+
*/}
|
|
984
|
+
<ChevronDownIcon />
|
|
912
985
|
</button>
|
|
913
986
|
|
|
914
987
|
{isOpen && (
|
|
@@ -44,6 +44,12 @@ export const autocompleteVariants = tv({
|
|
|
44
44
|
'text-neutral-600 hover:text-neutral-900',
|
|
45
45
|
'disabled:cursor-not-allowed disabled:opacity-40',
|
|
46
46
|
'transition-colors',
|
|
47
|
+
// Chevron flip on open — targets the SVG child via the aria-
|
|
48
|
+
// expanded state on the button itself (set by React). Smooth
|
|
49
|
+
// rotate gated on prefers-reduced-motion (WCAG 2.3.3); the
|
|
50
|
+
// 180° state still applies, just without the animated tween.
|
|
51
|
+
'[&[aria-expanded=true]>svg]:rotate-180',
|
|
52
|
+
'[&>svg]:transition-transform [&>svg]:duration-150 motion-reduce:[&>svg]:transition-none',
|
|
47
53
|
],
|
|
48
54
|
clearButton: [
|
|
49
55
|
'flex items-center justify-center shrink-0 px-2',
|
|
@@ -23,6 +23,11 @@ export const breadcrumbsVariants = tv({
|
|
|
23
23
|
'rounded-sm outline-none',
|
|
24
24
|
'focus-visible:ring-2 focus-visible:ring-primary-500/50',
|
|
25
25
|
'transition-colors',
|
|
26
|
+
// Defensive `no-underline` — see the matching comment in
|
|
27
|
+
// LeftNav itemLink slot. The Breadcrumbs root cause covers
|
|
28
|
+
// TopBar too because Breadcrumbs is typically rendered in
|
|
29
|
+
// TopBar's center slot.
|
|
30
|
+
'no-underline hover:no-underline',
|
|
26
31
|
],
|
|
27
32
|
current: [
|
|
28
33
|
'inline-flex items-center gap-1 truncate',
|
|
@@ -109,6 +109,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
109
109
|
sx
|
|
110
110
|
);
|
|
111
111
|
|
|
112
|
+
/*
|
|
113
|
+
* `aria-busy` announces the loading state to assistive tech.
|
|
114
|
+
* `disabled` alone hides the reason (perm-denied vs loading vs
|
|
115
|
+
* intrinsic), so SR users only hear "dimmed/inactive" without
|
|
116
|
+
* knowing why. Adding `aria-busy={true}` while loading distinguishes
|
|
117
|
+
* "wait for the action to finish" from "you can't do this".
|
|
118
|
+
*/
|
|
119
|
+
const ariaBusy = loading ? true : undefined;
|
|
120
|
+
|
|
112
121
|
// `asChild` renders through Radix Slot: the immediate child element
|
|
113
122
|
// gets the resolved className, no extra Button DOM is emitted.
|
|
114
123
|
if (asChild) {
|
|
@@ -118,6 +127,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
118
127
|
className={classes}
|
|
119
128
|
data-disabled={effectiveDisabled || undefined}
|
|
120
129
|
aria-disabled={effectiveDisabled || undefined}
|
|
130
|
+
aria-busy={ariaBusy}
|
|
121
131
|
>
|
|
122
132
|
{children}
|
|
123
133
|
</Slot>
|
|
@@ -129,6 +139,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
129
139
|
ref={ref}
|
|
130
140
|
type={rest.type ?? 'button'}
|
|
131
141
|
disabled={effectiveDisabled}
|
|
142
|
+
aria-busy={ariaBusy}
|
|
132
143
|
className={classes}
|
|
133
144
|
{...rest}
|
|
134
145
|
>
|
|
@@ -10,8 +10,8 @@ import { checkboxVariants } from './checkbox.variants.js';
|
|
|
10
10
|
import type { CheckboxProps } from './checkbox.types.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Inline checkmark SVG used by `Radix.Indicator
|
|
14
|
-
* `currentColor` so the parent's `text-white` propagates
|
|
13
|
+
* Inline checkmark SVG used by `Radix.Indicator` when fully checked.
|
|
14
|
+
* Stroke uses `currentColor` so the parent's `text-white` propagates.
|
|
15
15
|
*
|
|
16
16
|
* @internal
|
|
17
17
|
*/
|
|
@@ -34,6 +34,37 @@ function CheckIcon({ className }: { className?: string }) {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Inline dash SVG used by `Radix.Indicator` when the checkbox is in
|
|
39
|
+
* the `indeterminate` tri-state ("some, but not all, children
|
|
40
|
+
* selected" — the canonical "select all" partial state).
|
|
41
|
+
*
|
|
42
|
+
* Pre-0.2.2-beta the indicator rendered the CheckIcon for BOTH
|
|
43
|
+
* checked and indeterminate (Radix mounts the Indicator for either
|
|
44
|
+
* state). We now discriminate via the `data-state` attribute Radix
|
|
45
|
+
* sets on the Indicator element itself — see the parent `Indicator`
|
|
46
|
+
* rendering below for the CSS toggle.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
function DashIcon({ className }: { className?: string }) {
|
|
51
|
+
return (
|
|
52
|
+
<svg
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
viewBox="0 0 16 16"
|
|
55
|
+
fill="none"
|
|
56
|
+
className={className}
|
|
57
|
+
>
|
|
58
|
+
<path
|
|
59
|
+
d="M3 8h10"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
strokeWidth="2.5"
|
|
62
|
+
strokeLinecap="round"
|
|
63
|
+
/>
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
/**
|
|
38
69
|
* Dashforge TW Checkbox — bridge-integrated form control.
|
|
39
70
|
*
|
|
@@ -193,11 +224,38 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
193
224
|
ref={registration?.ref as React.Ref<HTMLButtonElement> | undefined}
|
|
194
225
|
className={cn(v.control(), slotProps?.control?.className)}
|
|
195
226
|
>
|
|
227
|
+
{/*
|
|
228
|
+
* Radix.Indicator natively mounts only when `data-state` is
|
|
229
|
+
* `checked` or `indeterminate` — i.e. it tracks Radix's
|
|
230
|
+
* internal state directly, no React state dependency.
|
|
231
|
+
*
|
|
232
|
+
* The previous implementation used `forceMount` + a React
|
|
233
|
+
* conditional `{resolvedChecked === true ? <CheckIcon /> : null}`,
|
|
234
|
+
* which broke standalone uncontrolled mode: Radix would flip
|
|
235
|
+
* its internal `data-state` on click (turning the control blue
|
|
236
|
+
* via `data-[state=checked]:bg-primary-500`) but the React
|
|
237
|
+
* snapshot for `resolvedChecked` stayed stale, so the
|
|
238
|
+
* `<CheckIcon />` never mounted. Result: blue box with no
|
|
239
|
+
* tick after user interaction.
|
|
240
|
+
*
|
|
241
|
+
* Dropping forceMount + the conditional defers the mount
|
|
242
|
+
* decision to Radix (the single source of truth in all three
|
|
243
|
+
* modes — controlled, uncontrolled, bridge). Indicator mounts
|
|
244
|
+
* exactly when the checkbox is checked OR indeterminate.
|
|
245
|
+
*
|
|
246
|
+
* Per-state glyph (Sprint 2 P4): when Indicator is mounted we
|
|
247
|
+
* render BOTH glyphs and toggle visibility via the Indicator's
|
|
248
|
+
* own `data-state` attribute. The `group` class on Indicator
|
|
249
|
+
* lets the SVG children target the parent's state with
|
|
250
|
+
* `group-data-[state=checked]:hidden` / `…:indeterminate:hidden`.
|
|
251
|
+
* No React state required — Radix's data-state attribute is
|
|
252
|
+
* the single source of truth.
|
|
253
|
+
*/}
|
|
196
254
|
<RadixCheckbox.Indicator
|
|
197
|
-
className={cn(v.indicator(), slotProps?.indicator?.className)}
|
|
198
|
-
forceMount
|
|
255
|
+
className={cn(v.indicator(), 'group', slotProps?.indicator?.className)}
|
|
199
256
|
>
|
|
200
|
-
|
|
257
|
+
<CheckIcon className="h-full w-full group-data-[state=indeterminate]:hidden" />
|
|
258
|
+
<DashIcon className="h-full w-full group-data-[state=checked]:hidden" />
|
|
201
259
|
</RadixCheckbox.Indicator>
|
|
202
260
|
</RadixCheckbox.Root>
|
|
203
261
|
|