@agentero/design-system 0.31.3 → 0.31.5
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/mcp/manifests/components.html +471 -82
- package/mcp/manifests/components.json +35 -29
- package/package.json +1 -1
- package/src/combobox/combobox.js +47 -56
- package/src/command/command.d.ts +37 -1
- package/src/command/command.js +23 -22
- package/src/command/index.d.ts +2 -1
- package/src/command/index.js +3 -3
|
@@ -588,13 +588,13 @@
|
|
|
588
588
|
<header>
|
|
589
589
|
<div class="wrap">
|
|
590
590
|
<h1>Manifest Debugger</h1>
|
|
591
|
-
<div class="summary"><a class="filter-pill all" data-k="all" href="#filter-all">All</a><span class="filter-pill ok" aria-disabled="true">33 components ok</span><span class="filter-pill ok" aria-disabled="true">
|
|
591
|
+
<div class="summary"><a class="filter-pill all" data-k="all" href="#filter-all">All</a><span class="filter-pill ok" aria-disabled="true">33 components ok</span><span class="filter-pill ok" aria-disabled="true">265 stories ok</span></div>
|
|
592
592
|
</div>
|
|
593
593
|
</header>
|
|
594
594
|
<main>
|
|
595
595
|
<div class="wrap">
|
|
596
596
|
<div class="note ok" style="margin-bottom: 16px;">
|
|
597
|
-
Using <code>react-component-meta</code>. Generation took <strong>
|
|
597
|
+
Using <code>react-component-meta</code>. Generation took <strong>4.8s</strong>.
|
|
598
598
|
</div>
|
|
599
599
|
<h2 class="section-title">Components</h2>
|
|
600
600
|
<div class="grid" role="list">
|
|
@@ -720,7 +720,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
720
720
|
|
|
721
721
|
<div class="hint">Exclusive accordion: one item open at a time, `collapsible` lets it close.</div>
|
|
722
722
|
<pre><code>const Default = () => <Accordion.Root type="single" collapsible enclosed={false} disabled={false}>
|
|
723
|
-
|
|
723
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
724
|
+
<Accordion.Item key={value} value={value}>
|
|
725
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
726
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
727
|
+
</Accordion.Item>
|
|
728
|
+
))}
|
|
724
729
|
</Accordion.Root>;</code></pre>
|
|
725
730
|
</div>
|
|
726
731
|
<div class="note ok">
|
|
@@ -732,7 +737,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
732
737
|
<div class="hint">`type="multiple"` lets several sections stay open at once.</div>
|
|
733
738
|
<pre><code>const Multiple = () => (
|
|
734
739
|
<Accordion.Root type="multiple" defaultValue={['covered', 'payment']}>
|
|
735
|
-
|
|
740
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
741
|
+
<Accordion.Item key={value} value={value}>
|
|
742
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
743
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
744
|
+
</Accordion.Item>
|
|
745
|
+
))}
|
|
736
746
|
</Accordion.Root>
|
|
737
747
|
);</code></pre>
|
|
738
748
|
</div>
|
|
@@ -746,10 +756,20 @@ value?: string | string[] | undefined</code></pre>
|
|
|
746
756
|
<pre><code>const Enclosed = () => (
|
|
747
757
|
<div className="flex flex-col gap-8">
|
|
748
758
|
<Accordion.Root type="multiple" defaultValue={['covered']}>
|
|
749
|
-
|
|
759
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
760
|
+
<Accordion.Item key={value} value={value}>
|
|
761
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
762
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
763
|
+
</Accordion.Item>
|
|
764
|
+
))}
|
|
750
765
|
</Accordion.Root>
|
|
751
766
|
<Accordion.Root type="multiple" enclosed defaultValue={['claim']}>
|
|
752
|
-
|
|
767
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
768
|
+
<Accordion.Item key={value} value={value}>
|
|
769
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
770
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
771
|
+
</Accordion.Item>
|
|
772
|
+
))}
|
|
753
773
|
</Accordion.Root>
|
|
754
774
|
</div>
|
|
755
775
|
);</code></pre>
|
|
@@ -801,7 +821,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
801
821
|
|
|
802
822
|
<div class="hint">`collapsible` lets clicking the open item close it (reach an all-closed state).</div>
|
|
803
823
|
<pre><code>const ExpandCollapse = () => <Accordion.Root type="single" collapsible>
|
|
804
|
-
|
|
824
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
825
|
+
<Accordion.Item key={value} value={value}>
|
|
826
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
827
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
828
|
+
</Accordion.Item>
|
|
829
|
+
))}
|
|
805
830
|
</Accordion.Root>;</code></pre>
|
|
806
831
|
</div>
|
|
807
832
|
<div class="note ok">
|
|
@@ -812,7 +837,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
812
837
|
|
|
813
838
|
<div class="hint">Without `collapsible`, clicking the already-open item keeps it open.</div>
|
|
814
839
|
<pre><code>const NonCollapsible = () => <Accordion.Root type="single" collapsible={false} defaultValue="covered">
|
|
815
|
-
|
|
840
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
841
|
+
<Accordion.Item key={value} value={value}>
|
|
842
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
843
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
844
|
+
</Accordion.Item>
|
|
845
|
+
))}
|
|
816
846
|
</Accordion.Root>;</code></pre>
|
|
817
847
|
</div>
|
|
818
848
|
</div>
|
|
@@ -3182,7 +3212,29 @@ that suspends on the request, so a `Suspense` boundary inside `Content` shows
|
|
|
3182
3212
|
placeholder rows until they land. Nothing tracks `loading` by hand and no stale
|
|
3183
3213
|
list lingers under the wait: the data is read by query, which is also what makes
|
|
3184
3214
|
out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</div>
|
|
3185
|
-
<pre><code>const AsyncSearch = () =>
|
|
3215
|
+
<pre><code>const AsyncSearch = () => {
|
|
3216
|
+
const [query, setQuery] = useState('');
|
|
3217
|
+
|
|
3218
|
+
return (
|
|
3219
|
+
// No `items` and `filter={null}`: the server did the filtering, the child renders what came
|
|
3220
|
+
// back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.
|
|
3221
|
+
<Combobox.Root
|
|
3222
|
+
filter={null}
|
|
3223
|
+
itemToStringLabel={(agency: Agency) => agency.name}
|
|
3224
|
+
isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}
|
|
3225
|
+
onInputValueChange={(value, { reason }) => {
|
|
3226
|
+
// Picking a row writes its label into the input; that is not a new search.
|
|
3227
|
+
if (reason !== 'item-press') setQuery(value);
|
|
3228
|
+
}}>
|
|
3229
|
+
<Combobox.Input placeholder="Search agencies" aria-label="Search agencies" />
|
|
3230
|
+
<Combobox.Content>
|
|
3231
|
+
<Suspense fallback={<SkeletonRows />}>
|
|
3232
|
+
<AgencyItems query={query} />
|
|
3233
|
+
</Suspense>
|
|
3234
|
+
</Combobox.Content>
|
|
3235
|
+
</Combobox.Root>
|
|
3236
|
+
);
|
|
3237
|
+
};</code></pre>
|
|
3186
3238
|
</div>
|
|
3187
3239
|
<div class="note ok">
|
|
3188
3240
|
<div class="row">
|
|
@@ -3194,7 +3246,54 @@ out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</di
|
|
|
3194
3246
|
its data layer instead: `filter={null}`, the rows arrive through `filteredItems`,
|
|
3195
3247
|
`Content` is marked busy and `Status` announces the wait. The list is cleared
|
|
3196
3248
|
before each request and a request that is no longer the latest is dropped.</div>
|
|
3197
|
-
<pre><code>const AsyncSearchWithStatus = () =>
|
|
3249
|
+
<pre><code>const AsyncSearchWithStatus = () => {
|
|
3250
|
+
const [results, setResults] = useState<Agency[]>([]);
|
|
3251
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
3252
|
+
// Only the latest request may land; fast typing returns responses out of order.
|
|
3253
|
+
const latestRequest = useRef(0);
|
|
3254
|
+
|
|
3255
|
+
const onInputValueChange = async (query: string) => {
|
|
3256
|
+
const request = ++latestRequest.current;
|
|
3257
|
+
|
|
3258
|
+
// The previous rows go before the wait starts: a list that no longer matches what is typed
|
|
3259
|
+
// must not sit under the loading state.
|
|
3260
|
+
setResults([]);
|
|
3261
|
+
setIsLoading(true);
|
|
3262
|
+
const agencies = await searchAgencies(query);
|
|
3263
|
+
if (request !== latestRequest.current) return;
|
|
3264
|
+
|
|
3265
|
+
setResults(agencies);
|
|
3266
|
+
setIsLoading(false);
|
|
3267
|
+
};
|
|
3268
|
+
|
|
3269
|
+
return (
|
|
3270
|
+
// `filter={null}` hands filtering to the server; `filteredItems` is what came back.
|
|
3271
|
+
<Combobox.Root
|
|
3272
|
+
items={results}
|
|
3273
|
+
filteredItems={results}
|
|
3274
|
+
filter={null}
|
|
3275
|
+
itemToStringLabel={(agency: Agency) => agency.name}
|
|
3276
|
+
isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}
|
|
3277
|
+
onInputValueChange={onInputValueChange}>
|
|
3278
|
+
<Combobox.Input placeholder="Search agencies" aria-label="Search agencies" />
|
|
3279
|
+
<Combobox.Content aria-busy={isLoading || undefined}>
|
|
3280
|
+
{/* The rows show the wait; `Status` only announces it. */}
|
|
3281
|
+
<Combobox.Status className="sr-only">
|
|
3282
|
+
{isLoading ? 'Searching…' : undefined}
|
|
3283
|
+
</Combobox.Status>
|
|
3284
|
+
{isLoading && <SkeletonRows />}
|
|
3285
|
+
<Combobox.Empty>{isLoading ? undefined : 'No agencies found'}</Combobox.Empty>
|
|
3286
|
+
<Combobox.List>
|
|
3287
|
+
{(agency: Agency) => (
|
|
3288
|
+
<Combobox.Item key={agency.id} value={agency}>
|
|
3289
|
+
{agency.name}
|
|
3290
|
+
</Combobox.Item>
|
|
3291
|
+
)}
|
|
3292
|
+
</Combobox.List>
|
|
3293
|
+
</Combobox.Content>
|
|
3294
|
+
</Combobox.Root>
|
|
3295
|
+
);
|
|
3296
|
+
};</code></pre>
|
|
3198
3297
|
</div>
|
|
3199
3298
|
<div class="note ok">
|
|
3200
3299
|
<div class="row">
|
|
@@ -3242,7 +3341,7 @@ padded past it.</div>
|
|
|
3242
3341
|
<div className="relative">
|
|
3243
3342
|
<IconSearch
|
|
3244
3343
|
aria-hidden
|
|
3245
|
-
className="pointer-events-none absolute top-1/2 left-3 z-
|
|
3344
|
+
className="pointer-events-none absolute top-1/2 left-3 -z-hide size-6 -translate-y-1/2 [&>path]:fill-icon-default-base-tertiary"
|
|
3246
3345
|
/>
|
|
3247
3346
|
<Combobox.Input className="pl-11" placeholder="Search states" aria-label="Search states" />
|
|
3248
3347
|
</div>
|
|
@@ -3313,7 +3412,7 @@ list nor treats a click in it as a click outside and closes.</div>
|
|
|
3313
3412
|
<div class="badges">
|
|
3314
3413
|
<label for="c-8-components-command-props" class="badge ok as-toggle">10 prop types</label>
|
|
3315
3414
|
|
|
3316
|
-
<label for="c-8-components-command-stories" class="badge ok as-toggle">
|
|
3415
|
+
<label for="c-8-components-command-stories" class="badge ok as-toggle">7 stories</label>
|
|
3317
3416
|
|
|
3318
3417
|
|
|
3319
3418
|
</div>
|
|
@@ -3322,7 +3421,9 @@ list nor treats a click in it as a click outside and closes.</div>
|
|
|
3322
3421
|
<div>Filterable command palette container</div>
|
|
3323
3422
|
<div class="hint">Command is a filterable list built on `cmdk`. Type to narrow the options,
|
|
3324
3423
|
navigate with the arrow keys, select with Enter. It carries its own panel
|
|
3325
|
-
chrome, so dropping it inside a `Popover.Content` yields a combobox
|
|
3424
|
+
chrome, so dropping it inside a `Popover.Content` yields a combobox.
|
|
3425
|
+
`useCommandState` reads the search text and results of the surrounding
|
|
3426
|
+
`Command.Root` from any component rendered inside it.</div>
|
|
3326
3427
|
<div class="kv"><span class="chip">summary: Filterable command palette container</span><span class="chip">see: {@link https://github.com/pacocoursey/cmdk cmdk}</span><span class="chip">example: ```tsx
|
|
3327
3428
|
import { Command } from '@agentero/design-system/command';
|
|
3328
3429
|
|
|
@@ -3544,6 +3645,23 @@ list does not invite the pointer.</div>
|
|
|
3544
3645
|
</Command.Item>
|
|
3545
3646
|
</Command.List>
|
|
3546
3647
|
<Command.Empty>No results found.</Command.Empty>
|
|
3648
|
+
</Command.Root>;</code></pre>
|
|
3649
|
+
</div>
|
|
3650
|
+
<div class="note ok">
|
|
3651
|
+
<div class="row">
|
|
3652
|
+
<span class="ex-name">Reading State</span>
|
|
3653
|
+
<span class="badge ok">story ok</span>
|
|
3654
|
+
</div>
|
|
3655
|
+
|
|
3656
|
+
<div class="hint">`useCommandState` reads the surrounding `Root` from a component inside it. Here the row
|
|
3657
|
+
offering to create an agency appears only when the search text matches no existing one.</div>
|
|
3658
|
+
<pre><code>const ReadingState = () => <Command.Root label="Search agencies">
|
|
3659
|
+
<Command.Input placeholder="Search agencies..." />
|
|
3660
|
+
<Command.List>
|
|
3661
|
+
<Command.Item>Pinnacle Shield Insurance</Command.Item>
|
|
3662
|
+
<Command.Item>Horizon Risk Solutions</Command.Item>
|
|
3663
|
+
<CreateItem />
|
|
3664
|
+
</Command.List>
|
|
3547
3665
|
</Command.Root>;</code></pre>
|
|
3548
3666
|
</div>
|
|
3549
3667
|
</div>
|
|
@@ -5317,7 +5435,7 @@ label with its description, or a control with its messages, in
|
|
|
5317
5435
|
`Field.Content`. A control sets its own width: `Input` is `w-full`, so it
|
|
5318
5436
|
gets one here.</div>
|
|
5319
5437
|
<pre><code>const Horizontal = () => (
|
|
5320
|
-
<Field.Group className="w-
|
|
5438
|
+
<Field.Group className="w-160">
|
|
5321
5439
|
<Field.Root orientation="horizontal" data-testid="short">
|
|
5322
5440
|
<Label>Full name</Label>
|
|
5323
5441
|
<DemoInput defaultValue="Rafa Moro" className="w-72" />
|
|
@@ -5360,7 +5478,7 @@ measures the field itself, so it needs no particular wrapper. A width that
|
|
|
5360
5478
|
should only apply once horizontal goes behind `@md/field:`.</div>
|
|
5361
5479
|
<pre><code>const Responsive = () => (
|
|
5362
5480
|
<div className="flex flex-col gap-8">
|
|
5363
|
-
<div className="w-full max-w-
|
|
5481
|
+
<div className="w-full max-w-160" data-testid="wide">
|
|
5364
5482
|
<Field.Root orientation="responsive">
|
|
5365
5483
|
<Label>Email</Label>
|
|
5366
5484
|
<Field.Content className="@md/field:w-72">
|
|
@@ -5455,7 +5573,27 @@ part wins over what `Field.Root` provides.</div>
|
|
|
5455
5573
|
errors, a bare `<Field.Error />` renders them — here with `useState` and no
|
|
5456
5574
|
form library. `InputContext` extends the same way with `value`, `onChange`,
|
|
5457
5575
|
`onBlur` and `name`; see `FieldText`.</div>
|
|
5458
|
-
<pre><code>const ErrorsFromContext = () =>
|
|
5576
|
+
<pre><code>const ErrorsFromContext = () => {
|
|
5577
|
+
const [value, setValue] = useState('');
|
|
5578
|
+
const [touched, setTouched] = useState(false);
|
|
5579
|
+
|
|
5580
|
+
const errors = touched && !value ? [{ message: 'Required' }] : undefined;
|
|
5581
|
+
|
|
5582
|
+
return (
|
|
5583
|
+
<Field.Root invalid={!!errors} required>
|
|
5584
|
+
<Label>Email</Label>
|
|
5585
|
+
<DemoInput
|
|
5586
|
+
type="email"
|
|
5587
|
+
value={value}
|
|
5588
|
+
onChange={event => setValue(event.target.value)}
|
|
5589
|
+
onBlur={() => setTouched(true)}
|
|
5590
|
+
/>
|
|
5591
|
+
<ErrorProvider errors={errors}>
|
|
5592
|
+
<Field.Error />
|
|
5593
|
+
</ErrorProvider>
|
|
5594
|
+
</Field.Root>
|
|
5595
|
+
);
|
|
5596
|
+
};</code></pre>
|
|
5459
5597
|
</div>
|
|
5460
5598
|
</div>
|
|
5461
5599
|
|
|
@@ -5703,7 +5841,29 @@ through untouched.</div>
|
|
|
5703
5841
|
`<Input />` becomes controlled, and the errors to `FieldContext`, so a bare
|
|
5704
5842
|
`<Field.Error />` renders them — here with `useState` and no form library.
|
|
5705
5843
|
The input's own `onChange` still runs, chained after the provider's.</div>
|
|
5706
|
-
<pre><code>const ControlledFromContext = () =>
|
|
5844
|
+
<pre><code>const ControlledFromContext = () => {
|
|
5845
|
+
const [value, setValue] = useState('');
|
|
5846
|
+
const [touched, setTouched] = useState(false);
|
|
5847
|
+
|
|
5848
|
+
const errors = touched && !value ? [{ message: 'Required' }] : undefined;
|
|
5849
|
+
|
|
5850
|
+
return (
|
|
5851
|
+
<FieldText invalid={!!errors} required>
|
|
5852
|
+
<ContextExtension
|
|
5853
|
+
inputProps={{
|
|
5854
|
+
name: 'email',
|
|
5855
|
+
value,
|
|
5856
|
+
onChange: event => setValue(event.target.value),
|
|
5857
|
+
onBlur: () => setTouched(true)
|
|
5858
|
+
}}
|
|
5859
|
+
errors={errors}>
|
|
5860
|
+
<Label>Email</Label>
|
|
5861
|
+
<Input type="email" onChange={onOwnChange} />
|
|
5862
|
+
<Field.Error />
|
|
5863
|
+
</ContextExtension>
|
|
5864
|
+
</FieldText>
|
|
5865
|
+
);
|
|
5866
|
+
};</code></pre>
|
|
5707
5867
|
</div>
|
|
5708
5868
|
</div>
|
|
5709
5869
|
|
|
@@ -5820,7 +5980,34 @@ onSubmit?: SubmitHandler<AgencyValues></code></pre>
|
|
|
5820
5980
|
both errors, marks both inputs invalid and focuses the first one — proof that
|
|
5821
5981
|
react-hook-form's `ref` reached the inputs. Once filled in, `onSubmit`
|
|
5822
5982
|
receives the typed values.</div>
|
|
5823
|
-
<pre><code>const Default = () =>
|
|
5983
|
+
<pre><code>const Default = () => {
|
|
5984
|
+
const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '', email: '' } });
|
|
5985
|
+
|
|
5986
|
+
return (
|
|
5987
|
+
<Form.Root
|
|
5988
|
+
methods={methods}
|
|
5989
|
+
onSubmit={onSubmit}
|
|
5990
|
+
aria-label="Agency profile"
|
|
5991
|
+
className="flex flex-col items-start gap-7">
|
|
5992
|
+
<Field.Group>
|
|
5993
|
+
<FormText
|
|
5994
|
+
name="agencyName"
|
|
5995
|
+
label="Agency name"
|
|
5996
|
+
required
|
|
5997
|
+
rules={{ required: 'Enter the agency name.' }}
|
|
5998
|
+
/>
|
|
5999
|
+
<FormText
|
|
6000
|
+
name="email"
|
|
6001
|
+
label="Email"
|
|
6002
|
+
required
|
|
6003
|
+
rules={{ required: 'Enter the agency email.' }}
|
|
6004
|
+
inputProps={{ type: 'email', autoComplete: 'email' }}
|
|
6005
|
+
/>
|
|
6006
|
+
</Field.Group>
|
|
6007
|
+
<Button type="submit">Save</Button>
|
|
6008
|
+
</Form.Root>
|
|
6009
|
+
);
|
|
6010
|
+
};</code></pre>
|
|
5824
6011
|
</div>
|
|
5825
6012
|
<div class="note ok">
|
|
5826
6013
|
<div class="row">
|
|
@@ -5832,7 +6019,20 @@ receives the typed values.</div>
|
|
|
5832
6019
|
announces it, but the form renders `noValidate`: without `rules`, submitting
|
|
5833
6020
|
the field empty is not blocked by the browser and `onSubmit` still runs.
|
|
5834
6021
|
Validation is react-hook-form's alone.</div>
|
|
5835
|
-
<pre><code>const NoValidate = () =>
|
|
6022
|
+
<pre><code>const NoValidate = () => {
|
|
6023
|
+
const methods = useForm<NicknameValues>({ defaultValues: { nickname: '' } });
|
|
6024
|
+
|
|
6025
|
+
return (
|
|
6026
|
+
<Form.Root
|
|
6027
|
+
methods={methods}
|
|
6028
|
+
onSubmit={onSubmit}
|
|
6029
|
+
aria-label="Display name"
|
|
6030
|
+
className="flex flex-col items-start gap-7">
|
|
6031
|
+
<FormText name="nickname" label="Display name" required />
|
|
6032
|
+
<Button type="submit">Save</Button>
|
|
6033
|
+
</Form.Root>
|
|
6034
|
+
);
|
|
6035
|
+
};</code></pre>
|
|
5836
6036
|
</div>
|
|
5837
6037
|
</div>
|
|
5838
6038
|
|
|
@@ -5907,7 +6107,7 @@ with the `FieldText`, `Label`, `Input` and `Field.*` primitives directly.</div>
|
|
|
5907
6107
|
* a `Form.Root`. A `control` whose resolver transforms the values is
|
|
5908
6108
|
* accepted as is.
|
|
5909
6109
|
*/
|
|
5910
|
-
control?: Control<FieldValues, any,
|
|
6110
|
+
control?: Control<FieldValues, any, unknown>
|
|
5911
6111
|
|
|
5912
6112
|
/**
|
|
5913
6113
|
* `id` for the control. Generated when omitted. Set it here, not on the
|
|
@@ -6033,14 +6233,29 @@ tooltipSide?: "top" | "bottom" | "left" | "ri
|
|
|
6033
6233
|
<div class="hint">The field consumers write: a `name`, a `label` and the validation rule.
|
|
6034
6234
|
Submitting empty renders the error and marks the input invalid; typing
|
|
6035
6235
|
clears both and the value reaches `onSubmit`.</div>
|
|
6036
|
-
<pre><code>const Default = () =>
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6236
|
+
<pre><code>const Default = () => {
|
|
6237
|
+
const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '' } });
|
|
6238
|
+
|
|
6239
|
+
return (
|
|
6240
|
+
<Form.Root
|
|
6241
|
+
methods={methods}
|
|
6242
|
+
onSubmit={onSubmit}
|
|
6243
|
+
aria-label="Agency"
|
|
6244
|
+
className="flex flex-col items-start gap-7">
|
|
6245
|
+
<FormText
|
|
6246
|
+
rules={{ required: 'Enter the agency name.' }}
|
|
6247
|
+
inputProps={{ placeholder: 'Acme Insurance' }}
|
|
6248
|
+
label="Agency name"
|
|
6249
|
+
orientation="vertical"
|
|
6250
|
+
required
|
|
6251
|
+
optional={false}
|
|
6252
|
+
disabled={false}
|
|
6253
|
+
readOnly={false}
|
|
6254
|
+
name="agencyName" />
|
|
6255
|
+
<Button type="submit">Save</Button>
|
|
6256
|
+
</Form.Root>
|
|
6257
|
+
);
|
|
6258
|
+
};</code></pre>
|
|
6044
6259
|
</div>
|
|
6045
6260
|
<div class="note ok">
|
|
6046
6261
|
<div class="row">
|
|
@@ -6052,7 +6267,21 @@ clears both and the value reaches `onSubmit`.</div>
|
|
|
6052
6267
|
`aria-describedby`. `tooltip` is an info button beside the label for details
|
|
6053
6268
|
worth a click; it is a sibling of the `<label>`, so the field's accessible
|
|
6054
6269
|
name stays the label text.</div>
|
|
6055
|
-
<pre><code>const WithDescriptionAndTooltip = () =>
|
|
6270
|
+
<pre><code>const WithDescriptionAndTooltip = () => {
|
|
6271
|
+
const methods = useForm<TaxValues>({ defaultValues: { taxId: '' } });
|
|
6272
|
+
|
|
6273
|
+
return (
|
|
6274
|
+
<Form.Root methods={methods} aria-label="Tax details">
|
|
6275
|
+
<FormText
|
|
6276
|
+
name="taxId"
|
|
6277
|
+
label="Tax ID"
|
|
6278
|
+
tooltip="The EIN the IRS issued to the agency."
|
|
6279
|
+
description="Nine digits, with or without the dash."
|
|
6280
|
+
inputProps={{ inputMode: 'numeric', autoComplete: 'off' }}
|
|
6281
|
+
/>
|
|
6282
|
+
</Form.Root>
|
|
6283
|
+
);
|
|
6284
|
+
};</code></pre>
|
|
6056
6285
|
</div>
|
|
6057
6286
|
<div class="note ok">
|
|
6058
6287
|
<div class="row">
|
|
@@ -6063,7 +6292,15 @@ name stays the label text.</div>
|
|
|
6063
6292
|
<div class="hint">The forms mark optional fields rather than required ones: `optional`
|
|
6064
6293
|
appends a muted suffix to the label and leaves the input without the
|
|
6065
6294
|
`required` attribute.</div>
|
|
6066
|
-
<pre><code>const Optional = () =>
|
|
6295
|
+
<pre><code>const Optional = () => {
|
|
6296
|
+
const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });
|
|
6297
|
+
|
|
6298
|
+
return (
|
|
6299
|
+
<Form.Root methods={methods} aria-label="Contact">
|
|
6300
|
+
<FormText name="phone" label="Phone number" optional inputProps={{ type: 'tel' }} />
|
|
6301
|
+
</Form.Root>
|
|
6302
|
+
);
|
|
6303
|
+
};</code></pre>
|
|
6067
6304
|
</div>
|
|
6068
6305
|
<div class="note ok">
|
|
6069
6306
|
<div class="row">
|
|
@@ -6074,7 +6311,28 @@ appends a muted suffix to the label and leaves the input without the
|
|
|
6074
6311
|
<div class="hint">`rules` are react-hook-form's, and `name` accepts nested paths typed against
|
|
6075
6312
|
the form's values through the generic. `rules.required` validates but does
|
|
6076
6313
|
not mark the field required: that stays an explicit `required` prop.</div>
|
|
6077
|
-
<pre><code>const WithRules = () =>
|
|
6314
|
+
<pre><code>const WithRules = () => {
|
|
6315
|
+
const methods = useForm<LicenseValues>({ defaultValues: { agency: { npn: '' } } });
|
|
6316
|
+
|
|
6317
|
+
return (
|
|
6318
|
+
<Form.Root
|
|
6319
|
+
methods={methods}
|
|
6320
|
+
onSubmit={onSubmit}
|
|
6321
|
+
aria-label="License"
|
|
6322
|
+
className="flex flex-col items-start gap-7">
|
|
6323
|
+
<FormText<LicenseValues>
|
|
6324
|
+
name="agency.npn"
|
|
6325
|
+
label="NPN"
|
|
6326
|
+
rules={{
|
|
6327
|
+
required: 'Enter your NPN.',
|
|
6328
|
+
pattern: { value: /^\d{6,10}$/, message: 'An NPN is 6 to 10 digits.' }
|
|
6329
|
+
}}
|
|
6330
|
+
inputProps={{ inputMode: 'numeric' }}
|
|
6331
|
+
/>
|
|
6332
|
+
<Button type="submit">Save</Button>
|
|
6333
|
+
</Form.Root>
|
|
6334
|
+
);
|
|
6335
|
+
};</code></pre>
|
|
6078
6336
|
</div>
|
|
6079
6337
|
<div class="note ok">
|
|
6080
6338
|
<div class="row">
|
|
@@ -6086,7 +6344,36 @@ not mark the field required: that stays an explicit `required` prop.</div>
|
|
|
6086
6344
|
that failed instead of stopping at the first, and the field lists them all.
|
|
6087
6345
|
With the default `'firstError'` a single message is rendered, as in the other
|
|
6088
6346
|
stories.</div>
|
|
6089
|
-
<pre><code>const AllErrors = () =>
|
|
6347
|
+
<pre><code>const AllErrors = () => {
|
|
6348
|
+
const methods = useForm<PasswordValues>({
|
|
6349
|
+
defaultValues: { password: '' },
|
|
6350
|
+
criteriaMode: 'all'
|
|
6351
|
+
});
|
|
6352
|
+
|
|
6353
|
+
return (
|
|
6354
|
+
<Form.Root
|
|
6355
|
+
methods={methods}
|
|
6356
|
+
onSubmit={onSubmit}
|
|
6357
|
+
aria-label="Account security"
|
|
6358
|
+
className="flex flex-col items-start gap-7">
|
|
6359
|
+
<FormText
|
|
6360
|
+
name="password"
|
|
6361
|
+
label="Password"
|
|
6362
|
+
required
|
|
6363
|
+
inputProps={{ type: 'password' }}
|
|
6364
|
+
rules={{
|
|
6365
|
+
required: 'Enter a password.',
|
|
6366
|
+
validate: {
|
|
6367
|
+
uppercase: value => /[A-Z]/.test(value) || 'At least one uppercase letter.',
|
|
6368
|
+
number: value => /[0-9]/.test(value) || 'At least one number.',
|
|
6369
|
+
length: value => value.length >= 10 || 'At least 10 characters.'
|
|
6370
|
+
}
|
|
6371
|
+
}}
|
|
6372
|
+
/>
|
|
6373
|
+
<Button type="submit">Save</Button>
|
|
6374
|
+
</Form.Root>
|
|
6375
|
+
);
|
|
6376
|
+
};</code></pre>
|
|
6090
6377
|
</div>
|
|
6091
6378
|
<div class="note ok">
|
|
6092
6379
|
<div class="row">
|
|
@@ -6098,7 +6385,25 @@ stories.</div>
|
|
|
6098
6385
|
`maxLength`, `size`… — and its handlers. A consumer `onChange` runs after
|
|
6099
6386
|
react-hook-form's: by the time it fires, `getValues` already returns the new
|
|
6100
6387
|
value.</div>
|
|
6101
|
-
<pre><code>const InputPropsChaining = () =>
|
|
6388
|
+
<pre><code>const InputPropsChaining = () => {
|
|
6389
|
+
const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });
|
|
6390
|
+
|
|
6391
|
+
return (
|
|
6392
|
+
<Form.Root methods={methods} aria-label="Mobile">
|
|
6393
|
+
<FormText
|
|
6394
|
+
name="phone"
|
|
6395
|
+
label="Mobile"
|
|
6396
|
+
inputProps={{
|
|
6397
|
+
type: 'tel',
|
|
6398
|
+
placeholder: '5550000000',
|
|
6399
|
+
maxLength: 10,
|
|
6400
|
+
size: 'lg',
|
|
6401
|
+
onChange: event => onOwnChange(methods.getValues('phone'), event.target.value)
|
|
6402
|
+
}}
|
|
6403
|
+
/>
|
|
6404
|
+
</Form.Root>
|
|
6405
|
+
);
|
|
6406
|
+
};</code></pre>
|
|
6102
6407
|
</div>
|
|
6103
6408
|
<div class="note ok">
|
|
6104
6409
|
<div class="row">
|
|
@@ -6110,7 +6415,33 @@ value.</div>
|
|
|
6110
6415
|
control in a row, and `className` on the root or in `inputProps` sizes them.
|
|
6111
6416
|
The `Input` is full-width by default, so a settings row gives it a width.
|
|
6112
6417
|
`responsive` stacks the field below 28rem and rows it from there.</div>
|
|
6113
|
-
<pre><code>const Horizontal = () =>
|
|
6418
|
+
<pre><code>const Horizontal = () => {
|
|
6419
|
+
const methods = useForm<ProfileValues>({
|
|
6420
|
+
defaultValues: { fullName: 'Rafa Moro', title: '' }
|
|
6421
|
+
});
|
|
6422
|
+
|
|
6423
|
+
return (
|
|
6424
|
+
<Form.Root methods={methods} aria-label="Profile" className="w-[40rem]">
|
|
6425
|
+
<Field.Group>
|
|
6426
|
+
<FormText
|
|
6427
|
+
name="fullName"
|
|
6428
|
+
label="Full name"
|
|
6429
|
+
orientation="horizontal"
|
|
6430
|
+
inputProps={{ className: 'w-72' }}
|
|
6431
|
+
data-testid="short"
|
|
6432
|
+
/>
|
|
6433
|
+
<FormText
|
|
6434
|
+
name="title"
|
|
6435
|
+
label="Title"
|
|
6436
|
+
description="Shown on your public profile."
|
|
6437
|
+
orientation="horizontal"
|
|
6438
|
+
inputProps={{ className: 'w-72', placeholder: 'Software engineer' }}
|
|
6439
|
+
data-testid="long"
|
|
6440
|
+
/>
|
|
6441
|
+
</Field.Group>
|
|
6442
|
+
</Form.Root>
|
|
6443
|
+
);
|
|
6444
|
+
};</code></pre>
|
|
6114
6445
|
</div>
|
|
6115
6446
|
<div class="note ok">
|
|
6116
6447
|
<div class="row">
|
|
@@ -6121,7 +6452,25 @@ The `Input` is full-width by default, so a settings row gives it a width.
|
|
|
6121
6452
|
<div class="hint">`disabled` disables the input and marks the field, and the value still
|
|
6122
6453
|
reaches `onSubmit` — unlike react-hook-form's own `disabled` option, which
|
|
6123
6454
|
drops it. Use `readOnly` for a value the user can copy but not edit.</div>
|
|
6124
|
-
<pre><code>const Disabled = () =>
|
|
6455
|
+
<pre><code>const Disabled = () => {
|
|
6456
|
+
const methods = useForm<AccountValues>({
|
|
6457
|
+
defaultValues: { agencyId: 'AG-48213', email: 'ops@acme-insurance.com' }
|
|
6458
|
+
});
|
|
6459
|
+
|
|
6460
|
+
return (
|
|
6461
|
+
<Form.Root
|
|
6462
|
+
methods={methods}
|
|
6463
|
+
onSubmit={onSubmit}
|
|
6464
|
+
aria-label="Account"
|
|
6465
|
+
className="flex flex-col items-start gap-7">
|
|
6466
|
+
<Field.Group>
|
|
6467
|
+
<FormText name="agencyId" label="Agency ID" disabled />
|
|
6468
|
+
<FormText name="email" label="Email" inputProps={{ type: 'email' }} />
|
|
6469
|
+
</Field.Group>
|
|
6470
|
+
<Button type="submit">Save</Button>
|
|
6471
|
+
</Form.Root>
|
|
6472
|
+
);
|
|
6473
|
+
};</code></pre>
|
|
6125
6474
|
</div>
|
|
6126
6475
|
<div class="note ok">
|
|
6127
6476
|
<div class="row">
|
|
@@ -6132,7 +6481,26 @@ drops it. Use `readOnly` for a value the user can copy but not edit.</div>
|
|
|
6132
6481
|
<div class="hint">react-hook-form's `ref` reaches the `<input>`, so `setFocus` and the focus
|
|
6133
6482
|
on the first invalid field after submit both work with no ref written by
|
|
6134
6483
|
hand.</div>
|
|
6135
|
-
<pre><code>const SetFocus = () =>
|
|
6484
|
+
<pre><code>const SetFocus = () => {
|
|
6485
|
+
const methods = useForm<AgencyValues & PhoneValues>({
|
|
6486
|
+
defaultValues: { agencyName: '', phone: '' }
|
|
6487
|
+
});
|
|
6488
|
+
|
|
6489
|
+
return (
|
|
6490
|
+
<Form.Root
|
|
6491
|
+
methods={methods}
|
|
6492
|
+
aria-label="Agency contact"
|
|
6493
|
+
className="flex flex-col items-start gap-7">
|
|
6494
|
+
<Field.Group>
|
|
6495
|
+
<FormText name="agencyName" label="Agency name" />
|
|
6496
|
+
<FormText name="phone" label="Phone number" inputProps={{ type: 'tel' }} />
|
|
6497
|
+
</Field.Group>
|
|
6498
|
+
<Button type="button" variant="secondary" onClick={() => methods.setFocus('phone')}>
|
|
6499
|
+
Focus phone
|
|
6500
|
+
</Button>
|
|
6501
|
+
</Form.Root>
|
|
6502
|
+
);
|
|
6503
|
+
};</code></pre>
|
|
6136
6504
|
</div>
|
|
6137
6505
|
</div>
|
|
6138
6506
|
|
|
@@ -7101,7 +7469,30 @@ open?: boolean</code></pre>
|
|
|
7101
7469
|
</div>
|
|
7102
7470
|
|
|
7103
7471
|
<div class="hint">Controlled with `open`/`onOpenChange` — the dominant pattern in both apps. Also closes on `Escape`.</div>
|
|
7104
|
-
<pre><code>const Controlled = () =>
|
|
7472
|
+
<pre><code>const Controlled = () => {
|
|
7473
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7474
|
+
|
|
7475
|
+
return (
|
|
7476
|
+
<>
|
|
7477
|
+
<Button variant="secondary" onClick={() => setIsOpen(true)}>
|
|
7478
|
+
Open controlled
|
|
7479
|
+
</Button>
|
|
7480
|
+
<Modal.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
7481
|
+
<Modal.Content>
|
|
7482
|
+
<Modal.Title>Controlled modal</Modal.Title>
|
|
7483
|
+
<Modal.Body>
|
|
7484
|
+
<p>The open state lives in the consumer.</p>
|
|
7485
|
+
</Modal.Body>
|
|
7486
|
+
<Modal.Footer>
|
|
7487
|
+
<Button variant="primary" onClick={() => setIsOpen(false)}>
|
|
7488
|
+
Done
|
|
7489
|
+
</Button>
|
|
7490
|
+
</Modal.Footer>
|
|
7491
|
+
</Modal.Content>
|
|
7492
|
+
</Modal.Root>
|
|
7493
|
+
</>
|
|
7494
|
+
);
|
|
7495
|
+
};</code></pre>
|
|
7105
7496
|
</div>
|
|
7106
7497
|
<div class="note ok">
|
|
7107
7498
|
<div class="row">
|
|
@@ -8273,52 +8664,50 @@ keeps its padding.</div>
|
|
|
8273
8664
|
</div>
|
|
8274
8665
|
<div>Expandable rows with a detail panel</div>
|
|
8275
8666
|
<div class="hint">`Table.ExpandButton` toggles a `Table.ExpandedRow` detail panel under its row.</div>
|
|
8276
|
-
<pre><code>const ExpandableRows =
|
|
8277
|
-
const
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8667
|
+
<pre><code>const ExpandableRows = () => {
|
|
8668
|
+
const [openId, setOpenId] = useState<string | null>(ROWS[0]!.id);
|
|
8669
|
+
|
|
8670
|
+
return (
|
|
8671
|
+
<Table.Root size="md" embed={false}>
|
|
8672
|
+
<Table.Head>
|
|
8673
|
+
<Table.Row>
|
|
8674
|
+
<Table.Header>
|
|
8675
|
+
<span className="sr-only">Expand</span>
|
|
8676
|
+
</Table.Header>
|
|
8677
|
+
<Table.Header>Name</Table.Header>
|
|
8678
|
+
<Table.Header>Email</Table.Header>
|
|
8679
|
+
<Table.Header>Role</Table.Header>
|
|
8680
|
+
</Table.Row>
|
|
8681
|
+
</Table.Head>
|
|
8682
|
+
<Table.Body>
|
|
8683
|
+
{ROWS.slice(0, 4).map(row => (
|
|
8684
|
+
<Fragment key={row.id}>
|
|
8685
|
+
<Table.Row>
|
|
8686
|
+
<Table.Cell>
|
|
8687
|
+
<Table.ExpandButton
|
|
8688
|
+
isExpanded={openId === row.id}
|
|
8689
|
+
toggleExpanded={() => setOpenId(openId === row.id ? null : row.id)}
|
|
8690
|
+
aria-label={`Toggle ${row.name}`}
|
|
8691
|
+
/>
|
|
8692
|
+
</Table.Cell>
|
|
8693
|
+
<Table.Cell className="font-bold">{row.name}</Table.Cell>
|
|
8694
|
+
<Table.Cell className="text-text-default-base-secondary">{row.email}</Table.Cell>
|
|
8695
|
+
<Table.Cell>{row.role}</Table.Cell>
|
|
8696
|
+
</Table.Row>
|
|
8697
|
+
{openId === row.id && (
|
|
8698
|
+
<Table.ExpandedRow>
|
|
8699
|
+
<Table.Cell colSpan={4}>
|
|
8700
|
+
<div className="py-2 text-text-default-base-secondary">
|
|
8701
|
+
Extended details for {row.name} — {row.amount} lifetime value.
|
|
8702
|
+
</div>
|
|
8301
8703
|
</Table.Cell>
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
<div className="py-2 text-text-default-base-secondary">
|
|
8310
|
-
Extended details for {row.name} — {row.amount} lifetime value.
|
|
8311
|
-
</div>
|
|
8312
|
-
</Table.Cell>
|
|
8313
|
-
</Table.ExpandedRow>
|
|
8314
|
-
)}
|
|
8315
|
-
</Fragment>
|
|
8316
|
-
))}
|
|
8317
|
-
</Table.Body>
|
|
8318
|
-
</Table.Root>
|
|
8319
|
-
);
|
|
8320
|
-
};
|
|
8321
|
-
return <Demo />;
|
|
8704
|
+
</Table.ExpandedRow>
|
|
8705
|
+
)}
|
|
8706
|
+
</Fragment>
|
|
8707
|
+
))}
|
|
8708
|
+
</Table.Body>
|
|
8709
|
+
</Table.Root>
|
|
8710
|
+
);
|
|
8322
8711
|
};</code></pre>
|
|
8323
8712
|
</div>
|
|
8324
8713
|
</div>
|