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
@@ -2,9 +2,10 @@
2
2
 
3
3
  The theme is `src/styles/theme/*.css`: about eighty variables, declared once at `:root`. Every
4
4
  colour is `light-dark(light, dark)`, and the element's `color-scheme` (set by `.light`,
5
- `.dark`, `data-theme` or the operating system) picks the half. Nothing below `:root`
6
- declares a theme variable again, so a value set anywhere reaches every reader below it, and
7
- a dark region inside a light page needs no second set of values.
5
+ `.dark`, `data-theme` or the operating system) picks the half. Component styles inherit
6
+ theme defaults rather than redeclaring them. Explicit scope overrides and density presets
7
+ replace the corresponding inherited values on their regions. A dark region inside a light
8
+ page needs no second set of colour declarations.
8
9
 
9
10
  Components read the theme and compute the rest in the rule that uses it. The package's checks
10
11
  fail on a `var()` nothing defines, a literal colour, radius, spacing or hairline in a
@@ -10,13 +10,13 @@ import { B as p, D as m, E as h, H as g, I as _, L as ee, O as te, S as ne, T as
10
10
  import { t as de } from "./badge-VlJVZjdm.js";
11
11
  import { t as fe } from "./batch-action-bar-DZVMVW83.js";
12
12
  import { p as pe } from "./feedback-D4qVsKlD.js";
13
- import { t as me } from "./skeleton-CubvHel9.js";
14
- import { d as b, v as x } from "./display-BGN834xV.js";
13
+ import { t as me } from "./skeleton-BTKNKTdj.js";
14
+ import { d as b, v as x } from "./display-a7AoX_u7.js";
15
15
  import { t as S } from "./scroll-edges-CWjEJktN.js";
16
- import { x as C } from "./choice-inputs-CCw-0gD6.js";
16
+ import { x as C } from "./choice-inputs-NwRxvmZN.js";
17
17
  import { l as w } from "./text-inputs-BboE77nZ.js";
18
18
  import { t as T } from "./use-latest-CF0678ae.js";
19
- import { s as he } from "./navigation-BTX5Ti0Z.js";
19
+ import { s as he } from "./navigation-Bgu7Vc0M.js";
20
20
  import { r as ge } from "./navigation-B4eUU9E8.js";
21
21
  import { c as _e, i as ve, l as E, n as D, s as O, t as ye } from "./table-Ds_eO9av.js";
22
22
  import { n as k, o as A, t as j } from "./toolbar-DBK-c4yy.js";
@@ -2,7 +2,7 @@ import "#core.css";
2
2
  import { s as e } from "./typography-DmeR05Er.js";
3
3
  import { t } from "./cx--CBrhWH0.js";
4
4
  import { t as n } from "./strings-Co9_qArR.js";
5
- import { d as r } from "./display-BGN834xV.js";
5
+ import { d as r } from "./display-a7AoX_u7.js";
6
6
  import { CheckIcon as i } from "lucide-react";
7
7
  import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
8
8
  import "#css/timeline.css";
@@ -5,7 +5,7 @@ import { t as n } from "./strings-Co9_qArR.js";
5
5
  import { B as r, T as i } from "./primitives-BskKlf35.js";
6
6
  import { t as a } from "./badge-VlJVZjdm.js";
7
7
  import { s as o } from "./feedback-D4qVsKlD.js";
8
- import { r as s, t as c } from "./timeline-BRZbSdhf.js";
8
+ import { r as s, t as c } from "./timeline-BR_jNgvz.js";
9
9
  import { CheckIcon as l, CircleIcon as u, ClockIcon as d, XIcon as f } from "lucide-react";
10
10
  import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
11
11
  import "#css/timelines.css";
@@ -6,7 +6,7 @@ import { t as r } from "./strings-Co9_qArR.js";
6
6
  import { C as i, S as a } from "./primitives-BskKlf35.js";
7
7
  import { t as o } from "./badge-VlJVZjdm.js";
8
8
  import { h as s } from "./feedback-D4qVsKlD.js";
9
- import { d as c } from "./display-BGN834xV.js";
9
+ import { d as c } from "./display-a7AoX_u7.js";
10
10
  import { CheckIcon as l, CircleAlertIcon as u, ClockIcon as d, FileIcon as f, ImageIcon as p, ImagePlusIcon as m, Loader2Icon as h, PaperclipIcon as g, PencilIcon as _, RotateCwIcon as v, TrashIcon as y, UploadCloudIcon as b, UserIcon as x, XIcon as S } from "lucide-react";
11
11
  import { useCallback as C, useEffect as w, useId as T, useMemo as E, useRef as D, useState as O } from "react";
12
12
  import { Fragment as k, jsx as A, jsxs as j } from "react/jsx-runtime";
@@ -1,11 +1,11 @@
1
1
  import "#core.css";
2
2
  import { c as e, s as t } from "./typography-DmeR05Er.js";
3
3
  import { t as n } from "./cx--CBrhWH0.js";
4
- import { o as r } from "./structure-B9Y5NkP_.js";
5
- import { d as i } from "./display-BGN834xV.js";
6
- import { s as a } from "./choice-inputs-CCw-0gD6.js";
4
+ import { o as r } from "./structure-CynEZ-OY.js";
5
+ import { d as i } from "./display-a7AoX_u7.js";
6
+ import { s as a } from "./choice-inputs-NwRxvmZN.js";
7
7
  import { d as o, l as s } from "./text-inputs-BboE77nZ.js";
8
- import { c, u as l } from "./date-pickers-C2HgjpFU.js";
8
+ import { c, u as l } from "./date-pickers-BwCwKIVM.js";
9
9
  import { XIcon as u } from "lucide-react";
10
10
  import * as d from "react";
11
11
  import { forwardRef as f, useCallback as p, useId as m, useMemo as h, useRef as g, useState as _ } from "react";
package/docs/README.md CHANGED
@@ -4,7 +4,7 @@ Documents are grouped by audience. The first three directories ship in the npm p
4
4
 
5
5
  | Directory | Audience | What belongs there |
6
6
  |---|---|---|
7
- | `learn/` | **Learn** | Installation, composition, framework wiring, provider and scoping, theming, forms, i18n, troubleshooting, verification, and migration policy. |
7
+ | `learn/` | **Learn** | Installation, composition, structure and spacing, subject blocks, framework wiring, provider and scoping, theming, forms, i18n, troubleshooting, verification, and migration policy. |
8
8
  | `generated/` | **Components** | The exact public API — imports, props, types, recipes, and choose/avoid guidance. Written by generators, never edited by hand. |
9
9
  | `build/` | **Build** | Adaptable live-preview recipes, with surrounding app-owned responsibilities called out. |
10
10
  | `adr/` | Decisions | Why an expensive or irreversible choice was made, and what evidence would justify revisiting it. Repository only. |
@@ -21,6 +21,8 @@ docs/generated/profiles.md which modules each profile ships
21
21
  docs/generated/component-index.json the machine-readable catalogue the component finder and the skill read
22
22
  docs/generated/components/ one complete API reference per module
23
23
  docs/generated/recipes.json live-preview recipes, keyed by task
24
+ docs/generated/workflows/index.json complete workflow source/helper inventory and package version
25
+ docs/generated/consumer-contract.json generated export and private-style facts for read-only diagnosis
24
26
  ```
25
27
 
26
28
  ## Where to start
@@ -30,6 +32,14 @@ Start with [Installation](./learn/installation.md), then read
30
32
  [component API](./generated/components/INDEX.md), use [Framework wiring](./learn/framework-wiring.md)
31
33
  to connect application policy, and finish with the [consumer verification](./learn/verification.md)
32
34
  matrix. [Troubleshooting](./learn/troubleshooting.md) covers the common integration failures.
35
+ Use [complete workflows](./learn/reference-workflows.md) where several modules meet.
36
+
37
+ For layout decisions, use [Structure and spacing](./learn/structure-and-spacing.md): page
38
+ scroll, gutters, reading widths, responsive columns, surface insets and the two gaps.
39
+ [Choosing blocks](./learn/blocks.md) maps ready subject presentations to their data,
40
+ permission and mutation seams. Use [Theming](./learn/theming.md) for global geometry and
41
+ type, and [Provider and scoping](./learn/provider-and-scoping.md) for regional overrides
42
+ and portals.
33
43
 
34
44
  To find a component by task, run the offline finder; `--explain` adds the matched
35
45
  components, selection reasons and API anchors:
@@ -0,0 +1,28 @@
1
+ /** Caller supplies an already open fixture. This observer performs no navigation or actions. */
2
+ export interface CompositionPage {
3
+ evaluate<Result, Arg>(callback: (arg: Arg) => Result, arg: Arg): Promise<Result>
4
+ }
5
+ export async function observeComposition(page: CompositionPage, selector: string) {
6
+ return page.evaluate(rootSelector => {
7
+ const root = document.querySelector<HTMLElement>(rootSelector)
8
+ if (!root) return { complete: false, reason: "Caller fixture root is missing", findings: [] }
9
+ const visible = (element: Element) => {
10
+ const box = element.getBoundingClientRect(), style = getComputedStyle(element)
11
+ return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none"
12
+ }
13
+ const findings: { ruleId: string; evidence: string; severity: "warning" }[] = []
14
+ if (root.scrollWidth > root.clientWidth + 1) findings.push({ ruleId: "horizontal-overflow", severity: "warning", evidence: `scrollWidth ${root.scrollWidth}; clientWidth ${root.clientWidth}` })
15
+ const headings = [...root.querySelectorAll("h1,h2,h3,h4,h5,h6,[role=heading]")].filter(visible)
16
+ for (const heading of headings) {
17
+ const text = heading.textContent?.trim()
18
+ const sameScope = headings.filter(other => other !== heading && other.parentElement === heading.parentElement && other.textContent?.trim() === text)
19
+ if (text && sameScope.length) findings.push({ ruleId: "duplicate-heading", severity: "warning", evidence: `${text}; same parent, ${sameScope.length + 1} occurrences` })
20
+ }
21
+ for (const control of root.querySelectorAll("button,input,select,textarea,a[href]")) {
22
+ if (!visible(control) || control.closest("[hidden],[inert],[aria-hidden=true]")) continue
23
+ const box = control.getBoundingClientRect()
24
+ if (box.right < 0 || box.left > innerWidth) findings.push({ ruleId: "control-outside-viewport", severity: "warning", evidence: `${control.getAttribute("aria-label") ?? control.textContent?.trim() ?? control.tagName}; left ${box.left}; right ${box.right}` })
25
+ }
26
+ return { complete: true, reason: "Observed caller fixture geometry and visible DOM only", findings }
27
+ }, selector)
28
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Recipes
4
4
 
5
- 400 adaptable live-preview snippets, taken from the pages that render them —
5
+ 409 adaptable live-preview snippets, taken from the pages that render them —
6
6
  so a recipe here and the example on the page cannot drift apart. Excerpts may depend on
7
7
  surrounding values supplied by the application. Each section names the exact
8
8
  import for the module.
@@ -3729,6 +3729,57 @@ export default function FieldGroupExample() {
3729
3729
  }
3730
3730
  ```
3731
3731
 
3732
+ ### Multi-section form reference
3733
+
3734
+ ```tsx
3735
+ import { useRef, useState } from "react"
3736
+ import { Card } from "themelia-ui/base/cards"
3737
+ import { ContentBlock } from "themelia-ui/base/display"
3738
+ import { FieldGroup, FormField, FormSection, FormActionsBar, SubmitStateButton } from "themelia-ui/base/forms"
3739
+ import { Stack } from "themelia-ui/base/structure"
3740
+ import { Input } from "themelia-ui/base/text-inputs"
3741
+ import { Text } from "themelia-ui/base/typography"
3742
+ import { useFormFieldBinding, useStateFormControl, type FormControl } from "themelia-ui/forms"
3743
+
3744
+ type Draft = { firstName: string; lastName: string; company: string }
3745
+ function BoundField({ control, name, label, error }: { control: FormControl<Draft>; name: keyof Draft; label: string; error?: string }) {
3746
+ const field = useFormFieldBinding<string, Draft>({ control, name })
3747
+ 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>
3748
+ }
3749
+
3750
+ export default function MultiSectionReference() {
3751
+ const control = useStateFormControl<Draft>({ firstName: "Alex", lastName: "Example", company: "Example team" })
3752
+ const [pending, setPending] = useState(false)
3753
+ const [error, setError] = useState<string>()
3754
+ const [rootError, setRootError] = useState<string>()
3755
+ const [saved, setSaved] = useState(0)
3756
+ const running = useRef(false)
3757
+ return <Stack>
3758
+ <Card title="Account details" description="One outer surface, editable sections with retained region insets.">
3759
+ <form onSubmit={async event => {
3760
+ event.preventDefault(); if (running.current) return
3761
+ running.current = true; setPending(true); setError(undefined); setRootError(undefined)
3762
+ try {
3763
+ await new Promise(resolve => setTimeout(resolve, 250))
3764
+ if (control.values.company === "offline") throw new Error("Request failed. Your values remain here.")
3765
+ if (control.values.company.length < 3) { setError("Company must have at least three characters."); return }
3766
+ setSaved(count => count + 1)
3767
+ } catch (failure) { setRootError((failure as Error).message) }
3768
+ finally { running.current = false; setPending(false) }
3769
+ }}>
3770
+ <Stack gap="none">
3771
+ {rootError && <Text role="alert" type="error">{rootError}</Text>}
3772
+ <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>
3773
+ <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>
3774
+ <FormActionsBar leading={<Text role="status">Successful submissions {saved}</Text>}><SubmitStateButton type="submit" state={pending ? "submitting" : "idle"} strings={{ idle: "Save account" }} /></FormActionsBar>
3775
+ </Stack>
3776
+ </form>
3777
+ </Card>
3778
+ <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>
3779
+ </Stack>
3780
+ }
3781
+ ```
3782
+
3732
3783
  ### FormSection
3733
3784
 
3734
3785
  ```tsx
@@ -6274,6 +6325,7 @@ export default function GridExample() {
6274
6325
  <GridCell><Box>three</Box></GridCell>
6275
6326
  <GridCell span={{ base: 1, md: 2 }}><Box>span 2 from md</Box></GridCell>
6276
6327
  <GridCell><Box>four</Box></GridCell>
6328
+ <GridCell span={{ base: 1, md: "full", xl: 2 }}><Box>responsive full row</Box></GridCell>
6277
6329
  </Grid>
6278
6330
  )
6279
6331
  }
@@ -8718,6 +8770,30 @@ import { … } from "themelia-ui/blocks/analytics"
8718
8770
 
8719
8771
  Preview route: Analytics — `/analytics`
8720
8772
 
8773
+ ### Communications overview reference
8774
+
8775
+ ```tsx
8776
+ import { useState } from "react"
8777
+ import { Button } from "themelia-ui/base/buttons"
8778
+ import { Card } from "themelia-ui/base/cards"
8779
+ import { MetadataList } from "themelia-ui/base/display"
8780
+ import { Stack } from "themelia-ui/base/structure"
8781
+ import { Text } from "themelia-ui/base/typography"
8782
+ import { MetricGrid } from "themelia-ui/blocks/analytics"
8783
+
8784
+ export default function CommunicationsReference() {
8785
+ const [retried, setRetried] = useState(false)
8786
+ return <Stack>
8787
+ <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" />
8788
+ <Card title="Latest communication" headerAction={<Button disabled={retried} onClick={() => setRetried(true)}>Retry delivery</Button>}>
8789
+ <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" }]} />
8790
+ <Text>The application owns delivery policy and permission checks. Metrics describe quantities; this subject and its delivery facts stay in a record surface.</Text>
8791
+ </Card>
8792
+ <Text role="status">{retried ? "Mock retry completed" : "Mock delivery is waiting"}</Text>
8793
+ </Stack>
8794
+ }
8795
+ ```
8796
+
8721
8797
  ### Metric bar
8722
8798
 
8723
8799
  ```tsx
@@ -10007,6 +10083,61 @@ import { … } from "themelia-ui/features/combobox"
10007
10083
 
10008
10084
  Preview route: Combobox — `/combobox`
10009
10085
 
10086
+ ### Scoped resource selection
10087
+
10088
+ ```tsx
10089
+ import { useState } from "react"
10090
+ import { Button } from "themelia-ui/base/buttons"
10091
+ import { FormField } from "themelia-ui/base/forms"
10092
+ import { Overlay, OverlayTrigger, OverlayContent, OverlayHeader, OverlayTitle, OverlayDescription, OverlayBody } from "themelia-ui/base/overlay"
10093
+ import { Stack } from "themelia-ui/base/structure"
10094
+ import { Text } from "themelia-ui/base/typography"
10095
+ import { ResourceCombobox } from "themelia-ui/features/combobox"
10096
+
10097
+ type Resource = { id: string; label: string }
10098
+ type Mode = "results" | "pending" | "empty" | "error"
10099
+
10100
+ export default function ScopedResourceReference() {
10101
+ const [scope, setScope] = useState("A")
10102
+ const [mode, setMode] = useState<Mode>("results")
10103
+ const [query, setQuery] = useState("")
10104
+ const [value, setValue] = useState<Resource | null>({ id: "A-1", label: "A project" })
10105
+ const [selections, setSelections] = useState(0)
10106
+ const [submissions, setSubmissions] = useState(0)
10107
+ const [completions, setCompletions] = useState(0)
10108
+ return <Overlay>
10109
+ <OverlayTrigger render={<Button />}>Choose scoped resource</OverlayTrigger>
10110
+ <OverlayContent>
10111
+ <OverlayHeader><OverlayTitle>Assign resource</OverlayTitle><OverlayDescription>Lookup validity belongs to a workspace. The application decides whether a selected resource remains valid.</OverlayDescription></OverlayHeader>
10112
+ <OverlayBody>
10113
+ <form onSubmit={event => { event.preventDefault(); setSubmissions(count => count + 1) }}>
10114
+ <Stack>
10115
+ <Stack direction="horizontal" gap="sm" wrap>
10116
+ {["A", "B"].map(key => <Button key={key} tone="neutral" appearance="outline" onClick={() => { setScope(key); setValue(null) }}>Workspace {key}</Button>)}
10117
+ </Stack>
10118
+ <Stack direction="horizontal" gap="sm" wrap>
10119
+ {(["results", "pending", "empty", "error"] as const).map(key => <Button key={key} tone="neutral" appearance="ghost" onClick={() => setMode(key)}>{key}</Button>)}
10120
+ </Stack>
10121
+ <FormField label="Resource" hint={`Searching workspace ${scope}`}>
10122
+ <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}
10123
+ fetcher={async ({ query: search, signal }) => {
10124
+ // The mock deliberately ignores abort; the picker must suppress its late answer.
10125
+ void signal
10126
+ await new Promise(resolve => setTimeout(resolve, mode === "pending" ? 2400 : scope === "A" ? 600 : 80))
10127
+ if (mode === "error") throw new Error("Lookup unavailable")
10128
+ return mode === "empty" || (search && !"project".includes(search.toLowerCase())) ? [] : [{ id: `${scope}-1`, label: `${scope} project` }]
10129
+ }} onAfterFetch={() => setCompletions(count => count + 1)} />
10130
+ </FormField>
10131
+ <Button type="submit">Assign</Button>
10132
+ <Text role="status">Selections {selections}; submissions {submissions}; completions {completions}</Text>
10133
+ </Stack>
10134
+ </form>
10135
+ </OverlayBody>
10136
+ </OverlayContent>
10137
+ </Overlay>
10138
+ }
10139
+ ```
10140
+
10010
10141
  ### A picker that fetches
10011
10142
 
10012
10143
  ```tsx
@@ -10599,6 +10730,62 @@ import { … } from "themelia-ui/features/data-view"
10599
10730
 
10600
10731
  Preview route: Data view & data table — `/data-view`
10601
10732
 
10733
+ ### Server resource index reference
10734
+
10735
+ ```tsx
10736
+ import { useEffect, useRef, useState } from "react"
10737
+ import { Button } from "themelia-ui/base/buttons"
10738
+ import { Stack } from "themelia-ui/base/structure"
10739
+ import { Heading, Text } from "themelia-ui/base/typography"
10740
+ import { DataView, type DataViewProps } from "themelia-ui/features/data-view"
10741
+ import { FilterType, type ActiveFilter, type FilterConfig } from "themelia-ui/features/filters"
10742
+
10743
+ type RecordRow = { id: string; name: string; status: string; searchTerms: string }
10744
+ 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" }))
10745
+ const COLUMNS: DataViewProps<RecordRow>["columns"] = [{ accessorKey: "name", header: "Resource" }, { accessorKey: "status", header: "Status" }]
10746
+ 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" }] }]
10747
+
10748
+ export default function ResourceIndexReference() {
10749
+ const [filters, setFilters] = useState<ActiveFilter[]>([])
10750
+ const [page, setPage] = useState(0)
10751
+ const [rows, setRows] = useState<RecordRow[]>([])
10752
+ const [hasNext, setHasNext] = useState(false)
10753
+ const [pending, setPending] = useState(true)
10754
+ const [error, setError] = useState<string>()
10755
+ const [refresh, setRefresh] = useState(0)
10756
+ const [selection, setSelection] = useState<Record<string, true>>({})
10757
+ const [answeredQuery, setAnsweredQuery] = useState("")
10758
+ const [commands, setCommands] = useState("")
10759
+ const failNext = useRef(false)
10760
+ useEffect(() => {
10761
+ let active = true
10762
+ async function load() {
10763
+ setPending(true); setError(undefined)
10764
+ await new Promise(resolve => setTimeout(resolve, 250))
10765
+ if (!active) return
10766
+ if (failNext.current) { failNext.current = false; setError("Refresh failed. Previous rows remain available."); setPending(false); return }
10767
+ const search = filters.find(filter => filter.key === "search")?.value[0]?.toLowerCase() ?? ""
10768
+ const status = filters.find(filter => filter.key === "status")?.value[0]
10769
+ const matches = RECORDS.filter(row => `${row.name} ${row.searchTerms}`.toLowerCase().includes(search) && (!status || row.status === status))
10770
+ setAnsweredQuery(search); setRows(matches.slice(page * 2, page * 2 + 2)); setHasNext(matches.length > page * 2 + 2); setPending(false)
10771
+ }
10772
+ void load()
10773
+ return () => { active = false }
10774
+ }, [filters, page, refresh])
10775
+ return <div style={{ width: "100%", minWidth: 0, maxWidth: "48rem" }}>
10776
+ <Stack>
10777
+ <Heading level={3}>Resource index</Heading>
10778
+ <Text type="secondary">Server matching includes fields absent from the visible columns. Totals are unknown; selection applies to the current page.</Text>
10779
+ {error && <Text role="alert" type="error">{error}</Text>}
10780
+ <DataView data={rows} columns={COLUMNS} filtering={{ filters: FILTERS, activeFilters: filters, onFilterChange: next => { setFilters(next); setPage(0); setSelection({}) }, filterRows: ({ data }) => [...data], isFiltering: pending }}
10781
+ 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> }}
10782
+ 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> }} />
10783
+ <Text role="status">{pending ? "Loading page" : `${rows.length} rows supplied by server; matched ${answeredQuery || "all"}`}{commands && `; ${commands}`}</Text>
10784
+ </Stack>
10785
+ </div>
10786
+ }
10787
+ ```
10788
+
10602
10789
  ### An index
10603
10790
 
10604
10791
  ```tsx
