@agentero/design-system 0.31.4 → 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/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>
|
|
@@ -248,19 +248,19 @@
|
|
|
248
248
|
{
|
|
249
249
|
"id": "components-accordion--default",
|
|
250
250
|
"name": "Default",
|
|
251
|
-
"snippet": "const Default = () => <Accordion.Root type=\"single\" collapsible enclosed={false} disabled={false}>\n <
|
|
251
|
+
"snippet": "const Default = () => <Accordion.Root type=\"single\" collapsible enclosed={false} disabled={false}>\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n</Accordion.Root>;",
|
|
252
252
|
"description": "Exclusive accordion: one item open at a time, `collapsible` lets it close."
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"id": "components-accordion--multiple",
|
|
256
256
|
"name": "Multiple",
|
|
257
|
-
"snippet": "const Multiple = () => (\n <Accordion.Root type=\"multiple\" defaultValue={['covered', 'payment']}>\n <
|
|
257
|
+
"snippet": "const Multiple = () => (\n <Accordion.Root type=\"multiple\" defaultValue={['covered', 'payment']}>\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n </Accordion.Root>\n);",
|
|
258
258
|
"description": "`type=\"multiple\"` lets several sections stay open at once."
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
"id": "components-accordion--enclosed",
|
|
262
262
|
"name": "Enclosed",
|
|
263
|
-
"snippet": "const Enclosed = () => (\n <div className=\"flex flex-col gap-8\">\n <Accordion.Root type=\"multiple\" defaultValue={['covered']}>\n <
|
|
263
|
+
"snippet": "const Enclosed = () => (\n <div className=\"flex flex-col gap-8\">\n <Accordion.Root type=\"multiple\" defaultValue={['covered']}>\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n </Accordion.Root>\n <Accordion.Root type=\"multiple\" enclosed defaultValue={['claim']}>\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n </Accordion.Root>\n </div>\n);",
|
|
264
264
|
"description": "`enclosed` wraps the group in a bordered card (default borderless on top)."
|
|
265
265
|
},
|
|
266
266
|
{
|
|
@@ -278,13 +278,13 @@
|
|
|
278
278
|
{
|
|
279
279
|
"id": "components-accordion--expand-collapse",
|
|
280
280
|
"name": "Expand Collapse",
|
|
281
|
-
"snippet": "const ExpandCollapse = () => <Accordion.Root type=\"single\" collapsible>\n <
|
|
281
|
+
"snippet": "const ExpandCollapse = () => <Accordion.Root type=\"single\" collapsible>\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n</Accordion.Root>;",
|
|
282
282
|
"description": "`collapsible` lets clicking the open item close it (reach an all-closed state)."
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
"id": "components-accordion--non-collapsible",
|
|
286
286
|
"name": "Non Collapsible",
|
|
287
|
-
"snippet": "const NonCollapsible = () => <Accordion.Root type=\"single\" collapsible={false} defaultValue=\"covered\">\n <
|
|
287
|
+
"snippet": "const NonCollapsible = () => <Accordion.Root type=\"single\" collapsible={false} defaultValue=\"covered\">\n {FAQS.map(({ value, question, answer }) => (\n <Accordion.Item key={value} value={value}>\n <Accordion.Trigger>{question}</Accordion.Trigger>\n <Accordion.Content>{answer}</Accordion.Content>\n </Accordion.Item>\n ))}\n</Accordion.Root>;",
|
|
288
288
|
"description": "Without `collapsible`, clicking the already-open item keeps it open."
|
|
289
289
|
}
|
|
290
290
|
],
|
|
@@ -2788,13 +2788,13 @@
|
|
|
2788
2788
|
{
|
|
2789
2789
|
"id": "components-combobox--async-search",
|
|
2790
2790
|
"name": "Async Search",
|
|
2791
|
-
"snippet": "const AsyncSearch = () => <
|
|
2791
|
+
"snippet": "const AsyncSearch = () => {\n const [query, setQuery] = useState('');\n\n return (\n // No `items` and `filter={null}`: the server did the filtering, the child renders what came\n // back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.\n <Combobox.Root\n filter={null}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n onInputValueChange={(value, { reason }) => {\n // Picking a row writes its label into the input; that is not a new search.\n if (reason !== 'item-press') setQuery(value);\n }}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content>\n <Suspense fallback={<SkeletonRows />}>\n <AgencyItems query={query} />\n </Suspense>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
|
|
2792
2792
|
"description": "Options fetched as you type, the way the apps load data: the rows are a component\nthat suspends on the request, so a `Suspense` boundary inside `Content` shows\nplaceholder rows until they land. Nothing tracks `loading` by hand and no stale\nlist lingers under the wait: the data is read by query, which is also what makes\nout-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is."
|
|
2793
2793
|
},
|
|
2794
2794
|
{
|
|
2795
2795
|
"id": "components-combobox--async-search-with-status",
|
|
2796
2796
|
"name": "Async Search With Status",
|
|
2797
|
-
"snippet": "const AsyncSearchWithStatus = () => <
|
|
2797
|
+
"snippet": "const AsyncSearchWithStatus = () => {\n const [results, setResults] = useState<Agency[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n // Only the latest request may land; fast typing returns responses out of order.\n const latestRequest = useRef(0);\n\n const onInputValueChange = async (query: string) => {\n const request = ++latestRequest.current;\n\n // The previous rows go before the wait starts: a list that no longer matches what is typed\n // must not sit under the loading state.\n setResults([]);\n setIsLoading(true);\n const agencies = await searchAgencies(query);\n if (request !== latestRequest.current) return;\n\n setResults(agencies);\n setIsLoading(false);\n };\n\n return (\n // `filter={null}` hands filtering to the server; `filteredItems` is what came back.\n <Combobox.Root\n items={results}\n filteredItems={results}\n filter={null}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n onInputValueChange={onInputValueChange}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content aria-busy={isLoading || undefined}>\n {/* The rows show the wait; `Status` only announces it. */}\n <Combobox.Status className=\"sr-only\">\n {isLoading ? 'Searching…' : undefined}\n </Combobox.Status>\n {isLoading && <SkeletonRows />}\n <Combobox.Empty>{isLoading ? undefined : 'No agencies found'}</Combobox.Empty>\n <Combobox.List>\n {(agency: Agency) => (\n <Combobox.Item key={agency.id} value={agency}>\n {agency.name}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
|
|
2798
2798
|
"description": "The same search without Suspense, for a consumer that gets a `loading` flag from\nits data layer instead: `filter={null}`, the rows arrive through `filteredItems`,\n`Content` is marked busy and `Status` announces the wait. The list is cleared\nbefore each request and a request that is no longer the latest is dropped."
|
|
2799
2799
|
},
|
|
2800
2800
|
{
|
|
@@ -2806,7 +2806,7 @@
|
|
|
2806
2806
|
{
|
|
2807
2807
|
"id": "components-combobox--with-leading-icon",
|
|
2808
2808
|
"name": "With Leading Icon",
|
|
2809
|
-
"snippet": "const WithLeadingIcon = () => (\n <Combobox.Root items={STATES}>\n <div className=\"relative\">\n <IconSearch\n aria-hidden\n className=\"pointer-events-none absolute top-1/2 left-3 z-
|
|
2809
|
+
"snippet": "const WithLeadingIcon = () => (\n <Combobox.Root items={STATES}>\n <div className=\"relative\">\n <IconSearch\n aria-hidden\n className=\"pointer-events-none absolute top-1/2 left-3 -z-hide size-6 -translate-y-1/2 [&>path]:fill-icon-default-base-tertiary\"\n />\n <Combobox.Input className=\"pl-11\" placeholder=\"Search states\" aria-label=\"Search states\" />\n </div>\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
|
|
2810
2810
|
"description": "A leading icon is composed around the input, not baked in: not every combobox\nis a search (a FEIN lookup or a template picker are not), so the component\nships no icon of its own. Until `InputGroup` lands in the design system, the\nmarketplace search fields get theirs like this — the icon overlaid, the input\npadded past it."
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
@@ -2834,13 +2834,13 @@
|
|
|
2834
2834
|
"```tsx\n// Filtering handled upstream (async search): let every item through.\n<Command.Root label=\"Search options\" shouldFilter={false}>\n <Command.Input placeholder=\"Search...\" />\n <Command.List>...</Command.List>\n</Command.Root>\n```"
|
|
2835
2835
|
]
|
|
2836
2836
|
},
|
|
2837
|
-
"description": "Command is a filterable list built on `cmdk`. Type to narrow the options,\nnavigate with the arrow keys, select with Enter. It carries its own panel\nchrome, so dropping it inside a `Popover.Content` yields a combobox.",
|
|
2837
|
+
"description": "Command is a filterable list built on `cmdk`. Type to narrow the options,\nnavigate with the arrow keys, select with Enter. It carries its own panel\nchrome, so dropping it inside a `Popover.Content` yields a combobox.\n`useCommandState` reads the search text and results of the surrounding\n`Command.Root` from any component rendered inside it.",
|
|
2838
2838
|
"summary": "Filterable command palette container",
|
|
2839
2839
|
"reactComponentMeta": {
|
|
2840
2840
|
"displayName": "Command.Root",
|
|
2841
2841
|
"exportName": "Command",
|
|
2842
2842
|
"filePath": "/home/runner/work/design-system/design-system/src/command/index.ts",
|
|
2843
|
-
"description": "Surface of a filterable list built on `cmdk`: type to narrow the options,\nnavigate with the arrow keys, select with Enter. Compose `Root` with `Input` /\n`List` / `Empty` / `Item`, plus `Group` when the options need sections.\n\n`Root` owns the search state, the filtering and the keyboard navigation, and\ncarries the panel chrome (border, radius, shadow), so it can be dropped\nstraight into a `Popover.Content` without extra wrapping to yield a combobox.\nReach for `DropdownMenu` instead when the options are actions and there is\nnothing to search.\n\n`label` names the search input — it is the only prop that can, see `Input`.",
|
|
2843
|
+
"description": "Surface of a filterable list built on `cmdk`: type to narrow the options,\nnavigate with the arrow keys, select with Enter. Compose `Root` with `Input` /\n`List` / `Empty` / `Item`, plus `Group` when the options need sections.\n\n`Root` owns the search state, the filtering and the keyboard navigation, and\ncarries the panel chrome (border, radius, shadow), so it can be dropped\nstraight into a `Popover.Content` without extra wrapping to yield a combobox.\nReach for `DropdownMenu` instead when the options are actions and there is\nnothing to search.\n\n`label` names the search input — it is the only prop that can, see `Input`.\nA component rendered inside `Root` reads its search text and results with\n`useCommandState`, exported next to `Command`.",
|
|
2844
2844
|
"jsDocTags": {
|
|
2845
2845
|
"summary": [
|
|
2846
2846
|
"Filterable command palette container"
|
|
@@ -3049,6 +3049,12 @@
|
|
|
3049
3049
|
"name": "Disabled Item",
|
|
3050
3050
|
"snippet": "const DisabledItem = () => <Command.Root label=\"Search agencies\">\n <Command.Input placeholder=\"Search...\" />\n <Command.List>\n <Command.Item value=\"pinnacle\" onSelect={handleDisabledSelect}>Pinnacle Shield Insurance\n </Command.Item>\n <Command.Item value=\"horizon\" disabled onSelect={handleDisabledSelect}>Horizon Risk Solutions\n </Command.Item>\n </Command.List>\n <Command.Empty>No results found.</Command.Empty>\n</Command.Root>;",
|
|
3051
3051
|
"description": "A `disabled` row stays visible and searchable but cannot be chosen. cmdk skips it with the\narrow keys and swallows the click, and the row drops its hover and active styling so the\nlist does not invite the pointer."
|
|
3052
|
+
},
|
|
3053
|
+
{
|
|
3054
|
+
"id": "components-command--reading-state",
|
|
3055
|
+
"name": "Reading State",
|
|
3056
|
+
"snippet": "const ReadingState = () => <Command.Root label=\"Search agencies\">\n <Command.Input placeholder=\"Search agencies...\" />\n <Command.List>\n <Command.Item>Pinnacle Shield Insurance</Command.Item>\n <Command.Item>Horizon Risk Solutions</Command.Item>\n <CreateItem />\n </Command.List>\n</Command.Root>;",
|
|
3057
|
+
"description": "`useCommandState` reads the surrounding `Root` from a component inside it. Here the row\noffering to create an agency appears only when the search text matches no existing one."
|
|
3052
3058
|
}
|
|
3053
3059
|
],
|
|
3054
3060
|
"import": "import { Command } from \"@agentero/design-system\";"
|
|
@@ -5438,7 +5444,7 @@
|
|
|
5438
5444
|
{
|
|
5439
5445
|
"id": "components-fieldtext--controlled-from-context",
|
|
5440
5446
|
"name": "Controlled From Context",
|
|
5441
|
-
"snippet": "const ControlledFromContext = () => <
|
|
5447
|
+
"snippet": "const ControlledFromContext = () => {\n const [value, setValue] = useState('');\n const [touched, setTouched] = useState(false);\n\n const errors = touched && !value ? [{ message: 'Required' }] : undefined;\n\n return (\n <FieldText invalid={!!errors} required>\n <ContextExtension\n inputProps={{\n name: 'email',\n value,\n onChange: event => setValue(event.target.value),\n onBlur: () => setTouched(true)\n }}\n errors={errors}>\n <Label>Email</Label>\n <Input type=\"email\" onChange={onOwnChange} />\n <Field.Error />\n </ContextExtension>\n </FieldText>\n );\n};",
|
|
5442
5448
|
"description": "The contexts are an extension point: a provider nested in the field adds\n`name`, `value`, `onChange` and `onBlur` to `InputContext`, so a bare\n`<Input />` becomes controlled, and the errors to `FieldContext`, so a bare\n`<Field.Error />` renders them — here with `useState` and no form library.\nThe input's own `onChange` still runs, chained after the provider's.",
|
|
5443
5449
|
"summary": "Input controlled and errors supplied from outside the field"
|
|
5444
5450
|
}
|
|
@@ -5685,14 +5691,14 @@
|
|
|
5685
5691
|
{
|
|
5686
5692
|
"id": "components-field--horizontal",
|
|
5687
5693
|
"name": "Horizontal",
|
|
5688
|
-
"snippet": "const Horizontal = () => (\n <Field.Group className=\"w-
|
|
5694
|
+
"snippet": "const Horizontal = () => (\n <Field.Group className=\"w-160\">\n <Field.Root orientation=\"horizontal\" data-testid=\"short\">\n <Label>Full name</Label>\n <DemoInput defaultValue=\"Rafa Moro\" className=\"w-72\" />\n </Field.Root>\n\n <Field.Root orientation=\"horizontal\" data-testid=\"long\">\n <Label>Title</Label>\n <Field.Content className=\"w-72\">\n <DemoInput placeholder=\"Software engineer\" />\n <Field.Description>Shown on your public profile.</Field.Description>\n </Field.Content>\n </Field.Root>\n\n <Field.Root orientation=\"horizontal\" data-testid=\"switch\">\n <Field.Content>\n <Label>Auto-renew</Label>\n <Field.Description>Renews the policy automatically before it expires.</Field.Description>\n </Field.Content>\n <DemoInput type=\"checkbox\" className=\"size-5\" />\n </Field.Root>\n\n <div className=\"w-[20rem]\" data-testid=\"narrow\">\n <Field.Root orientation=\"horizontal\">\n <Label>Email</Label>\n <DemoInput type=\"email\" placeholder=\"you@example.com\" className=\"w-40\" />\n </Field.Root>\n </div>\n </Field.Group>\n);",
|
|
5689
5695
|
"description": "`horizontal` is one row at every width. The first child fills the row and\neverything after it keeps its natural width, aligned to the right edge, so\nthe controls of stacked fields line up whatever their labels measure. Wrap a\nlabel with its description, or a control with its messages, in\n`Field.Content`. A control sets its own width: `Input` is `w-full`, so it\ngets one here.",
|
|
5690
5696
|
"summary": "Horizontal field, one row at every width, controls right-aligned"
|
|
5691
5697
|
},
|
|
5692
5698
|
{
|
|
5693
5699
|
"id": "components-field--responsive",
|
|
5694
5700
|
"name": "Responsive",
|
|
5695
|
-
"snippet": "const Responsive = () => (\n <div className=\"flex flex-col gap-8\">\n <div className=\"w-full max-w-
|
|
5701
|
+
"snippet": "const Responsive = () => (\n <div className=\"flex flex-col gap-8\">\n <div className=\"w-full max-w-160\" data-testid=\"wide\">\n <Field.Root orientation=\"responsive\">\n <Label>Email</Label>\n <Field.Content className=\"@md/field:w-72\">\n <DemoInput type=\"email\" placeholder=\"you@example.com\" />\n <Field.Description>\n We only use this to send policy documents, and never to contact you about anything\n else.\n </Field.Description>\n </Field.Content>\n </Field.Root>\n </div>\n\n <div className=\"w-full max-w-[20rem]\" data-testid=\"narrow\">\n <Field.Root orientation=\"responsive\">\n <Label>Email</Label>\n <Field.Content>\n <DemoInput type=\"email\" placeholder=\"you@example.com\" />\n <Field.Description>We only use this to send policy documents.</Field.Description>\n </Field.Content>\n </Field.Root>\n </div>\n </div>\n);",
|
|
5696
5702
|
"description": "`responsive` stacks the parts below `28rem` and behaves like `horizontal`\nfrom there: first child fills, the rest keep their width on the right. It\nmeasures the field itself, so it needs no particular wrapper. A width that\nshould only apply once horizontal goes behind `@md/field:`.",
|
|
5697
5703
|
"summary": "Responsive field that stacks when narrow"
|
|
5698
5704
|
},
|
|
@@ -5720,7 +5726,7 @@
|
|
|
5720
5726
|
{
|
|
5721
5727
|
"id": "components-field--errors-from-context",
|
|
5722
5728
|
"name": "Errors From Context",
|
|
5723
|
-
"snippet": "const ErrorsFromContext = () => <
|
|
5729
|
+
"snippet": "const ErrorsFromContext = () => {\n const [value, setValue] = useState('');\n const [touched, setTouched] = useState(false);\n\n const errors = touched && !value ? [{ message: 'Required' }] : undefined;\n\n return (\n <Field.Root invalid={!!errors} required>\n <Label>Email</Label>\n <DemoInput\n type=\"email\"\n value={value}\n onChange={event => setValue(event.target.value)}\n onBlur={() => setTouched(true)}\n />\n <ErrorProvider errors={errors}>\n <Field.Error />\n </ErrorProvider>\n </Field.Root>\n );\n};",
|
|
5724
5730
|
"description": "`FieldContext` is an extension point: re-provided under the root with the\nerrors, a bare `<Field.Error />` renders them — here with `useState` and no\nform library. `InputContext` extends the same way with `value`, `onChange`,\n`onBlur` and `name`; see `FieldText`.",
|
|
5725
5731
|
"summary": "Errors supplied to a bare Field.Error through context"
|
|
5726
5732
|
}
|
|
@@ -5893,7 +5899,7 @@
|
|
|
5893
5899
|
"name": "control",
|
|
5894
5900
|
"required": false,
|
|
5895
5901
|
"type": {
|
|
5896
|
-
"name": "Control<FieldValues, any,
|
|
5902
|
+
"name": "Control<FieldValues, any, unknown>"
|
|
5897
5903
|
},
|
|
5898
5904
|
"description": "The form's `control`, from `useForm()`. Defaults to the one the\nsurrounding `Form.Root` provides; pass it explicitly to get `name`\ninference without writing the generic, or when the field renders outside\na `Form.Root`. A `control` whose resolver transforms the values is\naccepted as is.",
|
|
5899
5905
|
"defaultValue": null,
|
|
@@ -6081,63 +6087,63 @@
|
|
|
6081
6087
|
{
|
|
6082
6088
|
"id": "components-formtext--default",
|
|
6083
6089
|
"name": "Default",
|
|
6084
|
-
"snippet": "const Default = () => <
|
|
6090
|
+
"snippet": "const Default = () => {\n const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '' } });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"Agency\"\n className=\"flex flex-col items-start gap-7\">\n <FormText\n rules={{ required: 'Enter the agency name.' }}\n inputProps={{ placeholder: 'Acme Insurance' }}\n label=\"Agency name\"\n orientation=\"vertical\"\n required\n optional={false}\n disabled={false}\n readOnly={false}\n name=\"agencyName\" />\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6085
6091
|
"description": "The field consumers write: a `name`, a `label` and the validation rule.\nSubmitting empty renders the error and marks the input invalid; typing\nclears both and the value reaches `onSubmit`.",
|
|
6086
6092
|
"summary": "Required text field validating on submit"
|
|
6087
6093
|
},
|
|
6088
6094
|
{
|
|
6089
6095
|
"id": "components-formtext--with-description-and-tooltip",
|
|
6090
6096
|
"name": "With Description And Tooltip",
|
|
6091
|
-
"snippet": "const WithDescriptionAndTooltip = () => <
|
|
6097
|
+
"snippet": "const WithDescriptionAndTooltip = () => {\n const methods = useForm<TaxValues>({ defaultValues: { taxId: '' } });\n\n return (\n <Form.Root methods={methods} aria-label=\"Tax details\">\n <FormText\n name=\"taxId\"\n label=\"Tax ID\"\n tooltip=\"The EIN the IRS issued to the agency.\"\n description=\"Nine digits, with or without the dash.\"\n inputProps={{ inputMode: 'numeric', autoComplete: 'off' }}\n />\n </Form.Root>\n );\n};",
|
|
6092
6098
|
"description": "`description` is helper text under the input, announced through\n`aria-describedby`. `tooltip` is an info button beside the label for details\nworth a click; it is a sibling of the `<label>`, so the field's accessible\nname stays the label text.",
|
|
6093
6099
|
"summary": "Helper text under the input and an info tooltip beside the label"
|
|
6094
6100
|
},
|
|
6095
6101
|
{
|
|
6096
6102
|
"id": "components-formtext--optional",
|
|
6097
6103
|
"name": "Optional",
|
|
6098
|
-
"snippet": "const Optional = () => <
|
|
6104
|
+
"snippet": "const Optional = () => {\n const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });\n\n return (\n <Form.Root methods={methods} aria-label=\"Contact\">\n <FormText name=\"phone\" label=\"Phone number\" optional inputProps={{ type: 'tel' }} />\n </Form.Root>\n );\n};",
|
|
6099
6105
|
"description": "The forms mark optional fields rather than required ones: `optional`\nappends a muted suffix to the label and leaves the input without the\n`required` attribute.",
|
|
6100
6106
|
"summary": "Optional field with the \" (optional)\" label suffix"
|
|
6101
6107
|
},
|
|
6102
6108
|
{
|
|
6103
6109
|
"id": "components-formtext--with-rules",
|
|
6104
6110
|
"name": "With Rules",
|
|
6105
|
-
"snippet": "const WithRules = () => <
|
|
6111
|
+
"snippet": "const WithRules = () => {\n const methods = useForm<LicenseValues>({ defaultValues: { agency: { npn: '' } } });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"License\"\n className=\"flex flex-col items-start gap-7\">\n <FormText<LicenseValues>\n name=\"agency.npn\"\n label=\"NPN\"\n rules={{\n required: 'Enter your NPN.',\n pattern: { value: /^\\d{6,10}$/, message: 'An NPN is 6 to 10 digits.' }\n }}\n inputProps={{ inputMode: 'numeric' }}\n />\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6106
6112
|
"description": "`rules` are react-hook-form's, and `name` accepts nested paths typed against\nthe form's values through the generic. `rules.required` validates but does\nnot mark the field required: that stays an explicit `required` prop.",
|
|
6107
6113
|
"summary": "Nested path with validation rules, typed through the generic"
|
|
6108
6114
|
},
|
|
6109
6115
|
{
|
|
6110
6116
|
"id": "components-formtext--all-errors",
|
|
6111
6117
|
"name": "All Errors",
|
|
6112
|
-
"snippet": "const AllErrors = () => <
|
|
6118
|
+
"snippet": "const AllErrors = () => {\n const methods = useForm<PasswordValues>({\n defaultValues: { password: '' },\n criteriaMode: 'all'\n });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"Account security\"\n className=\"flex flex-col items-start gap-7\">\n <FormText\n name=\"password\"\n label=\"Password\"\n required\n inputProps={{ type: 'password' }}\n rules={{\n required: 'Enter a password.',\n validate: {\n uppercase: value => /[A-Z]/.test(value) || 'At least one uppercase letter.',\n number: value => /[0-9]/.test(value) || 'At least one number.',\n length: value => value.length >= 10 || 'At least 10 characters.'\n }\n }}\n />\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6113
6119
|
"description": "A form set to react-hook-form's `criteriaMode: 'all'` collects every rule\nthat failed instead of stopping at the first, and the field lists them all.\nWith the default `'firstError'` a single message is rendered, as in the other\nstories.",
|
|
6114
6120
|
"summary": "Every failed rule listed under criteriaMode 'all'"
|
|
6115
6121
|
},
|
|
6116
6122
|
{
|
|
6117
6123
|
"id": "components-formtext--input-props-chaining",
|
|
6118
6124
|
"name": "Input Props Chaining",
|
|
6119
|
-
"snippet": "const InputPropsChaining = () => <
|
|
6125
|
+
"snippet": "const InputPropsChaining = () => {\n const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });\n\n return (\n <Form.Root methods={methods} aria-label=\"Mobile\">\n <FormText\n name=\"phone\"\n label=\"Mobile\"\n inputProps={{\n type: 'tel',\n placeholder: '5550000000',\n maxLength: 10,\n size: 'lg',\n onChange: event => onOwnChange(methods.getValues('phone'), event.target.value)\n }}\n />\n </Form.Root>\n );\n};",
|
|
6120
6126
|
"description": "`inputProps` carries the input's own attributes — `type`, `placeholder`,\n`maxLength`, `size`… — and its handlers. A consumer `onChange` runs after\nreact-hook-form's: by the time it fires, `getValues` already returns the new\nvalue.",
|
|
6121
6127
|
"summary": "Input attributes and a consumer onChange chained after the form's"
|
|
6122
6128
|
},
|
|
6123
6129
|
{
|
|
6124
6130
|
"id": "components-formtext--horizontal",
|
|
6125
6131
|
"name": "Horizontal",
|
|
6126
|
-
"snippet": "const Horizontal = () => <
|
|
6132
|
+
"snippet": "const Horizontal = () => {\n const methods = useForm<ProfileValues>({\n defaultValues: { fullName: 'Rafa Moro', title: '' }\n });\n\n return (\n <Form.Root methods={methods} aria-label=\"Profile\" className=\"w-[40rem]\">\n <Field.Group>\n <FormText\n name=\"fullName\"\n label=\"Full name\"\n orientation=\"horizontal\"\n inputProps={{ className: 'w-72' }}\n data-testid=\"short\"\n />\n <FormText\n name=\"title\"\n label=\"Title\"\n description=\"Shown on your public profile.\"\n orientation=\"horizontal\"\n inputProps={{ className: 'w-72', placeholder: 'Software engineer' }}\n data-testid=\"long\"\n />\n </Field.Group>\n </Form.Root>\n );\n};",
|
|
6127
6133
|
"description": "Root props pass through: `orientation=\"horizontal\"` lays the label and the\ncontrol in a row, and `className` on the root or in `inputProps` sizes them.\nThe `Input` is full-width by default, so a settings row gives it a width.\n`responsive` stacks the field below 28rem and rows it from there.",
|
|
6128
6134
|
"summary": "Horizontal orientation and class names through the root props"
|
|
6129
6135
|
},
|
|
6130
6136
|
{
|
|
6131
6137
|
"id": "components-formtext--disabled",
|
|
6132
6138
|
"name": "Disabled",
|
|
6133
|
-
"snippet": "const Disabled = () => <
|
|
6139
|
+
"snippet": "const Disabled = () => {\n const methods = useForm<AccountValues>({\n defaultValues: { agencyId: 'AG-48213', email: 'ops@acme-insurance.com' }\n });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"Account\"\n className=\"flex flex-col items-start gap-7\">\n <Field.Group>\n <FormText name=\"agencyId\" label=\"Agency ID\" disabled />\n <FormText name=\"email\" label=\"Email\" inputProps={{ type: 'email' }} />\n </Field.Group>\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6134
6140
|
"description": "`disabled` disables the input and marks the field, and the value still\nreaches `onSubmit` — unlike react-hook-form's own `disabled` option, which\ndrops it. Use `readOnly` for a value the user can copy but not edit.",
|
|
6135
6141
|
"summary": "Disabled field whose value is still submitted"
|
|
6136
6142
|
},
|
|
6137
6143
|
{
|
|
6138
6144
|
"id": "components-formtext--set-focus",
|
|
6139
6145
|
"name": "Set Focus",
|
|
6140
|
-
"snippet": "const SetFocus = () => <
|
|
6146
|
+
"snippet": "const SetFocus = () => {\n const methods = useForm<AgencyValues & PhoneValues>({\n defaultValues: { agencyName: '', phone: '' }\n });\n\n return (\n <Form.Root\n methods={methods}\n aria-label=\"Agency contact\"\n className=\"flex flex-col items-start gap-7\">\n <Field.Group>\n <FormText name=\"agencyName\" label=\"Agency name\" />\n <FormText name=\"phone\" label=\"Phone number\" inputProps={{ type: 'tel' }} />\n </Field.Group>\n <Button type=\"button\" variant=\"secondary\" onClick={() => methods.setFocus('phone')}>\n Focus phone\n </Button>\n </Form.Root>\n );\n};",
|
|
6141
6147
|
"description": "react-hook-form's `ref` reaches the `<input>`, so `setFocus` and the focus\non the first invalid field after submit both work with no ref written by\nhand.",
|
|
6142
6148
|
"summary": "setFocus reaches the input through the merged ref"
|
|
6143
6149
|
}
|
|
@@ -6239,14 +6245,14 @@
|
|
|
6239
6245
|
{
|
|
6240
6246
|
"id": "components-form--default",
|
|
6241
6247
|
"name": "Default",
|
|
6242
|
-
"snippet": "const Default = () => <
|
|
6248
|
+
"snippet": "const Default = () => {\n const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '', email: '' } });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"Agency profile\"\n className=\"flex flex-col items-start gap-7\">\n <Field.Group>\n <FormText\n name=\"agencyName\"\n label=\"Agency name\"\n required\n rules={{ required: 'Enter the agency name.' }}\n />\n <FormText\n name=\"email\"\n label=\"Email\"\n required\n rules={{ required: 'Enter the agency email.' }}\n inputProps={{ type: 'email', autoComplete: 'email' }}\n />\n </Field.Group>\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6243
6249
|
"description": "A form with two required fields and a submit button. Submitting empty shows\nboth errors, marks both inputs invalid and focuses the first one — proof that\nreact-hook-form's `ref` reached the inputs. Once filled in, `onSubmit`\nreceives the typed values.",
|
|
6244
6250
|
"summary": "Form.Root with FormText fields validating on submit"
|
|
6245
6251
|
},
|
|
6246
6252
|
{
|
|
6247
6253
|
"id": "components-form--no-validate",
|
|
6248
6254
|
"name": "No Validate",
|
|
6249
|
-
"snippet": "const NoValidate = () => <
|
|
6255
|
+
"snippet": "const NoValidate = () => {\n const methods = useForm<NicknameValues>({ defaultValues: { nickname: '' } });\n\n return (\n <Form.Root\n methods={methods}\n onSubmit={onSubmit}\n aria-label=\"Display name\"\n className=\"flex flex-col items-start gap-7\">\n <FormText name=\"nickname\" label=\"Display name\" required />\n <Button type=\"submit\">Save</Button>\n </Form.Root>\n );\n};",
|
|
6250
6256
|
"description": "`required` on a field reaches the native attribute, so assistive technology\nannounces it, but the form renders `noValidate`: without `rules`, submitting\nthe field empty is not blocked by the browser and `onSubmit` still runs.\nValidation is react-hook-form's alone.",
|
|
6251
6257
|
"summary": "Native required never triggers the browser's validation bubble"
|
|
6252
6258
|
}
|
|
@@ -6967,7 +6973,7 @@
|
|
|
6967
6973
|
{
|
|
6968
6974
|
"id": "components-modal--controlled",
|
|
6969
6975
|
"name": "Controlled",
|
|
6970
|
-
"snippet": "const Controlled = () => <
|
|
6976
|
+
"snippet": "const Controlled = () => {\n const [isOpen, setIsOpen] = useState(false);\n\n return (\n <>\n <Button variant=\"secondary\" onClick={() => setIsOpen(true)}>\n Open controlled\n </Button>\n <Modal.Root open={isOpen} onOpenChange={setIsOpen}>\n <Modal.Content>\n <Modal.Title>Controlled modal</Modal.Title>\n <Modal.Body>\n <p>The open state lives in the consumer.</p>\n </Modal.Body>\n <Modal.Footer>\n <Button variant=\"primary\" onClick={() => setIsOpen(false)}>\n Done\n </Button>\n </Modal.Footer>\n </Modal.Content>\n </Modal.Root>\n </>\n );\n};",
|
|
6971
6977
|
"description": "Controlled with `open`/`onOpenChange` — the dominant pattern in both apps. Also closes on `Escape`."
|
|
6972
6978
|
},
|
|
6973
6979
|
{
|
|
@@ -7940,7 +7946,7 @@
|
|
|
7940
7946
|
{
|
|
7941
7947
|
"id": "components-table--expandable-rows",
|
|
7942
7948
|
"name": "Expandable Rows",
|
|
7943
|
-
"snippet": "const ExpandableRows =
|
|
7949
|
+
"snippet": "const ExpandableRows = () => {\n const [openId, setOpenId] = useState<string | null>(ROWS[0]!.id);\n\n return (\n <Table.Root size=\"md\" embed={false}>\n <Table.Head>\n <Table.Row>\n <Table.Header>\n <span className=\"sr-only\">Expand</span>\n </Table.Header>\n <Table.Header>Name</Table.Header>\n <Table.Header>Email</Table.Header>\n <Table.Header>Role</Table.Header>\n </Table.Row>\n </Table.Head>\n <Table.Body>\n {ROWS.slice(0, 4).map(row => (\n <Fragment key={row.id}>\n <Table.Row>\n <Table.Cell>\n <Table.ExpandButton\n isExpanded={openId === row.id}\n toggleExpanded={() => setOpenId(openId === row.id ? null : row.id)}\n aria-label={`Toggle ${row.name}`}\n />\n </Table.Cell>\n <Table.Cell className=\"font-bold\">{row.name}</Table.Cell>\n <Table.Cell className=\"text-text-default-base-secondary\">{row.email}</Table.Cell>\n <Table.Cell>{row.role}</Table.Cell>\n </Table.Row>\n {openId === row.id && (\n <Table.ExpandedRow>\n <Table.Cell colSpan={4}>\n <div className=\"py-2 text-text-default-base-secondary\">\n Extended details for {row.name} — {row.amount} lifetime value.\n </div>\n </Table.Cell>\n </Table.ExpandedRow>\n )}\n </Fragment>\n ))}\n </Table.Body>\n </Table.Root>\n );\n};",
|
|
7944
7950
|
"description": "`Table.ExpandButton` toggles a `Table.ExpandedRow` detail panel under its row.",
|
|
7945
7951
|
"summary": "Expandable rows with a detail panel"
|
|
7946
7952
|
}
|
|
@@ -9414,6 +9420,6 @@
|
|
|
9414
9420
|
},
|
|
9415
9421
|
"meta": {
|
|
9416
9422
|
"docgen": "react-component-meta",
|
|
9417
|
-
"durationMs":
|
|
9423
|
+
"durationMs": 4808
|
|
9418
9424
|
}
|
|
9419
9425
|
}
|
package/package.json
CHANGED
package/src/command/command.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentProps } from 'react';
|
|
2
|
-
import { Command as CommandPrimitive } from 'cmdk';
|
|
2
|
+
import { Command as CommandPrimitive, useCommandState as useCommandStatePrimitive } from 'cmdk';
|
|
3
3
|
export declare const commandRecipe: import('tailwind-variants').TVReturnType<{
|
|
4
4
|
[key: string]: {
|
|
5
5
|
[key: string]: import('tailwind-merge').ClassNameValue | {
|
|
@@ -85,6 +85,8 @@ type RootProps = ComponentProps<typeof CommandPrimitive>;
|
|
|
85
85
|
* nothing to search.
|
|
86
86
|
*
|
|
87
87
|
* `label` names the search input — it is the only prop that can, see `Input`.
|
|
88
|
+
* A component rendered inside `Root` reads its search text and results with
|
|
89
|
+
* `useCommandState`, exported next to `Command`.
|
|
88
90
|
*
|
|
89
91
|
* @summary Filterable command palette container
|
|
90
92
|
* @see {@link https://github.com/pacocoursey/cmdk|cmdk}
|
|
@@ -242,4 +244,38 @@ export declare const Item: {
|
|
|
242
244
|
({ className, ...props }: ItemProps): import("react/jsx-runtime").JSX.Element;
|
|
243
245
|
displayName: string;
|
|
244
246
|
};
|
|
247
|
+
/**
|
|
248
|
+
* Snapshot of the surrounding `Root` that `useCommandState` selects from.
|
|
249
|
+
*
|
|
250
|
+
* @property {string} search - Current text in `Input`.
|
|
251
|
+
* @property {string} value - Value of the active item.
|
|
252
|
+
* @property {string} [selectedItemId] - DOM id of the active item.
|
|
253
|
+
* @property {{ count: number; items: Map<string, number>; groups: Set<string> }} filtered -
|
|
254
|
+
* Result of the current search: how many items match, each matching item's score keyed by
|
|
255
|
+
* value, and the values of the groups that still hold a match.
|
|
256
|
+
*/
|
|
257
|
+
export type CommandState = Parameters<Parameters<typeof useCommandStatePrimitive>[0]>[0];
|
|
258
|
+
/**
|
|
259
|
+
* Reads the state of the nearest `Command.Root`: the search text, the active
|
|
260
|
+
* value and the filtered results. The selector picks the slice a component
|
|
261
|
+
* needs, and the component re-renders only when that slice changes.
|
|
262
|
+
*
|
|
263
|
+
* Call it from a component rendered inside `Command.Root`. Import it from here,
|
|
264
|
+
* not from `cmdk`: this hook and `Root` resolve the same copy of `cmdk`, so it
|
|
265
|
+
* always finds the state, even when the app installs a different `cmdk` version.
|
|
266
|
+
*
|
|
267
|
+
* @summary Hook that reads the search text and results of the surrounding Command
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```tsx
|
|
271
|
+
* import { Command, useCommandState } from '@agentero/design-system/command';
|
|
272
|
+
*
|
|
273
|
+
* const CreateItem = ({ onCreate }: { onCreate: (name: string) => void }) => {
|
|
274
|
+
* const search = useCommandState(state => state.search);
|
|
275
|
+
* if (!search) return null;
|
|
276
|
+
* return <Command.Item onSelect={() => onCreate(search)}>Create "{search}"</Command.Item>;
|
|
277
|
+
* };
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export declare const useCommandState: <T>(selector: (state: CommandState) => T) => T;
|
|
245
281
|
export {};
|
package/src/command/command.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { IconSearch as e } from "./icons.js";
|
|
3
3
|
import { tv as t } from "tailwind-variants";
|
|
4
4
|
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
5
|
-
import { Command as i } from "cmdk";
|
|
5
|
+
import { Command as i, useCommandState as a } from "cmdk";
|
|
6
6
|
//#region src/command/command.tsx
|
|
7
|
-
var
|
|
7
|
+
var o = t({ slots: {
|
|
8
8
|
root: ["overflow-hidden rounded-md bg-bg-default-base-primary shadow-xl", "border border-border-default-base-primary"],
|
|
9
9
|
search: ["flex items-center gap-2 px-4 py-3", "border-b border-border-default-base-primary"],
|
|
10
10
|
searchIcon: "shrink-0 pointer-events-none [&>path]:fill-icon-default-base-tertiary",
|
|
@@ -18,48 +18,49 @@ var a = t({ slots: {
|
|
|
18
18
|
"data-[disabled=true]:cursor-default data-[disabled=true]:text-text-default-disable-primary",
|
|
19
19
|
"data-[disabled=true]:hover:bg-transparent data-[disabled=true]:data-[selected=true]:bg-transparent"
|
|
20
20
|
]
|
|
21
|
-
} }),
|
|
21
|
+
} }), s = o(), c = ({ className: e, ...t }) => /* @__PURE__ */ n(i, {
|
|
22
22
|
"data-slot": "command-root",
|
|
23
|
-
className:
|
|
23
|
+
className: s.root({ className: e }),
|
|
24
24
|
...t
|
|
25
25
|
});
|
|
26
|
-
|
|
27
|
-
var
|
|
26
|
+
c.displayName = "Command.Root";
|
|
27
|
+
var l = ({ className: t, ...a }) => /* @__PURE__ */ r("div", {
|
|
28
28
|
"data-slot": "command-search",
|
|
29
|
-
className:
|
|
29
|
+
className: s.search({ className: t }),
|
|
30
30
|
children: [/* @__PURE__ */ n(e, {
|
|
31
31
|
"data-slot": "command-search-icon",
|
|
32
|
-
className:
|
|
32
|
+
className: s.searchIcon(),
|
|
33
33
|
"aria-hidden": !0
|
|
34
34
|
}), /* @__PURE__ */ n(i.Input, {
|
|
35
35
|
"data-slot": "command-input",
|
|
36
|
-
className:
|
|
36
|
+
className: s.input(),
|
|
37
37
|
...a
|
|
38
38
|
})]
|
|
39
39
|
});
|
|
40
|
-
|
|
41
|
-
var
|
|
40
|
+
l.displayName = "Command.Input";
|
|
41
|
+
var u = ({ className: e, ...t }) => /* @__PURE__ */ n(i.List, {
|
|
42
42
|
"data-slot": "command-list",
|
|
43
|
-
className:
|
|
43
|
+
className: s.list({ className: e }),
|
|
44
44
|
...t
|
|
45
45
|
});
|
|
46
|
-
|
|
47
|
-
var
|
|
46
|
+
u.displayName = "Command.List";
|
|
47
|
+
var d = ({ className: e, ...t }) => /* @__PURE__ */ n(i.Empty, {
|
|
48
48
|
"data-slot": "command-empty",
|
|
49
|
-
className:
|
|
49
|
+
className: s.empty({ className: e }),
|
|
50
50
|
...t
|
|
51
51
|
});
|
|
52
|
-
|
|
53
|
-
var
|
|
52
|
+
d.displayName = "Command.Empty";
|
|
53
|
+
var f = (e) => /* @__PURE__ */ n(i.Group, {
|
|
54
54
|
"data-slot": "command-group",
|
|
55
55
|
...e
|
|
56
56
|
});
|
|
57
|
-
|
|
58
|
-
var
|
|
57
|
+
f.displayName = "Command.Group";
|
|
58
|
+
var p = ({ className: e, ...t }) => /* @__PURE__ */ n(i.Item, {
|
|
59
59
|
"data-slot": "command-item",
|
|
60
|
-
className:
|
|
60
|
+
className: s.item({ className: e }),
|
|
61
61
|
...t
|
|
62
62
|
});
|
|
63
|
-
|
|
63
|
+
p.displayName = "Command.Item";
|
|
64
|
+
var m = (e) => a(e);
|
|
64
65
|
//#endregion
|
|
65
|
-
export {
|
|
66
|
+
export { d as Empty, f as Group, l as Input, p as Item, u as List, c as Root, o as commandRecipe, m as useCommandState };
|
package/src/command/index.d.ts
CHANGED
package/src/command/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Empty as e, Group as t, Input as n, Item as r, List as i, Root as a, commandRecipe as o } from "./command.js";
|
|
1
|
+
import { Empty as e, Group as t, Input as n, Item as r, List as i, Root as a, commandRecipe as o, useCommandState as s } from "./command.js";
|
|
2
2
|
//#region src/command/index.ts
|
|
3
|
-
var
|
|
3
|
+
var c = {
|
|
4
4
|
Root: a,
|
|
5
5
|
Input: n,
|
|
6
6
|
List: i,
|
|
@@ -9,4 +9,4 @@ var s = {
|
|
|
9
9
|
Item: r
|
|
10
10
|
};
|
|
11
11
|
//#endregion
|
|
12
|
-
export {
|
|
12
|
+
export { c as Command, o as commandRecipe, s as useCommandState };
|