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.
Files changed (178) hide show
  1. package/.agents/skills/themelia-ui/SKILL.md +38 -9
  2. package/.agents/skills/themelia-ui/manifest.json +23 -0
  3. package/.agents/skills/themelia-ui/references/api-compatibility.md +3 -2
  4. package/.agents/skills/themelia-ui/references/blocks.md +123 -0
  5. package/.agents/skills/themelia-ui/references/composition-ladder.md +8 -8
  6. package/.agents/skills/themelia-ui/references/composition.md +98 -1
  7. package/.agents/skills/themelia-ui/references/forms.md +23 -3
  8. package/.agents/skills/themelia-ui/references/framework-wiring.md +34 -6
  9. package/.agents/skills/themelia-ui/references/i18n.md +4 -2
  10. package/.agents/skills/themelia-ui/references/imports.md +1 -1
  11. package/.agents/skills/themelia-ui/references/installation.md +5 -4
  12. package/.agents/skills/themelia-ui/references/migration.md +22 -2
  13. package/.agents/skills/themelia-ui/references/provider-and-scoping.md +27 -9
  14. package/.agents/skills/themelia-ui/references/reference-workflows.md +43 -0
  15. package/.agents/skills/themelia-ui/references/structure-and-spacing.md +147 -0
  16. package/.agents/skills/themelia-ui/references/theming.md +6 -2
  17. package/.agents/skills/themelia-ui/references/troubleshooting.md +28 -5
  18. package/.agents/skills/themelia-ui/references/verification.md +58 -2
  19. package/CHANGELOG.md +25 -0
  20. package/README.md +3 -0
  21. package/dist/{alert-dialog-Bmo1YrTm.js → alert-dialog-Cg2HhuBe.js} +1 -1
  22. package/dist/base/alert-dialog.js +1 -1
  23. package/dist/base/cards.js +1 -1
  24. package/dist/base/choice-inputs.js +1 -1
  25. package/dist/base/combobox.js +1 -1
  26. package/dist/base/command.js +1 -1
  27. package/dist/base/date-pickers.js +1 -1
  28. package/dist/base/dialog.js +1 -1
  29. package/dist/base/display.js +1 -1
  30. package/dist/base/forms-numeric.js +1 -1
  31. package/dist/base/forms.js +1 -1
  32. package/dist/base/item.js +1 -1
  33. package/dist/base/navigation.js +1 -1
  34. package/dist/base/otp-input.js +1 -1
  35. package/dist/base/overlay.js +1 -1
  36. package/dist/base/popover-menu.js +1 -1
  37. package/dist/base/repeaters.js +1 -1
  38. package/dist/base/sheet.js +1 -1
  39. package/dist/base/sidebar.js +1 -1
  40. package/dist/base/skeleton.js +1 -1
  41. package/dist/base/structure.js +1 -1
  42. package/dist/base/timeline.js +1 -1
  43. package/dist/base/upload.js +1 -1
  44. package/dist/base/value-inputs.js +1 -1
  45. package/dist/blocks/admin/access.js +2 -2
  46. package/dist/blocks/admin/commerce.js +9 -9
  47. package/dist/blocks/analytics.js +3 -3
  48. package/dist/blocks/onboarding.js +1 -1
  49. package/dist/blocks/timelines.js +1 -1
  50. package/dist/{cards-DdiICdMM.js → cards-Ddhz43Bz.js} +2 -2
  51. package/dist/{choice-inputs-CCw-0gD6.js → choice-inputs-NwRxvmZN.js} +2 -2
  52. package/dist/combobox-519ubijc.js +292 -0
  53. package/dist/{command-CnB_IZSo.js → command-JjApYFds.js} +2 -2
  54. package/dist/{comments-C6Y_ddMt.js → comments-CCQ2QRaU.js} +4 -4
  55. package/dist/components/base/display/icon-badge.d.ts +2 -2
  56. package/dist/components/base/forms/form-field.d.ts +2 -2
  57. package/dist/components/base/structure/bleed.d.ts +1 -0
  58. package/dist/components/base/structure/grid.d.ts +8 -2
  59. package/dist/components/base/structure/split.d.ts +1 -0
  60. package/dist/components/features/combobox/fetching-combobox.d.ts +2 -1
  61. package/dist/components/features/combobox/resource-combobox.d.ts +3 -0
  62. package/dist/components/features/combobox/use-suggestions.d.ts +7 -0
  63. package/dist/css/structure.css +1 -1
  64. package/dist/{date-pickers-C2HgjpFU.js → date-pickers-BwCwKIVM.js} +1 -1
  65. package/dist/{dialog-0nRFg5Zs.js → dialog-CAnwRNrm.js} +1 -1
  66. package/dist/{display-BGN834xV.js → display-a7AoX_u7.js} +1 -1
  67. package/dist/features/actions.js +1 -1
  68. package/dist/features/activities.js +3 -3
  69. package/dist/features/ai-chat.js +2 -2
  70. package/dist/features/combobox.js +285 -273
  71. package/dist/features/comments.js +1 -1
  72. package/dist/features/data-view.js +5 -5
  73. package/dist/features/event-calendar.js +3 -3
  74. package/dist/features/filters.js +1 -1
  75. package/dist/features/global-search.js +4 -4
  76. package/dist/features/map.js +2 -2
  77. package/dist/features/media-library.js +8 -8
  78. package/dist/features/mentions.js +1 -1
  79. package/dist/features/overlays.js +1 -1
  80. package/dist/features/products.js +9 -9
  81. package/dist/features/resource-assignment.js +3 -3
  82. package/dist/features/resource.js +3 -3
  83. package/dist/features/schema-form.js +6 -6
  84. package/dist/features/sync.js +2 -2
  85. package/dist/features/table.js +1 -1
  86. package/dist/features/theme-tweaker.js +10 -10
  87. package/dist/{filters-geLuSlFL.js → filters-BhsTkWpZ.js} +9 -9
  88. package/dist/{forms-NxbJK4Mh.js → forms-C0WeNPFF.js} +1 -1
  89. package/dist/{forms-numeric-C6NMdj6G.js → forms-numeric-IwQ1ltNf.js} +1 -1
  90. package/dist/hooks/use-native-dialog.d.ts +1 -1
  91. package/dist/{item-CuUJyETl.js → item-M2pmpWXh.js} +1 -1
  92. package/dist/layout/app-shell.js +1 -1
  93. package/dist/layout/auth.js +1 -1
  94. package/dist/layout/header.js +3 -3
  95. package/dist/layout/navigation.js +1 -1
  96. package/dist/layout/page.js +2 -2
  97. package/dist/layout/settings.js +2 -2
  98. package/dist/layout/sidebar.js +10 -6
  99. package/dist/layout/workspace.js +3 -3
  100. package/dist/lib/ui-provider/ui-scope.d.ts +1 -1
  101. package/dist/{mentions-oeYNv_j1.js → mentions-BOxbigwp.js} +2 -2
  102. package/dist/{navigation-BTX5Ti0Z.js → navigation-Bgu7Vc0M.js} +2 -2
  103. package/dist/{navigation-WYKjDhZq.js → navigation-DiYACqpE.js} +2 -2
  104. package/dist/{overlay-C8RAgi2_.js → overlay-BuQOl8aP.js} +5 -5
  105. package/dist/{overlays-9BxwFTou.js → overlays-1sULyytH.js} +5 -5
  106. package/dist/{popover-menu-DcLeiw9r.js → popover-menu-QXgQeWeG.js} +1 -1
  107. package/dist/{repeaters-DfptvbGA.js → repeaters-CeH20dvr.js} +4 -4
  108. package/dist/{sheet-D-DVZH_b.js → sheet-CJ1-OAla.js} +1 -1
  109. package/dist/{sidebar-DXQOy_Cb.js → sidebar-D8uGqxio.js} +4 -4
  110. package/dist/{skeleton-CubvHel9.js → skeleton-BTKNKTdj.js} +1 -1
  111. package/dist/{structure-B9Y5NkP_.js → structure-CynEZ-OY.js} +1 -2
  112. package/dist/style.css +1 -1
  113. package/dist/styles/TOKENS.md +4 -3
  114. package/dist/{table-DT_20ity.js → table-B7dtIFUx.js} +4 -4
  115. package/dist/{timeline-BRZbSdhf.js → timeline-BR_jNgvz.js} +1 -1
  116. package/dist/{timelines-DczZKW5p.js → timelines-DXG1BfIN.js} +1 -1
  117. package/dist/{upload-C_6mlbfl.js → upload-CYuoJykZ.js} +1 -1
  118. package/dist/{value-inputs-CnO6RZeT.js → value-inputs-ng8aVJJZ.js} +4 -4
  119. package/docs/README.md +11 -1
  120. package/docs/build/browser-composition-fixture.ts +28 -0
  121. package/docs/build/recipes.md +398 -1
  122. package/docs/generated/component-index.json +1993 -862
  123. package/docs/generated/components/base--display.md +2 -2
  124. package/docs/generated/components/base--forms.md +72 -1
  125. package/docs/generated/components/base--structure.md +56 -5
  126. package/docs/generated/components/base--typography.md +38 -0
  127. package/docs/generated/components/blocks--admin--access.md +10 -0
  128. package/docs/generated/components/blocks--analytics.md +44 -0
  129. package/docs/generated/components/features--combobox.md +67 -0
  130. package/docs/generated/components/features--data-view.md +59 -3
  131. package/docs/generated/components/features--filters.md +10 -0
  132. package/docs/generated/components/features--media-library.md +68 -0
  133. package/docs/generated/components/features--overlays.md +54 -0
  134. package/docs/generated/components/features--rich-text-editor.md +38 -0
  135. package/docs/generated/components/layout--app-shell.md +31 -0
  136. package/docs/generated/components/layout--containers.md +40 -0
  137. package/docs/generated/components/layout--page.md +30 -1
  138. package/docs/generated/components/layout--sidebar.md +41 -0
  139. package/docs/generated/components/ui-provider.md +1 -1
  140. package/docs/generated/composition-ladder.md +8 -8
  141. package/docs/generated/consumer-contract.json +5736 -0
  142. package/docs/generated/imports.md +1 -1
  143. package/docs/generated/recipes.json +171 -1
  144. package/docs/generated/workflows/communications-overview/analytics/communications-reference.tsx +19 -0
  145. package/docs/generated/workflows/index.json +691 -0
  146. package/docs/generated/workflows/media-picking/media-library/media-picking-reference.tsx +63 -0
  147. package/docs/generated/workflows/media-picking/media-library/reference-data.ts +3 -0
  148. package/docs/generated/workflows/multi-section-form/form-workflow/multi-section-reference.tsx +46 -0
  149. package/docs/generated/workflows/nested-action-flow/action-overlays/nested-action-reference.tsx +39 -0
  150. package/docs/generated/workflows/record-detail/page-layout/record-detail-reference.tsx +24 -0
  151. package/docs/generated/workflows/resource-index/data-view/resource-index-reference.tsx +51 -0
  152. package/docs/generated/workflows/scoped-resource-selection/combobox/scoped-resource-reference.tsx +50 -0
  153. package/docs/generated/workflows/website-content-editor/media-library/reference-data.ts +3 -0
  154. package/docs/generated/workflows/website-content-editor/rich-text-editor/website-content-reference.tsx +33 -0
  155. package/docs/generated/workflows/workspace-navigation/app-shell/workspace-navigation-reference.tsx +26 -0
  156. package/docs/learn/api-compatibility.md +3 -2
  157. package/docs/learn/blocks.md +123 -0
  158. package/docs/learn/composition.md +98 -1
  159. package/docs/learn/forms.md +23 -3
  160. package/docs/learn/framework-wiring.md +34 -6
  161. package/docs/learn/i18n.md +4 -2
  162. package/docs/learn/installation.md +5 -4
  163. package/docs/learn/migration.md +22 -2
  164. package/docs/learn/provider-and-scoping.md +27 -9
  165. package/docs/learn/reference-workflows.md +43 -0
  166. package/docs/learn/structure-and-spacing.md +147 -0
  167. package/docs/learn/theming.md +6 -2
  168. package/docs/learn/troubleshooting.md +28 -5
  169. package/docs/learn/verification.md +58 -2
  170. package/package.json +4 -1
  171. package/scripts/consumer/codemod.mjs +3 -24
  172. package/scripts/consumer/contract-reader.mjs +42 -0
  173. package/scripts/consumer/css-scan.mjs +51 -0
  174. package/scripts/consumer/diagnose.mjs +136 -0
  175. package/scripts/consumer/find-component.mjs +28 -13
  176. package/scripts/consumer/install-skill.mjs +93 -11
  177. package/src/styles/TOKENS.md +4 -3
  178. package/dist/combobox-Byu8N-Oe.js +0 -274