@@ -11385,6 +11572,74 @@ import { … } from "themelia-ui/features/media-library"
11385
11572
 
11386
11573
  Preview route: Media library — `/media-library`
11387
11574
 
11575
+ ### Media picking reference
11576
+
11577
+ ```tsx
11578
+ import { useEffect, useMemo, useState } from "react"
11579
+ import { Button } from "themelia-ui/base/buttons"
11580
+ import { Stack } from "themelia-ui/base/structure"
11581
+ import { Text } from "themelia-ui/base/typography"
11582
+ import { MediaLibraryDialog, type MediaLibraryItem, type MediaLibraryFetcher } from "themelia-ui/features/media-library"
11583
+
11584
+ import { SAMPLE_MEDIA } from "./reference-data"
11585
+
11586
+ export default function MediaPickingReference() {
11587
+ const [open, setOpen] = useState(false)
11588
+ const [scope, setScope] = useState("site")
11589
+ const [fail, setFail] = useState(false)
11590
+ const [selected, setSelected] = useState<string[]>(["unavailable-id"])
11591
+ const [pickerDraft, setPickerDraft] = useState(selected)
11592
+ const [resolved, setResolved] = useState<{ scope: string; ids: string[]; items: MediaLibraryItem[] }>()
11593
+ const [page, setPage] = useState(0)
11594
+ const [query, setQuery] = useState("")
11595
+ const [pending, setPending] = useState(true)
11596
+ const [hasNext, setHasNext] = useState(false)
11597
+ // A saved id is resolved separately from the current browser page, with scope access applied.
11598
+ useEffect(() => {
11599
+ const controller = new AbortController()
11600
+ const timer = setTimeout(() => {
11601
+ if (!controller.signal.aborted) setResolved({ scope, ids: selected, items: SAMPLE_MEDIA.filter(item => selected.includes(item.id) && item.collection === scope) })
11602
+ }, 150)
11603
+ return () => { controller.abort(); clearTimeout(timer) }
11604
+ }, [selected, scope])
11605
+ const chosen = resolved?.scope === scope && resolved.ids === selected ? resolved.items : []
11606
+ const unavailable = resolved?.scope === scope && resolved.ids === selected && chosen.length < selected.length
11607
+ const changeOpen = (next: boolean) => {
11608
+ if (next) setPickerDraft(selected)
11609
+ setOpen(next)
11610
+ }
11611
+ const fetcher = useMemo<MediaLibraryFetcher>(() => async ({ query: search, signal }) => {
11612
+ setPending(true)
11613
+ try {
11614
+ await new Promise(resolve => setTimeout(resolve, 200))
11615
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
11616
+ if (fail) throw new Error("Media lookup unavailable")
11617
+ const matches = SAMPLE_MEDIA.filter(item => item.collection === scope && item.name.toLowerCase().includes(search.toLowerCase()))
11618
+ const window = matches.slice(page, page + 2)
11619
+ setHasNext(window.length > 1)
11620
+ return { items: window.slice(0, 1) }
11621
+ } finally { if (!signal?.aborted) setPending(false) }
11622
+ }, [scope, fail, page])
11623
+ return <Stack>
11624
+ {unavailable && <Text role="alert">The saved asset is unavailable. Choose a replacement; the draft id is retained until then.</Text>}
11625
+ <Stack direction="horizontal" gap="sm" wrap>
11626
+ <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}
11627
+ strings={{ showingCount: shown => `${shown} loaded on this page; total unknown` }}
11628
+ slots={{ footer: <Stack direction="horizontal" gap="sm" wrap align="center">
11629
+ <Text>Media page {page + 1}; total unknown</Text>
11630
+ <Button disabled={pending || page === 0} onClick={() => { setPage(value => value - 1); setPending(true) }}>Previous media page</Button>
11631
+ <Button disabled={pending || !hasNext} onClick={() => { setPage(value => value + 1); setPending(true) }}>Next media page</Button>
11632
+ </Stack> }}
11633
+ onConfirm={(items, ids) => { setSelected(ids); setResolved({ scope, ids, items }); setOpen(false) }} />
11634
+ <Button tone="neutral" appearance="outline" onClick={() => setSelected(["image-2"])}>Load saved media draft</Button>
11635
+ <Button tone="neutral" appearance="outline" onClick={() => { setScope(value => value === "site" ? "other" : "site"); setPage(0); setPending(true) }}>Switch media scope</Button>
11636
+ <Button tone="neutral" appearance="outline" onClick={() => { setFail(value => !value); setPending(true) }}>Toggle media failure</Button>
11637
+ </Stack>
11638
+ <Text role="status">{chosen.length ? `Hydrated selection: ${chosen.map(item => `${item.name} — ${item.alt}`).join(", ")}` : `Saved draft ids: ${selected.join(", ")}`}</Text>
11639
+ </Stack>
11640
+ }
11641
+ ```
11642
+
11388
11643
  ### Asset manager
