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
@@ -947,6 +947,74 @@ Kind: interface.
947
947
 
948
948
  Preview route: Media library — `/media-library`
949
949
 
950
+ ### Media picking reference
951
+
952
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
953
+ import { useEffect, useMemo, useState } from "react"
954
+ import { Button } from "themelia-ui/base/buttons"
955
+ import { Stack } from "themelia-ui/base/structure"
956
+ import { Text } from "themelia-ui/base/typography"
957
+ import { MediaLibraryDialog, type MediaLibraryItem, type MediaLibraryFetcher } from "themelia-ui/features/media-library"
958
+
959
+ import { SAMPLE_MEDIA } from "./reference-data"
960
+
961
+ export default function MediaPickingReference() {
962
+ const [open, setOpen] = useState(false)
963
+ const [scope, setScope] = useState("site")
964
+ const [fail, setFail] = useState(false)
965
+ const [selected, setSelected] = useState<string[]>(["unavailable-id"])
966
+ const [pickerDraft, setPickerDraft] = useState(selected)
967
+ const [resolved, setResolved] = useState<{ scope: string; ids: string[]; items: MediaLibraryItem[] }>()
968
+ const [page, setPage] = useState(0)
969
+ const [query, setQuery] = useState("")
970
+ const [pending, setPending] = useState(true)
971
+ const [hasNext, setHasNext] = useState(false)
972
+ // A saved id is resolved separately from the current browser page, with scope access applied.
973
+ useEffect(() => {
974
+ const controller = new AbortController()
975
+ const timer = setTimeout(() => {
976
+ if (!controller.signal.aborted) setResolved({ scope, ids: selected, items: SAMPLE_MEDIA.filter(item => selected.includes(item.id) && item.collection === scope) })
977
+ }, 150)
978
+ return () => { controller.abort(); clearTimeout(timer) }
979
+ }, [selected, scope])
980
+ const chosen = resolved?.scope === scope && resolved.ids === selected ? resolved.items : []
981
+ const unavailable = resolved?.scope === scope && resolved.ids === selected && chosen.length < selected.length
982
+ const changeOpen = (next: boolean) => {
983
+ if (next) setPickerDraft(selected)
984
+ setOpen(next)
985
+ }
986
+ const fetcher = useMemo<MediaLibraryFetcher>(() => async ({ query: search, signal }) => {
987
+ setPending(true)
988
+ try {
989
+ await new Promise(resolve => setTimeout(resolve, 200))
990
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
991
+ if (fail) throw new Error("Media lookup unavailable")
992
+ const matches = SAMPLE_MEDIA.filter(item => item.collection === scope && item.name.toLowerCase().includes(search.toLowerCase()))
993
+ const window = matches.slice(page, page + 2)
994
+ setHasNext(window.length > 1)
995
+ return { items: window.slice(0, 1) }
996
+ } finally { if (!signal?.aborted) setPending(false) }
997
+ }, [scope, fail, page])
998
+ return <Stack>
999
+ {unavailable && <Text role="alert">The saved asset is unavailable. Choose a replacement; the draft id is retained until then.</Text>}
1000
+ <Stack direction="horizontal" gap="sm" wrap>
1001
+ <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}
1002
+ strings={{ showingCount: shown => `${shown} loaded on this page; total unknown` }}
1003
+ slots={{ footer: <Stack direction="horizontal" gap="sm" wrap align="center">
1004
+ <Text>Media page {page + 1}; total unknown</Text>
1005
+ <Button disabled={pending || page === 0} onClick={() => { setPage(value => value - 1); setPending(true) }}>Previous media page</Button>
1006
+ <Button disabled={pending || !hasNext} onClick={() => { setPage(value => value + 1); setPending(true) }}>Next media page</Button>
1007
+ </Stack> }}
1008
+ onConfirm={(items, ids) => { setSelected(ids); setResolved({ scope, ids, items }); setOpen(false) }} />
1009
+ <Button tone="neutral" appearance="outline" onClick={() => setSelected(["image-2"])}>Load saved media draft</Button>
1010
+ <Button tone="neutral" appearance="outline" onClick={() => { setScope(value => value === "site" ? "other" : "site"); setPage(0); setPending(true) }}>Switch media scope</Button>
1011
+ <Button tone="neutral" appearance="outline" onClick={() => { setFail(value => !value); setPending(true) }}>Toggle media failure</Button>
1012
+ </Stack>
1013
+ <Text role="status">{chosen.length ? `Hydrated selection: ${chosen.map(item => `${item.name} — ${item.alt}`).join(", ")}` : `Saved draft ids: ${selected.join(", ")}`}</Text>
1014
+ </Stack>
1015
+ }
1016
+ ```
1017
+
950
1018
  ### Asset manager
951
1019
 
952
1020
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -34,6 +34,16 @@ A dialog with a generated footer: cancel and confirm in a fixed order, an async-
34
34
  form-aware confirm, and a tone for the glyph and the confirm colour. Use the base Dialog when
35
35
  the body owns its actions.
36
36
 
37
+ **Use when:** A temporary focused task or nested confirmation with explicit completion.
38
+
39
+ **Avoid when:** Persistent route content or application-owned business policy.
40
+
41
+ **Compose:** Use controlled open and closeOnAsyncComplete=false when resolved validation can fail; use formId for a native form with one submit owner. Busy confirmation blocks dismissal.
42
+
43
+ **Capabilities:** nested confirmation; resolved validation failure; async form completion.
44
+
45
+ **Alternatives:** ActionSheet, ConfirmDialog.
46
+
37
47
  ```text
