@agentero/design-system 0.31.4 → 0.31.6

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.
@@ -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">264 stories ok</span></div>
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">266 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>2.7s</strong>.
597
+ Using <code>react-component-meta</code>. Generation took <strong>3.9s</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 = () =&gt; &lt;Accordion.Root type=&quot;single&quot; collapsible enclosed={false} disabled={false}&gt;
723
- &lt;FaqItems /&gt;
723
+ {FAQS.map(({ value, question, answer }) =&gt; (
724
+ &lt;Accordion.Item key={value} value={value}&gt;
725
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
726
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
727
+ &lt;/Accordion.Item&gt;
728
+ ))}
724
729
  &lt;/Accordion.Root&gt;;</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=&quot;multiple&quot;` lets several sections stay open at once.</div>
733
738
  <pre><code>const Multiple = () =&gt; (
734
739
  &lt;Accordion.Root type=&quot;multiple&quot; defaultValue={[&#39;covered&#39;, &#39;payment&#39;]}&gt;
735
- &lt;FaqItems /&gt;
740
+ {FAQS.map(({ value, question, answer }) =&gt; (
741
+ &lt;Accordion.Item key={value} value={value}&gt;
742
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
743
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
744
+ &lt;/Accordion.Item&gt;
745
+ ))}
736
746
  &lt;/Accordion.Root&gt;
737
747
  );</code></pre>
738
748
  </div>
@@ -746,10 +756,20 @@ value?: string | string[] | undefined</code></pre>
746
756
  <pre><code>const Enclosed = () =&gt; (
747
757
  &lt;div className=&quot;flex flex-col gap-8&quot;&gt;
748
758
  &lt;Accordion.Root type=&quot;multiple&quot; defaultValue={[&#39;covered&#39;]}&gt;
749
- &lt;FaqItems /&gt;
759
+ {FAQS.map(({ value, question, answer }) =&gt; (
760
+ &lt;Accordion.Item key={value} value={value}&gt;
761
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
762
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
763
+ &lt;/Accordion.Item&gt;
764
+ ))}
750
765
  &lt;/Accordion.Root&gt;
751
766
  &lt;Accordion.Root type=&quot;multiple&quot; enclosed defaultValue={[&#39;claim&#39;]}&gt;
752
- &lt;FaqItems /&gt;
767
+ {FAQS.map(({ value, question, answer }) =&gt; (
768
+ &lt;Accordion.Item key={value} value={value}&gt;
769
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
770
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
771
+ &lt;/Accordion.Item&gt;
772
+ ))}
753
773
  &lt;/Accordion.Root&gt;
754
774
  &lt;/div&gt;
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 = () =&gt; &lt;Accordion.Root type=&quot;single&quot; collapsible&gt;
804
- &lt;FaqItems /&gt;
824
+ {FAQS.map(({ value, question, answer }) =&gt; (
825
+ &lt;Accordion.Item key={value} value={value}&gt;
826
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
827
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
828
+ &lt;/Accordion.Item&gt;
829
+ ))}
805
830
  &lt;/Accordion.Root&gt;;</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 = () =&gt; &lt;Accordion.Root type=&quot;single&quot; collapsible={false} defaultValue=&quot;covered&quot;&gt;
815
- &lt;FaqItems /&gt;
840
+ {FAQS.map(({ value, question, answer }) =&gt; (
841
+ &lt;Accordion.Item key={value} value={value}&gt;
842
+ &lt;Accordion.Trigger&gt;{question}&lt;/Accordion.Trigger&gt;
843
+ &lt;Accordion.Content&gt;{answer}&lt;/Accordion.Content&gt;
844
+ &lt;/Accordion.Item&gt;
845
+ ))}
816
846
  &lt;/Accordion.Root&gt;;</code></pre>
817
847
  </div>
818
848
  </div>
@@ -2827,7 +2857,7 @@ import { CheckList } from &#39;@agentero/design-system/check-list&#39;;
2827
2857
  <div class="badges">
2828
2858
  <label for="c-7-components-combobox-props" class="badge ok as-toggle">38 prop types</label>
2829
2859
 
2830
- <label for="c-7-components-combobox-stories" class="badge ok as-toggle">7 stories</label>
2860
+ <label for="c-7-components-combobox-stories" class="badge ok as-toggle">8 stories</label>
2831
2861
 
2832
2862
 
2833
2863
  </div>
@@ -2836,8 +2866,9 @@ import { CheckList } from &#39;@agentero/design-system/check-list&#39;;
2836
2866
  <div>Root provider for an input-triggered filterable list</div>
2837
2867
  <div class="hint">Combobox is a text input that filters a list and writes the chosen option back
2838
2868
  into itself. Focusing the input or typing opens the list; picking a row fills the
2839
- input and closes it. Built on Base UI, whose Combobox primitive owns the
2840
- filtering, the keyboard navigation and the `role=&quot;combobox&quot;` wiring.</div>
2869
+ input and closes it. With `multiple` the rows become checkbox-like toggles and the
2870
+ list stays open between picks. Built on Base UI, whose Combobox primitive owns
2871
+ the filtering, the keyboard navigation and the `role=&quot;combobox&quot;` wiring.</div>
2841
2872
  <div class="kv"><span class="chip">summary: Root provider for an input-triggered filterable list</span><span class="chip">see: {@link https://base-ui.com/react/components/combobox Base UI Combobox}</span><span class="chip">example: ```tsx
2842
2873
  import { Combobox } from &#39;@agentero/design-system/combobox&#39;;
2843
2874
 
@@ -3118,7 +3149,17 @@ virtualized?: boolean = false</code></pre>
3118
3149
  <div class="row">
3119
3150
  <span class="ex-name">Imports</span>
3120
3151
  </div>
3121
- <pre><code>import { Button, Combobox, Field, FieldText, IconSearch, Label, Modal, Skeleton } from "@agentero/design-system";
3152
+ <pre><code>import {
3153
+ Button,
3154
+ Combobox,
3155
+ Field,
3156
+ FieldText,
3157
+ IconSearch,
3158
+ Label,
3159
+ Modal,
3160
+ Skeleton,
3161
+ Tag,
3162
+ } from "@agentero/design-system";
3122
3163
  import { Suspense } from "react";</code></pre>
3123
3164
  </div>
3124
3165
 