11389
11644
 
11390
11645
  ```tsx
@@ -11933,6 +12188,50 @@ import { … } from "themelia-ui/features/overlays"
11933
12188
 
11934
12189
  Preview route: Action overlays — `/action-overlays`
11935
12190
 
12191
+ ### Nested action flow
12192
+
12193
+ ```tsx
12194
+ import { useRef, useState } from "react"
12195
+ import { Button } from "themelia-ui/base/buttons"
12196
+ import { FormField } from "themelia-ui/base/forms"
12197
+ import { Stack } from "themelia-ui/base/structure"
12198
+ import { Input } from "themelia-ui/base/text-inputs"
12199
+ import { Text } from "themelia-ui/base/typography"
12200
+ import { ActionDialog } from "themelia-ui/features/overlays"
12201
+
12202
+ export default function NestedActionReference() {
12203
+ const [open, setOpen] = useState(false)
12204
+ const [child, setChild] = useState(false)
12205
+ const [name, setName] = useState("Draft record")
12206
+ const [error, setError] = useState<string>()
12207
+ const [saved, setSaved] = useState(0)
12208
+ const session = useRef(0)
12209
+ const changeOpen = (next: boolean) => { session.current++; setOpen(next); setChild(false) }
12210
+ return <Stack>
12211
+ <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}>
12212
+ <Stack>
12213
+ <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>
12214
+ <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}
12215
+ alertMessage={error} onError={() => setError("Could not save; your draft is still here.")}
12216
+ onAsyncConfirm={async () => {
12217
+ const attempt = session.current
12218
+ await new Promise(resolve => setTimeout(resolve, 300))
12219
+ if (attempt !== session.current) return
12220
+ if (name === "offline") throw new Error("Unavailable")
12221
+ if (name.trim().length < 3) { setError("Enter at least three characters."); return }
12222
+ setError(undefined); setSaved(count => count + 1); changeOpen(false)
12223
+ }}>
12224
+ <Text>{name}</Text>
12225
+ </ActionDialog>
12226
+ {/* A native form can instead use formId: its submit handler owns validation and closure. */}
12227
+ <Text size="xs" type="secondary">Confirmation {child ? "open" : "closed"}</Text>
12228
+ </Stack>
12229
+ </ActionDialog>
12230
+ <Text role="status">Successful saves {saved}</Text>
12231
+ </Stack>
12232
+ }
12233
+ ```
12234
+
11936
12235
  ### ActionDialog
11937
12236
 
11938
12237
  ```tsx