@@ -7,7 +7,7 @@ siblings do not exist, because one specifier would pull an entire tier and every
7
7
  peer inside it. The root carries the provider and the display primitives, both peer-free.
8
8
 
9
9
  A component brings its own CSS in a bundled app. Pages with no bundler link `style.css`; see
10
- docs/learn/installation.md.
10
+ [Installation](../learn/installation.md).
11
11
 
12
12
  ## general profile — 95 modules
13
13
 
@@ -125,6 +125,25 @@
125
125
  "import": "themelia-ui/base/action-menu",
126
126
  "code": "import { ArchiveIcon, SendIcon, PencilIcon, TrashIcon } from \"lucide-react\"\n\nimport {\n\tActionButtons, resolveContextActions, type ContextAction,\n} from \"themelia-ui/base/action-menu\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\n\nimport { QUIET } from \"./data\"\n\ninterface Invoice { number: string; paid: boolean; locked: boolean }\n\n/* Declared once against the record type; predicates run per record and handlers receive it. */\nconst INVOICE_ACTIONS: ContextAction<Invoice>[] = [\n\t{ id: \"send\", label: \"Send reminder\", icon: SendIcon, visible: (invoice) => !invoice.paid, placement: \"inline\" },\n\t{ id: \"edit\", label: \"Edit\", icon: PencilIcon, disabled: (invoice) => invoice.locked, ...QUIET },\n\t{ id: \"archive\", label: \"Archive\", icon: ArchiveIcon, ...QUIET },\n\t{ id: \"delete\", label: \"Delete\", icon: TrashIcon, tone: \"destructive\", placement: \"menu\" },\n]\n\nconst INVOICES: Invoice[] = [\n\t{ number: \"INV-1042\", paid: false, locked: false },\n\t{ number: \"INV-1038\", paid: true, locked: true },\n]\n\nexport default function ContextActions() {\n\treturn (\n\t\t<Stack style={{ width: \"100%\" }}>\n\t\t\t{INVOICES.map((invoice) => (\n\t\t\t\t<Stack key={invoice.number} gap=\"sm\">\n\t\t\t\t\t<Text size=\"xs\" type=\"secondary\">\n\t\t\t\t\t\t{invoice.number} · {invoice.paid ? \"paid, locked\" : \"unpaid\"}\n\t\t\t\t\t</Text>\n\t\t\t\t\t<ActionButtons actions={resolveContextActions(INVOICE_ACTIONS, invoice)} max={2} />\n\t\t\t\t</Stack>\n\t\t\t))}\n\t\t</Stack>\n\t)\n}"
127
127
  },
