themelia-ui 3.1.0 → 3.2.0
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/.agents/skills/themelia-ui/SKILL.md +38 -9
- package/.agents/skills/themelia-ui/manifest.json +23 -0
- package/.agents/skills/themelia-ui/references/api-compatibility.md +3 -2
- package/.agents/skills/themelia-ui/references/blocks.md +123 -0
- package/.agents/skills/themelia-ui/references/composition-ladder.md +8 -8
- package/.agents/skills/themelia-ui/references/composition.md +98 -1
- package/.agents/skills/themelia-ui/references/forms.md +23 -3
- package/.agents/skills/themelia-ui/references/framework-wiring.md +34 -6
- package/.agents/skills/themelia-ui/references/i18n.md +4 -2
- package/.agents/skills/themelia-ui/references/imports.md +1 -1
- package/.agents/skills/themelia-ui/references/installation.md +5 -4
- package/.agents/skills/themelia-ui/references/migration.md +22 -2
- package/.agents/skills/themelia-ui/references/provider-and-scoping.md +27 -9
- package/.agents/skills/themelia-ui/references/reference-workflows.md +43 -0
- package/.agents/skills/themelia-ui/references/structure-and-spacing.md +147 -0
- package/.agents/skills/themelia-ui/references/theming.md +6 -2
- package/.agents/skills/themelia-ui/references/troubleshooting.md +28 -5
- package/.agents/skills/themelia-ui/references/verification.md +58 -2
- package/CHANGELOG.md +25 -0
- package/README.md +3 -0
- package/dist/{alert-dialog-Bmo1YrTm.js → alert-dialog-Cg2HhuBe.js} +1 -1
- package/dist/base/alert-dialog.js +1 -1
- package/dist/base/cards.js +1 -1
- package/dist/base/choice-inputs.js +1 -1
- package/dist/base/combobox.js +1 -1
- package/dist/base/command.js +1 -1
- package/dist/base/date-pickers.js +1 -1
- package/dist/base/dialog.js +1 -1
- package/dist/base/display.js +1 -1
- package/dist/base/forms-numeric.js +1 -1
- package/dist/base/forms.js +1 -1
- package/dist/base/item.js +1 -1
- package/dist/base/navigation.js +1 -1
- package/dist/base/otp-input.js +1 -1
- package/dist/base/overlay.js +1 -1
- package/dist/base/popover-menu.js +1 -1
- package/dist/base/repeaters.js +1 -1
- package/dist/base/sheet.js +1 -1
- package/dist/base/sidebar.js +1 -1
- package/dist/base/skeleton.js +1 -1
- package/dist/base/structure.js +1 -1
- package/dist/base/timeline.js +1 -1
- package/dist/base/upload.js +1 -1
- package/dist/base/value-inputs.js +1 -1
- package/dist/blocks/admin/access.js +2 -2
- package/dist/blocks/admin/commerce.js +9 -9
- package/dist/blocks/analytics.js +3 -3
- package/dist/blocks/onboarding.js +1 -1
- package/dist/blocks/timelines.js +1 -1
- package/dist/{cards-DdiICdMM.js → cards-Ddhz43Bz.js} +2 -2
- package/dist/{choice-inputs-CCw-0gD6.js → choice-inputs-NwRxvmZN.js} +2 -2
- package/dist/combobox-519ubijc.js +292 -0
- package/dist/{command-CnB_IZSo.js → command-JjApYFds.js} +2 -2
- package/dist/{comments-C6Y_ddMt.js → comments-CCQ2QRaU.js} +4 -4
- package/dist/components/base/display/icon-badge.d.ts +2 -2
- package/dist/components/base/forms/form-field.d.ts +2 -2
- package/dist/components/base/structure/bleed.d.ts +1 -0
- package/dist/components/base/structure/grid.d.ts +8 -2
- package/dist/components/base/structure/split.d.ts +1 -0
- package/dist/components/features/combobox/fetching-combobox.d.ts +2 -1
- package/dist/components/features/combobox/resource-combobox.d.ts +3 -0
- package/dist/components/features/combobox/use-suggestions.d.ts +7 -0
- package/dist/css/structure.css +1 -1
- package/dist/{date-pickers-C2HgjpFU.js → date-pickers-BwCwKIVM.js} +1 -1
- package/dist/{dialog-0nRFg5Zs.js → dialog-CAnwRNrm.js} +1 -1
- package/dist/{display-BGN834xV.js → display-a7AoX_u7.js} +1 -1
- package/dist/features/actions.js +1 -1
- package/dist/features/activities.js +3 -3
- package/dist/features/ai-chat.js +2 -2
- package/dist/features/combobox.js +285 -273
- package/dist/features/comments.js +1 -1
- package/dist/features/data-view.js +5 -5
- package/dist/features/event-calendar.js +3 -3
- package/dist/features/filters.js +1 -1
- package/dist/features/global-search.js +4 -4
- package/dist/features/map.js +2 -2
- package/dist/features/media-library.js +8 -8
- package/dist/features/mentions.js +1 -1
- package/dist/features/overlays.js +1 -1
- package/dist/features/products.js +9 -9
- package/dist/features/resource-assignment.js +3 -3
- package/dist/features/resource.js +3 -3
- package/dist/features/schema-form.js +6 -6
- package/dist/features/sync.js +2 -2
- package/dist/features/table.js +1 -1
- package/dist/features/theme-tweaker.js +10 -10
- package/dist/{filters-geLuSlFL.js → filters-BhsTkWpZ.js} +9 -9
- package/dist/{forms-NxbJK4Mh.js → forms-C0WeNPFF.js} +1 -1
- package/dist/{forms-numeric-C6NMdj6G.js → forms-numeric-IwQ1ltNf.js} +1 -1
- package/dist/hooks/use-native-dialog.d.ts +1 -1
- package/dist/{item-CuUJyETl.js → item-M2pmpWXh.js} +1 -1
- package/dist/layout/app-shell.js +1 -1
- package/dist/layout/auth.js +1 -1
- package/dist/layout/header.js +3 -3
- package/dist/layout/navigation.js +1 -1
- package/dist/layout/page.js +2 -2
- package/dist/layout/settings.js +2 -2
- package/dist/layout/sidebar.js +10 -6
- package/dist/layout/workspace.js +3 -3
- package/dist/lib/ui-provider/ui-scope.d.ts +1 -1
- package/dist/{mentions-oeYNv_j1.js → mentions-BOxbigwp.js} +2 -2
- package/dist/{navigation-BTX5Ti0Z.js → navigation-Bgu7Vc0M.js} +2 -2
- package/dist/{navigation-WYKjDhZq.js → navigation-DiYACqpE.js} +2 -2
- package/dist/{overlay-C8RAgi2_.js → overlay-BuQOl8aP.js} +5 -5
- package/dist/{overlays-9BxwFTou.js → overlays-1sULyytH.js} +5 -5
- package/dist/{popover-menu-DcLeiw9r.js → popover-menu-QXgQeWeG.js} +1 -1
- package/dist/{repeaters-DfptvbGA.js → repeaters-CeH20dvr.js} +4 -4
- package/dist/{sheet-D-DVZH_b.js → sheet-CJ1-OAla.js} +1 -1
- package/dist/{sidebar-DXQOy_Cb.js → sidebar-D8uGqxio.js} +4 -4
- package/dist/{skeleton-CubvHel9.js → skeleton-BTKNKTdj.js} +1 -1
- package/dist/{structure-B9Y5NkP_.js → structure-CynEZ-OY.js} +1 -2
- package/dist/style.css +1 -1
- package/dist/styles/TOKENS.md +4 -3
- package/dist/{table-DT_20ity.js → table-B7dtIFUx.js} +4 -4
- package/dist/{timeline-BRZbSdhf.js → timeline-BR_jNgvz.js} +1 -1
- package/dist/{timelines-DczZKW5p.js → timelines-DXG1BfIN.js} +1 -1
- package/dist/{upload-C_6mlbfl.js → upload-CYuoJykZ.js} +1 -1
- package/dist/{value-inputs-CnO6RZeT.js → value-inputs-ng8aVJJZ.js} +4 -4
- package/docs/README.md +11 -1
- package/docs/build/browser-composition-fixture.ts +28 -0
- package/docs/build/recipes.md +398 -1
- package/docs/generated/component-index.json +1993 -862
- package/docs/generated/components/base--display.md +2 -2
- package/docs/generated/components/base--forms.md +72 -1
- package/docs/generated/components/base--structure.md +56 -5
- package/docs/generated/components/base--typography.md +38 -0
- package/docs/generated/components/blocks--admin--access.md +10 -0
- package/docs/generated/components/blocks--analytics.md +44 -0
- package/docs/generated/components/features--combobox.md +67 -0
- package/docs/generated/components/features--data-view.md +59 -3
- package/docs/generated/components/features--filters.md +10 -0
- package/docs/generated/components/features--media-library.md +68 -0
- package/docs/generated/components/features--overlays.md +54 -0
- package/docs/generated/components/features--rich-text-editor.md +38 -0
- package/docs/generated/components/layout--app-shell.md +31 -0
- package/docs/generated/components/layout--containers.md +40 -0
- package/docs/generated/components/layout--page.md +30 -1
- package/docs/generated/components/layout--sidebar.md +41 -0
- package/docs/generated/components/ui-provider.md +1 -1
- package/docs/generated/composition-ladder.md +8 -8
- package/docs/generated/consumer-contract.json +5736 -0
- package/docs/generated/imports.md +1 -1
- package/docs/generated/recipes.json +171 -1
- package/docs/generated/workflows/communications-overview/analytics/communications-reference.tsx +19 -0
- package/docs/generated/workflows/index.json +691 -0
- package/docs/generated/workflows/media-picking/media-library/media-picking-reference.tsx +63 -0
- package/docs/generated/workflows/media-picking/media-library/reference-data.ts +3 -0
- package/docs/generated/workflows/multi-section-form/form-workflow/multi-section-reference.tsx +46 -0
- package/docs/generated/workflows/nested-action-flow/action-overlays/nested-action-reference.tsx +39 -0
- package/docs/generated/workflows/record-detail/page-layout/record-detail-reference.tsx +24 -0
- package/docs/generated/workflows/resource-index/data-view/resource-index-reference.tsx +51 -0
- package/docs/generated/workflows/scoped-resource-selection/combobox/scoped-resource-reference.tsx +50 -0
- package/docs/generated/workflows/website-content-editor/media-library/reference-data.ts +3 -0
- package/docs/generated/workflows/website-content-editor/rich-text-editor/website-content-reference.tsx +33 -0
- package/docs/generated/workflows/workspace-navigation/app-shell/workspace-navigation-reference.tsx +26 -0
- package/docs/learn/api-compatibility.md +3 -2
- package/docs/learn/blocks.md +123 -0
- package/docs/learn/composition.md +98 -1
- package/docs/learn/forms.md +23 -3
- package/docs/learn/framework-wiring.md +34 -6
- package/docs/learn/i18n.md +4 -2
- package/docs/learn/installation.md +5 -4
- package/docs/learn/migration.md +22 -2
- package/docs/learn/provider-and-scoping.md +27 -9
- package/docs/learn/reference-workflows.md +43 -0
- package/docs/learn/structure-and-spacing.md +147 -0
- package/docs/learn/theming.md +6 -2
- package/docs/learn/troubleshooting.md +28 -5
- package/docs/learn/verification.md +58 -2
- package/package.json +4 -1
- package/scripts/consumer/codemod.mjs +3 -24
- package/scripts/consumer/contract-reader.mjs +42 -0
- package/scripts/consumer/css-scan.mjs +51 -0
- package/scripts/consumer/diagnose.mjs +136 -0
- package/scripts/consumer/find-component.mjs +28 -13
- package/scripts/consumer/install-skill.mjs +93 -11
- package/src/styles/TOKENS.md +4 -3
- package/dist/combobox-Byu8N-Oe.js +0 -274
|
@@ -207,8 +207,8 @@ MetadataListStrings
|
|
|
207
207
|
|
|
208
208
|
Kind: callable.
|
|
209
209
|
|
|
210
|
-
A glyph in a tinted medallion.
|
|
211
|
-
|
|
210
|
+
A glyph in a tinted medallion. `tone` coordinates fill and glyph; `shape` chooses
|
|
211
|
+
rounded or circular geometry. Internal sizing variables are not consumer overrides.
|
|
212
212
|
|
|
213
213
|
```text
|
|
214
214
|
({ icon, children, tone, shape, solid, bordered, className, ...props }: IconBadgeProps) => import("react").JSX.Element
|
|
@@ -14,7 +14,7 @@ Assembling a form's visible structure — label, control, support text, sections
|
|
|
14
14
|
## Import
|
|
15
15
|
|
|
16
16
|
```tsx fragment — import-only example
|
|
17
|
-
import {
|
|
17
|
+
import { FormField, FormSection } from "themelia-ui/base/forms"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Composition
|
|
@@ -135,6 +135,16 @@ Several fields sharing one label and one supporting line — a date range, a nam
|
|
|
135
135
|
two. A real `<fieldset>` and `<legend>`, so the legend is announced when focus enters the
|
|
136
136
|
group.
|
|
137
137
|
|
|
138
|
+
**Use when:** Related controls sharing an announced fieldset legend.
|
|
139
|
+
|
|
140
|
+
**Avoid when:** A whole independently titled subject surface.
|
|
141
|
+
|
|
142
|
+
**Compose:** Use one legend for the group and distinct FormField labels for its controls.
|
|
143
|
+
|
|
144
|
+
**Capabilities:** semantic fieldset legend; related editable controls.
|
|
145
|
+
|
|
146
|
+
**Alternatives:** FormSection.
|
|
147
|
+
|
|
138
148
|
```text
|
|
139
149
|
({ legend, description, className, children, ...props }: FieldGroupProps) => React.JSX.Element
|
|
140
150
|
```
|
|
@@ -230,6 +240,16 @@ Kind: callable.
|
|
|
230
240
|
|
|
231
241
|
A titled group of fields, on a card's surface.
|
|
232
242
|
|
|
243
|
+
**Use when:** Editable form sections, including groups inside an existing Card.
|
|
244
|
+
|
|
245
|
+
**Avoid when:** Read-only facts or a semantic fieldset with one shared legend.
|
|
246
|
+
|
|
247
|
+
**Compose:** Use flat inside a surface; flat retains Card region insets. Let its title name the section and FormField name each field.
|
|
248
|
+
|
|
249
|
+
**Capabilities:** editable form sections; existing surface grouping.
|
|
250
|
+
|
|
251
|
+
**Alternatives:** ContentBlock, FieldGroup.
|
|
252
|
+
|
|
233
253
|
```text
|
|
234
254
|
({ title, description, actions, children, footer, surface, className, }: FormSectionProps) => import("react").JSX.Element
|
|
235
255
|
```
|
|
@@ -391,6 +411,57 @@ export default function FieldGroupExample() {
|
|
|
391
411
|
}
|
|
392
412
|
```
|
|
393
413
|
|
|
414
|
+
### Multi-section form reference
|
|
415
|
+
|
|
416
|
+
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
417
|
+
import { useRef, useState } from "react"
|
|
418
|
+
import { Card } from "themelia-ui/base/cards"
|
|
419
|
+
import { ContentBlock } from "themelia-ui/base/display"
|
|
420
|
+
import { FieldGroup, FormField, FormSection, FormActionsBar, SubmitStateButton } from "themelia-ui/base/forms"
|
|
421
|
+
import { Stack } from "themelia-ui/base/structure"
|
|
422
|
+
import { Input } from "themelia-ui/base/text-inputs"
|
|
423
|
+
import { Text } from "themelia-ui/base/typography"
|
|
424
|
+
import { useFormFieldBinding, useStateFormControl, type FormControl } from "themelia-ui/forms"
|
|
425
|
+
|
|
426
|
+
type Draft = { firstName: string; lastName: string; company: string }
|
|
427
|
+
function BoundField({ control, name, label, error }: { control: FormControl<Draft>; name: keyof Draft; label: string; error?: string }) {
|
|
428
|
+
const field = useFormFieldBinding<string, Draft>({ control, name })
|
|
429
|
+
return <FormField label={label} error={error} hint={name === "company" ? "The name shown on invoices." : undefined}><Input required value={field.value ?? ""} onChange={event => field.onValueChange(event.target.value)} onBlur={field.onBlur} /></FormField>
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export default function MultiSectionReference() {
|
|
433
|
+
const control = useStateFormControl<Draft>({ firstName: "Alex", lastName: "Example", company: "Example team" })
|
|
434
|
+
const [pending, setPending] = useState(false)
|
|
435
|
+
const [error, setError] = useState<string>()
|
|
436
|
+
const [rootError, setRootError] = useState<string>()
|
|
437
|
+
const [saved, setSaved] = useState(0)
|
|
438
|
+
const running = useRef(false)
|
|
439
|
+
return <Stack>
|
|
440
|
+
<Card title="Account details" description="One outer surface, editable sections with retained region insets.">
|
|
441
|
+
<form onSubmit={async event => {
|
|
442
|
+
event.preventDefault(); if (running.current) return
|
|
443
|
+
running.current = true; setPending(true); setError(undefined); setRootError(undefined)
|
|
444
|
+
try {
|
|
445
|
+
await new Promise(resolve => setTimeout(resolve, 250))
|
|
446
|
+
if (control.values.company === "offline") throw new Error("Request failed. Your values remain here.")
|
|
447
|
+
if (control.values.company.length < 3) { setError("Company must have at least three characters."); return }
|
|
448
|
+
setSaved(count => count + 1)
|
|
449
|
+
} catch (failure) { setRootError((failure as Error).message) }
|
|
450
|
+
finally { running.current = false; setPending(false) }
|
|
451
|
+
}}>
|
|
452
|
+
<Stack gap="none">
|
|
453
|
+
{rootError && <Text role="alert" type="error">{rootError}</Text>}
|
|
454
|
+
<FormSection surface="flat" title="Contact"><FieldGroup legend="Legal name" disabled={pending}><BoundField control={control} name="firstName" label="First name" /><BoundField control={control} name="lastName" label="Last name" /></FieldGroup></FormSection>
|
|
455
|
+
<FormSection surface="flat" title="Billing"><fieldset disabled={pending} style={{ border: 0, padding: 0, margin: 0 }}><BoundField control={control} name="company" label="Company" error={error} /></fieldset></FormSection>
|
|
456
|
+
<FormActionsBar leading={<Text role="status">Successful submissions {saved}</Text>}><SubmitStateButton type="submit" state={pending ? "submitting" : "idle"} strings={{ idle: "Save account" }} /></FormActionsBar>
|
|
457
|
+
</Stack>
|
|
458
|
+
</form>
|
|
459
|
+
</Card>
|
|
460
|
+
<Card title="Preferences" description="A sibling subject owns its own surface."><ContentBlock surface="plain" title="Delivery instructions"><Text>Use ContentBlock for supporting prose; FieldGroup supplies fieldset semantics for related controls.</Text></ContentBlock></Card>
|
|
461
|
+
</Stack>
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
394
465
|
### FormSection
|
|
395
466
|
|
|
396
467
|
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
@@ -30,6 +30,16 @@ outside the package and arrives through the public props, callbacks, slots, or a
|
|
|
30
30
|
|
|
31
31
|
Kind: const.
|
|
32
32
|
|
|
33
|
+
**Use when:** Equal tracks should be added as minimum-width columns fit the available panel.
|
|
34
|
+
|
|
35
|
+
**Avoid when:** The design requires an explicit column count or a fixed summary rail.
|
|
36
|
+
|
|
37
|
+
**Compose:** Set minColumnWidth and shared gaps. Column count follows available width; responsive gap and alignment use viewport steps.
|
|
38
|
+
|
|
39
|
+
**Capabilities:** auto fit columns; minimum column width; responsive panel grid.
|
|
40
|
+
|
|
41
|
+
**Alternatives:** Grid, Split.
|
|
42
|
+
|
|
33
43
|
```text
|
|
34
44
|
React.ForwardRefExoticComponent<Omit<AdaptiveGridProps, "ref"> & React.RefAttributes<HTMLDivElement>>
|
|
35
45
|
```
|
|
@@ -58,6 +68,16 @@ Extends: `React.ComponentProps<"div">`.
|
|
|
58
68
|
|
|
59
69
|
Kind: const.
|
|
60
70
|
|
|
71
|
+
**Use when:** Intentional media or a rule must escape a known parent inset.
|
|
72
|
+
|
|
73
|
+
**Avoid when:** Removing duplicate wrapper padding or hiding accidental overflow.
|
|
74
|
+
|
|
75
|
+
**Compose:** Match amount to the actual inset. Shipped padding and gap pairs match; check custom themes and focus visibility.
|
|
76
|
+
|
|
77
|
+
**Capabilities:** edge to edge inset escape; inline block negative margins.
|
|
78
|
+
|
|
79
|
+
**Alternatives:** ContentBlock.
|
|
80
|
+
|
|
61
81
|
```text
|
|
62
82
|
React.ForwardRefExoticComponent<Omit<BleedProps, "ref"> & React.RefAttributes<HTMLDivElement>>
|
|
63
83
|
```
|
|
@@ -81,7 +101,7 @@ Extends: `React.ComponentProps<"div">`.
|
|
|
81
101
|
| member | required | type | description / documented default |
|
|
82
102
|
| --- | :-: | --- | --- |
|
|
83
103
|
| `amount` | no | `ResponsiveValue<StructureGap>` | How far to escape: one of the two gaps, which equal the two paddings. Match it to the<br>padding being cancelled — a surface padded by `--padding-sm` bleeds `sm` — so the two<br>move together under a density change. @default "none" |
|
|
84
|
-
| `axis` | no | `BleedAxis` | Which way to escape; `both` covers every direction. Sideways is the common case: an<br>image bleeds across and keeps its vertical rhythm. |
|
|
104
|
+
| `axis` | no | `BleedAxis` | Which way to escape; `both` covers every direction. Sideways is the common case: an<br>image bleeds across and keeps its vertical rhythm. @default "inline" |
|
|
85
105
|
|
|
86
106
|
### `CssLength`
|
|
87
107
|
|
|
@@ -97,6 +117,16 @@ A CSS length: a number with a unit or a percentage, or a math or `var()` express
|
|
|
97
117
|
|
|
98
118
|
Kind: const.
|
|
99
119
|
|
|
120
|
+
**Use when:** Passive siblings need an explicit responsive column count.
|
|
121
|
+
|
|
122
|
+
**Avoid when:** The column count should follow how many minimum-width tracks fit, or a feature already arranges its children.
|
|
123
|
+
|
|
124
|
+
**Compose:** Grid owns gaps, not child insets. It uses nearest-container thresholds when containment exists, otherwise viewport thresholds.
|
|
125
|
+
|
|
126
|
+
**Capabilities:** responsive columns; grid row column gaps; nearest container width.
|
|
127
|
+
|
|
128
|
+
**Alternatives:** AdaptiveGrid, Stack.
|
|
129
|
+
|
|
100
130
|
```text
|
|
101
131
|
React.ForwardRefExoticComponent<Omit<GridProps, "ref"> & React.RefAttributes<HTMLDivElement>>
|
|
102
132
|
```
|
|
@@ -105,6 +135,16 @@ React.ForwardRefExoticComponent<Omit<GridProps, "ref"> & React.RefAttributes<HTM
|
|
|
105
135
|
|
|
106
136
|
Kind: const.
|
|
107
137
|
|
|
138
|
+
**Use when:** A direct Grid child needs a numeric span or a full row.
|
|
139
|
+
|
|
140
|
+
**Avoid when:** The parent is not a Grid, or a span would exceed its current column count.
|
|
141
|
+
|
|
142
|
+
**Compose:** Coordinate numeric spans with the parent columns. GridCell spans use viewport steps even when Grid uses container steps.
|
|
143
|
+
|
|
144
|
+
**Capabilities:** responsive cell span; full row span.
|
|
145
|
+
|
|
146
|
+
**Alternatives:** Stack.
|
|
147
|
+
|
|
108
148
|
```text
|
|
109
149
|
React.ForwardRefExoticComponent<Omit<GridCellProps, "ref"> & React.RefAttributes<HTMLDivElement>>
|
|
110
150
|
```
|
|
@@ -117,7 +157,7 @@ Extends: `React.ComponentProps<"div">`.
|
|
|
117
157
|
|
|
118
158
|
| member | required | type | description / documented default |
|
|
119
159
|
| --- | :-: | --- | --- |
|
|
120
|
-
| `span` | no | `ResponsiveValue<GridSpan>` | Columns occupied. `full` spans whatever the grid currently has. |
|
|
160
|
+
| `span` | no | `ResponsiveValue<GridSpan>` | Columns occupied. `full` spans whatever the grid currently has, including in a<br>responsive object. Span breakpoints follow the viewport, not Grid's container. |
|
|
121
161
|
|
|
122
162
|
### `GridColumns`
|
|
123
163
|
|
|
@@ -135,7 +175,7 @@ Extends: `React.ComponentProps<"div">`.
|
|
|
135
175
|
|
|
136
176
|
| member | required | type | description / documented default |
|
|
137
177
|
| --- | :-: | --- | --- |
|
|
138
|
-
| `columns` | no | `ResponsiveValue<GridColumns>` | Defaults to one column, then two from the md
|
|
178
|
+
| `columns` | no | `ResponsiveValue<GridColumns>` | Defaults to one column, then two from md. Uses the nearest inline-size query<br>container when present (md = 32rem), otherwise the viewport (md = 48rem). |
|
|
139
179
|
| `gap` | no | `ResponsiveValue<StructureGap>` | |
|
|
140
180
|
| `rowGap` | no | `ResponsiveValue<StructureGap>` | |
|
|
141
181
|
| `columnGap` | no | `ResponsiveValue<StructureGap>` | |
|
|
@@ -164,6 +204,16 @@ T | Partial<Record<Breakpoint, T>>
|
|
|
164
204
|
|
|
165
205
|
Kind: const.
|
|
166
206
|
|
|
207
|
+
**Use when:** A fluid main region sits beside a constrained rail that collapses at a chosen viewport step.
|
|
208
|
+
|
|
209
|
+
**Avoid when:** Needing equal tracks or the standard semantic main/aside layout.
|
|
210
|
+
|
|
211
|
+
**Compose:** Supply main first and side second. side changes visual placement, not DOM order. Width is a rail maximum, with collapseBelow controlling stacking.
|
|
212
|
+
|
|
213
|
+
**Capabilities:** fluid main fixed rail; responsive two columns; collapse breakpoint.
|
|
214
|
+
|
|
215
|
+
**Alternatives:** TwoColumnLayout, Grid.
|
|
216
|
+
|
|
167
217
|
```text
|
|
168
218
|
React.ForwardRefExoticComponent<Omit<SplitProps, "ref"> & React.RefAttributes<HTMLDivElement>>
|
|
169
219
|
```
|
|
@@ -179,7 +229,7 @@ Extends: `React.ComponentProps<"div">`.
|
|
|
179
229
|
| `side` | no | `SplitSide` | Which visual column is the fixed one. The DOM order never changes with it: the first<br>child is the main content and the second the side on both settings, so a reader<br>tabbing through reaches the content first either way. |
|
|
180
230
|
| `sideWidth` | no | `ResponsiveValue<StructureWidth \| CssLength>` | The fixed column's width — a step or any CSS length. A maximum rather than an exact<br>size, so a narrow viewport shrinks the rail instead of squeezing the content beside it<br>to nothing. @default "18rem" |
|
|
181
231
|
| `gap` | no | `ResponsiveValue<StructureGap>` | Space between the two columns. @default "default" |
|
|
182
|
-
| `collapseBelow` | no | `"sm" \| "md" \| "lg" \| "xl" \| "never"` | Below this breakpoint the two columns become one. `never` keeps them side by side at<br>every width. |
|
|
232
|
+
| `collapseBelow` | no | `"sm" \| "md" \| "lg" \| "xl" \| "never"` | Below this breakpoint the two columns become one. `never` keeps them side by side at<br>every width. @default "md" |
|
|
183
233
|
|
|
184
234
|
### `SplitSide`
|
|
185
235
|
|
|
@@ -199,7 +249,7 @@ Kind: const.
|
|
|
199
249
|
|
|
200
250
|
**Avoid when:** An existing component already owns the layout.
|
|
201
251
|
|
|
202
|
-
**Compose:** Use
|
|
252
|
+
**Compose:** Use gap, direction and alignment props; Stack adds no inset. Let child surfaces own padding.
|
|
203
253
|
|
|
204
254
|
**Capabilities:** vertical horizontal gap spacing; responsive arrangement.
|
|
205
255
|
|
|
@@ -751,6 +801,7 @@ export default function GridExample() {
|
|
|
751
801
|
<GridCell><Box>three</Box></GridCell>
|
|
752
802
|
<GridCell span={{ base: 1, md: 2 }}><Box>span 2 from md</Box></GridCell>
|
|
753
803
|
<GridCell><Box>four</Box></GridCell>
|
|
804
|
+
<GridCell span={{ base: 1, md: "full", xl: 2 }}><Box>responsive full row</Box></GridCell>
|
|
754
805
|
</Grid>
|
|
755
806
|
)
|
|
756
807
|
}
|
|
@@ -427,6 +427,44 @@ export default function RolesBlueprint() {
|
|
|
427
427
|
}
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
+
### Website content reference
|
|
431
|
+
|
|
432
|
+
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
433
|
+
import { useState } from "react"
|
|
434
|
+
import { Button } from "themelia-ui/base/buttons"
|
|
435
|
+
import { Card } from "themelia-ui/base/cards"
|
|
436
|
+
import { PillRadioGroup } from "themelia-ui/base/choice-inputs"
|
|
437
|
+
import { FormField } from "themelia-ui/base/forms"
|
|
438
|
+
import { Stack } from "themelia-ui/base/structure"
|
|
439
|
+
import { Input } from "themelia-ui/base/text-inputs"
|
|
440
|
+
import { RichText, Text } from "themelia-ui/base/typography"
|
|
441
|
+
import { MediaLibraryDialog, type MediaLibraryItem } from "themelia-ui/features/media-library"
|
|
442
|
+
import { RichTextEditor } from "themelia-ui/features/rich-text-editor"
|
|
443
|
+
import { SAMPLE_MEDIA } from "../media-library/reference-data"
|
|
444
|
+
|
|
445
|
+
export default function WebsiteContentReference() {
|
|
446
|
+
const [locale, setLocale] = useState("en")
|
|
447
|
+
const [drafts, setDrafts] = useState<Record<string, string>>({ en: "<p>Welcome to our workspace.</p>", ar: "<p>مرحباً بكم</p>" })
|
|
448
|
+
const [mediaOpen, setMediaOpen] = useState(false)
|
|
449
|
+
const [image, setImage] = useState<MediaLibraryItem>()
|
|
450
|
+
const [alt, setAlt] = useState("")
|
|
451
|
+
const [failed, setFailed] = useState(true)
|
|
452
|
+
const [pending, setPending] = useState(false)
|
|
453
|
+
const [message, setMessage] = useState("Drafts are local until saved")
|
|
454
|
+
return <Card title="Website content" description="Each locale owns a retained draft. Preview sanitizes HTML; the application also validates and sanitizes persisted content.">
|
|
455
|
+
<Stack>
|
|
456
|
+
<PillRadioGroup aria-label="Content locale" name="reference-content-locale" value={locale} onValueChange={value => value && setLocale(value)} options={[{ value: "en", label: "English" }, { value: "ar", label: "العربية" }]} />
|
|
457
|
+
<div dir={locale === "ar" ? "rtl" : "ltr"}><RichTextEditor value={drafts[locale] ?? ""} onValueChange={html => setDrafts(current => ({ ...current, [locale]: html }))} /></div>
|
|
458
|
+
<MediaLibraryDialog open={mediaOpen} trigger={<Button tone="neutral" appearance="outline">Choose hero media</Button>} onOpenChange={setMediaOpen} items={SAMPLE_MEDIA} selectionMode="single" confirmOnSelect allowUpload={false} onConfirm={items => { const item = items[0]; setImage(item); setAlt(item?.alt ?? ""); setMediaOpen(false) }} />
|
|
459
|
+
{image && <FormField label="Hero image alternative text" hint={image.name}><Input value={alt} onChange={event => setAlt(event.target.value)} /></FormField>}
|
|
460
|
+
<Stack direction="horizontal" wrap gap="sm"><Button disabled={pending} onClick={async () => { if (pending) return; setPending(true); await new Promise(resolve => setTimeout(resolve, 250)); setMessage(failed ? "Save failed. All locale drafts and image text remain here." : "Mock website save completed"); setPending(false) }}>Save website content</Button><Button tone="neutral" appearance="outline" onClick={() => setFailed(value => !value)}>Toggle save failure</Button></Stack>
|
|
461
|
+
<Text role="status">{message}</Text>
|
|
462
|
+
<RichText html={drafts[locale] ?? ""} />
|
|
463
|
+
</Stack>
|
|
464
|
+
</Card>
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
430
468
|
### The editor and what comes out
|
|
431
469
|
|
|
432
470
|
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
@@ -118,6 +118,16 @@ Kind: interface.
|
|
|
118
118
|
|
|
119
119
|
Kind: callable.
|
|
120
120
|
|
|
121
|
+
**Use when:** A read-only role permission summary grouped by product area.
|
|
122
|
+
|
|
123
|
+
**Avoid when:** Editing permission values or enforcing authorization.
|
|
124
|
+
|
|
125
|
+
**Compose:** Supply roleName and grouped permission records. The app computes authorization and owns the edit callback; the block supplies visible and accessible state presentation.
|
|
126
|
+
|
|
127
|
+
**Capabilities:** role permissions; permission summary; granted denied access facts.
|
|
128
|
+
|
|
129
|
+
**Alternatives:** Table, FormSection.
|
|
130
|
+
|
|
121
131
|
```text
|
|
122
132
|
({ roleName, description, memberCount, groups, onEdit, strings, className, ...props }: RolePermissionsProps) => import("react").JSX.Element
|
|
123
133
|
```
|
|
@@ -161,6 +161,16 @@ Kind: callable.
|
|
|
161
161
|
|
|
162
162
|
Kind: callable.
|
|
163
163
|
|
|
164
|
+
**Use when:** One quantitative measure needs value hierarchy and optional comparison, trend or progress.
|
|
165
|
+
|
|
166
|
+
**Avoid when:** An ordinary fact or a group that MetricGrid already arranges.
|
|
167
|
+
|
|
168
|
+
**Compose:** Read MetricProps for state and surface variants; they are not shared by every analytics export. The analytics module requires recharts.
|
|
169
|
+
|
|
170
|
+
**Capabilities:** single quantitative measure; metric comparison trend.
|
|
171
|
+
|
|
172
|
+
**Alternatives:** MetricGrid, MetadataList.
|
|
173
|
+
|
|
164
174
|
```text
|
|
165
175
|
({ data, variant, tone, showSparkline, showChange, showIcon, loading, error, strings, progress, className, ...props }: MetricProps) => import("react").JSX.Element
|
|
166
176
|
```
|
|
@@ -318,6 +328,16 @@ Four pairs off the categorical ramp. Named for the look, not for a meaning.
|
|
|
318
328
|
|
|
319
329
|
Kind: callable.
|
|
320
330
|
|
|
331
|
+
**Use when:** Several quantitative measures need responsive tiles with comparison or trend.
|
|
332
|
+
|
|
333
|
+
**Avoid when:** Ordinary label/value facts, identifiers or prose summaries.
|
|
334
|
+
|
|
335
|
+
**Compose:** Pass MetricData and the period context. Use null for an unavailable quantity, zero for measured zero. The module requires recharts and already owns tile arrangement.
|
|
336
|
+
|
|
337
|
+
**Capabilities:** quantitative metrics; dashboard figures; responsive metric tiles.
|
|
338
|
+
|
|
339
|
+
**Alternatives:** Metric, MetadataList.
|
|
340
|
+
|
|
321
341
|
```text
|
|
322
342
|
({ metrics, variant, tone, columns, showSparklines, showChanges, showIcons, loading, className, ...props }: MetricGridProps) => import("react").JSX.Element
|
|
323
343
|
```
|
|
@@ -569,6 +589,30 @@ Kind: interface.
|
|
|
569
589
|
|
|
570
590
|
Preview route: Analytics — `/analytics`
|
|
571
591
|
|
|
592
|
+
### Communications overview reference
|
|
593
|
+
|
|
594
|
+
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
595
|
+
import { useState } from "react"
|
|
596
|
+
import { Button } from "themelia-ui/base/buttons"
|
|
597
|
+
import { Card } from "themelia-ui/base/cards"
|
|
598
|
+
import { MetadataList } from "themelia-ui/base/display"
|
|
599
|
+
import { Stack } from "themelia-ui/base/structure"
|
|
600
|
+
import { Text } from "themelia-ui/base/typography"
|
|
601
|
+
import { MetricGrid } from "themelia-ui/blocks/analytics"
|
|
602
|
+
|
|
603
|
+
export default function CommunicationsReference() {
|
|
604
|
+
const [retried, setRetried] = useState(false)
|
|
605
|
+
return <Stack>
|
|
606
|
+
<MetricGrid metrics={[{ id: "delivered", label: "Messages delivered during the selected reporting period", value: 128, valueType: "number" }, { id: "retry", label: "Messages awaiting retry", value: retried ? 0 : 3, valueType: "number" }]} variant="card" />
|
|
607
|
+
<Card title="Latest communication" headerAction={<Button disabled={retried} onClick={() => setRetried(true)}>Retry delivery</Button>}>
|
|
608
|
+
<MetadataList items={[{ id: "subject", label: "Subject", value: "Workspace invitation" }, { id: "status", label: "Delivery state", value: retried ? "Delivered" : "Retry available" }, { id: "recipient", label: "Recipient", value: "member@example.test" }, { id: "time", label: "Last attempt", value: { kind: "datetime", value: "2026-09-01T09:30:00Z" } }, { id: "opened", label: "Opened", value: "Unknown; tracking unavailable" }]} />
|
|
609
|
+
<Text>The application owns delivery policy and permission checks. Metrics describe quantities; this subject and its delivery facts stay in a record surface.</Text>
|
|
610
|
+
</Card>
|
|
611
|
+
<Text role="status">{retried ? "Mock retry completed" : "Mock delivery is waiting"}</Text>
|
|
612
|
+
</Stack>
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
572
616
|
### Metric bar
|
|
573
617
|
|
|
574
618
|
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
@@ -228,6 +228,16 @@ Browsable from the first keystroke, and preloaded on mount so the list is there
|
|
|
228
228
|
failed fetch replaces the list content, as `AsyncCombobox`'s `showListContent` does when
|
|
229
229
|
off, but keeps the selection and the query, for retry.
|
|
230
230
|
|
|
231
|
+
**Use when:** Resource selection with self-owned debounce, abort and scoped requests.
|
|
232
|
+
|
|
233
|
+
**Avoid when:** An existing application query/cache already owns lookup results.
|
|
234
|
+
|
|
235
|
+
**Compose:** Change scopeKey when workspace/tenant/parent changes; preserve query/open state and explicitly clear an invalid controlled selection.
|
|
236
|
+
|
|
237
|
+
**Capabilities:** resource selection; scoped resource lookup; pending empty error retry.
|
|
238
|
+
|
|
239
|
+
**Alternatives:** AsyncCombobox, SuggestionsCombobox.
|
|
240
|
+
|
|
231
241
|
```text
|
|
232
242
|
({ fetcher, onAfterFetch, onError, getKey, getLabel, limit, debounceMs, preload, minSearchLength, defaultValue, errorMessage, ...props }: ResourceComboboxProps<T>) => import("react").JSX.Element
|
|
233
243
|
```
|
|
@@ -280,6 +290,7 @@ Extends: `Omit<AsyncComboboxProps<T>, "items" | "selectedValue" | "onSelectedVal
|
|
|
280
290
|
| member | required | type | description / documented default |
|
|
281
291
|
| --- | :-: | --- | --- |
|
|
282
292
|
| `fetcher` | yes | `ResourceComboboxFetcher<T>` | Fetches the options for a query. Debounce, abort, and the race guard are owned for you,<br>and the query arrives trimmed. Honour `signal`, so an abort does not surface as a failure. |
|
|
293
|
+
| `scopeKey` | no | `string \| number` | Resource scope identity. A change aborts stale lookups and refetches the current<br>eligible query. The application owns clearing a selection invalid in the new scope. |
|
|
283
294
|
| `value` | no | `T \| null` | Controlled. Omit entirely to let the component own the selection. |
|
|
284
295
|
| `defaultValue` | no | `T \| null` | The starting selection when uncontrolled. Ignored once `value` is passed. |
|
|
285
296
|
| `onValueChange` | no | `(value: T \| null) => void` | |
|
|
@@ -438,6 +449,7 @@ Kind: interface.
|
|
|
438
449
|
| member | required | type | description / documented default |
|
|
439
450
|
| --- | :-: | --- | --- |
|
|
440
451
|
| `fetchData` | yes | `(query: string, context?: SuggestionsFetchContext) => MaybePromise<T[]>` | Fetches the suggestions for a query, as an array or a promise of one. Pass<br>`context.signal` to the request: it aborts when a newer query starts. |
|
|
452
|
+
| `scopeKey` | no | `string \| number` | Identity of the resource scope (workspace, tenant or parent record). Changing it<br>aborts previous work, hides its results/errors and immediately refetches an eligible<br>query. Compared with Object.is; callback identity alone never refetches. Selection,<br>query and open state survive: the application clears selections invalid in the new scope. |
|
|
441
453
|
| `minQueryLength` | no | `number` | Characters required before a request is issued, not counting surrounding whitespace.<br>Defaults to 1. |
|
|
442
454
|
| `debounceMs` | no | `number` | The quiet period that collapses a burst of typing into one request. Defaults to 300. |
|
|
443
455
|
| `requestDelay` | no | `number` | A further wait before the fetcher runs, after the loading state is showing (unlike<br>`debounceMs`, which delays deciding to fetch). For rate-limited endpoints. @default 0 |
|
|
@@ -475,6 +487,61 @@ Kind: interface.
|
|
|
475
487
|
|
|
476
488
|
Preview route: Combobox — `/combobox`
|
|
477
489
|
|
|
490
|
+
### Scoped resource selection
|
|
491
|
+
|
|
492
|
+
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
493
|
+
import { useState } from "react"
|
|
494
|
+
import { Button } from "themelia-ui/base/buttons"
|
|
495
|
+
import { FormField } from "themelia-ui/base/forms"
|
|
496
|
+
import { Overlay, OverlayTrigger, OverlayContent, OverlayHeader, OverlayTitle, OverlayDescription, OverlayBody } from "themelia-ui/base/overlay"
|
|
497
|
+
import { Stack } from "themelia-ui/base/structure"
|
|
498
|
+
import { Text } from "themelia-ui/base/typography"
|
|
499
|
+
import { ResourceCombobox } from "themelia-ui/features/combobox"
|
|
500
|
+
|
|
501
|
+
type Resource = { id: string; label: string }
|
|
502
|
+
type Mode = "results" | "pending" | "empty" | "error"
|
|
503
|
+
|
|
504
|
+
export default function ScopedResourceReference() {
|
|
505
|
+
const [scope, setScope] = useState("A")
|
|
506
|
+
const [mode, setMode] = useState<Mode>("results")
|
|
507
|
+
const [query, setQuery] = useState("")
|
|
508
|
+
const [value, setValue] = useState<Resource | null>({ id: "A-1", label: "A project" })
|
|
509
|
+
const [selections, setSelections] = useState(0)
|
|
510
|
+
const [submissions, setSubmissions] = useState(0)
|
|
511
|
+
const [completions, setCompletions] = useState(0)
|
|
512
|
+
return <Overlay>
|
|
513
|
+
<OverlayTrigger render={<Button />}>Choose scoped resource</OverlayTrigger>
|
|
514
|
+
<OverlayContent>
|
|
515
|
+
<OverlayHeader><OverlayTitle>Assign resource</OverlayTitle><OverlayDescription>Lookup validity belongs to a workspace. The application decides whether a selected resource remains valid.</OverlayDescription></OverlayHeader>
|
|
516
|
+
<OverlayBody>
|
|
517
|
+
<form onSubmit={event => { event.preventDefault(); setSubmissions(count => count + 1) }}>
|
|
518
|
+
<Stack>
|
|
519
|
+
<Stack direction="horizontal" gap="sm" wrap>
|
|
520
|
+
{["A", "B"].map(key => <Button key={key} tone="neutral" appearance="outline" onClick={() => { setScope(key); setValue(null) }}>Workspace {key}</Button>)}
|
|
521
|
+
</Stack>
|
|
522
|
+
<Stack direction="horizontal" gap="sm" wrap>
|
|
523
|
+
{(["results", "pending", "empty", "error"] as const).map(key => <Button key={key} tone="neutral" appearance="ghost" onClick={() => setMode(key)}>{key}</Button>)}
|
|
524
|
+
</Stack>
|
|
525
|
+
<FormField label="Resource" hint={`Searching workspace ${scope}`}>
|
|
526
|
+
<ResourceCombobox<Resource> scopeKey={`${scope}:${mode}`} value={value} onValueChange={next => { setValue(next); setSelections(count => count + 1) }} searchValue={query} onSearchValueChange={setQuery} debounceMs={100} getKey={item => item.id} getLabel={item => item.label}
|
|
527
|
+
fetcher={async ({ query: search, signal }) => {
|
|
528
|
+
// The mock deliberately ignores abort; the picker must suppress its late answer.
|
|
529
|
+
void signal
|
|
530
|
+
await new Promise(resolve => setTimeout(resolve, mode === "pending" ? 2400 : scope === "A" ? 600 : 80))
|
|
531
|
+
if (mode === "error") throw new Error("Lookup unavailable")
|
|
532
|
+
return mode === "empty" || (search && !"project".includes(search.toLowerCase())) ? [] : [{ id: `${scope}-1`, label: `${scope} project` }]
|
|
533
|
+
}} onAfterFetch={() => setCompletions(count => count + 1)} />
|
|
534
|
+
</FormField>
|
|
535
|
+
<Button type="submit">Assign</Button>
|
|
536
|
+
<Text role="status">Selections {selections}; submissions {submissions}; completions {completions}</Text>
|
|
537
|
+
</Stack>
|
|
538
|
+
</form>
|
|
539
|
+
</OverlayBody>
|
|
540
|
+
</OverlayContent>
|
|
541
|
+
</Overlay>
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
478
545
|
### A picker that fetches
|
|
479
546
|
|
|
480
547
|
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# `features/data-view`
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A resource collection needs search, filters, saved views, table controls and paging.
|
|
6
6
|
|
|
7
7
|
## Selection
|
|
8
8
|
|
|
9
|
-
- **Use when:**
|
|
10
|
-
- **Avoid when:**
|
|
9
|
+
- **Use when:** A resource collection needs search, filters, saved views, table controls and paging.
|
|
10
|
+
- **Avoid when:** Only tabular behavior is needed inside an existing application-owned browser shell.
|
|
11
11
|
- **Consider instead:** `features/table`, `base/table`
|
|
12
12
|
- **Stability:** stable.
|
|
13
13
|
|
|
@@ -365,6 +365,62 @@ Kind: interface.
|
|
|
365
365
|
|
|
366
366
|
Preview route: Data view & data table — `/data-view`
|
|
367
367
|
|
|
368
|
+
### Server resource index reference
|
|
369
|
+
|
|
370
|
+
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
371
|
+
import { useEffect, useRef, useState } from "react"
|
|
372
|
+
import { Button } from "themelia-ui/base/buttons"
|
|
373
|
+
import { Stack } from "themelia-ui/base/structure"
|
|
374
|
+
import { Heading, Text } from "themelia-ui/base/typography"
|
|
375
|
+
import { DataView, type DataViewProps } from "themelia-ui/features/data-view"
|
|
376
|
+
import { FilterType, type ActiveFilter, type FilterConfig } from "themelia-ui/features/filters"
|
|
377
|
+
|
|
378
|
+
type RecordRow = { id: string; name: string; status: string; searchTerms: string }
|
|
379
|
+
const RECORDS: RecordRow[] = Array.from({ length: 6 }, (_, index) => ({ id: `r${index + 1}`, name: `Resource ${index + 1}`, status: index % 2 ? "Closed" : "Open", searchTerms: "hidden reference term" }))
|
|
380
|
+
const COLUMNS: DataViewProps<RecordRow>["columns"] = [{ accessorKey: "name", header: "Resource" }, { accessorKey: "status", header: "Status" }]
|
|
381
|
+
const FILTERS: FilterConfig[] = [{ key: "search", label: "Search hidden metadata and resource names", type: FilterType.SEARCH }, { key: "status", label: "Publication and review status with a long translated label", type: FilterType.SELECT, options: [{ value: "Open", label: "Open" }, { value: "Closed", label: "Closed" }] }]
|
|
382
|
+
|
|
383
|
+
export default function ResourceIndexReference() {
|
|
384
|
+
const [filters, setFilters] = useState<ActiveFilter[]>([])
|
|
385
|
+
const [page, setPage] = useState(0)
|
|
386
|
+
const [rows, setRows] = useState<RecordRow[]>([])
|
|
387
|
+
const [hasNext, setHasNext] = useState(false)
|
|
388
|
+
const [pending, setPending] = useState(true)
|
|
389
|
+
const [error, setError] = useState<string>()
|
|
390
|
+
const [refresh, setRefresh] = useState(0)
|
|
391
|
+
const [selection, setSelection] = useState<Record<string, true>>({})
|
|
392
|
+
const [answeredQuery, setAnsweredQuery] = useState("")
|
|
393
|
+
const [commands, setCommands] = useState("")
|
|
394
|
+
const failNext = useRef(false)
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
let active = true
|
|
397
|
+
async function load() {
|
|
398
|
+
setPending(true); setError(undefined)
|
|
399
|
+
await new Promise(resolve => setTimeout(resolve, 250))
|
|
400
|
+
if (!active) return
|
|
401
|
+
if (failNext.current) { failNext.current = false; setError("Refresh failed. Previous rows remain available."); setPending(false); return }
|
|
402
|
+
const search = filters.find(filter => filter.key === "search")?.value[0]?.toLowerCase() ?? ""
|
|
403
|
+
const status = filters.find(filter => filter.key === "status")?.value[0]
|
|
404
|
+
const matches = RECORDS.filter(row => `${row.name} ${row.searchTerms}`.toLowerCase().includes(search) && (!status || row.status === status))
|
|
405
|
+
setAnsweredQuery(search); setRows(matches.slice(page * 2, page * 2 + 2)); setHasNext(matches.length > page * 2 + 2); setPending(false)
|
|
406
|
+
}
|
|
407
|
+
void load()
|
|
408
|
+
return () => { active = false }
|
|
409
|
+
}, [filters, page, refresh])
|
|
410
|
+
return <div style={{ width: "100%", minWidth: 0, maxWidth: "48rem" }}>
|
|
411
|
+
<Stack>
|
|
412
|
+
<Heading level={3}>Resource index</Heading>
|
|
413
|
+
<Text type="secondary">Server matching includes fields absent from the visible columns. Totals are unknown; selection applies to the current page.</Text>
|
|
414
|
+
{error && <Text role="alert" type="error">{error}</Text>}
|
|
415
|
+
<DataView data={rows} columns={COLUMNS} filtering={{ filters: FILTERS, activeFilters: filters, onFilterChange: next => { setFilters(next); setPage(0); setSelection({}) }, filterRows: ({ data }) => [...data], isFiltering: pending }}
|
|
416
|
+
table={{ enableColumnVisibility: true, enableRowSelection: true, getRowId: row => row.id, rowSelection: selection, onRowSelectionChange: setSelection, bulkActions: ({ selectedRows }) => <Button onClick={() => setCommands(`Selected-page command: ${selectedRows.map(row => row.id).join(", ")}`)}>Archive selected page rows</Button> }}
|
|
417
|
+
slots={{ topbarEnd: <Stack direction="horizontal" gap="sm" wrap><Button tone="neutral" appearance="outline" disabled={pending} onClick={() => setRefresh(count => count + 1)}>Refresh</Button><Button tone="neutral" appearance="outline" onClick={() => { failNext.current = true; setRefresh(count => count + 1) }}>Fail next refresh</Button><Button tone="neutral" appearance="outline" onClick={() => setCommands(`Export current page: ${rows.map(row => row.id).join(", ")}`)}>Export page</Button></Stack>, footer: <Stack direction="horizontal" wrap align="center"><Text>Total unknown; page {page + 1}</Text><Button disabled={pending || page === 0} onClick={() => { setPage(value => value - 1); setSelection({}) }}>Previous page</Button><Button disabled={pending || !hasNext} onClick={() => { setPage(value => value + 1); setSelection({}) }}>Next page</Button></Stack> }} />
|
|
418
|
+
<Text role="status">{pending ? "Loading page" : `${rows.length} rows supplied by server; matched ${answeredQuery || "all"}`}{commands && `; ${commands}`}</Text>
|
|
419
|
+
</Stack>
|
|
420
|
+
</div>
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
368
424
|
### An index
|
|
369
425
|
|
|
370
426
|
```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
|
|
@@ -292,6 +292,16 @@ Kind: callable.
|
|
|
292
292
|
The default bar: saved tabs, search boxes, active pills, always-shown pills, the add button,
|
|
293
293
|
then Clear. For a custom bar, place the exported parts using `useFilterGroups()`.
|
|
294
294
|
|
|
295
|
+
**Use when:** Responsive filter controls shared between a desktop region and mobile sheet.
|
|
296
|
+
|
|
297
|
+
**Avoid when:** A complete resource browser already provides its filter layout, or the controls are page commands.
|
|
298
|
+
|
|
299
|
+
**Compose:** Share controlled filter state across presentations. Place create and export in action slots outside filter controls.
|
|
300
|
+
|
|
301
|
+
**Capabilities:** responsive filters; mobile sheet desktop filter region.
|
|
302
|
+
|
|
303
|
+
**Alternatives:** —.
|
|
304
|
+
|
|
295
305
|
```text
|
|
296
306
|
({ className, dynamicFilterOptions, loadingFilters, variant, showClearFilters, tabs, mobilePresentation, }: FilterLayoutProps) => import("react").JSX.Element
|
|
297
307
|
```
|