@@ -12966,6 +13265,44 @@ import { … } from "themelia-ui/features/rich-text-editor"
12966
13265
 
12967
13266
  Preview route: Rich text editor — `/rich-text-editor`
12968
13267
 
13268
+ ### Website content reference
13269
+
13270
+ ```tsx
13271
+ import { useState } from "react"
13272
+ import { Button } from "themelia-ui/base/buttons"
13273
+ import { Card } from "themelia-ui/base/cards"
13274
+ import { PillRadioGroup } from "themelia-ui/base/choice-inputs"
13275
+ import { FormField } from "themelia-ui/base/forms"
13276
+ import { Stack } from "themelia-ui/base/structure"
13277
+ import { Input } from "themelia-ui/base/text-inputs"
13278
+ import { RichText, Text } from "themelia-ui/base/typography"
13279
+ import { MediaLibraryDialog, type MediaLibraryItem } from "themelia-ui/features/media-library"
13280
+ import { RichTextEditor } from "themelia-ui/features/rich-text-editor"
13281
+ import { SAMPLE_MEDIA } from "../media-library/reference-data"
13282
+
13283
+ export default function WebsiteContentReference() {
13284
+ const [locale, setLocale] = useState("en")
13285
+ const [drafts, setDrafts] = useState<Record<string, string>>({ en: "<p>Welcome to our workspace.</p>", ar: "<p>مرحباً بكم</p>" })
13286
+ const [mediaOpen, setMediaOpen] = useState(false)
13287
+ const [image, setImage] = useState<MediaLibraryItem>()
13288
+ const [alt, setAlt] = useState("")
13289
+ const [failed, setFailed] = useState(true)
13290
+ const [pending, setPending] = useState(false)
13291
+ const [message, setMessage] = useState("Drafts are local until saved")
13292
+ return <Card title="Website content" description="Each locale owns a retained draft. Preview sanitizes HTML; the application also validates and sanitizes persisted content.">
13293
+ <Stack>
13294
+ <PillRadioGroup aria-label="Content locale" name="reference-content-locale" value={locale} onValueChange={value => value && setLocale(value)} options={[{ value: "en", label: "English" }, { value: "ar", label: "العربية" }]} />
13295
+ <div dir={locale === "ar" ? "rtl" : "ltr"}><RichTextEditor value={drafts[locale] ?? ""} onValueChange={html => setDrafts(current => ({ ...current, [locale]: html }))} /></div>
13296
+ <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) }} />
13297
+ {image && <FormField label="Hero image alternative text" hint={image.name}><Input value={alt} onChange={event => setAlt(event.target.value)} /></FormField>}
13298
+ <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>
13299
+ <Text role="status">{message}</Text>
13300
+ <RichText html={drafts[locale] ?? ""} />
13301
+ </Stack>
13302
+ </Card>
13303
+ }
13304
+ ```
13305
+
12969
13306
  ### The editor and what comes out
