themelia-ui 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/themelia-ui/SKILL.md +38 -9
- package/.agents/skills/themelia-ui/manifest.json +23 -0
- package/.agents/skills/themelia-ui/references/api-compatibility.md +3 -2
- package/.agents/skills/themelia-ui/references/blocks.md +123 -0
- package/.agents/skills/themelia-ui/references/composition-ladder.md +8 -8
- package/.agents/skills/themelia-ui/references/composition.md +98 -1
- package/.agents/skills/themelia-ui/references/forms.md +23 -3
- package/.agents/skills/themelia-ui/references/framework-wiring.md +34 -6
- package/.agents/skills/themelia-ui/references/i18n.md +4 -2
- package/.agents/skills/themelia-ui/references/imports.md +1 -1
- package/.agents/skills/themelia-ui/references/installation.md +5 -4
- package/.agents/skills/themelia-ui/references/migration.md +22 -2
- package/.agents/skills/themelia-ui/references/provider-and-scoping.md +27 -9
- package/.agents/skills/themelia-ui/references/reference-workflows.md +43 -0
- package/.agents/skills/themelia-ui/references/structure-and-spacing.md +147 -0
- package/.agents/skills/themelia-ui/references/theming.md +6 -2
- package/.agents/skills/themelia-ui/references/troubleshooting.md +28 -5
- package/.agents/skills/themelia-ui/references/verification.md +58 -2
- package/CHANGELOG.md +25 -0
- package/README.md +3 -0
- package/dist/{alert-dialog-Bmo1YrTm.js → alert-dialog-Cg2HhuBe.js} +1 -1
- package/dist/base/alert-dialog.js +1 -1
- package/dist/base/cards.js +1 -1
- package/dist/base/choice-inputs.js +1 -1
- package/dist/base/combobox.js +1 -1
- package/dist/base/command.js +1 -1
- package/dist/base/date-pickers.js +1 -1
- package/dist/base/dialog.js +1 -1
- package/dist/base/display.js +1 -1
- package/dist/base/forms-numeric.js +1 -1
- package/dist/base/forms.js +1 -1
- package/dist/base/item.js +1 -1
- package/dist/base/navigation.js +1 -1
- package/dist/base/otp-input.js +1 -1
- package/dist/base/overlay.js +1 -1
- package/dist/base/popover-menu.js +1 -1
- package/dist/base/repeaters.js +1 -1
- package/dist/base/sheet.js +1 -1
- package/dist/base/sidebar.js +1 -1
- package/dist/base/skeleton.js +1 -1
- package/dist/base/structure.js +1 -1
- package/dist/base/timeline.js +1 -1
- package/dist/base/upload.js +1 -1
- package/dist/base/value-inputs.js +1 -1
- package/dist/blocks/admin/access.js +2 -2
- package/dist/blocks/admin/commerce.js +9 -9
- package/dist/blocks/analytics.js +3 -3
- package/dist/blocks/onboarding.js +1 -1
- package/dist/blocks/timelines.js +1 -1
- package/dist/{cards-DdiICdMM.js → cards-Ddhz43Bz.js} +2 -2
- package/dist/{choice-inputs-CCw-0gD6.js → choice-inputs-NwRxvmZN.js} +2 -2
- package/dist/combobox-519ubijc.js +292 -0
- package/dist/{command-CnB_IZSo.js → command-JjApYFds.js} +2 -2
- package/dist/{comments-C6Y_ddMt.js → comments-CCQ2QRaU.js} +4 -4
- package/dist/components/base/display/icon-badge.d.ts +2 -2
- package/dist/components/base/forms/form-field.d.ts +2 -2
- package/dist/components/base/structure/bleed.d.ts +1 -0
- package/dist/components/base/structure/grid.d.ts +8 -2
- package/dist/components/base/structure/split.d.ts +1 -0
- package/dist/components/features/combobox/fetching-combobox.d.ts +2 -1
- package/dist/components/features/combobox/resource-combobox.d.ts +3 -0
- package/dist/components/features/combobox/use-suggestions.d.ts +7 -0
- package/dist/css/structure.css +1 -1
- package/dist/{date-pickers-C2HgjpFU.js → date-pickers-BwCwKIVM.js} +1 -1
- package/dist/{dialog-0nRFg5Zs.js → dialog-CAnwRNrm.js} +1 -1
- package/dist/{display-BGN834xV.js → display-a7AoX_u7.js} +1 -1
- package/dist/features/actions.js +1 -1
- package/dist/features/activities.js +3 -3
- package/dist/features/ai-chat.js +2 -2
- package/dist/features/combobox.js +285 -273
- package/dist/features/comments.js +1 -1
- package/dist/features/data-view.js +5 -5
- package/dist/features/event-calendar.js +3 -3
- package/dist/features/filters.js +1 -1
- package/dist/features/global-search.js +4 -4
- package/dist/features/map.js +2 -2
- package/dist/features/media-library.js +8 -8
- package/dist/features/mentions.js +1 -1
- package/dist/features/overlays.js +1 -1
- package/dist/features/products.js +9 -9
- package/dist/features/resource-assignment.js +3 -3
- package/dist/features/resource.js +3 -3
- package/dist/features/schema-form.js +6 -6
- package/dist/features/sync.js +2 -2
- package/dist/features/table.js +1 -1
- package/dist/features/theme-tweaker.js +10 -10
- package/dist/{filters-geLuSlFL.js → filters-BhsTkWpZ.js} +9 -9
- package/dist/{forms-NxbJK4Mh.js → forms-C0WeNPFF.js} +1 -1
- package/dist/{forms-numeric-C6NMdj6G.js → forms-numeric-IwQ1ltNf.js} +1 -1
- package/dist/hooks/use-native-dialog.d.ts +1 -1
- package/dist/{item-CuUJyETl.js → item-M2pmpWXh.js} +1 -1
- package/dist/layout/app-shell.js +1 -1
- package/dist/layout/auth.js +1 -1
- package/dist/layout/header.js +3 -3
- package/dist/layout/navigation.js +1 -1
- package/dist/layout/page.js +2 -2
- package/dist/layout/settings.js +2 -2
- package/dist/layout/sidebar.js +10 -6
- package/dist/layout/workspace.js +3 -3
- package/dist/lib/ui-provider/ui-scope.d.ts +1 -1
- package/dist/{mentions-oeYNv_j1.js → mentions-BOxbigwp.js} +2 -2
- package/dist/{navigation-BTX5Ti0Z.js → navigation-Bgu7Vc0M.js} +2 -2
- package/dist/{navigation-WYKjDhZq.js → navigation-DiYACqpE.js} +2 -2
- package/dist/{overlay-C8RAgi2_.js → overlay-BuQOl8aP.js} +5 -5
- package/dist/{overlays-9BxwFTou.js → overlays-1sULyytH.js} +5 -5
- package/dist/{popover-menu-DcLeiw9r.js → popover-menu-QXgQeWeG.js} +1 -1
- package/dist/{repeaters-DfptvbGA.js → repeaters-CeH20dvr.js} +4 -4
- package/dist/{sheet-D-DVZH_b.js → sheet-CJ1-OAla.js} +1 -1
- package/dist/{sidebar-DXQOy_Cb.js → sidebar-D8uGqxio.js} +4 -4
- package/dist/{skeleton-CubvHel9.js → skeleton-BTKNKTdj.js} +1 -1
- package/dist/{structure-B9Y5NkP_.js → structure-CynEZ-OY.js} +1 -2
- package/dist/style.css +1 -1
- package/dist/styles/TOKENS.md +4 -3
- package/dist/{table-DT_20ity.js → table-B7dtIFUx.js} +4 -4
- package/dist/{timeline-BRZbSdhf.js → timeline-BR_jNgvz.js} +1 -1
- package/dist/{timelines-DczZKW5p.js → timelines-DXG1BfIN.js} +1 -1
- package/dist/{upload-C_6mlbfl.js → upload-CYuoJykZ.js} +1 -1
- package/dist/{value-inputs-CnO6RZeT.js → value-inputs-ng8aVJJZ.js} +4 -4
- package/docs/README.md +11 -1
- package/docs/build/browser-composition-fixture.ts +28 -0
- package/docs/build/recipes.md +398 -1
- package/docs/generated/component-index.json +1993 -862
- package/docs/generated/components/base--display.md +2 -2
- package/docs/generated/components/base--forms.md +72 -1
- package/docs/generated/components/base--structure.md +56 -5
- package/docs/generated/components/base--typography.md +38 -0
- package/docs/generated/components/blocks--admin--access.md +10 -0
- package/docs/generated/components/blocks--analytics.md +44 -0
- package/docs/generated/components/features--combobox.md +67 -0
- package/docs/generated/components/features--data-view.md +59 -3
- package/docs/generated/components/features--filters.md +10 -0
- package/docs/generated/components/features--media-library.md +68 -0
- package/docs/generated/components/features--overlays.md +54 -0
- package/docs/generated/components/features--rich-text-editor.md +38 -0
- package/docs/generated/components/layout--app-shell.md +31 -0
- package/docs/generated/components/layout--containers.md +40 -0
- package/docs/generated/components/layout--page.md +30 -1
- package/docs/generated/components/layout--sidebar.md +41 -0
- package/docs/generated/components/ui-provider.md +1 -1
- package/docs/generated/composition-ladder.md +8 -8
- package/docs/generated/consumer-contract.json +5736 -0
- package/docs/generated/imports.md +1 -1
- package/docs/generated/recipes.json +171 -1
- package/docs/generated/workflows/communications-overview/analytics/communications-reference.tsx +19 -0
- package/docs/generated/workflows/index.json +691 -0
- package/docs/generated/workflows/media-picking/media-library/media-picking-reference.tsx +63 -0
- package/docs/generated/workflows/media-picking/media-library/reference-data.ts +3 -0
- package/docs/generated/workflows/multi-section-form/form-workflow/multi-section-reference.tsx +46 -0
- package/docs/generated/workflows/nested-action-flow/action-overlays/nested-action-reference.tsx +39 -0
- package/docs/generated/workflows/record-detail/page-layout/record-detail-reference.tsx +24 -0
- package/docs/generated/workflows/resource-index/data-view/resource-index-reference.tsx +51 -0
- package/docs/generated/workflows/scoped-resource-selection/combobox/scoped-resource-reference.tsx +50 -0
- package/docs/generated/workflows/website-content-editor/media-library/reference-data.ts +3 -0
- package/docs/generated/workflows/website-content-editor/rich-text-editor/website-content-reference.tsx +33 -0
- package/docs/generated/workflows/workspace-navigation/app-shell/workspace-navigation-reference.tsx +26 -0
- package/docs/learn/api-compatibility.md +3 -2
- package/docs/learn/blocks.md +123 -0
- package/docs/learn/composition.md +98 -1
- package/docs/learn/forms.md +23 -3
- package/docs/learn/framework-wiring.md +34 -6
- package/docs/learn/i18n.md +4 -2
- package/docs/learn/installation.md +5 -4
- package/docs/learn/migration.md +22 -2
- package/docs/learn/provider-and-scoping.md +27 -9
- package/docs/learn/reference-workflows.md +43 -0
- package/docs/learn/structure-and-spacing.md +147 -0
- package/docs/learn/theming.md +6 -2
- package/docs/learn/troubleshooting.md +28 -5
- package/docs/learn/verification.md +58 -2
- package/package.json +4 -1
- package/scripts/consumer/codemod.mjs +3 -24
- package/scripts/consumer/contract-reader.mjs +42 -0
- package/scripts/consumer/css-scan.mjs +51 -0
- package/scripts/consumer/diagnose.mjs +136 -0
- package/scripts/consumer/find-component.mjs +28 -13
- package/scripts/consumer/install-skill.mjs +93 -11
- package/src/styles/TOKENS.md +4 -3
- package/dist/combobox-Byu8N-Oe.js +0 -274
|
@@ -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
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
##
|
|
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** —
|
|
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** —
|
|
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** —
|
|
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** —
|
|
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.
|