38
48
  ({ width, surfaceStyle, ...props }: ActionDialogProps) => import("react").JSX.Element
39
49
  ```
@@ -362,6 +372,50 @@ Kind: interface.
362
372
 
363
373
  Preview route: Action overlays — `/action-overlays`
364
374
 
375
+ ### Nested action flow
376
+
377
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
378
+ import { useRef, useState } from "react"
379
+ import { Button } from "themelia-ui/base/buttons"
380
+ import { FormField } from "themelia-ui/base/forms"
381
+ import { Stack } from "themelia-ui/base/structure"
382
+ import { Input } from "themelia-ui/base/text-inputs"
383
+ import { Text } from "themelia-ui/base/typography"
384
+ import { ActionDialog } from "themelia-ui/features/overlays"
385
+
386
+ export default function NestedActionReference() {
387
+ const [open, setOpen] = useState(false)
388
+ const [child, setChild] = useState(false)
389
+ const [name, setName] = useState("Draft record")
390
+ const [error, setError] = useState<string>()
391
+ const [saved, setSaved] = useState(0)
392
+ const session = useRef(0)
393
+ const changeOpen = (next: boolean) => { session.current++; setOpen(next); setChild(false) }
394
+ return <Stack>
395
+ <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}>
396
+ <Stack>
397
+ <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>
398
+ <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}
399
+ alertMessage={error} onError={() => setError("Could not save; your draft is still here.")}
400
+ onAsyncConfirm={async () => {
401
+ const attempt = session.current
402
+ await new Promise(resolve => setTimeout(resolve, 300))
403
+ if (attempt !== session.current) return
404
+ if (name === "offline") throw new Error("Unavailable")
405
+ if (name.trim().length < 3) { setError("Enter at least three characters."); return }
406
+ setError(undefined); setSaved(count => count + 1); changeOpen(false)
407
+ }}>
408
+ <Text>{name}</Text>
409
+ </ActionDialog>
410
+ {/* A native form can instead use formId: its submit handler owns validation and closure. */}
411
+ <Text size="xs" type="secondary">Confirmation {child ? "open" : "closed"}</Text>
412
+ </Stack>
413
+ </ActionDialog>
414
+ <Text role="status">Successful saves {saved}</Text>
415
+ </Stack>
416
+ }
417
+ ```
418
+
365
419
  ### ActionDialog
366
420
 
367
421
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -306,6 +306,44 @@ The shape the toolbar renders. Built-ins and consumer items resolve to this.
306
306
 
307
307
  Preview route: Rich text editor — `/rich-text-editor`
308
308
 