12970
13307
 
12971
13308
  ```tsx
@@ -13840,6 +14177,37 @@ import { … } from "themelia-ui/layout/app-shell"
13840
14177
 
13841
14178
  Preview route: App shell — `/app-shell`
13842
14179
 
14180
+ ### Workspace navigation reference
14181
+
14182
+ ```tsx
14183
+ import { useState } from "react"
14184
+ import { Text } from "themelia-ui/base/typography"
14185
+ import { TopbarSidebarLayout } from "themelia-ui/layout/app-shell"
14186
+ import { AppSidebar, type LinkRenderer } from "themelia-ui/layout/sidebar"
14187
+
14188
+ export default function WorkspaceNavigationReference() {
14189
+ const [route, setRoute] = useState("/a/overview")
14190
+ const [navigations, setNavigations] = useState(0)
14191
+ const workspace = route.startsWith("/b/") ? "B" : "A"
14192
+ const renderLink: LinkRenderer = ({ href, children, external, active, disabled, onClick, ...props }) => {
14193
+ void active
14194
+ if (disabled || !href) return <span {...props}>{children}</span>
14195
+ return <a {...props} href={href} target={props.target ?? (external ? "_blank" : undefined)} rel={props.rel ?? (external ? "noopener noreferrer" : undefined)} onClick={event => {
14196
+ onClick?.(event)
14197
+ if (event.defaultPrevented || external || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
14198
+ event.preventDefault(); setRoute(href); setNavigations(count => count + 1)
14199
+ }}>{children}</a>
14200
+ }
14201
+ return <div style={{ width: "100%", height: "28rem", minWidth: 0 }}>
14202
+ <TopbarSidebarLayout contained contentRender={<div />} logo="Reference workspace" sidebarProviderProps={{ persist: false, keyboardShortcut: false }} sidebar={<AppSidebar logo={`Workspace ${workspace}`} renderLink={renderLink} currentUrl={route}
14203
+ workspaceLinks={[{ 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" }]}
14204
+ navigationGroups={{ "": [{ label: `${workspace} overview`, href: `/${workspace.toLowerCase()}/overview` }, { label: `${workspace} settings`, href: `/${workspace.toLowerCase()}/settings` }] }} />}>
14205
+ <Text role="status">Workspace {workspace}; route {route}; navigations {navigations}</Text>
14206
+ </TopbarSidebarLayout>
14207
+ </div>
14208
+ }
14209
+ ```
14210
+
13843
14211
  ### The shell
13844
14212
 
13845
14213
  ```tsx
@@ -14538,6 +14906,35 @@ import { … } from "themelia-ui/layout/page"
14538
14906
 
14539
14907
  Preview route: Page & page header — `/page`
14540
14908
 
14909
+ ### Record detail reference
14910
+
14911
+ ```tsx
14912
+ import { useState } from "react"
14913
+ import { Button } from "themelia-ui/base/buttons"
14914
+ import { Card, CardContent, CardHeader } from "themelia-ui/base/cards"
14915
+ import { ContentBlock, MetadataList } from "themelia-ui/base/display"
14916
+ import { Stack } from "themelia-ui/base/structure"
14917
+ import { Text } from "themelia-ui/base/typography"
14918
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "themelia-ui/base/table"
14919
+ import { Page } from "themelia-ui/layout/page"
14920
+
14921
+ export default function RecordDetailReference() {
14922
+ const [activity, setActivity] = useState("No commands yet")
14923
+ return <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> }}>
14924
+ <Stack>
14925
+ <Card title="Identity" titleLevel={2} headerAction={<Button tone="neutral" appearance="outline" onClick={() => setActivity("Identity command: edit R-104")}>Edit identity</Button>}>
14926
+ <MetadataList items={[{ id: "owner", label: "Owner", value: "Example team" }, { id: "state", label: "State", value: "Active" }]} />
14927
+ <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>
14928
+ </Card>
14929
+ <Card><CardHeader title="Related records" titleLevel={2} description="The header is composed from parts; omit Card title props when supplying it." /><CardContent>
14930
+ <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>
14931
+ </CardContent></Card>
14932
+ <Text role="status">{activity}</Text>
14933
+ </Stack>
14934
+ </Page>
14935
+ }
14936
+ ```
14937
+
14541
14938
  ### PageHeader
14542
14939
 
14543
14940
  ```tsx