128
+ {
129
+ "id": "nested-action-reference",
130
+ "title": "Nested action flow",
131
+ "route": "/action-overlays",
132
+ "page": "Action overlays",
133
+ "module": "features/overlays",
134
+ "modules": [
135
+ "features/overlays"
136
+ ],
137
+ "supportingModules": [
138
+ "base/buttons",
139
+ "base/forms",
140
+ "base/structure",
141
+ "base/text-inputs",
142
+ "base/typography"
143
+ ],
144
+ "import": "themelia-ui/features/overlays",
145
+ "code": "import { useRef, useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { FormField } from \"themelia-ui/base/forms\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Input } from \"themelia-ui/base/text-inputs\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { ActionDialog } from \"themelia-ui/features/overlays\"\n\nexport default function NestedActionReference() {\n\tconst [open, setOpen] = useState(false)\n\tconst [child, setChild] = useState(false)\n\tconst [name, setName] = useState(\"Draft record\")\n\tconst [error, setError] = useState<string>()\n\tconst [saved, setSaved] = useState(0)\n\tconst session = useRef(0)\n\tconst changeOpen = (next: boolean) => { session.current++; setOpen(next); setChild(false) }\n\treturn <Stack>\n\t\t<ActionDialog open={open} trigger={<Button>Inspect draft</Button>} onOpenChange={changeOpen} title=\"Record inspector\" description=\"The draft remains here after cancellation, validation failure or transport failure.\" showConfirm={false}>\n\t\t\t<Stack>\n\t\t\t\t<FormField label=\"Record name\" error={error} hint=\"Use at least three characters. Use offline to simulate a rejected request.\"><Input value={name} onChange={event => setName(event.target.value)} /></FormField>\n\t\t\t\t<ActionDialog open={child} trigger={<Button>Review save</Button>} onOpenChange={setChild} title=\"Save this draft?\" description=\"A resolved response can contain validation errors. Closure follows business success.\" closeOnAsyncComplete={false}\n\t\t\t\t\talertMessage={error} onError={() => setError(\"Could not save; your draft is still here.\")}\n\t\t\t\t\tonAsyncConfirm={async () => {\n\t\t\t\t\t\tconst attempt = session.current\n\t\t\t\t\t\tawait new Promise(resolve => setTimeout(resolve, 300))\n\t\t\t\t\t\tif (attempt !== session.current) return\n\t\t\t\t\t\tif (name === \"offline\") throw new Error(\"Unavailable\")\n\t\t\t\t\t\tif (name.trim().length < 3) { setError(\"Enter at least three characters.\"); return }\n\t\t\t\t\t\tsetError(undefined); setSaved(count => count + 1); changeOpen(false)\n\t\t\t\t\t}}>\n\t\t\t\t\t<Text>{name}</Text>\n\t\t\t\t</ActionDialog>\n\t\t\t\t{/* A native form can instead use formId: its submit handler owns validation and closure. */}\n\t\t\t\t<Text size=\"xs\" type=\"secondary\">Confirmation {child ? \"open\" : \"closed\"}</Text>\n\t\t\t</Stack>\n\t\t</ActionDialog>\n\t\t<Text role=\"status\">Successful saves {saved}</Text>\n\t</Stack>\n}"
146
+ },
128
147
  {
129
148
  "id": "action-dialog",
130
149
  "title": "ActionDialog",
@@ -385,6 +404,25 @@
385
404
  "import": "themelia-ui/base/feedback",
386
405
  "code": "import { SearchXIcon } from \"lucide-react\"\n\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Alert, AlertAction, AlertDescription, AlertTitle } from \"themelia-ui/base/feedback\"\n\nexport default function AlertLayout() {\n\treturn (\n\t\t<>\n\t\t\t<Alert tone=\"destructive\" icon={<SearchXIcon />}>\n\t\t\t\t<AlertTitle>With a leading icon</AlertTitle>\n\t\t\t\t<AlertDescription>The grid becomes two columns automatically.</AlertDescription>\n\t\t\t</Alert>\n\t\t\t<Alert tone=\"info\">\n\t\t\t\t<AlertTitle>With an action</AlertTitle>\n\t\t\t\t<AlertDescription>Inline space is reserved on the trailing edge.</AlertDescription>\n\t\t\t\t<AlertAction>\n\t\t\t\t\t<Button tone=\"neutral\" appearance=\"ghost\">\n\t\t\t\t\t\tUndo\n\t\t\t\t\t</Button>\n\t\t\t\t</AlertAction>\n\t\t\t</Alert>\n\t\t\t<Alert>\n\t\t\t\t<AlertTitle>Neither — single column</AlertTitle>\n\t\t\t\t<AlertDescription>No icon, no action, no reserved space.</AlertDescription>\n\t\t\t</Alert>\n\t\t</>\n\t)\n}"
387
406
  },
407
+ {
408
+ "id": "communications-reference",
409
+ "title": "Communications overview reference",
410
+ "route": "/analytics",
411
+ "page": "Analytics",
412
+ "module": "blocks/analytics",
413
+ "modules": [
414
+ "blocks/analytics"
415
+ ],
416
+ "supportingModules": [
417
+ "base/buttons",
418
+ "base/cards",
419
+ "base/display",
420
+ "base/structure",
421
+ "base/typography"
422
+ ],
423
+ "import": "themelia-ui/blocks/analytics",
424
+ "code": "import { useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Card } from \"themelia-ui/base/cards\"\nimport { MetadataList } from \"themelia-ui/base/display\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { MetricGrid } from \"themelia-ui/blocks/analytics\"\n\nexport default function CommunicationsReference() {\n\tconst [retried, setRetried] = useState(false)\n\treturn <Stack>\n\t\t<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\" />\n\t\t<Card title=\"Latest communication\" headerAction={<Button disabled={retried} onClick={() => setRetried(true)}>Retry delivery</Button>}>\n\t\t\t<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\" }]} />\n\t\t\t<Text>The application owns delivery policy and permission checks. Metrics describe quantities; this subject and its delivery facts stay in a record surface.</Text>\n\t\t</Card>\n\t\t<Text role=\"status\">{retried ? \"Mock retry completed\" : \"Mock delivery is waiting\"}</Text>\n\t</Stack>\n}"
425
+ },
388
426
  {
389
427
  "id": "analytics-bar",
390
428
  "title": "Metric bar",
@@ -525,6 +563,22 @@
525
563
  "import": "themelia-ui/base/badge",
526
564
  "code": "import { ArrowUpIcon, CreditCardIcon } from \"lucide-react\"\n\nimport { Badge } from \"themelia-ui/base/badge\"\nimport { Card } from \"themelia-ui/base/cards\"\nimport { Sparkline } from \"themelia-ui/base/chart\"\nimport { IconBadge } from \"themelia-ui/base/display\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { DisplayLabel, Text } from \"themelia-ui/base/typography\"\nimport { Money } from \"themelia-ui/primitives\"\n\nexport default function AnalyticsBlueprint() {\n\treturn (\n\t\t<Card style={{ maxWidth: \"20rem\" }}>\n\t\t\t<Stack gap=\"sm\">\n\t\t\t\t<Stack direction=\"horizontal\" align=\"center\" gap=\"sm\">\n\t\t\t\t\t<IconBadge icon={CreditCardIcon} tone=\"primary\" />\n\t\t\t\t\t<DisplayLabel>Revenue</DisplayLabel>\n\t\t\t\t</Stack>\n\t\t\t\t<Stack direction=\"horizontal\" align=\"center\" justify=\"between\" gap=\"sm\">\n\t\t\t\t\t<Money amount={48200} currency=\"EUR\" size=\"xl\" weight=\"semibold\" />\n\t\t\t\t\t<Badge tone=\"success\">\n\t\t\t\t\t\t<ArrowUpIcon />\n\t\t\t\t\t\t12.4%\n\t\t\t\t\t</Badge>\n\t\t\t\t</Stack>\n\t\t\t\t<Sparkline data={[18, 22, 19, 27, 24, 31, 29, 38]} tone=\"success\" label=\"Revenue rising over eight weeks\" />\n\t\t\t\t<Text size=\"xs\" type=\"secondary\">\n\t\t\t\t\tNet of refunds, this quarter\n\t\t\t\t</Text>\n\t\t\t</Stack>\n\t\t</Card>\n\t)\n}"
527
565
  },
