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
package/dist/styles/TOKENS.md
CHANGED
|
@@ -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.
|
|
6
|
-
|
|
7
|
-
|
|
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-
|
|
14
|
-
import { d as b, v as x } from "./display-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import { d as i } from "./display-
|
|
6
|
-
import { s as a } from "./choice-inputs-
|
|
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-
|
|
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
|
+
}
|
package/docs/build/recipes.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Recipes
|
|
4
4
|
|
|
5
|
-
|
|
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
|