309
+ ### Website content reference
310
+
311
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
312
+ import { useState } from "react"
313
+ import { Button } from "themelia-ui/base/buttons"
314
+ import { Card } from "themelia-ui/base/cards"
315
+ import { PillRadioGroup } from "themelia-ui/base/choice-inputs"
316
+ import { FormField } from "themelia-ui/base/forms"
317
+ import { Stack } from "themelia-ui/base/structure"
318
+ import { Input } from "themelia-ui/base/text-inputs"
319
+ import { RichText, Text } from "themelia-ui/base/typography"
320
+ import { MediaLibraryDialog, type MediaLibraryItem } from "themelia-ui/features/media-library"
321
+ import { RichTextEditor } from "themelia-ui/features/rich-text-editor"
322
+ import { SAMPLE_MEDIA } from "../media-library/reference-data"
323
+
324
+ export default function WebsiteContentReference() {
325
+ const [locale, setLocale] = useState("en")
326
+ const [drafts, setDrafts] = useState<Record<string, string>>({ en: "<p>Welcome to our workspace.</p>", ar: "<p>مرحباً بكم</p>" })
327
+ const [mediaOpen, setMediaOpen] = useState(false)
328
+ const [image, setImage] = useState<MediaLibraryItem>()
329
+ const [alt, setAlt] = useState("")
330
+ const [failed, setFailed] = useState(true)
331
+ const [pending, setPending] = useState(false)
332
+ const [message, setMessage] = useState("Drafts are local until saved")
333
+ return <Card title="Website content" description="Each locale owns a retained draft. Preview sanitizes HTML; the application also validates and sanitizes persisted content.">
334
+ <Stack>
335
+ <PillRadioGroup aria-label="Content locale" name="reference-content-locale" value={locale} onValueChange={value => value && setLocale(value)} options={[{ value: "en", label: "English" }, { value: "ar", label: "العربية" }]} />
336
+ <div dir={locale === "ar" ? "rtl" : "ltr"}><RichTextEditor value={drafts[locale] ?? ""} onValueChange={html => setDrafts(current => ({ ...current, [locale]: html }))} /></div>
337
+ <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) }} />
338
+ {image && <FormField label="Hero image alternative text" hint={image.name}><Input value={alt} onChange={event => setAlt(event.target.value)} /></FormField>}
339
+ <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>
340
+ <Text role="status">{message}</Text>
341
+ <RichText html={drafts[locale] ?? ""} />
342
+ </Stack>
343
+ </Card>
344
+ }
345
+ ```
346
+
309
347
  ### The editor and what comes out
310
348
 
311
349
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -159,6 +159,37 @@ Kind: type.
159
159
 
160
160
  Preview route: App shell — `/app-shell`
161
161
 
162
+ ### Workspace navigation reference
163
+
164
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
165
+ import { useState } from "react"
166
+ import { Text } from "themelia-ui/base/typography"
167
+ import { TopbarSidebarLayout } from "themelia-ui/layout/app-shell"
168
+ import { AppSidebar, type LinkRenderer } from "themelia-ui/layout/sidebar"
169
+
170
+ export default function WorkspaceNavigationReference() {
171
+ const [route, setRoute] = useState("/a/overview")
172
+ const [navigations, setNavigations] = useState(0)
173
+ const workspace = route.startsWith("/b/") ? "B" : "A"
174
+ const renderLink: LinkRenderer = ({ href, children, external, active, disabled, onClick, ...props }) => {
175
+ void active
176
+ if (disabled || !href) return <span {...props}>{children}</span>
177
+ return <a {...props} href={href} target={props.target ?? (external ? "_blank" : undefined)} rel={props.rel ?? (external ? "noopener noreferrer" : undefined)} onClick={event => {
178
+ onClick?.(event)
179
+ if (event.defaultPrevented || external || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
180
+ event.preventDefault(); setRoute(href); setNavigations(count => count + 1)
181
+ }}>{children}</a>
182
+ }
183
+ return <div style={{ width: "100%", height: "28rem", minWidth: 0 }}>
184
+ <TopbarSidebarLayout contained contentRender={<div />} logo="Reference workspace" sidebarProviderProps={{ persist: false, keyboardShortcut: false }} sidebar={<AppSidebar logo={`Workspace ${workspace}`} renderLink={renderLink} currentUrl={route}
185
+ 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" }]}
186
+ navigationGroups={{ "": [{ label: `${workspace} overview`, href: `/${workspace.toLowerCase()}/overview` }, { label: `${workspace} settings`, href: `/${workspace.toLowerCase()}/settings` }] }} />}>
187
+ <Text role="status">Workspace {workspace}; route {route}; navigations {navigations}</Text>
188
+ </TopbarSidebarLayout>
189
+ </div>
190
+ }
191
+ ```
192
+
162
193
  ### The shell
163
194
 
164
195
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -30,6 +30,16 @@ outside the package and arrives through the public props, callbacks, slots, or a
30
30
 
31
31
  Kind: const.
32
32
 
33
+ **Use when:** A region needs centred reading width and an inline gutter without a page header.
34
+
35
+ **Avoid when:** Page or another shell already owns that width and gutter.
36
+
37
+ **Compose:** Use maxWidth and gutter from the Container contract; its values differ from Stack/Grid widths. It owns no scrolling or heading.
38
+
39
+ **Capabilities:** reading width; centred content; inline gutter.
40
+
41
+ **Alternatives:** Page, Stack.
42
+
33
43
  ```text
34
44
  React.ForwardRefExoticComponent<Omit<ContainerProps, "ref"> & React.RefAttributes<HTMLDivElement>>
35
45
  ```