566
+ {
567
+ "id": "workspace-navigation-reference",
568
+ "title": "Workspace navigation reference",
569
+ "route": "/app-shell",
570
+ "page": "App shell",
571
+ "module": "layout/app-shell",
572
+ "modules": [
573
+ "layout/app-shell",
574
+ "layout/sidebar"
575
+ ],
576
+ "supportingModules": [
577
+ "base/typography"
578
+ ],
579
+ "import": "themelia-ui/layout/app-shell",
580
+ "code": "import { useState } from \"react\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { TopbarSidebarLayout } from \"themelia-ui/layout/app-shell\"\nimport { AppSidebar, type LinkRenderer } from \"themelia-ui/layout/sidebar\"\n\nexport default function WorkspaceNavigationReference() {\n\tconst [route, setRoute] = useState(\"/a/overview\")\n\tconst [navigations, setNavigations] = useState(0)\n\tconst workspace = route.startsWith(\"/b/\") ? \"B\" : \"A\"\n\tconst renderLink: LinkRenderer = ({ href, children, external, active, disabled, onClick, ...props }) => {\n\t\tvoid active\n\t\tif (disabled || !href) return <span {...props}>{children}</span>\n\t\treturn <a {...props} href={href} target={props.target ?? (external ? \"_blank\" : undefined)} rel={props.rel ?? (external ? \"noopener noreferrer\" : undefined)} onClick={event => {\n\t\t\tonClick?.(event)\n\t\t\tif (event.defaultPrevented || external || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\t\tevent.preventDefault(); setRoute(href); setNavigations(count => count + 1)\n\t\t}}>{children}</a>\n\t}\n\treturn <div style={{ width: \"100%\", height: \"28rem\", minWidth: 0 }}>\n\t\t<TopbarSidebarLayout contained contentRender={<div />} logo=\"Reference workspace\" sidebarProviderProps={{ persist: false, keyboardShortcut: false }} sidebar={<AppSidebar logo={`Workspace ${workspace}`} renderLink={renderLink} currentUrl={route}\n\t\t\tworkspaceLinks={[{ label: \"Workspace A home\", url: \"/a/overview\", external: false }, { label: \"Workspace B home\", url: \"/b/overview\", external: false }, { label: \"External workspace\", url: \"https://example.com\" }]}\n\t\t\tnavigationGroups={{ \"\": [{ label: `${workspace} overview`, href: `/${workspace.toLowerCase()}/overview` }, { label: `${workspace} settings`, href: `/${workspace.toLowerCase()}/settings` }] }} />}>\n\t\t\t<Text role=\"status\">Workspace {workspace}; route {route}; navigations {navigations}</Text>\n\t\t</TopbarSidebarLayout>\n\t</div>\n}"
581
+ },
528
582
  {
529
583
  "id": "sidebar-shell",
530
584
  "title": "The shell",
@@ -1808,6 +1862,25 @@
1808
1862
  "import": "themelia-ui/base/value-inputs",
1809
1863
  "code": "import { FormField } from \"themelia-ui/base/forms\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { ColorInput } from \"themelia-ui/base/value-inputs\"\n\n\nexport default function ColorFormats() {\n\treturn (\n\t\t<Stack style={{ maxWidth: \"26rem\", width: \"100%\" }}>\n\t\t\t<FormField label=\"oklch (default)\" helperText=\"Pick a colour from the swatch to see the notation change.\">\n\t\t\t\t<ColorInput defaultValue=\"oklch(0.45 0.12 155)\" />\n\t\t\t</FormField>\n\t\t\t<FormField label=\"hex\">\n\t\t\t\t<ColorInput format=\"hex\" defaultValue=\"#2f6f4e\" />\n\t\t\t</FormField>\n\t\t\t<FormField label=\"rgb\">\n\t\t\t\t<ColorInput format=\"rgb\" defaultValue=\"rgb(47, 111, 78)\" />\n\t\t\t</FormField>\n\t\t\t<FormField label=\"hsl\">\n\t\t\t\t<ColorInput format=\"hsl\" defaultValue=\"hsl(151, 40%, 31%)\" />\n\t\t\t</FormField>\n\t\t</Stack>\n\t)\n}"
1810
1864
  },
1865
+ {
1866
+ "id": "scoped-resource-reference",
1867
+ "title": "Scoped resource selection",
1868
+ "route": "/combobox",
1869
+ "page": "Combobox",
1870
+ "module": "features/combobox",
1871
+ "modules": [
1872
+ "features/combobox"
1873
+ ],
1874
+ "supportingModules": [
1875
+ "base/buttons",
1876
+ "base/forms",
1877
+ "base/overlay",
1878
+ "base/structure",
1879
+ "base/typography"
1880
+ ],
1881
+ "import": "themelia-ui/features/combobox",
1882
+ "code": "import { useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { FormField } from \"themelia-ui/base/forms\"\nimport { Overlay, OverlayTrigger, OverlayContent, OverlayHeader, OverlayTitle, OverlayDescription, OverlayBody } from \"themelia-ui/base/overlay\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { ResourceCombobox } from \"themelia-ui/features/combobox\"\n\ntype Resource = { id: string; label: string }\ntype Mode = \"results\" | \"pending\" | \"empty\" | \"error\"\n\nexport default function ScopedResourceReference() {\n\tconst [scope, setScope] = useState(\"A\")\n\tconst [mode, setMode] = useState<Mode>(\"results\")\n\tconst [query, setQuery] = useState(\"\")\n\tconst [value, setValue] = useState<Resource | null>({ id: \"A-1\", label: \"A project\" })\n\tconst [selections, setSelections] = useState(0)\n\tconst [submissions, setSubmissions] = useState(0)\n\tconst [completions, setCompletions] = useState(0)\n\treturn <Overlay>\n\t\t<OverlayTrigger render={<Button />}>Choose scoped resource</OverlayTrigger>\n\t\t<OverlayContent>\n\t\t\t<OverlayHeader><OverlayTitle>Assign resource</OverlayTitle><OverlayDescription>Lookup validity belongs to a workspace. The application decides whether a selected resource remains valid.</OverlayDescription></OverlayHeader>\n\t\t\t<OverlayBody>\n\t\t\t\t<form onSubmit={event => { event.preventDefault(); setSubmissions(count => count + 1) }}>\n\t\t\t\t\t<Stack>\n\t\t\t\t\t\t<Stack direction=\"horizontal\" gap=\"sm\" wrap>\n\t\t\t\t\t\t\t{[\"A\", \"B\"].map(key => <Button key={key} tone=\"neutral\" appearance=\"outline\" onClick={() => { setScope(key); setValue(null) }}>Workspace {key}</Button>)}\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<Stack direction=\"horizontal\" gap=\"sm\" wrap>\n\t\t\t\t\t\t\t{([\"results\", \"pending\", \"empty\", \"error\"] as const).map(key => <Button key={key} tone=\"neutral\" appearance=\"ghost\" onClick={() => setMode(key)}>{key}</Button>)}\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<FormField label=\"Resource\" hint={`Searching workspace ${scope}`}>\n\t\t\t\t\t\t\t<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}\n\t\t\t\t\t\t\t\tfetcher={async ({ query: search, signal }) => {\n\t\t\t\t\t\t\t\t\t// The mock deliberately ignores abort; the picker must suppress its late answer.\n\t\t\t\t\t\t\t\t\tvoid signal\n\t\t\t\t\t\t\t\t\tawait new Promise(resolve => setTimeout(resolve, mode === \"pending\" ? 2400 : scope === \"A\" ? 600 : 80))\n\t\t\t\t\t\t\t\t\tif (mode === \"error\") throw new Error(\"Lookup unavailable\")\n\t\t\t\t\t\t\t\t\treturn mode === \"empty\" || (search && !\"project\".includes(search.toLowerCase())) ? [] : [{ id: `${scope}-1`, label: `${scope} project` }]\n\t\t\t\t\t\t\t\t}} onAfterFetch={() => setCompletions(count => count + 1)} />\n\t\t\t\t\t\t</FormField>\n\t\t\t\t\t\t<Button type=\"submit\">Assign</Button>\n\t\t\t\t\t\t<Text role=\"status\">Selections {selections}; submissions {submissions}; completions {completions}</Text>\n\t\t\t\t\t</Stack>\n\t\t\t\t</form>\n\t\t\t</OverlayBody>\n\t\t</OverlayContent>\n\t</Overlay>\n}"
1883
+ },
1811
1884
  {
1812
1885
  "id": "combobox-picker",
1813
1886
  "title": "A picker that fetches",
@@ -2242,6 +2315,24 @@
2242
2315
  "import": "themelia-ui/base/forms-numeric",
2243
2316
  "code": "import { useState } from \"react\"\n\nimport { FormField } from \"themelia-ui/base/forms\"\nimport { CurrencyInput } from \"themelia-ui/base/forms-numeric\"\nimport { Stack } from \"themelia-ui/base/structure\"\n\n\nexport default function Currency() {\n\tconst [amount, setAmount] = useState(\"1299.50\")\n\tconst [currency, setCurrency] = useState(\"EUR\")\n\n\treturn (\n\t\t<Stack style={{ maxWidth: \"26rem\", width: \"100%\" }}>\n\t\t\t<FormField label=\"Invoice total\">\n\t\t\t\t<CurrencyInput\n\t\t\t\t\tvalue={amount}\n\t\t\t\t\tonChange={(event) => setAmount(event.target.value)}\n\t\t\t\t\tcurrency={currency}\n\t\t\t\t\tonCurrencyChange={setCurrency}\n\t\t\t\t\tcurrencies={[\"EUR\", \"USD\", \"GBP\"]}\n\t\t\t\t/>\n\t\t\t</FormField>\n\t\t\t<FormField label=\"Selector at the end\">\n\t\t\t\t<CurrencyInput defaultValue=\"49.00\" defaultCurrency=\"USD\" currencyPosition=\"end\" />\n\t\t\t</FormField>\n\t\t</Stack>\n\t)\n}"
2244
2317
  },
