@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
|
@@ -24,7 +24,10 @@ export const leftNavVariants = tv({
|
|
|
24
24
|
root: [
|
|
25
25
|
'flex flex-col h-full',
|
|
26
26
|
'bg-neutral-50 border-r border-neutral-200',
|
|
27
|
-
|
|
27
|
+
// Width transition (rail-mode toggle) — gated on
|
|
28
|
+
// prefers-reduced-motion (WCAG 2.3.3). The new width still
|
|
29
|
+
// applies, just without the animated tween.
|
|
30
|
+
'transition-[width] duration-200 motion-reduce:transition-none motion-reduce:duration-0',
|
|
28
31
|
],
|
|
29
32
|
brand: [
|
|
30
33
|
'flex items-center gap-2 px-3 h-14 shrink-0',
|
|
@@ -39,6 +42,14 @@ export const leftNavVariants = tv({
|
|
|
39
42
|
'transition-colors w-full',
|
|
40
43
|
'aria-disabled:opacity-50 aria-disabled:cursor-not-allowed',
|
|
41
44
|
'aria-disabled:hover:bg-transparent',
|
|
45
|
+
// Defensive `no-underline` — Tailwind's preflight removes the
|
|
46
|
+
// default browser anchor underline globally, but environments
|
|
47
|
+
// that DISABLE preflight (e.g. our docs-lab, where the tw
|
|
48
|
+
// section coexists with MUI's chrome) get raw browser defaults
|
|
49
|
+
// back. Without this, `<a>` items in the nav render underlined
|
|
50
|
+
// in those contexts. Explicit `no-underline` + `hover:no-underline`
|
|
51
|
+
// keeps the appearance consistent regardless of preflight state.
|
|
52
|
+
'no-underline hover:no-underline',
|
|
42
53
|
],
|
|
43
54
|
itemActive: 'bg-primary-100 text-primary-900 font-medium',
|
|
44
55
|
itemIcon: 'shrink-0 w-5 h-5 flex items-center justify-center',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useContext, useEffect, useId, useRef } from 'react';
|
|
1
|
+
import { useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
|
|
2
2
|
import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
|
|
3
3
|
import type { DashFormBridge, FieldRegistration } from '@dashforge/ui-core';
|
|
4
4
|
import { useDashFieldMeta } from '@dashforge/forms';
|
|
@@ -98,6 +98,22 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
98
98
|
const accessState = useAccessState(access);
|
|
99
99
|
|
|
100
100
|
const inputId = useId();
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Local state for the STANDALONE UNCONTROLLED case (no bridge, no
|
|
104
|
+
* `value` prop, only `defaultValue`). Mirrors OTPField. Without this,
|
|
105
|
+
* `resolvedDisplayValue` would be a snapshot computed ONCE from
|
|
106
|
+
* `defaultValue` and the controlled `<input value={...}>` would
|
|
107
|
+
* snap user input back on every keystroke / stepper click — same
|
|
108
|
+
* trap that hit Checkbox + RadioGroup in this package.
|
|
109
|
+
*
|
|
110
|
+
* In form mode the bridge owns state. In standalone CONTROLLED mode
|
|
111
|
+
* (consumer passes `value`) the consumer owns state. Only this
|
|
112
|
+
* branch needs the local hook.
|
|
113
|
+
*/
|
|
114
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<string>(() =>
|
|
115
|
+
formatForDisplay(defaultValue)
|
|
116
|
+
);
|
|
101
117
|
const helperId = `${inputId}-help`;
|
|
102
118
|
|
|
103
119
|
// StrictMode-safe unregister-on-unmount
|
|
@@ -140,7 +156,7 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
140
156
|
resolvedDisplayValue =
|
|
141
157
|
userValue !== undefined
|
|
142
158
|
? formatForDisplay(userValue)
|
|
143
|
-
:
|
|
159
|
+
: uncontrolledValue;
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
const writeToBridge = (parsed: number | null | undefined) => {
|
|
@@ -157,6 +173,13 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
157
173
|
// internal logic still sees the raw string change.
|
|
158
174
|
void registration.onChange(e);
|
|
159
175
|
}
|
|
176
|
+
// Standalone uncontrolled mode: mirror the raw input string so the
|
|
177
|
+
// controlled `<input value={...}>` reflects what the user typed.
|
|
178
|
+
// Partial states (e.g. "-", "1.") are kept verbatim — `parseFromInput`
|
|
179
|
+
// returns `undefined` for them so `writeToBridge` is a no-op above.
|
|
180
|
+
if (!isFormMode && userValue === undefined) {
|
|
181
|
+
setUncontrolledValue(e.target.value);
|
|
182
|
+
}
|
|
160
183
|
userOnChange?.(e);
|
|
161
184
|
};
|
|
162
185
|
|
|
@@ -176,6 +199,11 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
176
199
|
if (typeof min === 'number') next = Math.max(min, next);
|
|
177
200
|
if (typeof max === 'number') next = Math.min(max, next);
|
|
178
201
|
writeToBridge(next);
|
|
202
|
+
// Standalone uncontrolled: also persist the new value to local
|
|
203
|
+
// state so the visible display tracks the stepper click.
|
|
204
|
+
if (!isFormMode && userValue === undefined) {
|
|
205
|
+
setUncontrolledValue(formatForDisplay(next));
|
|
206
|
+
}
|
|
179
207
|
};
|
|
180
208
|
|
|
181
209
|
const canIncrement =
|
|
@@ -200,9 +200,31 @@ export function RadioGroup(props: RadioGroupProps) {
|
|
|
200
200
|
</div>
|
|
201
201
|
)}
|
|
202
202
|
|
|
203
|
+
{/*
|
|
204
|
+
* Radix RadioGroup mode discrimination — mirrors the same fix
|
|
205
|
+
* applied to <Checkbox>:
|
|
206
|
+
*
|
|
207
|
+
* - Form mode (bridge.register present): controlled — `value`
|
|
208
|
+
* comes from the reactive bridge snapshot, `handleValueChange`
|
|
209
|
+
* writes back through bridge.setValue.
|
|
210
|
+
* - Standalone controlled (consumer passes `value`): controlled —
|
|
211
|
+
* consumer owns state, `handleValueChange` forwards via
|
|
212
|
+
* `onValueChange`.
|
|
213
|
+
* - Standalone uncontrolled (only `defaultValue`): UNCONTROLLED —
|
|
214
|
+
* Radix owns the state. Previously this code passed
|
|
215
|
+
* `value={resolvedValue}` in this branch too, putting Radix
|
|
216
|
+
* in controlled mode with a stale snapshot that never updated,
|
|
217
|
+
* so user clicks fired Radix's onValueChange but the controlled
|
|
218
|
+
* prop never changed and the selection snapped right back.
|
|
219
|
+
*
|
|
220
|
+
* Picking exactly one of `{ value, ... }` or `{ defaultValue, ... }`
|
|
221
|
+
* lets Radix track its own state correctly when nobody else can.
|
|
222
|
+
*/}
|
|
203
223
|
<RadixRadioGroup.Root
|
|
204
224
|
name={name}
|
|
205
|
-
|
|
225
|
+
{...(isFormMode || explicitValue !== undefined
|
|
226
|
+
? { value: resolvedValue }
|
|
227
|
+
: { defaultValue: defaultValue ?? undefined })}
|
|
206
228
|
onValueChange={handleValueChange}
|
|
207
229
|
onBlur={handleBlur}
|
|
208
230
|
disabled={groupEffectiveDisabled}
|
|
@@ -23,8 +23,10 @@ export const snackbarVariants = tv({
|
|
|
23
23
|
'rounded-lg border shadow-lg',
|
|
24
24
|
'text-sm bg-neutral-50 text-neutral-900',
|
|
25
25
|
// Subtle enter transition — opacity + translate, kept short so a
|
|
26
|
-
// burst of snackbars feels snappy.
|
|
27
|
-
|
|
26
|
+
// burst of snackbars feels snappy. Gated on motion-reduce
|
|
27
|
+
// (WCAG 2.3.3) — users who request reduced motion see snackbars
|
|
28
|
+
// pop in instantly without the slide animation.
|
|
29
|
+
'transition-all duration-200 motion-reduce:transition-none motion-reduce:duration-0',
|
|
28
30
|
'data-[state=entered]:opacity-100 data-[state=exited]:opacity-0',
|
|
29
31
|
],
|
|
30
32
|
icon: 'shrink-0 mt-0.5 w-5 h-5 inline-flex items-center justify-center',
|
|
@@ -25,7 +25,12 @@ export const switchVariants = tv({
|
|
|
25
25
|
],
|
|
26
26
|
thumb: [
|
|
27
27
|
'pointer-events-none inline-block rounded-full bg-white shadow ring-0',
|
|
28
|
-
|
|
28
|
+
// Slide animation gated on `prefers-reduced-motion: no-preference`
|
|
29
|
+
// (WCAG 2.3.3). The thumb still moves between positions instantly
|
|
30
|
+
// for users who request reduced motion — the data-state change
|
|
31
|
+
// applies the translate-x rule unconditionally, only the smooth
|
|
32
|
+
// transition between the two is suppressed.
|
|
33
|
+
'transition-transform motion-reduce:transition-none',
|
|
29
34
|
'data-[state=unchecked]:translate-x-0',
|
|
30
35
|
],
|
|
31
36
|
label: 'select-none cursor-pointer text-neutral-900',
|
|
@@ -178,6 +178,22 @@ export function TextField(props: TextFieldProps) {
|
|
|
178
178
|
)}
|
|
179
179
|
|
|
180
180
|
<div className={cn(v.inputWrapper(), slotProps?.inputWrapper?.className)}>
|
|
181
|
+
{/*
|
|
182
|
+
* Prefix slot (Sprint 2 P4.1) — inline adornment rendered
|
|
183
|
+
* BEFORE the input. Mounts only when `slotProps.prefix.children`
|
|
184
|
+
* is provided so empty configs don't add layout cost. Common
|
|
185
|
+
* use: currency symbols (`$`, `€`), units (`@`, `#`), status
|
|
186
|
+
* icons. The slot wrapper carries `pointer-events-none` to
|
|
187
|
+
* avoid stealing focus from the input on click.
|
|
188
|
+
*/}
|
|
189
|
+
{slotProps?.prefix?.children !== undefined && (
|
|
190
|
+
<span
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
className={cn(v.prefix(), slotProps.prefix.className)}
|
|
193
|
+
>
|
|
194
|
+
{slotProps.prefix.children}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
181
197
|
<input
|
|
182
198
|
{...rest}
|
|
183
199
|
id={inputId}
|
|
@@ -196,6 +212,19 @@ export function TextField(props: TextFieldProps) {
|
|
|
196
212
|
ref={inputRef}
|
|
197
213
|
className={cn(v.input(), slotProps?.input?.className)}
|
|
198
214
|
/>
|
|
215
|
+
{/*
|
|
216
|
+
* Suffix slot (Sprint 2 P4.1) — same pattern as prefix,
|
|
217
|
+
* rendered AFTER the input. Typical use: unit labels (`USD`,
|
|
218
|
+
* `kg`, `%`), trailing icons, length counters.
|
|
219
|
+
*/}
|
|
220
|
+
{slotProps?.suffix?.children !== undefined && (
|
|
221
|
+
<span
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
className={cn(v.suffix(), slotProps.suffix.className)}
|
|
224
|
+
>
|
|
225
|
+
{slotProps.suffix.children}
|
|
226
|
+
</span>
|
|
227
|
+
)}
|
|
199
228
|
</div>
|
|
200
229
|
|
|
201
230
|
{resolvedHelperText && (
|
|
@@ -6,9 +6,27 @@ import type { TextFieldVariants } from './textField.variants.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Per-slot overrides for `<TextField>`.
|
|
8
8
|
*
|
|
9
|
-
* Each slot accepts `{ className: string }`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Each slot accepts `{ className: string }` (and, for `prefix` /
|
|
10
|
+
* `suffix`, a `children` node) so the override path is extensible —
|
|
11
|
+
* extra fields like `style`, `aria-*`, `data-*` can be added without
|
|
12
|
+
* a breaking change. Mirrors the MUI-side `slotProps` shape.
|
|
13
|
+
*
|
|
14
|
+
* **`prefix` / `suffix`** are inline adornments rendered INSIDE the
|
|
15
|
+
* `inputWrapper`, before / after the `<input>` element. Typical use:
|
|
16
|
+
* currency symbols, units, status icons. Passing only `className`
|
|
17
|
+
* (no `children`) is allowed but renders an empty slot — usually
|
|
18
|
+
* you'll always pair the two.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <TextField
|
|
22
|
+
* name="price"
|
|
23
|
+
* type="number"
|
|
24
|
+
* slotProps={{
|
|
25
|
+
* prefix: { children: '$' },
|
|
26
|
+
* suffix: { children: 'USD' },
|
|
27
|
+
* }}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
12
30
|
*/
|
|
13
31
|
export interface TextFieldSlotProps {
|
|
14
32
|
root?: { className?: string };
|
|
@@ -18,6 +36,8 @@ export interface TextFieldSlotProps {
|
|
|
18
36
|
input?: { className?: string };
|
|
19
37
|
helperText?: { className?: string };
|
|
20
38
|
errorText?: { className?: string };
|
|
39
|
+
prefix?: { children?: ReactNode; className?: string };
|
|
40
|
+
suffix?: { children?: ReactNode; className?: string };
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
/**
|
|
@@ -34,6 +34,24 @@ export const textFieldVariants = tv({
|
|
|
34
34
|
],
|
|
35
35
|
helperText: 'mt-1 text-sm text-neutral-600',
|
|
36
36
|
errorText: 'mt-1 text-sm text-danger-600',
|
|
37
|
+
/*
|
|
38
|
+
* `prefix` / `suffix` slot — inline adornment rendered before /
|
|
39
|
+
* after the input INSIDE the inputWrapper. `shrink-0` keeps the
|
|
40
|
+
* adornment width fixed so it doesn't compete for space with the
|
|
41
|
+
* input; `select-none` + `pointer-events-none` avoids
|
|
42
|
+
* accidentally stealing focus from the input on click (the
|
|
43
|
+
* inputWrapper handles focus via `focus-within`).
|
|
44
|
+
*/
|
|
45
|
+
prefix: [
|
|
46
|
+
'shrink-0 inline-flex items-center select-none pointer-events-none',
|
|
47
|
+
'text-neutral-500',
|
|
48
|
+
'mr-2',
|
|
49
|
+
],
|
|
50
|
+
suffix: [
|
|
51
|
+
'shrink-0 inline-flex items-center select-none pointer-events-none',
|
|
52
|
+
'text-neutral-500',
|
|
53
|
+
'ml-2',
|
|
54
|
+
],
|
|
37
55
|
},
|
|
38
56
|
variants: {
|
|
39
57
|
size: {
|
package/src/index.ts
CHANGED
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Dashforge
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|