@@ -65,6 +75,16 @@ Extends: `React.ComponentProps<"div">`.
65
75
 
66
76
  Kind: const.
67
77
 
78
+ **Use when:** A bounded app shell needs its single page scroller and query-container boundary.
79
+
80
+ **Avoid when:** The shell already supplies a scroller, or the document owns scrolling.
81
+
82
+ **Compose:** Use once at the scroll boundary. Preserve its keyboard focusability; it supplies neither the main landmark nor the page gutter.
83
+
84
+ **Capabilities:** page scroll owner; keyboard scrolling; inline size containment.
85
+
86
+ **Alternatives:** Page.
87
+
68
88
  ```text
69
89
  React.ForwardRefExoticComponent<Omit<PageViewportProps, "ref"> & React.RefAttributes<HTMLDivElement>>
70
90
  ```
@@ -81,6 +101,16 @@ No own members are present in the normalized public snapshot.
81
101
 
82
102
  Kind: const.
83
103
 
104
+ **Use when:** A semantic page section needs consistent rhythm between groups.
105
+
106
+ **Avoid when:** Only needing passive spacing, or another component already owns the section.
107
+
108
+ **Compose:** Supply a heading or accessible name; Section supplies the section element and shared group rhythm, not a heading or inset.
109
+
110
+ **Capabilities:** semantic section; page group rhythm.
111
+
112
+ **Alternatives:** Stack, ContentBlock.
113
+
84
114
  ```text
85
115
  React.ForwardRefExoticComponent<Omit<SectionProps, "ref"> & React.RefAttributes<HTMLElement>>
86
116
  ```
@@ -97,6 +127,16 @@ No own members are present in the normalized public snapshot.
97
127
 
98
128
  Kind: const.
99
129
 
130
+ **Use when:** Primary work needs a secondary summary with semantic aside and optional spanning header/footer.
131
+
132
+ **Avoid when:** Needing equal columns or a custom rail width and collapse breakpoint.
133
+
134
+ **Compose:** The viewport lg step enables two columns; the aside remains second in the DOM. Keep sticky opt-in for content that fits the viewport.
135
+
136
+ **Capabilities:** main aside layout; responsive summary rail; sticky aside.
137
+
138
+ **Alternatives:** Split, Grid.
139
+
100
140
  ```text
101
141
  React.ForwardRefExoticComponent<Omit<TwoColumnLayoutProps, "ref"> & React.RefAttributes<HTMLDivElement>>
102
142
  ```
@@ -7,7 +7,7 @@ A page needs its title, description and primary actions in the standard arrangem
7
7
  ## Selection
8
8
 
9
9
  - **Use when:** A page needs its title, description and primary actions in the standard arrangement.
10
- - **Avoid when:** The page is a record's index or show view with a fuller shell.
10
+ - **Avoid when:** The task needs standard loading, empty and error presentation already supplied by ResourceIndexShell or ResourceShowShell.
11
11
  - **Consider instead:** `features/resource`, `layout/containers`
12
12
  - **Stability:** stable.
13
13
 
@@ -215,6 +215,35 @@ Extends: `Omit<React.ComponentProps<"div">, "title">`.
215
215
 
216
216
  Preview route: Page & page header — `/page`
217
217
 