2318
+ {
2319
+ "id": "resource-index-reference",
2320
+ "title": "Server resource index reference",
2321
+ "route": "/data-view",
2322
+ "page": "Data view & data table",
2323
+ "module": "features/data-view",
2324
+ "modules": [
2325
+ "features/data-view"
2326
+ ],
2327
+ "supportingModules": [
2328
+ "base/buttons",
2329
+ "base/structure",
2330
+ "base/typography",
2331
+ "features/filters"
2332
+ ],
2333
+ "import": "themelia-ui/features/data-view",
2334
+ "code": "import { useEffect, useRef, useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Heading, Text } from \"themelia-ui/base/typography\"\nimport { DataView, type DataViewProps } from \"themelia-ui/features/data-view\"\nimport { FilterType, type ActiveFilter, type FilterConfig } from \"themelia-ui/features/filters\"\n\ntype RecordRow = { id: string; name: string; status: string; searchTerms: string }\nconst RECORDS: RecordRow[] = Array.from({ length: 6 }, (_, index) => ({ id: `r${index + 1}`, name: `Resource ${index + 1}`, status: index % 2 ? \"Closed\" : \"Open\", searchTerms: \"hidden reference term\" }))\nconst COLUMNS: DataViewProps<RecordRow>[\"columns\"] = [{ accessorKey: \"name\", header: \"Resource\" }, { accessorKey: \"status\", header: \"Status\" }]\nconst 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\" }] }]\n\nexport default function ResourceIndexReference() {\n\tconst [filters, setFilters] = useState<ActiveFilter[]>([])\n\tconst [page, setPage] = useState(0)\n\tconst [rows, setRows] = useState<RecordRow[]>([])\n\tconst [hasNext, setHasNext] = useState(false)\n\tconst [pending, setPending] = useState(true)\n\tconst [error, setError] = useState<string>()\n\tconst [refresh, setRefresh] = useState(0)\n\tconst [selection, setSelection] = useState<Record<string, true>>({})\n\tconst [answeredQuery, setAnsweredQuery] = useState(\"\")\n\tconst [commands, setCommands] = useState(\"\")\n\tconst failNext = useRef(false)\n\tuseEffect(() => {\n\t\tlet active = true\n\t\tasync function load() {\n\t\t\tsetPending(true); setError(undefined)\n\t\t\tawait new Promise(resolve => setTimeout(resolve, 250))\n\t\t\tif (!active) return\n\t\t\tif (failNext.current) { failNext.current = false; setError(\"Refresh failed. Previous rows remain available.\"); setPending(false); return }\n\t\t\tconst search = filters.find(filter => filter.key === \"search\")?.value[0]?.toLowerCase() ?? \"\"\n\t\t\tconst status = filters.find(filter => filter.key === \"status\")?.value[0]\n\t\t\tconst matches = RECORDS.filter(row => `${row.name} ${row.searchTerms}`.toLowerCase().includes(search) && (!status || row.status === status))\n\t\t\tsetAnsweredQuery(search); setRows(matches.slice(page * 2, page * 2 + 2)); setHasNext(matches.length > page * 2 + 2); setPending(false)\n\t\t}\n\t\tvoid load()\n\t\treturn () => { active = false }\n\t}, [filters, page, refresh])\n\treturn <div style={{ width: \"100%\", minWidth: 0, maxWidth: \"48rem\" }}>\n\t\t<Stack>\n\t\t\t<Heading level={3}>Resource index</Heading>\n\t\t\t<Text type=\"secondary\">Server matching includes fields absent from the visible columns. Totals are unknown; selection applies to the current page.</Text>\n\t\t\t{error && <Text role=\"alert\" type=\"error\">{error}</Text>}\n\t\t\t<DataView data={rows} columns={COLUMNS} filtering={{ filters: FILTERS, activeFilters: filters, onFilterChange: next => { setFilters(next); setPage(0); setSelection({}) }, filterRows: ({ data }) => [...data], isFiltering: pending }}\n\t\t\t\ttable={{ 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> }}\n\t\t\t\tslots={{ 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> }} />\n\t\t\t<Text role=\"status\">{pending ? \"Loading page\" : `${rows.length} rows supplied by server; matched ${answeredQuery || \"all\"}`}{commands && `; ${commands}`}</Text>\n\t\t</Stack>\n\t</div>\n}"
2335
+ },
2245
2336
  {
2246
2337
  "id": "data-view",
2247
2338
  "title": "An index",
@@ -2864,6 +2955,26 @@
2864
2955
  "import": "themelia-ui/base/forms",
2865
2956
  "code": "import { FieldGroup, FormField } from \"themelia-ui/base/forms\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Input } from \"themelia-ui/base/text-inputs\"\n\n\nexport default function FieldGroupExample() {\n\treturn (\n\t\t<div style={{ maxWidth: \"34rem\", width: \"100%\" }}>\n\t\t\t<FieldGroup legend=\"Reporting period\" description=\"Both ends are inclusive.\">\n\t\t\t\t<Stack direction=\"horizontal\" gap=\"sm\">\n\t\t\t\t\t<FormField label=\"From\">\n\t\t\t\t\t\t<Input type=\"date\" defaultValue=\"2026-03-01\" />\n\t\t\t\t\t</FormField>\n\t\t\t\t\t<FormField label=\"To\">\n\t\t\t\t\t\t<Input type=\"date\" defaultValue=\"2026-03-31\" />\n\t\t\t\t\t</FormField>\n\t\t\t\t</Stack>\n\t\t\t</FieldGroup>\n\t\t</div>\n\t)\n}"
2866
2957
  },