@@ -3170,6 +3211,48 @@ children only while nothing matches. It needs `items` on `Root` to know that.</d
3170
3211
  &lt;/Combobox.Content&gt;
3171
3212
  &lt;/Combobox.Root&gt;
3172
3213
  );</code></pre>
3214
+ </div>
3215
+ <div class="note ok">
3216
+ <div class="row">
3217
+ <span class="ex-name">Multiple Selection</span>
3218
+ <span class="badge ok">story ok</span>
3219
+ </div>
3220
+
3221
+ <div class="hint">Several values from one list, the way Lines of Authority or locations are
3222
+ assigned: each row leads with a checkbox-shaped indicator, picking a row toggles
3223
+ it, and the list stays open with the query intact so the next pick is one click
3224
+ away. The input never shows the selection; the field renders it alongside,
3225
+ here as tags.</div>
3226
+ <pre><code>const MultipleSelection = () =&gt; {
3227
+ const [selected, setSelected] = useState&lt;string[]&gt;([]);
3228
+
3229
+ return (
3230
+ &lt;div className=&quot;flex flex-col gap-4&quot;&gt;
3231
+ &lt;Combobox.Root items={STATES} multiple value={selected} onValueChange={setSelected}&gt;
3232
+ &lt;Combobox.Input placeholder=&quot;Assign states&quot; aria-label=&quot;Assign states&quot; /&gt;
3233
+ &lt;Combobox.Content&gt;
3234
+ &lt;Combobox.Empty&gt;No matches found&lt;/Combobox.Empty&gt;
3235
+ &lt;Combobox.List aria-label=&quot;States&quot;&gt;
3236
+ {(state: string) =&gt; (
3237
+ &lt;Combobox.Item key={state} value={state}&gt;
3238
+ {state}
3239
+ &lt;/Combobox.Item&gt;
3240
+ )}
3241
+ &lt;/Combobox.List&gt;
3242
+ &lt;/Combobox.Content&gt;
3243
+ &lt;/Combobox.Root&gt;
3244
+ {selected.length &gt; 0 &amp;&amp; (
3245
+ &lt;ul aria-label=&quot;Selected states&quot; className=&quot;flex flex-wrap gap-2&quot;&gt;
3246
+ {selected.map(state =&gt; (
3247
+ &lt;li key={state}&gt;
3248
+ &lt;Tag size=&quot;sm&quot;&gt;{state}&lt;/Tag&gt;
3249
+ &lt;/li&gt;
3250
+ ))}
3251
+ &lt;/ul&gt;
3252
+ )}
3253
+ &lt;/div&gt;
3254
+ );
3255
+ };</code></pre>
3173
3256
  </div>
3174
3257
  <div class="note ok">
3175
3258
  <div class="row">
@@ -3182,7 +3265,29 @@ that suspends on the request, so a `Suspense` boundary inside `Content` shows
3182
3265
  placeholder rows until they land. Nothing tracks `loading` by hand and no stale
3183
3266
  list lingers under the wait: the data is read by query, which is also what makes
3184
3267
  out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</div>