218
+ ### Record detail reference
219
+
220
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
221
+ import { useState } from "react"
222
+ import { Button } from "themelia-ui/base/buttons"
223
+ import { Card, CardContent, CardHeader } from "themelia-ui/base/cards"
224
+ import { ContentBlock, MetadataList } from "themelia-ui/base/display"
225
+ import { Stack } from "themelia-ui/base/structure"
226
+ import { Text } from "themelia-ui/base/typography"
227
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "themelia-ui/base/table"
228
+ import { Page } from "themelia-ui/layout/page"
229
+
230
+ export default function RecordDetailReference() {
231
+ const [activity, setActivity] = useState("No commands yet")
232
+ 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> }}>
233
+ <Stack>
234
+ <Card title="Identity" titleLevel={2} headerAction={<Button tone="neutral" appearance="outline" onClick={() => setActivity("Identity command: edit R-104")}>Edit identity</Button>}>
235
+ <MetadataList items={[{ id: "owner", label: "Owner", value: "Example team" }, { id: "state", label: "State", value: "Active" }]} />
236
+ <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>
237
+ </Card>
238
+ <Card><CardHeader title="Related records" titleLevel={2} description="The header is composed from parts; omit Card title props when supplying it." /><CardContent>
239
+ <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>
240
+ </CardContent></Card>
241
+ <Text role="status">{activity}</Text>
242
+ </Stack>
243
+ </Page>
244
+ }
245
+ ```
246
+
218
247
  ### PageHeader
219
248
 
220
249
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -260,6 +260,16 @@ Kind: callable.
260
260
  The rail’s header as a workspace switcher. The whole header row is the trigger, not a
261
261
  chevron beside a decorative name — the name is what a reader aims at.
262
262
 
263
+ **Use when:** Workspace destinations from the rail header, including mobile navigation.
264
+
265
+ **Avoid when:** A logo without destinations or application business switching policy.
266
+
267
+ **Compose:** Disclosure keeps the sheet open; same-app destination selection dismisses it. The link adapter preserves native external targets.
268
+
269
+ **Capabilities:** workspace navigation; mobile workspace switcher.
270
+
271
+ **Alternatives:** SidebarLogo.
272
+
263
273
  ```text
264
274
  ({ logo, collapsedLogo, workspaceLinks, renderLink, strings, contentClassName, }: SidebarWorkspaceProps) => import("react").JSX.Element
265
275
  ```
@@ -312,6 +322,37 @@ Kind: interface.
312
322
 
313
323
  Preview route: App shell — `/app-shell`
314
324
 
325
+ ### Workspace navigation reference
326
+
327
+ ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
328
+ import { useState } from "react"
329
+ import { Text } from "themelia-ui/base/typography"
330
+ import { TopbarSidebarLayout } from "themelia-ui/layout/app-shell"
331
+ import { AppSidebar, type LinkRenderer } from "themelia-ui/layout/sidebar"
332
+
333
+ export default function WorkspaceNavigationReference() {
334
+ const [route, setRoute] = useState("/a/overview")
335
+ const [navigations, setNavigations] = useState(0)
336
+ const workspace = route.startsWith("/b/") ? "B" : "A"
337
+ const renderLink: LinkRenderer = ({ href, children, external, active, disabled, onClick, ...props }) => {
338
+ void active
339
+ if (disabled || !href) return <span {...props}>{children}</span>
340
+ return <a {...props} href={href} target={props.target ?? (external ? "_blank" : undefined)} rel={props.rel ?? (external ? "noopener noreferrer" : undefined)} onClick={event => {
341
+ onClick?.(event)
342
+ if (event.defaultPrevented || external || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
343
+ event.preventDefault(); setRoute(href); setNavigations(count => count + 1)
344
+ }}>{children}</a>
345
+ }
346
+ return <div style={{ width: "100%", height: "28rem", minWidth: 0 }}>
347
+ <TopbarSidebarLayout contained contentRender={<div />} logo="Reference workspace" sidebarProviderProps={{ persist: false, keyboardShortcut: false }} sidebar={<AppSidebar logo={`Workspace ${workspace}`} renderLink={renderLink} currentUrl={route}
348
+ 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" }]}
349
+ navigationGroups={{ "": [{ label: `${workspace} overview`, href: `/${workspace.toLowerCase()}/overview` }, { label: `${workspace} settings`, href: `/${workspace.toLowerCase()}/settings` }] }} />}>
350
+ <Text role="status">Workspace {workspace}; route {route}; navigations {navigations}</Text>
351
+ </TopbarSidebarLayout>
352
+ </div>
353
+ }
354
+ ```
355
+
315
356
  ### The shell
316
357
 
317
358
  ```tsx fragment — excerpt from the live preview; surrounding values are supplied by the application
@@ -501,7 +501,7 @@ Kind: callable.
501
501
  `<UIScope>` — a region with its own tokens. Nests freely; writes the custom properties
502
502
  its merged config names and never touches the document: a region governs its subtree,
503
503
  and reaching past it is the root's job, and only when asked. `render` picks the element
504
- — Base UI's contract, the same one Item and Stack use — e.g. `<UIScope render={<aside />}>`,
504
+ through Base UI's element-substitution contract, e.g. `<UIScope render={<aside />}>`,
505
505
  for places a `div` is invalid.
506
506
 
507
507
  ```text
@@ -10,9 +10,9 @@ The rungs are ordinary props; controlled state and callbacks when state exists;
10
10
  named slots; render props; exported parts; headless hooks; and complete recipes. Passive