2958
+ {
2959
+ "id": "multi-section-reference",
2960
+ "title": "Multi-section form reference",
2961
+ "route": "/form-workflow",
2962
+ "page": "Form workflow",
2963
+ "module": "base/forms",
2964
+ "modules": [
2965
+ "base/forms"
2966
+ ],
2967
+ "supportingModules": [
2968
+ "base/cards",
2969
+ "base/display",
2970
+ "base/structure",
2971
+ "base/text-inputs",
2972
+ "base/typography",
2973
+ "forms"
2974
+ ],
2975
+ "import": "themelia-ui/base/forms",
2976
+ "code": "import { useRef, useState } from \"react\"\nimport { Card } from \"themelia-ui/base/cards\"\nimport { ContentBlock } from \"themelia-ui/base/display\"\nimport { FieldGroup, FormField, FormSection, FormActionsBar, SubmitStateButton } from \"themelia-ui/base/forms\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Input } from \"themelia-ui/base/text-inputs\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { useFormFieldBinding, useStateFormControl, type FormControl } from \"themelia-ui/forms\"\n\ntype Draft = { firstName: string; lastName: string; company: string }\nfunction BoundField({ control, name, label, error }: { control: FormControl<Draft>; name: keyof Draft; label: string; error?: string }) {\n\tconst field = useFormFieldBinding<string, Draft>({ control, name })\n\treturn <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>\n}\n\nexport default function MultiSectionReference() {\n\tconst control = useStateFormControl<Draft>({ firstName: \"Alex\", lastName: \"Example\", company: \"Example team\" })\n\tconst [pending, setPending] = useState(false)\n\tconst [error, setError] = useState<string>()\n\tconst [rootError, setRootError] = useState<string>()\n\tconst [saved, setSaved] = useState(0)\n\tconst running = useRef(false)\n\treturn <Stack>\n\t\t<Card title=\"Account details\" description=\"One outer surface, editable sections with retained region insets.\">\n\t\t\t<form onSubmit={async event => {\n\t\t\t\tevent.preventDefault(); if (running.current) return\n\t\t\t\trunning.current = true; setPending(true); setError(undefined); setRootError(undefined)\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise(resolve => setTimeout(resolve, 250))\n\t\t\t\t\tif (control.values.company === \"offline\") throw new Error(\"Request failed. Your values remain here.\")\n\t\t\t\t\tif (control.values.company.length < 3) { setError(\"Company must have at least three characters.\"); return }\n\t\t\t\t\tsetSaved(count => count + 1)\n\t\t\t\t} catch (failure) { setRootError((failure as Error).message) }\n\t\t\t\tfinally { running.current = false; setPending(false) }\n\t\t\t}}>\n\t\t\t\t<Stack gap=\"none\">\n\t\t\t\t\t{rootError && <Text role=\"alert\" type=\"error\">{rootError}</Text>}\n\t\t\t\t\t<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>\n\t\t\t\t\t<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>\n\t\t\t\t\t<FormActionsBar leading={<Text role=\"status\">Successful submissions {saved}</Text>}><SubmitStateButton type=\"submit\" state={pending ? \"submitting\" : \"idle\"} strings={{ idle: \"Save account\" }} /></FormActionsBar>\n\t\t\t\t</Stack>\n\t\t\t</form>\n\t\t</Card>\n\t\t<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>\n\t</Stack>\n}"
2977
+ },
2867
2978
  {
2868
2979
  "id": "form-section",
2869
2980
  "title": "FormSection",
@@ -3332,6 +3443,23 @@
3332
3443
  "import": "themelia-ui/features/map",
3333
3444
  "code": "import { useState } from \"react\"\n\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport {\n\tMap, MapSearchControl, MapTileLayer, MapZoomControl, PlaceAutocomplete, type PlaceFeature,\n} from \"themelia-ui/features/map\"\n\nimport { MARLOW } from \"./data\"\n\nexport default function PlaceAutocompleteExample() {\n\tconst [place, setPlace] = useState<PlaceFeature | null>(null)\n\n\treturn (\n\t\t<>\n\t\t\t<Stack gap=\"sm\" maxWidth=\"24rem\">\n\t\t\t\t<PlaceAutocomplete limit={5} onPlaceSelect={setPlace} />\n\t\t\t</Stack>\n\t\t\t{!!place && (\n\t\t\t\t<Text size=\"sm\" type=\"secondary\" numeric>\n\t\t\t\t\t{place.properties.name} — {place.geometry.coordinates[1].toFixed(4)},{\" \"}\n\t\t\t\t\t{place.geometry.coordinates[0].toFixed(4)}\n\t\t\t\t</Text>\n\t\t\t)}\n\n\t\t\t<Map center={MARLOW} zoom={13} height=\"26rem\">\n\t\t\t\t<MapTileLayer />\n\t\t\t\t<MapSearchControl position=\"top-left\" limit={5} />\n\t\t\t\t<MapZoomControl position=\"top-right\" />\n\t\t\t</Map>\n\t\t</>\n\t)\n}"
3334
3445
  },