3185
- <pre><code>const AsyncSearch = () =&gt; &lt;SuspenseSearch /&gt;;</code></pre>
3268
+ <pre><code>const AsyncSearch = () =&gt; {
3269
+ const [query, setQuery] = useState(&#39;&#39;);
3270
+
3271
+ return (
3272
+ // No `items` and `filter={null}`: the server did the filtering, the child renders what came
3273
+ // back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.
3274
+ &lt;Combobox.Root
3275
+ filter={null}
3276
+ itemToStringLabel={(agency: Agency) =&gt; agency.name}
3277
+ isItemEqualToValue={(a: Agency, b: Agency) =&gt; a.id === b.id}
3278
+ onInputValueChange={(value, { reason }) =&gt; {
3279
+ // Picking a row writes its label into the input; that is not a new search.
3280
+ if (reason !== &#39;item-press&#39;) setQuery(value);
3281
+ }}&gt;
3282
+ &lt;Combobox.Input placeholder=&quot;Search agencies&quot; aria-label=&quot;Search agencies&quot; /&gt;
3283
+ &lt;Combobox.Content&gt;
3284
+ &lt;Suspense fallback={&lt;SkeletonRows /&gt;}&gt;
3285
+ &lt;AgencyItems query={query} /&gt;
3286
+ &lt;/Suspense&gt;
3287
+ &lt;/Combobox.Content&gt;
3288
+ &lt;/Combobox.Root&gt;
3289
+ );
3290
+ };</code></pre>
3186
3291
  </div>
3187
3292
  <div class="note ok">
3188
3293
  <div class="row">
@@ -3194,7 +3299,54 @@ out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</di
3194
3299
  its data layer instead: `filter={null}`, the rows arrive through `filteredItems`,
3195
3300
  `Content` is marked busy and `Status` announces the wait. The list is cleared
3196
3301
  before each request and a request that is no longer the latest is dropped.</div>
3197
- <pre><code>const AsyncSearchWithStatus = () =&gt; &lt;StatusSearch /&gt;;</code></pre>
3302
+ <pre><code>const AsyncSearchWithStatus = () =&gt; {
3303
+ const [results, setResults] = useState&lt;Agency[]&gt;([]);
3304
+ const [isLoading, setIsLoading] = useState(false);
3305
+ // Only the latest request may land; fast typing returns responses out of order.
3306
+ const latestRequest = useRef(0);
3307
+
3308
+ const onInputValueChange = async (query: string) =&gt; {
3309
+ const request = ++latestRequest.current;
3310
+
3311
+ // The previous rows go before the wait starts: a list that no longer matches what is typed
3312
+ // must not sit under the loading state.
3313
+ setResults([]);
3314
+ setIsLoading(true);
3315
+ const agencies = await searchAgencies(query);
3316
+ if (request !== latestRequest.current) return;
3317
+
3318
+ setResults(agencies);
3319
+ setIsLoading(false);
3320
+ };
3321
+
3322
+ return (
3323
+ // `filter={null}` hands filtering to the server; `filteredItems` is what came back.
3324
+ &lt;Combobox.Root
3325
+ items={results}
3326
+ filteredItems={results}
3327
+ filter={null}
3328
+ itemToStringLabel={(agency: Agency) =&gt; agency.name}
3329
+ isItemEqualToValue={(a: Agency, b: Agency) =&gt; a.id === b.id}
3330
+ onInputValueChange={onInputValueChange}&gt;
3331
+ &lt;Combobox.Input placeholder=&quot;Search agencies&quot; aria-label=&quot;Search agencies&quot; /&gt;
3332
+ &lt;Combobox.Content aria-busy={isLoading || undefined}&gt;
3333
+ {/* The rows show the wait; `Status` only announces it. */}
3334
+ &lt;Combobox.Status className=&quot;sr-only&quot;&gt;
3335
+ {isLoading ? &#39;Searching…&#39; : undefined}
3336
+ &lt;/Combobox.Status&gt;
3337
+ {isLoading &amp;&amp; &lt;SkeletonRows /&gt;}
3338
+ &lt;Combobox.Empty&gt;{isLoading ? undefined : &#39;No agencies found&#39;}&lt;/Combobox.Empty&gt;
3339
+ &lt;Combobox.List&gt;
3340
+ {(agency: Agency) =&gt; (
3341
+ &lt;Combobox.Item key={agency.id} value={agency}&gt;
3342
+ {agency.name}
3343
+ &lt;/Combobox.Item&gt;
3344
+ )}
3345
+ &lt;/Combobox.List&gt;
3346
+ &lt;/Combobox.Content&gt;
3347
+ &lt;/Combobox.Root&gt;
3348
+ );
3349
+ };</code></pre>
3198
3350
  </div>
3199
3351
  <div class="note ok">
3200
3352
  <div class="row">
@@ -3204,7 +3356,10 @@ before each request and a request that is no longer the latest is dropped.</div>
3204
3356
 
3205
3357
  <div class="hint">Dropped into a `FieldText`, the input takes the label, the description and
3206
3358
  the error from the field with nothing wired by hand: `Combobox.Input` reads the
3207
- same `InputContext` the plain `Input` does.</div>
3359
+ same `InputContext` the plain `Input` does. It also points `aria-labelledby`
3360
+ at the label, so the name survives the list opening: Base UI then hides the
3361
+ rest of the page from assistive technology, label included, and a
3362
+ `&lt;label for&gt;` alone would stop naming the input.</div>
3208
3363
  <pre><code>const InsideFieldText = () =&gt; (
3209
3364
  &lt;FieldText invalid required&gt;
3210
3365
  &lt;Label&gt;Agency&lt;/Label&gt;
@@ -3242,7 +3397,7 @@ padded past it.</div>
3242
3397
  &lt;div className=&quot;relative&quot;&gt;
3243
3398
  &lt;IconSearch
3244
3399
  aria-hidden
3245
- className=&quot;pointer-events-none absolute top-1/2 left-3 z-1 size-6 -translate-y-1/2 [&amp;&gt;path]:fill-icon-default-base-tertiary&quot;
3400
+ className=&quot;pointer-events-none absolute top-1/2 left-3 -z-hide size-6 -translate-y-1/2 [&amp;&gt;path]:fill-icon-default-base-tertiary&quot;
3246
3401
  /&gt;
3247
3402
  &lt;Combobox.Input className=&quot;pl-11&quot; placeholder=&quot;Search states&quot; aria-label=&quot;Search states&quot; /&gt;
3248
3403
  &lt;/div&gt;
@@ -3313,7 +3468,7 @@ list nor treats a click in it as a click outside and closes.</div>
3313
3468
  <div class="badges">
3314
3469
  <label for="c-8-components-command-props" class="badge ok as-toggle">10 prop types</label>
3315
3470
 
3316
- <label for="c-8-components-command-stories" class="badge ok as-toggle">6 stories</label>
3471
+ <label for="c-8-components-command-stories" class="badge ok as-toggle">7 stories</label>
3317
3472
 
3318
3473
 
3319
3474
  </div>
@@ -3322,7 +3477,9 @@ list nor treats a click in it as a click outside and closes.</div>
3322
3477
  <div>Filterable command palette container</div>
3323
3478
  <div class="hint">Command is a filterable list built on `cmdk`. Type to narrow the options,
3324
3479
  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.</div>
3480
+ chrome, so dropping it inside a `Popover.Content` yields a combobox.
3481
+ `useCommandState` reads the search text and results of the surrounding
3482
+ `Command.Root` from any component rendered inside it.</div>
3326
3483
  <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
3484
  import { Command } from &#39;@agentero/design-system/command&#39;;
3328
3485
 
@@ -3544,6 +3701,23 @@ list does not invite the pointer.</div>
3544
3701
  &lt;/Command.Item&gt;
3545
3702
  &lt;/Command.List&gt;
3546
3703
  &lt;Command.Empty&gt;No results found.&lt;/Command.Empty&gt;
3704
+ &lt;/Command.Root&gt;;</code></pre>
3705
+ </div>
3706
+ <div class="note ok">
3707
+ <div class="row">
3708
+ <span class="ex-name">Reading State</span>
3709
+ <span class="badge ok">story ok</span>
3710
+ </div>
3711
+
3712
+ <div class="hint">`useCommandState` reads the surrounding `Root` from a component inside it. Here the row
3713
+ offering to create an agency appears only when the search text matches no existing one.</div>
3714
+ <pre><code>const ReadingState = () =&gt; &lt;Command.Root label=&quot;Search agencies&quot;&gt;
3715
+ &lt;Command.Input placeholder=&quot;Search agencies...&quot; /&gt;
3716
+ &lt;Command.List&gt;
3717
+ &lt;Command.Item&gt;Pinnacle Shield Insurance&lt;/Command.Item&gt;
3718
+ &lt;Command.Item&gt;Horizon Risk Solutions&lt;/Command.Item&gt;
3719
+ &lt;CreateItem /&gt;
3720
+ &lt;/Command.List&gt;
3547
3721
  &lt;/Command.Root&gt;;</code></pre>
3548
3722
  </div>
3549
3723
  </div>
@@ -5089,7 +5263,7 @@ required?: boolean = false</code></pre>
5089
5263
  <div class="row">
5090
5264
  <span class="ex-name">Imports</span>
5091
5265
  </div>
5092
- <pre><code>import { Field, FieldContext, Label } from "@agentero/design-system";</code></pre>
5266
+ <pre><code>import { Field, FieldContext, Label, Switch } from "@agentero/design-system";</code></pre>
5093
5267
  </div>
5094
5268
 
5095
5269
 
@@ -5317,7 +5491,7 @@ label with its description, or a control with its messages, in
5317
5491
  `Field.Content`. A control sets its own width: `Input` is `w-full`, so it
5318
5492
  gets one here.</div>
5319
5493
  <pre><code>const Horizontal = () =&gt; (
5320
- &lt;Field.Group className=&quot;w-[40rem]&quot;&gt;
5494
+ &lt;Field.Group className=&quot;w-160&quot;&gt;
5321
5495
  &lt;Field.Root orientation=&quot;horizontal&quot; data-testid=&quot;short&quot;&gt;
5322
5496
  &lt;Label&gt;Full name&lt;/Label&gt;
5323
5497
  &lt;DemoInput defaultValue=&quot;Rafa Moro&quot; className=&quot;w-72&quot; /&gt;
@@ -5336,7 +5510,7 @@ gets one here.</div>
5336
5510
  &lt;Label&gt;Auto-renew&lt;/Label&gt;
5337
5511
  &lt;Field.Description&gt;Renews the policy automatically before it expires.&lt;/Field.Description&gt;
5338
5512
  &lt;/Field.Content&gt;
5339
- &lt;DemoInput type=&quot;checkbox&quot; className=&quot;size-5&quot; /&gt;
5513
+ &lt;DemoSwitch className=&quot;self-center&quot; /&gt;
5340
5514
  &lt;/Field.Root&gt;
5341
5515
 
5342
5516
  &lt;div className=&quot;w-[20rem]&quot; data-testid=&quot;narrow&quot;&gt;
@@ -5360,7 +5534,7 @@ measures the field itself, so it needs no particular wrapper. A width that
5360
5534
  should only apply once horizontal goes behind `@md/field:`.</div>
5361
5535
  <pre><code>const Responsive = () =&gt; (
5362
5536
  &lt;div className=&quot;flex flex-col gap-8&quot;&gt;
5363
- &lt;div className=&quot;w-full max-w-[40rem]&quot; data-testid=&quot;wide&quot;&gt;
5537
+ &lt;div className=&quot;w-full max-w-160&quot; data-testid=&quot;wide&quot;&gt;
5364
5538
  &lt;Field.Root orientation=&quot;responsive&quot;&gt;
5365
5539
  &lt;Label&gt;Email&lt;/Label&gt;
5366
5540
  &lt;Field.Content className=&quot;@md/field:w-72&quot;&gt;
@@ -5455,7 +5629,27 @@ part wins over what `Field.Root` provides.</div>
5455
5629
  errors, a bare `&lt;Field.Error /&gt;` renders them — here with `useState` and no
5456
5630
  form library. `InputContext` extends the same way with `value`, `onChange`,
5457
5631
  `onBlur` and `name`; see `FieldText`.</div>
5458
- <pre><code>const ErrorsFromContext = () =&gt; &lt;FieldWithSuppliedErrors /&gt;;</code></pre>
5632
+ <pre><code>const ErrorsFromContext = () =&gt; {
5633
+ const [value, setValue] = useState(&#39;&#39;);
5634
+ const [touched, setTouched] = useState(false);
5635
+
5636
+ const errors = touched &amp;&amp; !value ? [{ message: &#39;Required&#39; }] : undefined;
5637
+
5638
+ return (
5639
+ &lt;Field.Root invalid={!!errors} required&gt;
5640
+ &lt;Label&gt;Email&lt;/Label&gt;
5641
+ &lt;DemoInput
5642
+ type=&quot;email&quot;
5643
+ value={value}
5644
+ onChange={event =&gt; setValue(event.target.value)}
5645
+ onBlur={() =&gt; setTouched(true)}
5646
+ /&gt;
5647
+ &lt;ErrorProvider errors={errors}&gt;
5648
+ &lt;Field.Error /&gt;
5649
+ &lt;/ErrorProvider&gt;
5650
+ &lt;/Field.Root&gt;
5651
+ );
5652
+ };</code></pre>
5459
5653
  </div>
5460
5654
  </div>
5461
5655
 
@@ -5703,7 +5897,29 @@ through untouched.</div>
5703
5897
  `&lt;Input /&gt;` becomes controlled, and the errors to `FieldContext`, so a bare
5704
5898
  `&lt;Field.Error /&gt;` renders them — here with `useState` and no form library.
5705
5899
  The input&#39;s own `onChange` still runs, chained after the provider&#39;s.</div>
5706
- <pre><code>const ControlledFromContext = () =&gt; &lt;ControlledField /&gt;;</code></pre>
5900
+ <pre><code>const ControlledFromContext = () =&gt; {
5901
+ const [value, setValue] = useState(&#39;&#39;);
5902
+ const [touched, setTouched] = useState(false);
5903
+
5904
+ const errors = touched &amp;&amp; !value ? [{ message: &#39;Required&#39; }] : undefined;
5905
+
5906
+ return (
5907
+ &lt;FieldText invalid={!!errors} required&gt;
5908
+ &lt;ContextExtension
5909
+ inputProps={{
5910
+ name: &#39;email&#39;,
5911
+ value,
5912
+ onChange: event =&gt; setValue(event.target.value),
5913
+ onBlur: () =&gt; setTouched(true)
5914
+ }}
5915
+ errors={errors}&gt;
5916
+ &lt;Label&gt;Email&lt;/Label&gt;
5917
+ &lt;Input type=&quot;email&quot; onChange={onOwnChange} /&gt;
5918
+ &lt;Field.Error /&gt;
5919
+ &lt;/ContextExtension&gt;
5920
+ &lt;/FieldText&gt;
5921
+ );
5922
+ };</code></pre>
5707
5923
  </div>
5708
5924
  </div>
5709
5925
 
@@ -5820,7 +6036,34 @@ onSubmit?: SubmitHandler&lt;AgencyValues&gt;</code></pre>
5820
6036
  both errors, marks both inputs invalid and focuses the first one — proof that
5821
6037
  react-hook-form&#39;s `ref` reached the inputs. Once filled in, `onSubmit`
5822
6038
  receives the typed values.</div>
5823
- <pre><code>const Default = () =&gt; &lt;AgencyForm /&gt;;</code></pre>
6039
+ <pre><code>const Default = () =&gt; {
6040
+ const methods = useForm&lt;AgencyValues&gt;({ defaultValues: { agencyName: &#39;&#39;, email: &#39;&#39; } });
6041
+
6042
+ return (
6043
+ &lt;Form.Root
6044
+ methods={methods}
6045
+ onSubmit={onSubmit}
6046
+ aria-label=&quot;Agency profile&quot;
6047
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6048
+ &lt;Field.Group&gt;
6049
+ &lt;FormText
6050
+ name=&quot;agencyName&quot;
6051
+ label=&quot;Agency name&quot;
6052
+ required
6053
+ rules={{ required: &#39;Enter the agency name.&#39; }}
6054
+ /&gt;
6055
+ &lt;FormText
6056
+ name=&quot;email&quot;
6057
+ label=&quot;Email&quot;
6058
+ required
6059
+ rules={{ required: &#39;Enter the agency email.&#39; }}
6060
+ inputProps={{ type: &#39;email&#39;, autoComplete: &#39;email&#39; }}
6061
+ /&gt;
6062
+ &lt;/Field.Group&gt;
6063
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6064
+ &lt;/Form.Root&gt;
6065
+ );
6066
+ };</code></pre>
5824
6067
  </div>
5825
6068
  <div class="note ok">
5826
6069
  <div class="row">
@@ -5832,7 +6075,20 @@ receives the typed values.</div>
5832
6075
  announces it, but the form renders `noValidate`: without `rules`, submitting
5833
6076
  the field empty is not blocked by the browser and `onSubmit` still runs.
5834
6077
  Validation is react-hook-form&#39;s alone.</div>
5835
- <pre><code>const NoValidate = () =&gt; &lt;NicknameForm /&gt;;</code></pre>
6078
+ <pre><code>const NoValidate = () =&gt; {
6079
+ const methods = useForm&lt;NicknameValues&gt;({ defaultValues: { nickname: &#39;&#39; } });
6080
+
6081
+ return (
6082
+ &lt;Form.Root
6083
+ methods={methods}
6084
+ onSubmit={onSubmit}
6085
+ aria-label=&quot;Display name&quot;
6086
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6087
+ &lt;FormText name=&quot;nickname&quot; label=&quot;Display name&quot; required /&gt;
6088
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6089
+ &lt;/Form.Root&gt;
6090
+ );
6091
+ };</code></pre>
5836
6092
  </div>
5837
6093
  </div>
5838
6094
 
@@ -5907,7 +6163,7 @@ with the `FieldText`, `Label`, `Input` and `Field.*` primitives directly.</div>
5907
6163
  * a `Form.Root`. A `control` whose resolver transforms the values is
5908
6164
  * accepted as is.
5909
6165
  */
5910
- control?: Control&lt;FieldValues, any, FieldValues&gt;
6166
+ control?: Control&lt;FieldValues, any, unknown&gt;
5911
6167
 
5912
6168
  /**
5913
6169
  * `id` for the control. Generated when omitted. Set it here, not on the
@@ -6033,14 +6289,29 @@ tooltipSide?: &quot;top&quot; | &quot;bottom&quot; | &quot;left&quot; | &quot;ri
6033
6289
  <div class="hint">The field consumers write: a `name`, a `label` and the validation rule.
6034
6290
  Submitting empty renders the error and marks the input invalid; typing
6035
6291
  clears both and the value reaches `onSubmit`.</div>
6036
- <pre><code>const Default = () =&gt; &lt;AgencyNameForm
6037
- name=&quot;agencyName&quot;
6038
- label=&quot;Agency name&quot;
6039
- orientation=&quot;vertical&quot;
6040
- required
6041
- optional={false}
6042
- disabled={false}
6043
- readOnly={false} /&gt;;</code></pre>
6292
+ <pre><code>const Default = () =&gt; {
6293
+ const methods = useForm&lt;AgencyValues&gt;({ defaultValues: { agencyName: &#39;&#39; } });
6294
+
6295
+ return (
6296
+ &lt;Form.Root
6297
+ methods={methods}
6298
+ onSubmit={onSubmit}
6299
+ aria-label=&quot;Agency&quot;
6300
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6301
+ &lt;FormText
6302
+ rules={{ required: &#39;Enter the agency name.&#39; }}
6303
+ inputProps={{ placeholder: &#39;Acme Insurance&#39; }}
6304
+ label=&quot;Agency name&quot;
6305
+ orientation=&quot;vertical&quot;
6306
+ required
6307
+ optional={false}
6308
+ disabled={false}
6309
+ readOnly={false}
6310
+ name=&quot;agencyName&quot; /&gt;
6311
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6312
+ &lt;/Form.Root&gt;
6313
+ );
6314
+ };</code></pre>
6044
6315
  </div>
6045
6316
  <div class="note ok">
6046
6317
  <div class="row">
@@ -6052,7 +6323,21 @@ clears both and the value reaches `onSubmit`.</div>
6052
6323
  `aria-describedby`. `tooltip` is an info button beside the label for details
6053
6324
  worth a click; it is a sibling of the `&lt;label&gt;`, so the field&#39;s accessible
6054
6325
  name stays the label text.</div>
6055
- <pre><code>const WithDescriptionAndTooltip = () =&gt; &lt;TaxIdForm /&gt;;</code></pre>
6326
+ <pre><code>const WithDescriptionAndTooltip = () =&gt; {
6327
+ const methods = useForm&lt;TaxValues&gt;({ defaultValues: { taxId: &#39;&#39; } });
6328
+
6329
+ return (
6330
+ &lt;Form.Root methods={methods} aria-label=&quot;Tax details&quot;&gt;
6331
+ &lt;FormText
6332
+ name=&quot;taxId&quot;
6333
+ label=&quot;Tax ID&quot;
6334
+ tooltip=&quot;The EIN the IRS issued to the agency.&quot;
6335
+ description=&quot;Nine digits, with or without the dash.&quot;
6336
+ inputProps={{ inputMode: &#39;numeric&#39;, autoComplete: &#39;off&#39; }}
6337
+ /&gt;
6338
+ &lt;/Form.Root&gt;
6339
+ );
6340
+ };</code></pre>
6056
6341
  </div>
6057
6342
  <div class="note ok">
6058
6343
  <div class="row">
@@ -6063,7 +6348,15 @@ name stays the label text.</div>
6063
6348
  <div class="hint">The forms mark optional fields rather than required ones: `optional`
6064
6349
  appends a muted suffix to the label and leaves the input without the
6065
6350
  `required` attribute.</div>
6066
- <pre><code>const Optional = () =&gt; &lt;PhoneForm /&gt;;</code></pre>
6351
+ <pre><code>const Optional = () =&gt; {
6352
+ const methods = useForm&lt;PhoneValues&gt;({ defaultValues: { phone: &#39;&#39; } });
6353
+
6354
+ return (
6355
+ &lt;Form.Root methods={methods} aria-label=&quot;Contact&quot;&gt;
6356
+ &lt;FormText name=&quot;phone&quot; label=&quot;Phone number&quot; optional inputProps={{ type: &#39;tel&#39; }} /&gt;
6357
+ &lt;/Form.Root&gt;
6358
+ );
6359
+ };</code></pre>
6067
6360
  </div>
6068
6361
  <div class="note ok">
6069
6362
  <div class="row">
@@ -6074,7 +6367,28 @@ appends a muted suffix to the label and leaves the input without the
6074
6367
  <div class="hint">`rules` are react-hook-form&#39;s, and `name` accepts nested paths typed against
6075
6368
  the form&#39;s values through the generic. `rules.required` validates but does
6076
6369
  not mark the field required: that stays an explicit `required` prop.</div>
6077
- <pre><code>const WithRules = () =&gt; &lt;NpnForm /&gt;;</code></pre>
6370
+ <pre><code>const WithRules = () =&gt; {
6371
+ const methods = useForm&lt;LicenseValues&gt;({ defaultValues: { agency: { npn: &#39;&#39; } } });
6372
+
6373
+ return (
6374
+ &lt;Form.Root
6375
+ methods={methods}
6376
+ onSubmit={onSubmit}
6377
+ aria-label=&quot;License&quot;
6378
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6379
+ &lt;FormText&lt;LicenseValues&gt;
6380
+ name=&quot;agency.npn&quot;
6381
+ label=&quot;NPN&quot;
6382
+ rules={{
6383
+ required: &#39;Enter your NPN.&#39;,
6384
+ pattern: { value: /^\d{6,10}$/, message: &#39;An NPN is 6 to 10 digits.&#39; }
6385
+ }}
6386
+ inputProps={{ inputMode: &#39;numeric&#39; }}
6387
+ /&gt;
6388
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6389
+ &lt;/Form.Root&gt;
6390
+ );
6391
+ };</code></pre>
6078
6392
  </div>
6079
6393
  <div class="note ok">
6080
6394
  <div class="row">
@@ -6086,7 +6400,36 @@ not mark the field required: that stays an explicit `required` prop.</div>
6086
6400
  that failed instead of stopping at the first, and the field lists them all.
6087
6401
  With the default `&#39;firstError&#39;` a single message is rendered, as in the other
6088
6402
  stories.</div>
6089
- <pre><code>const AllErrors = () =&gt; &lt;PasswordRulesForm /&gt;;</code></pre>
6403
+ <pre><code>const AllErrors = () =&gt; {
6404
+ const methods = useForm&lt;PasswordValues&gt;({
6405
+ defaultValues: { password: &#39;&#39; },
6406
+ criteriaMode: &#39;all&#39;
6407
+ });
6408
+
6409
+ return (
6410
+ &lt;Form.Root
6411
+ methods={methods}
6412
+ onSubmit={onSubmit}
6413
+ aria-label=&quot;Account security&quot;
6414
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6415
+ &lt;FormText
6416
+ name=&quot;password&quot;
6417
+ label=&quot;Password&quot;
6418
+ required
6419
+ inputProps={{ type: &#39;password&#39; }}
6420
+ rules={{
6421
+ required: &#39;Enter a password.&#39;,
6422
+ validate: {
6423
+ uppercase: value =&gt; /[A-Z]/.test(value) || &#39;At least one uppercase letter.&#39;,
6424
+ number: value =&gt; /[0-9]/.test(value) || &#39;At least one number.&#39;,
6425
+ length: value =&gt; value.length &gt;= 10 || &#39;At least 10 characters.&#39;
6426
+ }
6427
+ }}
6428
+ /&gt;
6429
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6430
+ &lt;/Form.Root&gt;
6431
+ );
6432
+ };</code></pre>
6090
6433
  </div>
6091
6434
  <div class="note ok">
6092
6435
  <div class="row">
@@ -6098,7 +6441,25 @@ stories.</div>
6098
6441
  `maxLength`, `size`… — and its handlers. A consumer `onChange` runs after
6099
6442
  react-hook-form&#39;s: by the time it fires, `getValues` already returns the new
6100
6443
  value.</div>
6101
- <pre><code>const InputPropsChaining = () =&gt; &lt;MobileForm /&gt;;</code></pre>
6444
+ <pre><code>const InputPropsChaining = () =&gt; {
6445
+ const methods = useForm&lt;PhoneValues&gt;({ defaultValues: { phone: &#39;&#39; } });
6446
+
6447
+ return (
6448
+ &lt;Form.Root methods={methods} aria-label=&quot;Mobile&quot;&gt;
6449
+ &lt;FormText
6450
+ name=&quot;phone&quot;
6451
+ label=&quot;Mobile&quot;
6452
+ inputProps={{
6453
+ type: &#39;tel&#39;,
6454
+ placeholder: &#39;5550000000&#39;,
6455
+ maxLength: 10,
6456
+ size: &#39;lg&#39;,
6457
+ onChange: event =&gt; onOwnChange(methods.getValues(&#39;phone&#39;), event.target.value)
6458
+ }}
6459
+ /&gt;
6460
+ &lt;/Form.Root&gt;
6461
+ );
6462
+ };</code></pre>
6102
6463
  </div>
6103
6464
  <div class="note ok">
6104
6465
  <div class="row">
@@ -6110,7 +6471,33 @@ value.</div>
6110
6471
  control in a row, and `className` on the root or in `inputProps` sizes them.
6111
6472
  The `Input` is full-width by default, so a settings row gives it a width.
6112
6473
  `responsive` stacks the field below 28rem and rows it from there.</div>
6113
- <pre><code>const Horizontal = () =&gt; &lt;ProfileForm /&gt;;</code></pre>
6474
+ <pre><code>const Horizontal = () =&gt; {
6475
+ const methods = useForm&lt;ProfileValues&gt;({
6476
+ defaultValues: { fullName: &#39;Rafa Moro&#39;, title: &#39;&#39; }
6477
+ });
6478
+
6479
+ return (
6480
+ &lt;Form.Root methods={methods} aria-label=&quot;Profile&quot; className=&quot;w-[40rem]&quot;&gt;
6481
+ &lt;Field.Group&gt;
6482
+ &lt;FormText
6483
+ name=&quot;fullName&quot;
6484
+ label=&quot;Full name&quot;
6485
+ orientation=&quot;horizontal&quot;
6486
+ inputProps={{ className: &#39;w-72&#39; }}
6487
+ data-testid=&quot;short&quot;
6488
+ /&gt;
6489
+ &lt;FormText
6490
+ name=&quot;title&quot;
6491
+ label=&quot;Title&quot;
6492
+ description=&quot;Shown on your public profile.&quot;
6493
+ orientation=&quot;horizontal&quot;
6494
+ inputProps={{ className: &#39;w-72&#39;, placeholder: &#39;Software engineer&#39; }}
6495
+ data-testid=&quot;long&quot;
6496
+ /&gt;
6497
+ &lt;/Field.Group&gt;
6498
+ &lt;/Form.Root&gt;
6499
+ );
6500
+ };</code></pre>
6114
6501
  </div>
6115
6502
  <div class="note ok">
6116
6503
  <div class="row">
@@ -6121,7 +6508,25 @@ The `Input` is full-width by default, so a settings row gives it a width.
6121
6508
  <div class="hint">`disabled` disables the input and marks the field, and the value still
6122
6509
  reaches `onSubmit` — unlike react-hook-form&#39;s own `disabled` option, which
6123
6510
  drops it. Use `readOnly` for a value the user can copy but not edit.</div>
6124
- <pre><code>const Disabled = () =&gt; &lt;AccountForm /&gt;;</code></pre>
6511
+ <pre><code>const Disabled = () =&gt; {
6512
+ const methods = useForm&lt;AccountValues&gt;({
6513
+ defaultValues: { agencyId: &#39;AG-48213&#39;, email: &#39;ops@acme-insurance.com&#39; }
6514
+ });
6515
+
6516
+ return (
6517
+ &lt;Form.Root
6518
+ methods={methods}
6519
+ onSubmit={onSubmit}
6520
+ aria-label=&quot;Account&quot;
6521
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6522
+ &lt;Field.Group&gt;
6523
+ &lt;FormText name=&quot;agencyId&quot; label=&quot;Agency ID&quot; disabled /&gt;
6524
+ &lt;FormText name=&quot;email&quot; label=&quot;Email&quot; inputProps={{ type: &#39;email&#39; }} /&gt;
6525
+ &lt;/Field.Group&gt;
6526
+ &lt;Button type=&quot;submit&quot;&gt;Save&lt;/Button&gt;
6527
+ &lt;/Form.Root&gt;
6528
+ );
6529
+ };</code></pre>
6125
6530
  </div>
6126
6531
  <div class="note ok">
6127
6532
  <div class="row">
@@ -6132,7 +6537,26 @@ drops it. Use `readOnly` for a value the user can copy but not edit.</div>
6132
6537
  <div class="hint">react-hook-form&#39;s `ref` reaches the `&lt;input&gt;`, so `setFocus` and the focus
6133
6538
  on the first invalid field after submit both work with no ref written by
6134
6539
  hand.</div>
6135
- <pre><code>const SetFocus = () =&gt; &lt;SetFocusForm /&gt;;</code></pre>
6540
+ <pre><code>const SetFocus = () =&gt; {
6541
+ const methods = useForm&lt;AgencyValues &amp; PhoneValues&gt;({
6542
+ defaultValues: { agencyName: &#39;&#39;, phone: &#39;&#39; }
6543
+ });
6544
+
6545
+ return (
6546
+ &lt;Form.Root
6547
+ methods={methods}
6548
+ aria-label=&quot;Agency contact&quot;
6549
+ className=&quot;flex flex-col items-start gap-7&quot;&gt;
6550
+ &lt;Field.Group&gt;
6551
+ &lt;FormText name=&quot;agencyName&quot; label=&quot;Agency name&quot; /&gt;
6552
+ &lt;FormText name=&quot;phone&quot; label=&quot;Phone number&quot; inputProps={{ type: &#39;tel&#39; }} /&gt;
6553
+ &lt;/Field.Group&gt;
6554
+ &lt;Button type=&quot;button&quot; variant=&quot;secondary&quot; onClick={() =&gt; methods.setFocus(&#39;phone&#39;)}&gt;
6555
+ Focus phone
6556
+ &lt;/Button&gt;
6557
+ &lt;/Form.Root&gt;
6558
+ );
6559
+ };</code></pre>
6136
6560
  </div>
6137
6561
  </div>
6138
6562
 
@@ -7101,7 +7525,30 @@ open?: boolean</code></pre>
7101
7525
  </div>
7102
7526
 
7103
7527
  <div class="hint">Controlled with `open`/`onOpenChange` — the dominant pattern in both apps. Also closes on `Escape`.</div>
7104
- <pre><code>const Controlled = () =&gt; &lt;ControlledExample /&gt;;</code></pre>
7528
+ <pre><code>const Controlled = () =&gt; {
7529
+ const [isOpen, setIsOpen] = useState(false);
7530
+
7531
+ return (
7532
+ &lt;&gt;
7533
+ &lt;Button variant=&quot;secondary&quot; onClick={() =&gt; setIsOpen(true)}&gt;
7534
+ Open controlled
7535
+ &lt;/Button&gt;
7536
+ &lt;Modal.Root open={isOpen} onOpenChange={setIsOpen}&gt;
7537
+ &lt;Modal.Content&gt;
7538
+ &lt;Modal.Title&gt;Controlled modal&lt;/Modal.Title&gt;
7539
+ &lt;Modal.Body&gt;
7540
+ &lt;p&gt;The open state lives in the consumer.&lt;/p&gt;
7541
+ &lt;/Modal.Body&gt;
7542
+ &lt;Modal.Footer&gt;
7543
+ &lt;Button variant=&quot;primary&quot; onClick={() =&gt; setIsOpen(false)}&gt;
7544
+ Done
7545
+ &lt;/Button&gt;
7546
+ &lt;/Modal.Footer&gt;
7547
+ &lt;/Modal.Content&gt;
7548
+ &lt;/Modal.Root&gt;
7549
+ &lt;/&gt;
7550
+ );
7551
+ };</code></pre>
7105
7552
  </div>
7106
7553
  <div class="note ok">
7107
7554
  <div class="row">
@@ -8273,52 +8720,50 @@ keeps its padding.</div>
8273
8720
  </div>
8274
8721
  <div>Expandable rows with a detail panel</div>
8275
8722
  <div class="hint">`Table.ExpandButton` toggles a `Table.ExpandedRow` detail panel under its row.</div>
8276
- <pre><code>const ExpandableRows = args =&gt; {
8277
- const Demo = () =&gt; {
8278
- const [openId, setOpenId] = useState&lt;string | null&gt;(ROWS[0]!.id);
8279
- return (
8280
- &lt;Table.Root {...args}&gt;
8281
- &lt;Table.Head&gt;
8282
- &lt;Table.Row&gt;
8283
- &lt;Table.Header&gt;
8284
- &lt;span className=&quot;sr-only&quot;&gt;Expand&lt;/span&gt;
8285
- &lt;/Table.Header&gt;
8286
- &lt;Table.Header&gt;Name&lt;/Table.Header&gt;
8287
- &lt;Table.Header&gt;Email&lt;/Table.Header&gt;
8288
- &lt;Table.Header&gt;Role&lt;/Table.Header&gt;
8289
- &lt;/Table.Row&gt;
8290
- &lt;/Table.Head&gt;
8291
- &lt;Table.Body&gt;
8292
- {ROWS.slice(0, 4).map(row =&gt; (
8293
- &lt;Fragment key={row.id}&gt;
8294
- &lt;Table.Row&gt;
8295
- &lt;Table.Cell&gt;
8296
- &lt;Table.ExpandButton
8297
- isExpanded={openId === row.id}
8298
- toggleExpanded={() =&gt; setOpenId(openId === row.id ? null : row.id)}
8299
- aria-label={`Toggle ${row.name}`}
8300
- /&gt;
8723
+ <pre><code>const ExpandableRows = () =&gt; {
8724
+ const [openId, setOpenId] = useState&lt;string | null&gt;(ROWS[0]!.id);
8725
+
8726
+ return (
8727
+ &lt;Table.Root size=&quot;md&quot; embed={false}&gt;
8728
+ &lt;Table.Head&gt;
8729
+ &lt;Table.Row&gt;
8730
+ &lt;Table.Header&gt;
8731
+ &lt;span className=&quot;sr-only&quot;&gt;Expand&lt;/span&gt;
8732
+ &lt;/Table.Header&gt;
8733
+ &lt;Table.Header&gt;Name&lt;/Table.Header&gt;
8734
+ &lt;Table.Header&gt;Email&lt;/Table.Header&gt;
8735
+ &lt;Table.Header&gt;Role&lt;/Table.Header&gt;
8736
+ &lt;/Table.Row&gt;
8737
+ &lt;/Table.Head&gt;
8738
+ &lt;Table.Body&gt;
8739
+ {ROWS.slice(0, 4).map(row =&gt; (
8740
+ &lt;Fragment key={row.id}&gt;
8741
+ &lt;Table.Row&gt;
8742
+ &lt;Table.Cell&gt;
8743
+ &lt;Table.ExpandButton
8744
+ isExpanded={openId === row.id}
8745
+ toggleExpanded={() =&gt; setOpenId(openId === row.id ? null : row.id)}
8746
+ aria-label={`Toggle ${row.name}`}
8747
+ /&gt;
8748
+ &lt;/Table.Cell&gt;
8749
+ &lt;Table.Cell className=&quot;font-bold&quot;&gt;{row.name}&lt;/Table.Cell&gt;
8750
+ &lt;Table.Cell className=&quot;text-text-default-base-secondary&quot;&gt;{row.email}&lt;/Table.Cell&gt;
8751
+ &lt;Table.Cell&gt;{row.role}&lt;/Table.Cell&gt;
8752
+ &lt;/Table.Row&gt;
8753
+ {openId === row.id &amp;&amp; (
8754
+ &lt;Table.ExpandedRow&gt;
8755
+ &lt;Table.Cell colSpan={4}&gt;
8756
+ &lt;div className=&quot;py-2 text-text-default-base-secondary&quot;&gt;
8757
+ Extended details for {row.name} — {row.amount} lifetime value.
8758
+ &lt;/div&gt;
8301
8759
  &lt;/Table.Cell&gt;
8302
- &lt;Table.Cell className=&quot;font-bold&quot;&gt;{row.name}&lt;/Table.Cell&gt;
8303
- &lt;Table.Cell className=&quot;text-text-default-base-secondary&quot;&gt;{row.email}&lt;/Table.Cell&gt;
8304
- &lt;Table.Cell&gt;{row.role}&lt;/Table.Cell&gt;
8305
- &lt;/Table.Row&gt;
8306
- {openId === row.id &amp;&amp; (
8307
- &lt;Table.ExpandedRow&gt;
8308
- &lt;Table.Cell colSpan={4}&gt;
8309
- &lt;div className=&quot;py-2 text-text-default-base-secondary&quot;&gt;
8310
- Extended details for {row.name} — {row.amount} lifetime value.
8311
- &lt;/div&gt;
8312
- &lt;/Table.Cell&gt;
8313
- &lt;/Table.ExpandedRow&gt;
8314
- )}
8315
- &lt;/Fragment&gt;
8316
- ))}
8317
- &lt;/Table.Body&gt;
8318
- &lt;/Table.Root&gt;
8319
- );
8320
- };
8321
- return &lt;Demo /&gt;;
8760
+ &lt;/Table.ExpandedRow&gt;
8761
+ )}
8762
+ &lt;/Fragment&gt;
8763
+ ))}
8764
+ &lt;/Table.Body&gt;
8765
+ &lt;/Table.Root&gt;
8766
+ );
8322
8767
  };</code></pre>
8323
8768
  </div>
8324
8769
  </div>