11
11
  modules may stop at the first rung. Interaction features normally expose several.
12
12
 
13
- This table is derived from what each module declares, so it cannot describe a seam that
14
- was removed or miss one that was added. The table focuses on feature modules, where the
15
- higher rungs are meaningful; the columns start at accessors.
13
+ This table summarizes detected declarations and recipes. It is a discovery aid, not a
14
+ prop contract: open the selected module API for exact signatures and supported seams.
15
+ It focuses on feature modules; the columns start at accessors.
16
16
 
17
17
  | module | accessors | slots | render props | headless hook | partials | recipe |
18
18
  | --- | :-: | :-: | :-: | :-: | :-: | :-: |
@@ -41,7 +41,7 @@ higher rungs are meaningful; the columns start at accessors.
41
41
  | `features/table` | ● | ● | · | ● | ● | ● |
42
42
  | `features/theme-tweaker` | · | ● | · | ● | ● | ● |
43
43
 
44
- ## The four the plan names
44
+ ## Four feature examples
45
45
 
46
46
  Global Search, Filters, Comments and Data View are the demonstration cases, because a
47
47
  ladder that only works on a Badge proves nothing. Each is usable at three heights:
@@ -50,7 +50,7 @@ ladder that only works on a Badge proves nothing. Each is usable at three height
50
50
 
51
51
  **Ready-made** — `GlobalSearch`, dropped in with props and callbacks.
52
52
 
53
- **Region-customized** — 8 exported parts, so a region can be replaced without copying the whole: `GlobalSearchDialog`, `GlobalSearchEmptyState`, `GlobalSearchFooter`, `GlobalSearchIdleState`, `GlobalSearchInput`, and 2 more.
53
+ **Region-customized** — 7 other public exports to inspect for region customization: `GlobalSearchDialog`, `GlobalSearchEmptyState`, `GlobalSearchFooter`, `GlobalSearchIdleState`, `GlobalSearchInput`, and 2 more.
54
54
 
55
55
  **Headless** — `useGlobalSearch`, for presentation the parts cannot express.
56
56
 
@@ -58,7 +58,7 @@ ladder that only works on a Badge proves nothing. Each is usable at three height
58
58
 
59
59
  **Ready-made** — `FilterLayout`, dropped in with props and callbacks.
60
60
 
61
- **Region-customized** — 18 exported parts, so a region can be replaced without copying the whole: `AsyncFilterEditor`, `DateFilterEditor`, `FilterEditor`, `FilterErrorBoundary`, `FilterOperator`, and 12 more.
61
+ **Region-customized** — 17 other public exports to inspect for region customization: `AsyncFilterEditor`, `DateFilterEditor`, `FilterEditor`, `FilterErrorBoundary`, `FilterOperator`, and 12 more.
62
62
 
63
63
  **Headless** — `useAsyncOptions`, `useFilterCache`, `useFilterGroups`, `useFilters`, for presentation the parts cannot express.
64
64
 
@@ -66,7 +66,7 @@ ladder that only works on a Badge proves nothing. Each is usable at three height
66
66
 
67
67
  **Ready-made** — `Comments`, dropped in with props and callbacks.
68
68
 
69
- **Region-customized** — 6 exported parts, so a region can be replaced without copying the whole: `CommentAttachmentChip`, `CommentComposer`, `CommentContent`, `CommentItem`, `CommentTimeline`.
69
+ **Region-customized** — 5 other public exports to inspect for region customization: `CommentAttachmentChip`, `CommentComposer`, `CommentContent`, `CommentItem`, `CommentTimeline`.
70
70
 
71
71
  **Headless** — `useAttachmentUpload`, `useComments`, for presentation the parts cannot express.
72
72
 
@@ -74,6 +74,6 @@ ladder that only works on a Badge proves nothing. Each is usable at three height
74
74
 
75
75
  **Ready-made** — `DataView`, dropped in with props and callbacks.
76
76
 
77
- **Region-customized** — 6 exported parts, so a region can be replaced without copying the whole: `DataViewPagination`, `DataViewShell`, `DataViewTableFrame`, `DataViewToolbar`, `SavedViewTabs`.
77
+ **Region-customized** — 5 other public exports to inspect for region customization: `DataViewPagination`, `DataViewShell`, `DataViewTableFrame`, `DataViewToolbar`, `SavedViewTabs`.
78
78
 
79
79
  **Headless** — `useDataView`, for presentation the parts cannot express.