3446
+ {
3447
+ "id": "media-picking-reference",
3448
+ "title": "Media picking reference",
3449
+ "route": "/media-library",
3450
+ "page": "Media library",
3451
+ "module": "features/media-library",
3452
+ "modules": [
3453
+ "features/media-library"
3454
+ ],
3455
+ "supportingModules": [
3456
+ "base/buttons",
3457
+ "base/structure",
3458
+ "base/typography"
3459
+ ],
3460
+ "import": "themelia-ui/features/media-library",
3461
+ "code": "import { useEffect, useMemo, useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { MediaLibraryDialog, type MediaLibraryItem, type MediaLibraryFetcher } from \"themelia-ui/features/media-library\"\n\nimport { SAMPLE_MEDIA } from \"./reference-data\"\n\nexport default function MediaPickingReference() {\n\tconst [open, setOpen] = useState(false)\n\tconst [scope, setScope] = useState(\"site\")\n\tconst [fail, setFail] = useState(false)\n\tconst [selected, setSelected] = useState<string[]>([\"unavailable-id\"])\n\tconst [pickerDraft, setPickerDraft] = useState(selected)\n\tconst [resolved, setResolved] = useState<{ scope: string; ids: string[]; items: MediaLibraryItem[] }>()\n\tconst [page, setPage] = useState(0)\n\tconst [query, setQuery] = useState(\"\")\n\tconst [pending, setPending] = useState(true)\n\tconst [hasNext, setHasNext] = useState(false)\n\t// A saved id is resolved separately from the current browser page, with scope access applied.\n\tuseEffect(() => {\n\t\tconst controller = new AbortController()\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (!controller.signal.aborted) setResolved({ scope, ids: selected, items: SAMPLE_MEDIA.filter(item => selected.includes(item.id) && item.collection === scope) })\n\t\t}, 150)\n\t\treturn () => { controller.abort(); clearTimeout(timer) }\n\t}, [selected, scope])\n\tconst chosen = resolved?.scope === scope && resolved.ids === selected ? resolved.items : []\n\tconst unavailable = resolved?.scope === scope && resolved.ids === selected && chosen.length < selected.length\n\tconst changeOpen = (next: boolean) => {\n\t\tif (next) setPickerDraft(selected)\n\t\tsetOpen(next)\n\t}\n\tconst fetcher = useMemo<MediaLibraryFetcher>(() => async ({ query: search, signal }) => {\n\t\tsetPending(true)\n\t\ttry {\n\t\t\tawait new Promise(resolve => setTimeout(resolve, 200))\n\t\t\tif (signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\")\n\t\t\tif (fail) throw new Error(\"Media lookup unavailable\")\n\t\t\tconst matches = SAMPLE_MEDIA.filter(item => item.collection === scope && item.name.toLowerCase().includes(search.toLowerCase()))\n\t\t\tconst window = matches.slice(page, page + 2)\n\t\t\tsetHasNext(window.length > 1)\n\t\t\treturn { items: window.slice(0, 1) }\n\t\t} finally { if (!signal?.aborted) setPending(false) }\n\t}, [scope, fail, page])\n\treturn <Stack>\n\t\t{unavailable && <Text role=\"alert\">The saved asset is unavailable. Choose a replacement; the draft id is retained until then.</Text>}\n\t\t<Stack direction=\"horizontal\" gap=\"sm\" wrap>\n\t\t\t<MediaLibraryDialog open={open} trigger={<Button>Choose reference media</Button>} onOpenChange={changeOpen} fetcher={fetcher} value={pickerDraft} onValueChange={setPickerDraft} query={query} onQueryChange={next => { setQuery(next); setPage(0); setPending(true) }} selectionMode=\"single\" allowUpload={false}\n\t\t\t\tstrings={{ showingCount: shown => `${shown} loaded on this page; total unknown` }}\n\t\t\t\tslots={{ footer: <Stack direction=\"horizontal\" gap=\"sm\" wrap align=\"center\">\n\t\t\t\t\t<Text>Media page {page + 1}; total unknown</Text>\n\t\t\t\t\t<Button disabled={pending || page === 0} onClick={() => { setPage(value => value - 1); setPending(true) }}>Previous media page</Button>\n\t\t\t\t\t<Button disabled={pending || !hasNext} onClick={() => { setPage(value => value + 1); setPending(true) }}>Next media page</Button>\n\t\t\t\t</Stack> }}\n\t\t\t\tonConfirm={(items, ids) => { setSelected(ids); setResolved({ scope, ids, items }); setOpen(false) }} />\n\t\t\t<Button tone=\"neutral\" appearance=\"outline\" onClick={() => setSelected([\"image-2\"])}>Load saved media draft</Button>\n\t\t\t<Button tone=\"neutral\" appearance=\"outline\" onClick={() => { setScope(value => value === \"site\" ? \"other\" : \"site\"); setPage(0); setPending(true) }}>Switch media scope</Button>\n\t\t\t<Button tone=\"neutral\" appearance=\"outline\" onClick={() => { setFail(value => !value); setPending(true) }}>Toggle media failure</Button>\n\t\t</Stack>\n\t\t<Text role=\"status\">{chosen.length ? `Hydrated selection: ${chosen.map(item => `${item.name} — ${item.alt}`).join(\", \")}` : `Saved draft ids: ${selected.join(\", \")}`}</Text>\n\t</Stack>\n}"
3462
+ },
3335
3463
  {
3336
3464
  "id": "library",
3337
3465
  "title": "Asset manager",
@@ -3803,6 +3931,26 @@
3803
3931
  "import": "themelia-ui/base/dialog",
3804
3932
  "code": "import { Button } from \"themelia-ui/base/buttons\"\nimport { DialogContent } from \"themelia-ui/base/dialog\"\nimport {\n\tOverlay, OverlayDescription, OverlayHeader, OverlayTitle, OverlayTrigger,\n} from \"themelia-ui/base/overlay\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { UIProvider } from \"themelia-ui/ui-provider\"\n\nexport default function OverlayBackdrop() {\n\treturn (\n\t\t<Stack direction=\"horizontal\" wrap align=\"center\">\n\t\t\t<Overlay>\n\t\t\t\t<OverlayTrigger render={<Button tone=\"neutral\" appearance=\"outline\" />}>\n\t\t\t\t\tDefault scrim\n\t\t\t\t</OverlayTrigger>\n\t\t\t\t<DialogContent>\n\t\t\t\t\t<OverlayHeader>\n\t\t\t\t\t\t<OverlayTitle>Default scrim</OverlayTitle>\n\t\t\t\t\t\t<OverlayDescription>A tint only — the page behind stays sharp.</OverlayDescription>\n\t\t\t\t\t</OverlayHeader>\n\t\t\t\t</DialogContent>\n\t\t\t</Overlay>\n\n\t\t\t<UIProvider config={{ overlay: { backdropBlur: 4 } }}>\n\t\t\t\t<Overlay>\n\t\t\t\t\t<OverlayTrigger render={<Button tone=\"neutral\" appearance=\"outline\" />}>\n\t\t\t\t\t\tBlurred scrim\n\t\t\t\t\t</OverlayTrigger>\n\t\t\t\t\t<DialogContent>\n\t\t\t\t\t\t<OverlayHeader>\n\t\t\t\t\t\t\t<OverlayTitle>Blurred scrim</OverlayTitle>\n\t\t\t\t\t\t\t<OverlayDescription>The same dialog under a provider that asks for a 4px blur.</OverlayDescription>\n\t\t\t\t\t\t</OverlayHeader>\n\t\t\t\t\t</DialogContent>\n\t\t\t\t</Overlay>\n\t\t\t</UIProvider>\n\t\t</Stack>\n\t)\n}"
3805
3933
  },
3934
+ {
3935
+ "id": "record-detail-reference",
3936
+ "title": "Record detail reference",
3937
+ "route": "/page",
3938
+ "page": "Page & page header",
3939
+ "module": "layout/page",
3940
+ "modules": [
3941
+ "layout/page"
3942
+ ],
3943
+ "supportingModules": [
3944
+ "base/buttons",
3945
+ "base/cards",
3946
+ "base/display",
3947
+ "base/structure",
3948
+ "base/table",
3949
+ "base/typography"
3950
+ ],
3951
+ "import": "themelia-ui/layout/page",
3952
+ "code": "import { useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Card, CardContent, CardHeader } from \"themelia-ui/base/cards\"\nimport { ContentBlock, MetadataList } from \"themelia-ui/base/display\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Text } from \"themelia-ui/base/typography\"\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"themelia-ui/base/table\"\nimport { Page } from \"themelia-ui/layout/page\"\n\nexport default function RecordDetailReference() {\n\tconst [activity, setActivity] = useState(\"No commands yet\")\n\treturn <Page header={{ title: \"Record R-104\", description: \"A record with independent subjects and related records.\", actions: <Button onClick={() => setActivity(\"Route command: export R-104\")}>Export record</Button> }}>\n\t\t<Stack>\n\t\t\t<Card title=\"Identity\" titleLevel={2} headerAction={<Button tone=\"neutral\" appearance=\"outline\" onClick={() => setActivity(\"Identity command: edit R-104\")}>Edit identity</Button>}>\n\t\t\t\t<MetadataList items={[{ id: \"owner\", label: \"Owner\", value: \"Example team\" }, { id: \"state\", label: \"State\", value: \"Active\" }]} />\n\t\t\t\t<ContentBlock title=\"Notes\" surface=\"plain\"><Text>A long description belongs to prose, while the name and status above are facts. Commands stay beside the subject they affect. Repeated records can each have their own headings without repeating the route heading.</Text></ContentBlock>\n\t\t\t</Card>\n\t\t\t<Card><CardHeader title=\"Related records\" titleLevel={2} description=\"The header is composed from parts; omit Card title props when supplying it.\" /><CardContent>\n\t\t\t\t<Table aria-label=\"Related records\"><TableHeader><TableRow><TableHead>Record</TableHead><TableHead>Relationship</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>R-105</TableCell><TableCell>Follow-up</TableCell></TableRow></TableBody></Table>\n\t\t\t</CardContent></Card>\n\t\t\t<Text role=\"status\">{activity}</Text>\n\t\t</Stack>\n\t</Page>\n}"
3953
+ },
3806
3954
  {
3807
3955
  "id": "page-heading",
3808
3956
  "title": "PageHeading",
@@ -4957,6 +5105,28 @@
4957
5105
  "import": "themelia-ui/features/resource",
4958
5106
  "code": "import { PlusIcon } from \"lucide-react\"\n\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { StackedCardsIllustration } from \"themelia-ui/base/feedback\"\nimport { ResourceEmptyState } from \"themelia-ui/features/resource\"\n\nexport default function ResourceEmpty() {\n\treturn (\n\t\t<ResourceEmptyState\n\t\t\tmediaVariant=\"illustration\"\n\t\t\tmedia={<StackedCardsIllustration />}\n\t\t\ttitle=\"No invoices yet\"\n\t\t\tdescription=\"Invoices appear here once a customer is billed.\"\n\t\t\taction={<Button><PlusIcon />New invoice</Button>}\n\t\t\tborder\n\t\t/>\n\t)\n}"
4959
5107
  },
5108
+ {
5109
+ "id": "website-content-reference",
5110
+ "title": "Website content reference",
5111
+ "route": "/rich-text-editor",
5112
+ "page": "Rich text editor",
5113
+ "module": "features/rich-text-editor",
5114
+ "modules": [
5115
+ "base/typography",
5116
+ "features/rich-text-editor"
5117
+ ],
5118
+ "supportingModules": [
5119
+ "base/buttons",
5120
+ "base/cards",
5121
+ "base/choice-inputs",
5122
+ "base/forms",
5123
+ "base/structure",
5124
+ "base/text-inputs",
5125
+ "features/media-library"
5126
+ ],
5127
+ "import": "themelia-ui/features/rich-text-editor",
5128
+ "code": "import { useState } from \"react\"\nimport { Button } from \"themelia-ui/base/buttons\"\nimport { Card } from \"themelia-ui/base/cards\"\nimport { PillRadioGroup } from \"themelia-ui/base/choice-inputs\"\nimport { FormField } from \"themelia-ui/base/forms\"\nimport { Stack } from \"themelia-ui/base/structure\"\nimport { Input } from \"themelia-ui/base/text-inputs\"\nimport { RichText, Text } from \"themelia-ui/base/typography\"\nimport { MediaLibraryDialog, type MediaLibraryItem } from \"themelia-ui/features/media-library\"\nimport { RichTextEditor } from \"themelia-ui/features/rich-text-editor\"\nimport { SAMPLE_MEDIA } from \"../media-library/reference-data\"\n\nexport default function WebsiteContentReference() {\n\tconst [locale, setLocale] = useState(\"en\")\n\tconst [drafts, setDrafts] = useState<Record<string, string>>({ en: \"<p>Welcome to our workspace.</p>\", ar: \"<p>مرحباً بكم</p>\" })\n\tconst [mediaOpen, setMediaOpen] = useState(false)\n\tconst [image, setImage] = useState<MediaLibraryItem>()\n\tconst [alt, setAlt] = useState(\"\")\n\tconst [failed, setFailed] = useState(true)\n\tconst [pending, setPending] = useState(false)\n\tconst [message, setMessage] = useState(\"Drafts are local until saved\")\n\treturn <Card title=\"Website content\" description=\"Each locale owns a retained draft. Preview sanitizes HTML; the application also validates and sanitizes persisted content.\">\n\t\t<Stack>\n\t\t\t<PillRadioGroup aria-label=\"Content locale\" name=\"reference-content-locale\" value={locale} onValueChange={value => value && setLocale(value)} options={[{ value: \"en\", label: \"English\" }, { value: \"ar\", label: \"العربية\" }]} />\n\t\t\t<div dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}><RichTextEditor value={drafts[locale] ?? \"\"} onValueChange={html => setDrafts(current => ({ ...current, [locale]: html }))} /></div>\n\t\t\t<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) }} />\n\t\t\t{image && <FormField label=\"Hero image alternative text\" hint={image.name}><Input value={alt} onChange={event => setAlt(event.target.value)} /></FormField>}\n\t\t\t<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>\n\t\t\t<Text role=\"status\">{message}</Text>\n\t\t\t<RichText html={drafts[locale] ?? \"\"} />\n\t\t</Stack>\n\t</Card>\n}"
5129
+ },
4960
5130
  {
4961
5131
  "id": "editor",
4962
5132
  "title": "The editor and what comes out",
@@ -5500,7 +5670,7 @@
5500
5670
  ],
5501
5671
  "supportingModules": [],
5502
5672
  "import": "themelia-ui/base/structure",
5503
- "code": "import { Grid, GridCell } from \"themelia-ui/base/structure\"\n\nimport { Box } from \"./_shared\"\n\nexport default function GridExample() {\n\treturn (\n\t\t<Grid columns={{ base: 1, md: 3 }} gap=\"sm\">\n\t\t\t<GridCell span=\"full\"><Box>span full</Box></GridCell>\n\t\t\t<GridCell><Box>one</Box></GridCell>\n\t\t\t<GridCell><Box>two</Box></GridCell>\n\t\t\t<GridCell><Box>three</Box></GridCell>\n\t\t\t<GridCell span={{ base: 1, md: 2 }}><Box>span 2 from md</Box></GridCell>\n\t\t\t<GridCell><Box>four</Box></GridCell>\n\t\t</Grid>\n\t)\n}"
5673
+ "code": "import { Grid, GridCell } from \"themelia-ui/base/structure\"\n\nimport { Box } from \"./_shared\"\n\nexport default function GridExample() {\n\treturn (\n\t\t<Grid columns={{ base: 1, md: 3 }} gap=\"sm\">\n\t\t\t<GridCell span=\"full\"><Box>span full</Box></GridCell>\n\t\t\t<GridCell><Box>one</Box></GridCell>\n\t\t\t<GridCell><Box>two</Box></GridCell>\n\t\t\t<GridCell><Box>three</Box></GridCell>\n\t\t\t<GridCell span={{ base: 1, md: 2 }}><Box>span 2 from md</Box></GridCell>\n\t\t\t<GridCell><Box>four</Box></GridCell>\n\t\t\t<GridCell span={{ base: 1, md: \"full\", xl: 2 }}><Box>responsive full row</Box></GridCell>\n\t\t</Grid>\n\t)\n}"
5504
5674
  },
5505
5675
  {
5506
5676
  "id": "adaptivegrid",
@@ -0,0 +1,19 @@
1
+ import { useState } from "react"
2
+ import { Button } from "themelia-ui/base/buttons"
3
+ import { Card } from "themelia-ui/base/cards"
4
+ import { MetadataList } from "themelia-ui/base/display"
5
+ import { Stack } from "themelia-ui/base/structure"
6
+ import { Text } from "themelia-ui/base/typography"
7
+ import { MetricGrid } from "themelia-ui/blocks/analytics"
8
+
9
+ export default function CommunicationsReference() {
10
+ const [retried, setRetried] = useState(false)
11
+ return <Stack>
12
+ <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" />
13
+ <Card title="Latest communication" headerAction={<Button disabled={retried} onClick={() => setRetried(true)}>Retry delivery</Button>}>
14
+ <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" }]} />
15
+ <Text>The application owns delivery policy and permission checks. Metrics describe quantities; this subject and its delivery facts stay in a record surface.</Text>
16
+ </Card>
17
+ <Text role="status">{retried ? "Mock retry completed" : "Mock delivery is waiting"}</Text>
18
+ </Stack>
19
+ }