@godxjp/ui 33.12.8 → 33.13.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/README.md +4 -0
- package/agent/START-HERE.md +1 -1
- package/agent/components/BranchScopePicker.json +21 -1
- package/agent/components/Breadcrumb.json +8 -2
- package/agent/components/DataTable.json +1 -1
- package/agent/components/MobileShell.json +1 -0
- package/agent/components/PageContainer.json +6 -0
- package/agent/components/Sidebar.json +1 -0
- package/agent/components/Table.json +1 -1
- package/agent/components.json +39 -5
- package/agent/index.json +3 -3
- package/agent/llms.txt +3 -3
- package/agent/tokens.json +2 -2
- package/agent/vocabulary.json +13 -0
- package/dist/components/data-entry/branch-scope-picker.d.ts +3 -0
- package/dist/components/data-entry/branch-scope-picker.js +65 -11
- package/dist/components/layout/breadcrumb-ellipsis.js +33 -1
- package/dist/components/layout/breadcrumb.d.ts +4 -2
- package/dist/components/layout/breadcrumb.js +13 -3
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +5 -3
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +13 -12
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/data-entry.prop.d.ts +6 -0
- package/dist/props/components/layout.prop.d.ts +5 -1
- package/dist/props/registry.d.ts +19 -2
- package/dist/props/registry.js +21 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/navigation.prop.d.ts +2 -0
- package/dist/styles/control.css +29 -3
- package/dist/styles/layers.json +3 -2
- package/dist/styles/table-layout.css +16 -4
- package/dist/styles.css +1 -1
- package/dist/tokens/components/table.css +1 -1
- package/docs/TOKENS.md +11 -0
- package/package.json +2 -2
- package/scripts/cli.mjs +8 -2
- package/scripts/visual-audit.mjs +119 -27
package/README.md
CHANGED
|
@@ -274,8 +274,12 @@ pnpm release --ui <patch|minor|major> --mcp <…|skip> # publish lib + MCP in
|
|
|
274
274
|
node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
|
|
275
275
|
node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --format json # machine-readable
|
|
276
276
|
node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
|
|
277
|
+
# reuse a playwright-core + Chrome you already have, instead of installing the peer:
|
|
278
|
+
node node_modules/@godxjp/ui/scripts/visual-audit.mjs --playwright-module /abs/path/playwright-core --executable-path /path/to/chrome http://localhost:5173
|
|
277
279
|
```
|
|
278
280
|
|
|
281
|
+
The engine is resolved in this order: `--playwright-module` (a package directory or entry file exporting `chromium`; a bad path is an error, never a silent fallback) → the `playwright` peer → `playwright-core`. `--executable-path` is passed to `chromium.launch` and wins over `PLAYWRIGHT_CHROMIUM_EXECUTABLE`. When nothing resolves, the bootstrap error lists every place it looked.
|
|
282
|
+
|
|
279
283
|
**Tested peer range** (pin one of these): `playwright >=1.55 <2` (tested 1.61.1)
|
|
280
284
|
|
|
281
285
|
`--format json` **always** emits valid JSON — even on bootstrap failure — with a `status` (`ok` · `partial` · `error`) that separates **infrastructure errors** (missing peers, page won't load, axe won't inject → `errors[]`, `summary: null`/flagged) from **product findings** (`findings[]`). A tool failure can therefore never be misread as "zero violations". `pnpm check:visual-audit` is the CI smoke test: it serves a fixture page tripping all five rule families and asserts each one fires.
|
package/agent/START-HERE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are about to write code against a design system you did not author. This file is the whole
|
|
4
4
|
contract. Read it before you write JSX.
|
|
5
5
|
|
|
6
|
-
**This catalog describes `@godxjp/ui` 33.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 33.13.0.** If the project you are editing has a different
|
|
7
7
|
version in its `package.json`, read the pinned catalog for THAT version instead
|
|
8
8
|
(`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
|
|
9
9
|
not exist yet; older, and it hides props that do. Neither failure announces itself.
|
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
"importPath": "@godxjp/ui/data-entry",
|
|
6
6
|
"name": "BranchScopePicker",
|
|
7
7
|
"props": [
|
|
8
|
+
{
|
|
9
|
+
"defaultValue": "\"multiple\"",
|
|
10
|
+
"description": "Single compact Select for all/one branch; multiple keeps the existing all/subset editor.",
|
|
11
|
+
"name": "selectionMode",
|
|
12
|
+
"type": "\"single\" | \"multiple\""
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"defaultValue": "true",
|
|
16
|
+
"description": "Allow organization-wide scope. Set false for record fields, then validate exactly one permitted concrete branch on submit. Invalid supplied values are not silently coerced.",
|
|
17
|
+
"name": "allowAll",
|
|
18
|
+
"type": "boolean"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"defaultValue": "false",
|
|
22
|
+
"description": "Hide after a successful read of one enabled branch, except validation errors. Does not emit a change or normalize URL/default values; the app does that.",
|
|
23
|
+
"name": "hideWhenSingle",
|
|
24
|
+
"type": "boolean"
|
|
25
|
+
},
|
|
8
26
|
{
|
|
9
27
|
"description": "Giá trị có kiểm soát: phạm vi đang chọn.",
|
|
10
28
|
"name": "value",
|
|
@@ -70,7 +88,7 @@
|
|
|
70
88
|
"type": "ReactNode"
|
|
71
89
|
},
|
|
72
90
|
{
|
|
73
|
-
"description": "Native form name
|
|
91
|
+
"description": "Native form name: multiple submits the MODE radio; single submits the Select key (`all` or `branch:<id>`). Serialize the public `{ mode, branchIds }` value yourself for domain records; native keys are not branch ownership.",
|
|
74
92
|
"name": "name",
|
|
75
93
|
"type": "string"
|
|
76
94
|
},
|
|
@@ -91,6 +109,8 @@
|
|
|
91
109
|
"storyPath": "data-entry/branch-scope-picker.tsx",
|
|
92
110
|
"tagline": "Canonical scope control: all branches vs an explicit subset.",
|
|
93
111
|
"usage": [
|
|
112
|
+
"Use selectionMode=single in Sidebar action (above navigation) and a Sheet opened from MobileShell tabBar More. These are compositions, not a new switcher. OrgSwitcher changes organization and stays in platform chrome.",
|
|
113
|
+
"The app owns URL parsing, validation against authorized branches, popstate/router updates, and onValueChange navigation. Resolve a single authorized branch in the app before hiding the picker. Viewing scope is not record ownership: record forms use separate state, selectionMode=single and allowAll=false, and validate exactly one permitted id on submit. Future groups need an explicit value contract; do not encode them as branch ids.",
|
|
94
114
|
"DO treat scope as ONE form field: the single { mode, branchIds } value goes through FormField like any other control.",
|
|
95
115
|
"DO import it from @godxjp/ui/data-entry — never compose an ad-hoc radio+checkbox scope block per screen.",
|
|
96
116
|
"DO use `error` ONLY for validation (e.g. mode=selected with zero branches checked); a failed branch fetch is `listError`, a 403 is `denied`.",
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
"name": "Breadcrumb",
|
|
6
6
|
"props": [
|
|
7
7
|
{
|
|
8
|
-
"
|
|
8
|
+
"defaultValue": "\"last\"",
|
|
9
|
+
"description": "last | none. none renders an ancestor-only trail: every item with to stays linked and no default segment has aria-current. Missing paths remain text. menu and itemRender keep owning their segment bodies; custom renderers own their ARIA.",
|
|
10
|
+
"name": "currentItem",
|
|
11
|
+
"type": "BreadcrumbCurrentItemProp"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"description": "Array of { label, to?, menu?, ellipsis? } — omit `to` on the last (current) segment. `ellipsis` (`boolean | { tooltip?: ReactNode }`, gh#1046; antd Breadcrumb has none, the shape is antd Typography's) keeps the trail on one line and cuts that segment's label with an ellipsis, with the full label as aria-label and title on the clipped element (no duplicate aria-describedby), showing the full label in a tooltip on hover and on keyboard focus of the crumb link; `{ tooltip: false }` drops the tooltip. Link and current segments only. `--breadcrumb-item-max-inline-size` (default `none`) caps such a segment. `menu` (Ant Design `BreadcrumbItemType.menu`) hangs a sibling picker off that segment: `{ items: { value, label, to?, disabled? }[], onSelect? }`. The segment then renders as a menu button, not a link — an entry with `to` becomes a real anchor inside the menu while keeping `role=\"menuitem\"`.",
|
|
9
15
|
"name": "items",
|
|
10
16
|
"required": true,
|
|
11
17
|
"type": "BreadcrumbItemProp[]"
|
|
@@ -37,7 +43,7 @@
|
|
|
37
43
|
"tagline": "Standalone breadcrumb nav rendering an ordered trail of page segments.",
|
|
38
44
|
"usage": [
|
|
39
45
|
"DO import from `@godxjp/ui/layout` (not from a navigation or general sub-path) and pass a single `items` prop — an ordered array of `{ label, to? }` objects. No children, no sub-components, no render-prop API.",
|
|
40
|
-
"DO omit `to` on the last (current-page) segment — the component automatically renders it as a `<span aria-current=\"page\">` instead of a router `<Link>`.
|
|
46
|
+
"DO omit `to` on the last (current-page) segment — the component automatically renders it as a `<span aria-current=\"page\">` instead of a router `<Link>`. In the default currentItem=\"last\" mode, passing `to` on the last item does NOT make it a link. For ancestor-only trails use currentItem=\"none\" and supply every ancestor destination.",
|
|
41
47
|
"DO pass the Breadcrumb node as a ReactNode to the `breadcrumb` prop of `AppShell` (or `AppShell`) for shell-level breadcrumbs, or to `PageContainer`'s `breadcrumb` prop (which accepts `BreadcrumbItemProp[]` directly — not a ReactNode). When passing to `PageContainer`, pass the raw array; when passing to `AppShell`, wrap it: `breadcrumb={<Breadcrumb items={…} />}`.",
|
|
42
48
|
"DON'T hand-roll a breadcrumb strip (divs with chevrons, anchors, separators) — Breadcrumb ships the `<nav aria-label=\"Breadcrumb\">` + `<ol>` + `aria-hidden` chevrons. Any custom trail is a violation of the no-hand-roll rule and will fail `npm run ui:audit`.",
|
|
43
49
|
"DON'T use Breadcrumb for tab-style or step-style navigation (multi-step forms, wizard progress). Those flows belong to `Steps`. Breadcrumb is strictly a spatial location trail, not a process indicator.",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"defaultValue": "'default'",
|
|
89
|
-
"description": "Named collection contract — the SAME preset the Table primitive owns, forwarded to the table DataTable renders. 'default' emits NO attribute and matches no selector, so an existing DataTable is byte-identical. 'action-collection' is the canonical dense approval/action queue: below collapseBelow the desktop INTRINSIC column widths give way to the token-owned column-PRIORITY measures (--table-action-collection-*) under table-layout: fixed, cells wrap, and the bordered surface drops its --table-surface-min-inline-size floor — so requester · target · reason · requested date · row actions all stay inside a 390px frame with no horizontal scroll. Mark each column with `priority` on its ColumnDef. Semantics are untouched (no display change, no role rewriting, no card swap), so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. Measured: table 1182 / 766 / 388px at 1440 / 1024 / 390, document scrollWidth === clientWidth at every width, LTR and RTL. 'stacked-record-collection' is the other direction, for a WIDE, HETEROGENEOUS record set that has no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Each cell then carries its column's header inline above the value, DERIVED from the same ColumnDef.header the <th> uses — so the card cannot drift from the table, and a column with a deliberately empty header names itself with ariaLabel (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The labels are aria-hidden: the real <th> is still in the DOM and remains the accessible-name source, so screen-reader table navigation is unchanged at every width. Reach for this instead of building a parallel Card tree beside the table; two trees for one dataset means two sets of labels to keep in sync.",
|
|
89
|
+
"description": "Named collection contract — the SAME preset the Table primitive owns, forwarded to the table DataTable renders. 'default' emits NO attribute and matches no selector, so an existing DataTable is byte-identical. 'action-collection' is the canonical dense approval/action queue: below collapseBelow the desktop INTRINSIC column widths give way to the token-owned column-PRIORITY measures (--table-action-collection-*) under table-layout: fixed, cells wrap, and the bordered surface drops its --table-surface-min-inline-size floor — so requester · target · reason · requested date · row actions all stay inside a 390px frame with no horizontal scroll. Mark each column with `priority` on its ColumnDef. Semantics are untouched (no display change, no role rewriting, no card swap), so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. Measured: table 1182 / 766 / 388px at 1440 / 1024 / 390, document scrollWidth === clientWidth at every width, LTR and RTL. 'stacked-record-collection' is the other direction, for a WIDE, HETEROGENEOUS record set that has no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Each cell then carries its column's header inline above the value, DERIVED from the same ColumnDef.header the <th> uses — so the card cannot drift from the table, and a column with a deliberately empty header names itself with ariaLabel (gh#864). In card mode the row inset defaults to the live --card-space-inset minus its own border, sharing the CardTitle keyline at every density and in RTL (override --table-stacked-collection-card-padding-x independently). The CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The labels are aria-hidden: the real <th> is still in the DOM and remains the accessible-name source, so screen-reader table navigation is unchanged at every width. Reach for this instead of building a parallel Card tree beside the table; two trees for one dataset means two sets of labels to keep in sync.",
|
|
90
90
|
"name": "preset",
|
|
91
91
|
"type": "'default' | 'action-collection' | 'stacked-record-collection'"
|
|
92
92
|
},
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"storyPath": "layout/mobile-shell.tsx",
|
|
58
58
|
"tagline": "Handheld app shell (gh#354) — status band · app bar · the ONE scroll region · sticky action bar · bottom tab bar, with device safe-area insets and a document that never scrolls.",
|
|
59
59
|
"usage": [
|
|
60
|
+
"Compose a More destination in tabBar with SheetTrigger + Sheet + BranchScopePicker selectionMode=single; see layout/mobile-branch-scope.tsx. MobileShell does not own routes or a built-in More menu.",
|
|
60
61
|
"DO use MobileShell for a HANDHELD app screen — a warehouse/handy terminal, a driver app, a field-work PWA. It is the fourth root shell: AppShell (needs a sidebar) · the auth CenteredShell (unauthenticated card) · CenteredShell (authenticated scrolling document) · MobileShell (a phone app that does NOT scroll its document).",
|
|
61
62
|
"DO let `children` be the only thing that scrolls. Put the primary verb in `actions` and navigation in `tabBar` — both sit outside the scroll region, so neither needs `position: sticky`, a z-index, or bottom padding to clear the other.",
|
|
62
63
|
"DO NOT compose one out of <Card> + `ui-card-inset*` + `overflow-y-auto` (what docs/showcase/case6 did before gh#354). That reproduces the look and neither behaviour that matters on a device: the document still scrolls, and nothing pads out of env(safe-area-inset-*), so the notch covers the app bar and the home indicator covers the primary button.",
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
"importPath": "@godxjp/ui/layout",
|
|
5
5
|
"name": "PageContainer",
|
|
6
6
|
"props": [
|
|
7
|
+
{
|
|
8
|
+
"defaultValue": "\"last\"",
|
|
9
|
+
"description": "last | none, identical to Breadcrumb.currentItem. none keeps every supplied ancestor destination linked and emits no aria-current; the record name belongs in title. breadcrumb items also support ellipsis with the full label in aria-label and title, without a duplicate tooltip description.",
|
|
10
|
+
"name": "breadcrumbCurrentItem",
|
|
11
|
+
"type": "BreadcrumbCurrentItemProp"
|
|
12
|
+
},
|
|
7
13
|
{
|
|
8
14
|
"description": "Instance footer inset; omitted preserves the theme.",
|
|
9
15
|
"name": "footerPad",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
],
|
|
98
98
|
"tagline": "Data-driven vertical nav rail with collapsible submenu groups and a collapsed icon-only mode — never build nav manually with raw buttons.",
|
|
99
99
|
"usage": [
|
|
100
|
+
"For app-local branch viewing scope, put one BranchScopePicker selectionMode=single in action above navigation; see data-entry/branch-scope-navigation.tsx. This differs from platform OrgSwitcher.",
|
|
100
101
|
"DO: Define all nav items as a SidebarSectionProp[] data structure and pass it to sections — never hand-roll nav buttons alongside or instead of the Sidebar.",
|
|
101
102
|
"DO: Add children: SidebarItemProp[] to any SidebarItemProp to create a collapsible submenu group. The group auto-opens and highlights when activeId matches any descendant. The SAME field drives NavList (gh#815), so one grouped settings nav is one NavList and one <nav> landmark.",
|
|
102
103
|
"DO: Mirror the collapsed boolean between AppShell's sidebarCollapsed prop and Sidebar's collapsed prop — they must stay in sync so the shell layout grid adjusts correctly.",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"defaultValue": "\"default\"",
|
|
72
|
-
"description": "Named collection contract. \"default\" emits no attribute and keeps the plain table. \"action-collection\" is the canonical dense approval/action queue: the desktop INTRINSIC column widths (which make a five-column queue wider than its card and force a horizontal scroll at 390) are replaced by table-layout: fixed plus the token-owned column PRIORITY measures (--table-action-collection-*), and cells wrap. Mark each column with `priority` on its TableHead AND its TableCell. Semantics are untouched — no display change, no role rewriting, no card transformation — so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. \"stacked-record-collection\" is the other direction, for a WIDE, HETEROGENEOUS record set with no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Give each TableCell a `label` — its column header — because the <th> association it normally reads from is the thing that just went away; DataTable derives that label from ColumnDef.header for you (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The label is aria-hidden and the real <th> stays in the DOM, so the accessible name and table navigation are unchanged.",
|
|
72
|
+
"description": "Named collection contract. \"default\" emits no attribute and keeps the plain table. \"action-collection\" is the canonical dense approval/action queue: the desktop INTRINSIC column widths (which make a five-column queue wider than its card and force a horizontal scroll at 390) are replaced by table-layout: fixed plus the token-owned column PRIORITY measures (--table-action-collection-*), and cells wrap. Mark each column with `priority` on its TableHead AND its TableCell. Semantics are untouched — no display change, no role rewriting, no card transformation — so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. \"stacked-record-collection\" is the other direction, for a WIDE, HETEROGENEOUS record set with no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Give each TableCell a `label` — its column header — because the <th> association it normally reads from is the thing that just went away; DataTable derives that label from ColumnDef.header for you (gh#864). In card mode the row inset defaults to the live --card-space-inset minus its own border, sharing the CardTitle keyline at every density and in RTL (override --table-stacked-collection-card-padding-x independently). The CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The label is aria-hidden and the real <th> stays in the DOM, so the accessible name and table navigation are unchanged.",
|
|
73
73
|
"name": "preset",
|
|
74
74
|
"type": "\"default\" | \"action-collection\" | \"stacked-record-collection\""
|
|
75
75
|
},
|
package/agent/components.json
CHANGED
|
@@ -651,6 +651,12 @@
|
|
|
651
651
|
"importPath": "@godxjp/ui/layout",
|
|
652
652
|
"name": "PageContainer",
|
|
653
653
|
"props": [
|
|
654
|
+
{
|
|
655
|
+
"defaultValue": "\"last\"",
|
|
656
|
+
"description": "last | none, identical to Breadcrumb.currentItem. none keeps every supplied ancestor destination linked and emits no aria-current; the record name belongs in title. breadcrumb items also support ellipsis with the full label in aria-label and title, without a duplicate tooltip description.",
|
|
657
|
+
"name": "breadcrumbCurrentItem",
|
|
658
|
+
"type": "BreadcrumbCurrentItemProp"
|
|
659
|
+
},
|
|
654
660
|
{
|
|
655
661
|
"description": "Instance footer inset; omitted preserves the theme.",
|
|
656
662
|
"name": "footerPad",
|
|
@@ -1541,6 +1547,7 @@
|
|
|
1541
1547
|
"storyPath": "layout/mobile-shell.tsx",
|
|
1542
1548
|
"tagline": "Handheld app shell (gh#354) — status band · app bar · the ONE scroll region · sticky action bar · bottom tab bar, with device safe-area insets and a document that never scrolls.",
|
|
1543
1549
|
"usage": [
|
|
1550
|
+
"Compose a More destination in tabBar with SheetTrigger + Sheet + BranchScopePicker selectionMode=single; see layout/mobile-branch-scope.tsx. MobileShell does not own routes or a built-in More menu.",
|
|
1544
1551
|
"DO use MobileShell for a HANDHELD app screen — a warehouse/handy terminal, a driver app, a field-work PWA. It is the fourth root shell: AppShell (needs a sidebar) · the auth CenteredShell (unauthenticated card) · CenteredShell (authenticated scrolling document) · MobileShell (a phone app that does NOT scroll its document).",
|
|
1545
1552
|
"DO let `children` be the only thing that scrolls. Put the primary verb in `actions` and navigation in `tabBar` — both sit outside the scroll region, so neither needs `position: sticky`, a z-index, or bottom padding to clear the other.",
|
|
1546
1553
|
"DO NOT compose one out of <Card> + `ui-card-inset*` + `overflow-y-auto` (what docs/showcase/case6 did before gh#354). That reproduces the look and neither behaviour that matters on a device: the document still scrolls, and nothing pads out of env(safe-area-inset-*), so the notch covers the app bar and the home indicator covers the primary button.",
|
|
@@ -1654,6 +1661,7 @@
|
|
|
1654
1661
|
],
|
|
1655
1662
|
"tagline": "Data-driven vertical nav rail with collapsible submenu groups and a collapsed icon-only mode — never build nav manually with raw buttons.",
|
|
1656
1663
|
"usage": [
|
|
1664
|
+
"For app-local branch viewing scope, put one BranchScopePicker selectionMode=single in action above navigation; see data-entry/branch-scope-navigation.tsx. This differs from platform OrgSwitcher.",
|
|
1657
1665
|
"DO: Define all nav items as a SidebarSectionProp[] data structure and pass it to sections — never hand-roll nav buttons alongside or instead of the Sidebar.",
|
|
1658
1666
|
"DO: Add children: SidebarItemProp[] to any SidebarItemProp to create a collapsible submenu group. The group auto-opens and highlights when activeId matches any descendant. The SAME field drives NavList (gh#815), so one grouped settings nav is one NavList and one <nav> landmark.",
|
|
1659
1667
|
"DO: Mirror the collapsed boolean between AppShell's sidebarCollapsed prop and Sidebar's collapsed prop — they must stay in sync so the shell layout grid adjusts correctly.",
|
|
@@ -2495,7 +2503,13 @@
|
|
|
2495
2503
|
"name": "Breadcrumb",
|
|
2496
2504
|
"props": [
|
|
2497
2505
|
{
|
|
2498
|
-
"
|
|
2506
|
+
"defaultValue": "\"last\"",
|
|
2507
|
+
"description": "last | none. none renders an ancestor-only trail: every item with to stays linked and no default segment has aria-current. Missing paths remain text. menu and itemRender keep owning their segment bodies; custom renderers own their ARIA.",
|
|
2508
|
+
"name": "currentItem",
|
|
2509
|
+
"type": "BreadcrumbCurrentItemProp"
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
"description": "Array of { label, to?, menu?, ellipsis? } — omit `to` on the last (current) segment. `ellipsis` (`boolean | { tooltip?: ReactNode }`, gh#1046; antd Breadcrumb has none, the shape is antd Typography's) keeps the trail on one line and cuts that segment's label with an ellipsis, with the full label as aria-label and title on the clipped element (no duplicate aria-describedby), showing the full label in a tooltip on hover and on keyboard focus of the crumb link; `{ tooltip: false }` drops the tooltip. Link and current segments only. `--breadcrumb-item-max-inline-size` (default `none`) caps such a segment. `menu` (Ant Design `BreadcrumbItemType.menu`) hangs a sibling picker off that segment: `{ items: { value, label, to?, disabled? }[], onSelect? }`. The segment then renders as a menu button, not a link — an entry with `to` becomes a real anchor inside the menu while keeping `role=\"menuitem\"`.",
|
|
2499
2513
|
"name": "items",
|
|
2500
2514
|
"required": true,
|
|
2501
2515
|
"type": "BreadcrumbItemProp[]"
|
|
@@ -2527,7 +2541,7 @@
|
|
|
2527
2541
|
"tagline": "Standalone breadcrumb nav rendering an ordered trail of page segments.",
|
|
2528
2542
|
"usage": [
|
|
2529
2543
|
"DO import from `@godxjp/ui/layout` (not from a navigation or general sub-path) and pass a single `items` prop — an ordered array of `{ label, to? }` objects. No children, no sub-components, no render-prop API.",
|
|
2530
|
-
"DO omit `to` on the last (current-page) segment — the component automatically renders it as a `<span aria-current=\"page\">` instead of a router `<Link>`.
|
|
2544
|
+
"DO omit `to` on the last (current-page) segment — the component automatically renders it as a `<span aria-current=\"page\">` instead of a router `<Link>`. In the default currentItem=\"last\" mode, passing `to` on the last item does NOT make it a link. For ancestor-only trails use currentItem=\"none\" and supply every ancestor destination.",
|
|
2531
2545
|
"DO pass the Breadcrumb node as a ReactNode to the `breadcrumb` prop of `AppShell` (or `AppShell`) for shell-level breadcrumbs, or to `PageContainer`'s `breadcrumb` prop (which accepts `BreadcrumbItemProp[]` directly — not a ReactNode). When passing to `PageContainer`, pass the raw array; when passing to `AppShell`, wrap it: `breadcrumb={<Breadcrumb items={…} />}`.",
|
|
2532
2546
|
"DON'T hand-roll a breadcrumb strip (divs with chevrons, anchors, separators) — Breadcrumb ships the `<nav aria-label=\"Breadcrumb\">` + `<ol>` + `aria-hidden` chevrons. Any custom trail is a violation of the no-hand-roll rule and will fail `npm run ui:audit`.",
|
|
2533
2547
|
"DON'T use Breadcrumb for tab-style or step-style navigation (multi-step forms, wizard progress). Those flows belong to `Steps`. Breadcrumb is strictly a spatial location trail, not a process indicator.",
|
|
@@ -3475,7 +3489,7 @@
|
|
|
3475
3489
|
},
|
|
3476
3490
|
{
|
|
3477
3491
|
"defaultValue": "'default'",
|
|
3478
|
-
"description": "Named collection contract — the SAME preset the Table primitive owns, forwarded to the table DataTable renders. 'default' emits NO attribute and matches no selector, so an existing DataTable is byte-identical. 'action-collection' is the canonical dense approval/action queue: below collapseBelow the desktop INTRINSIC column widths give way to the token-owned column-PRIORITY measures (--table-action-collection-*) under table-layout: fixed, cells wrap, and the bordered surface drops its --table-surface-min-inline-size floor — so requester · target · reason · requested date · row actions all stay inside a 390px frame with no horizontal scroll. Mark each column with `priority` on its ColumnDef. Semantics are untouched (no display change, no role rewriting, no card swap), so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. Measured: table 1182 / 766 / 388px at 1440 / 1024 / 390, document scrollWidth === clientWidth at every width, LTR and RTL. 'stacked-record-collection' is the other direction, for a WIDE, HETEROGENEOUS record set that has no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Each cell then carries its column's header inline above the value, DERIVED from the same ColumnDef.header the <th> uses — so the card cannot drift from the table, and a column with a deliberately empty header names itself with ariaLabel (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The labels are aria-hidden: the real <th> is still in the DOM and remains the accessible-name source, so screen-reader table navigation is unchanged at every width. Reach for this instead of building a parallel Card tree beside the table; two trees for one dataset means two sets of labels to keep in sync.",
|
|
3492
|
+
"description": "Named collection contract — the SAME preset the Table primitive owns, forwarded to the table DataTable renders. 'default' emits NO attribute and matches no selector, so an existing DataTable is byte-identical. 'action-collection' is the canonical dense approval/action queue: below collapseBelow the desktop INTRINSIC column widths give way to the token-owned column-PRIORITY measures (--table-action-collection-*) under table-layout: fixed, cells wrap, and the bordered surface drops its --table-surface-min-inline-size floor — so requester · target · reason · requested date · row actions all stay inside a 390px frame with no horizontal scroll. Mark each column with `priority` on its ColumnDef. Semantics are untouched (no display change, no role rewriting, no card swap), so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. Measured: table 1182 / 766 / 388px at 1440 / 1024 / 390, document scrollWidth === clientWidth at every width, LTR and RTL. 'stacked-record-collection' is the other direction, for a WIDE, HETEROGENEOUS record set that has no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Each cell then carries its column's header inline above the value, DERIVED from the same ColumnDef.header the <th> uses — so the card cannot drift from the table, and a column with a deliberately empty header names itself with ariaLabel (gh#864). In card mode the row inset defaults to the live --card-space-inset minus its own border, sharing the CardTitle keyline at every density and in RTL (override --table-stacked-collection-card-padding-x independently). The CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The labels are aria-hidden: the real <th> is still in the DOM and remains the accessible-name source, so screen-reader table navigation is unchanged at every width. Reach for this instead of building a parallel Card tree beside the table; two trees for one dataset means two sets of labels to keep in sync.",
|
|
3479
3493
|
"name": "preset",
|
|
3480
3494
|
"type": "'default' | 'action-collection' | 'stacked-record-collection'"
|
|
3481
3495
|
},
|
|
@@ -5615,7 +5629,7 @@
|
|
|
5615
5629
|
},
|
|
5616
5630
|
{
|
|
5617
5631
|
"defaultValue": "\"default\"",
|
|
5618
|
-
"description": "Named collection contract. \"default\" emits no attribute and keeps the plain table. \"action-collection\" is the canonical dense approval/action queue: the desktop INTRINSIC column widths (which make a five-column queue wider than its card and force a horizontal scroll at 390) are replaced by table-layout: fixed plus the token-owned column PRIORITY measures (--table-action-collection-*), and cells wrap. Mark each column with `priority` on its TableHead AND its TableCell. Semantics are untouched — no display change, no role rewriting, no card transformation — so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. \"stacked-record-collection\" is the other direction, for a WIDE, HETEROGENEOUS record set with no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Give each TableCell a `label` — its column header — because the <th> association it normally reads from is the thing that just went away; DataTable derives that label from ColumnDef.header for you (gh#864). In card mode the CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The label is aria-hidden and the real <th> stays in the DOM, so the accessible name and table navigation are unchanged.",
|
|
5632
|
+
"description": "Named collection contract. \"default\" emits no attribute and keeps the plain table. \"action-collection\" is the canonical dense approval/action queue: the desktop INTRINSIC column widths (which make a five-column queue wider than its card and force a horizontal scroll at 390) are replaced by table-layout: fixed plus the token-owned column PRIORITY measures (--table-action-collection-*), and cells wrap. Mark each column with `priority` on its TableHead AND its TableCell. Semantics are untouched — no display change, no role rewriting, no card transformation — so header association, aria-sort and screen-reader table navigation are identical at 390 and 1440. \"stacked-record-collection\" is the other direction, for a WIDE, HETEROGENEOUS record set with no sensible narrow column measure: below collapseBelow the <thead> hides and every <tr> becomes a bordered key-value card (--table-stacked-collection-*). Give each TableCell a `label` — its column header — because the <th> association it normally reads from is the thing that just went away; DataTable derives that label from ColumnDef.header for you (gh#864). In card mode the row inset defaults to the live --card-space-inset minus its own border, sharing the CardTitle keyline at every density and in RTL (override --table-stacked-collection-card-padding-x independently). The CARD owns the inline inset: a folded cell has no inline padding of its own (--table-stacked-collection-cell-padding-x, default 0), and an EMPTY cell (content null) prints no label and drops out of the card (gh#1102). Every folded cell reads from the START: a column's `align`/`numeric` end or centre alignment belongs to the table, not the card, so a count sits under its label like every other field (gh#1310). The label is aria-hidden and the real <th> stays in the DOM, so the accessible name and table navigation are unchanged.",
|
|
5619
5633
|
"name": "preset",
|
|
5620
5634
|
"type": "\"default\" | \"action-collection\" | \"stacked-record-collection\""
|
|
5621
5635
|
},
|
|
@@ -15024,6 +15038,24 @@
|
|
|
15024
15038
|
"importPath": "@godxjp/ui/data-entry",
|
|
15025
15039
|
"name": "BranchScopePicker",
|
|
15026
15040
|
"props": [
|
|
15041
|
+
{
|
|
15042
|
+
"defaultValue": "\"multiple\"",
|
|
15043
|
+
"description": "Single compact Select for all/one branch; multiple keeps the existing all/subset editor.",
|
|
15044
|
+
"name": "selectionMode",
|
|
15045
|
+
"type": "\"single\" | \"multiple\""
|
|
15046
|
+
},
|
|
15047
|
+
{
|
|
15048
|
+
"defaultValue": "true",
|
|
15049
|
+
"description": "Allow organization-wide scope. Set false for record fields, then validate exactly one permitted concrete branch on submit. Invalid supplied values are not silently coerced.",
|
|
15050
|
+
"name": "allowAll",
|
|
15051
|
+
"type": "boolean"
|
|
15052
|
+
},
|
|
15053
|
+
{
|
|
15054
|
+
"defaultValue": "false",
|
|
15055
|
+
"description": "Hide after a successful read of one enabled branch, except validation errors. Does not emit a change or normalize URL/default values; the app does that.",
|
|
15056
|
+
"name": "hideWhenSingle",
|
|
15057
|
+
"type": "boolean"
|
|
15058
|
+
},
|
|
15027
15059
|
{
|
|
15028
15060
|
"description": "Giá trị có kiểm soát: phạm vi đang chọn.",
|
|
15029
15061
|
"name": "value",
|
|
@@ -15089,7 +15121,7 @@
|
|
|
15089
15121
|
"type": "ReactNode"
|
|
15090
15122
|
},
|
|
15091
15123
|
{
|
|
15092
|
-
"description": "Native form name
|
|
15124
|
+
"description": "Native form name: multiple submits the MODE radio; single submits the Select key (`all` or `branch:<id>`). Serialize the public `{ mode, branchIds }` value yourself for domain records; native keys are not branch ownership.",
|
|
15093
15125
|
"name": "name",
|
|
15094
15126
|
"type": "string"
|
|
15095
15127
|
},
|
|
@@ -15110,6 +15142,8 @@
|
|
|
15110
15142
|
"storyPath": "data-entry/branch-scope-picker.tsx",
|
|
15111
15143
|
"tagline": "Canonical scope control: all branches vs an explicit subset.",
|
|
15112
15144
|
"usage": [
|
|
15145
|
+
"Use selectionMode=single in Sidebar action (above navigation) and a Sheet opened from MobileShell tabBar More. These are compositions, not a new switcher. OrgSwitcher changes organization and stays in platform chrome.",
|
|
15146
|
+
"The app owns URL parsing, validation against authorized branches, popstate/router updates, and onValueChange navigation. Resolve a single authorized branch in the app before hiding the picker. Viewing scope is not record ownership: record forms use separate state, selectionMode=single and allowAll=false, and validate exactly one permitted id on submit. Future groups need an explicit value contract; do not encode them as branch ids.",
|
|
15113
15147
|
"DO treat scope as ONE form field: the single { mode, branchIds } value goes through FormField like any other control.",
|
|
15114
15148
|
"DO import it from @godxjp/ui/data-entry — never compose an ad-hoc radio+checkbox scope block per screen.",
|
|
15115
15149
|
"DO use `error` ONLY for validation (e.g. mode=selected with zero branches checked); a failed branch fetch is `listError`, a 403 is `denied`.",
|
package/agent/index.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"patterns": 28,
|
|
6
6
|
"rules": 44,
|
|
7
7
|
"tokens": 2241,
|
|
8
|
-
"vocabulary":
|
|
8
|
+
"vocabulary": 15
|
|
9
9
|
},
|
|
10
10
|
"design": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md",
|
|
11
11
|
"files": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
|
|
55
55
|
"read": {
|
|
56
56
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
57
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.
|
|
57
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.13.0/agent/index.json"
|
|
58
58
|
},
|
|
59
59
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
60
60
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
69
69
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
70
70
|
},
|
|
71
|
-
"version": "33.
|
|
71
|
+
"version": "33.13.0"
|
|
72
72
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
3
|
> A Japanese-enterprise React design system: 169 components, 2241 design tokens,
|
|
4
|
-
> 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.
|
|
4
|
+
> 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.13.0.
|
|
5
5
|
|
|
6
6
|
If your client can run a process, do not read these files — run the MCP server instead
|
|
7
|
-
(`npx @godxjp/ui-mcp@33.
|
|
7
|
+
(`npx @godxjp/ui-mcp@33.13.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -27,7 +27,7 @@ that can only fetch URLs.
|
|
|
27
27
|
## Pinning
|
|
28
28
|
|
|
29
29
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
30
|
-
the tag: `.../godx-jp/godxjp-ui/v33.
|
|
30
|
+
the tag: `.../godx-jp/godxjp-ui/v33.13.0/agent/...`. A catalog that does not match the installed
|
|
31
31
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
32
32
|
|
|
33
33
|
Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
|
package/agent/tokens.json
CHANGED
|
@@ -12534,10 +12534,10 @@
|
|
|
12534
12534
|
"value": "var(--space-stack-sm)"
|
|
12535
12535
|
},
|
|
12536
12536
|
{
|
|
12537
|
-
"description": "
|
|
12537
|
+
"description": "default = Card inset minus the row border, resolved at the row",
|
|
12538
12538
|
"name": "--table-stacked-collection-card-padding-x",
|
|
12539
12539
|
"tier": "component",
|
|
12540
|
-
"value": "
|
|
12540
|
+
"value": "initial"
|
|
12541
12541
|
},
|
|
12542
12542
|
{
|
|
12543
12543
|
"description": "Table component tokens: row height, cell padding.",
|
package/agent/vocabulary.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"concept": "Which breadcrumb item represents the current page.",
|
|
4
|
+
"name": "BreadcrumbCurrentItemProp",
|
|
5
|
+
"notes": "Defaults to last. none retains ancestor links without aria-current; supply to for each ancestor.",
|
|
6
|
+
"usedBy": [
|
|
7
|
+
"Breadcrumb",
|
|
8
|
+
"PageContainer"
|
|
9
|
+
],
|
|
10
|
+
"values": [
|
|
11
|
+
"last",
|
|
12
|
+
"none"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"concept": "Abstract controlled value.",
|
|
4
17
|
"name": "ValueProp<T = string>",
|
|
@@ -10,6 +10,9 @@ export declare const BranchScopePicker: React.ForwardRefExoticComponent<import("
|
|
|
10
10
|
value?: BranchScopeValueProp;
|
|
11
11
|
defaultValue?: BranchScopeValueProp;
|
|
12
12
|
onValueChange?: (value: BranchScopeValueProp) => void;
|
|
13
|
+
selectionMode?: "single" | "multiple";
|
|
14
|
+
allowAll?: boolean;
|
|
15
|
+
hideWhenSingle?: boolean;
|
|
13
16
|
disabled?: import("../../props/index.js").DisabledProp;
|
|
14
17
|
readOnly?: boolean;
|
|
15
18
|
searchable?: boolean;
|
|
@@ -11,17 +11,21 @@ import { SkeletonRows } from "../feedback/skeleton.js";
|
|
|
11
11
|
import { Text } from "../general/typography.js";
|
|
12
12
|
import { CheckboxGroup } from "./checkbox-group.js";
|
|
13
13
|
import { RadioGroup } from "./radio.js";
|
|
14
|
+
import { Select } from "./select.js";
|
|
14
15
|
import { SearchInput } from "./search-input.js";
|
|
15
16
|
const DEFAULT_VALUE = { mode: "all" };
|
|
16
17
|
const BranchScopePicker = React.forwardRef(
|
|
17
18
|
function BranchScopePicker2({
|
|
18
19
|
branches,
|
|
19
20
|
value: controlledValue,
|
|
20
|
-
defaultValue
|
|
21
|
+
defaultValue,
|
|
21
22
|
onValueChange,
|
|
22
23
|
disabled = false,
|
|
23
24
|
readOnly = false,
|
|
24
25
|
searchable = true,
|
|
26
|
+
selectionMode = "multiple",
|
|
27
|
+
allowAll = true,
|
|
28
|
+
hideWhenSingle = false,
|
|
25
29
|
error,
|
|
26
30
|
loading = false,
|
|
27
31
|
empty,
|
|
@@ -39,7 +43,9 @@ const BranchScopePicker = React.forwardRef(
|
|
|
39
43
|
const reactId = React.useId();
|
|
40
44
|
const rootId = id ?? `branch-scope-${reactId}`;
|
|
41
45
|
const errorId = `${rootId}-error`;
|
|
42
|
-
const [uncontrolled, setUncontrolled] = React.useState(
|
|
46
|
+
const [uncontrolled, setUncontrolled] = React.useState(
|
|
47
|
+
defaultValue ?? (allowAll ? DEFAULT_VALUE : { mode: "selected", branchIds: [] })
|
|
48
|
+
);
|
|
43
49
|
const value = controlledValue ?? uncontrolled;
|
|
44
50
|
const branchIds = React.useMemo(() => value.branchIds ?? [], [value.branchIds]);
|
|
45
51
|
const commit = (next) => {
|
|
@@ -109,12 +115,58 @@ const BranchScopePicker = React.forwardRef(
|
|
|
109
115
|
/* @__PURE__ */ jsx("div", { "aria-live": "polite", children: empty ?? /* @__PURE__ */ jsx(EmptyState, { variant: "compact", title: t("dataEntry.branchScope.empty"), titleAs: "p" }) })
|
|
110
116
|
);
|
|
111
117
|
}
|
|
118
|
+
if (hideWhenSingle && branches.length === 1 && !branches[0].disabled && !invalid) return null;
|
|
119
|
+
if (selectionMode === "single") {
|
|
120
|
+
const selected = value.mode === "selected" && branchIds.length === 1 && branches.some((branch) => branch.id === branchIds[0]) ? `branch:${branchIds[0]}` : value.mode === "all" && allowAll ? "all" : "";
|
|
121
|
+
return surface(
|
|
122
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
Select,
|
|
125
|
+
{
|
|
126
|
+
value: selected,
|
|
127
|
+
onValueChange: (next) => {
|
|
128
|
+
if (next === "all" && allowAll) commit({ mode: "all" });
|
|
129
|
+
else if (next.startsWith("branch:"))
|
|
130
|
+
commit({ mode: "selected", branchIds: [next.slice(7)] });
|
|
131
|
+
},
|
|
132
|
+
options: [
|
|
133
|
+
...allowAll ? [
|
|
134
|
+
{
|
|
135
|
+
value: "all",
|
|
136
|
+
label: typeof resolvedAllLabel === "string" ? resolvedAllLabel : t("dataEntry.branchScope.all")
|
|
137
|
+
}
|
|
138
|
+
] : [],
|
|
139
|
+
...branches.map((branch) => ({
|
|
140
|
+
value: `branch:${branch.id}`,
|
|
141
|
+
label: branch.name,
|
|
142
|
+
disabled: branch.disabled
|
|
143
|
+
}))
|
|
144
|
+
],
|
|
145
|
+
optionRender: (option) => option.value === "all" ? resolvedAllLabel : option.label,
|
|
146
|
+
labelRender: (option) => option.value === "all" ? resolvedAllLabel : option.label,
|
|
147
|
+
showSearch: searchable,
|
|
148
|
+
searchPlaceholder: t("dataEntry.branchScope.searchPlaceholder"),
|
|
149
|
+
placeholder: t("dataEntry.branchScope.choose"),
|
|
150
|
+
notFoundContent: notFoundContent ?? t("dataEntry.branchScope.noMatches"),
|
|
151
|
+
disabled,
|
|
152
|
+
readOnly,
|
|
153
|
+
name,
|
|
154
|
+
...fieldA11y,
|
|
155
|
+
"aria-label": fieldA11y["aria-label"] ?? t("dataEntry.branchScope.listLabel"),
|
|
156
|
+
"aria-invalid": invalid ? true : fieldA11y["aria-invalid"],
|
|
157
|
+
"aria-errormessage": invalid ? mergeAriaIds(errorId, fieldA11y["aria-errormessage"]) : void 0
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
invalid && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-error-strong text-xs", children: error })
|
|
161
|
+
] })
|
|
162
|
+
);
|
|
163
|
+
}
|
|
112
164
|
if (readOnly) {
|
|
113
165
|
const selectedBranches = branches.filter((branch) => branchIds.includes(branch.id));
|
|
114
166
|
return surface(
|
|
115
167
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
116
|
-
/* @__PURE__ */ jsx(Text, { size: "sm", weight: "medium", children: value.mode === "all" ? resolvedAllLabel : resolvedSelectedLabel }),
|
|
117
|
-
value.mode === "selected" && /* @__PURE__ */ jsx("div", { className: "ui-branch-scope-picker-badges", children: selectedBranches.map((branch) => /* @__PURE__ */ jsx(Badge, { variant: "outline", children: branch.name }, branch.id)) })
|
|
168
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", weight: "medium", children: value.mode === "all" && allowAll ? resolvedAllLabel : resolvedSelectedLabel }),
|
|
169
|
+
(value.mode === "selected" || !allowAll) && /* @__PURE__ */ jsx("div", { className: "ui-branch-scope-picker-badges", children: selectedBranches.map((branch) => /* @__PURE__ */ jsx(Badge, { variant: "outline", children: branch.name }, branch.id)) })
|
|
118
170
|
] })
|
|
119
171
|
);
|
|
120
172
|
}
|
|
@@ -123,16 +175,18 @@ const BranchScopePicker = React.forwardRef(
|
|
|
123
175
|
/* @__PURE__ */ jsx(
|
|
124
176
|
RadioGroup,
|
|
125
177
|
{
|
|
126
|
-
value: value.mode,
|
|
178
|
+
value: allowAll ? value.mode : "selected",
|
|
127
179
|
onValueChange: (mode) => {
|
|
128
180
|
commit({ ...value, mode });
|
|
129
181
|
},
|
|
130
182
|
options: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
...allowAll ? [
|
|
184
|
+
{
|
|
185
|
+
value: "all",
|
|
186
|
+
label: resolvedAllLabel,
|
|
187
|
+
description: allLabel === void 0 ? t("dataEntry.branchScope.allDescription") : void 0
|
|
188
|
+
}
|
|
189
|
+
] : [],
|
|
136
190
|
{
|
|
137
191
|
value: "selected",
|
|
138
192
|
label: resolvedSelectedLabel,
|
|
@@ -146,7 +200,7 @@ const BranchScopePicker = React.forwardRef(
|
|
|
146
200
|
"aria-errormessage": invalid ? mergeAriaIds(errorId, fieldA11y["aria-errormessage"]) : void 0
|
|
147
201
|
}
|
|
148
202
|
),
|
|
149
|
-
value.mode === "selected" && /* @__PURE__ */ jsxs("div", { className: "ui-branch-scope-picker-subset", children: [
|
|
203
|
+
(value.mode === "selected" || !allowAll) && /* @__PURE__ */ jsxs("div", { className: "ui-branch-scope-picker-subset", children: [
|
|
150
204
|
searchable && branches.length > 0 && /* @__PURE__ */ jsx(
|
|
151
205
|
SearchInput,
|
|
152
206
|
{
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef
|
|
9
|
+
} from "react";
|
|
4
10
|
import { cn } from "../../lib/utils.js";
|
|
5
11
|
import { EllipsisTooltip } from "../feedback/tooltip.js";
|
|
12
|
+
function labelText(label) {
|
|
13
|
+
return Children.toArray(label).map((node) => {
|
|
14
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
15
|
+
if (!isValidElement(node))
|
|
16
|
+
return "";
|
|
17
|
+
if (node.props["aria-hidden"] === true || node.props["aria-hidden"] === "true" || node.props.hidden)
|
|
18
|
+
return "";
|
|
19
|
+
return labelText(node.props.children);
|
|
20
|
+
}).join("");
|
|
21
|
+
}
|
|
6
22
|
function trailEllipsis(items) {
|
|
7
23
|
return items.some((item) => item.ellipsis) ? "" : void 0;
|
|
8
24
|
}
|
|
@@ -11,12 +27,28 @@ function BreadcrumbCrumbText({
|
|
|
11
27
|
children
|
|
12
28
|
}) {
|
|
13
29
|
const anchorRef = useRef(null);
|
|
30
|
+
const fullLabel = labelText(item.label);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const node = anchorRef.current;
|
|
33
|
+
if (!node || !item.ellipsis) return;
|
|
34
|
+
const rendered = node.cloneNode(true);
|
|
35
|
+
rendered.querySelectorAll('[aria-hidden="true"], [hidden]').forEach((child) => child.remove());
|
|
36
|
+
const text = rendered.textContent || fullLabel;
|
|
37
|
+
if (text) {
|
|
38
|
+
node.setAttribute("aria-label", text);
|
|
39
|
+
node.setAttribute("title", text);
|
|
40
|
+
}
|
|
41
|
+
}, [fullLabel, item.label, item.ellipsis]);
|
|
14
42
|
if (!item.ellipsis) return children;
|
|
15
43
|
const tooltip = typeof item.ellipsis === "object" && "tooltip" in item.ellipsis ? item.ellipsis.tooltip === true ? item.label : item.ellipsis.tooltip : item.label;
|
|
16
44
|
const hasTooltip = tooltip !== void 0 && tooltip !== null && tooltip !== false && tooltip !== "";
|
|
17
45
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18
46
|
cloneElement(children, {
|
|
47
|
+
// Keep the label's own start visible regardless of the page direction (#1370).
|
|
48
|
+
dir: "auto",
|
|
19
49
|
ref: anchorRef,
|
|
50
|
+
"aria-label": fullLabel || void 0,
|
|
51
|
+
title: fullLabel || void 0,
|
|
20
52
|
className: cn(children.props.className, "ui-breadcrumb-ellipsis")
|
|
21
53
|
}),
|
|
22
54
|
hasTooltip ? /* @__PURE__ */ jsx(EllipsisTooltip, { anchorRef, title: tooltip }) : null
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ElementType } from "react";
|
|
2
|
-
import type { BreadcrumbItemRenderProp, BreadcrumbProp, BreadcrumbSeparatorProp } from "../../props/vocabulary/navigation.prop.js";
|
|
2
|
+
import type { BreadcrumbCurrentItemProp, BreadcrumbItemRenderProp, BreadcrumbProp, BreadcrumbSeparatorProp } from "../../props/vocabulary/navigation.prop.js";
|
|
3
3
|
export type BreadcrumbProps = {
|
|
4
4
|
items: BreadcrumbProp;
|
|
5
|
+
/** Last item is current by default; none keeps an ancestor-only trail linked. */
|
|
6
|
+
currentItem?: BreadcrumbCurrentItemProp;
|
|
5
7
|
linkComponent?: ElementType;
|
|
6
8
|
/** Override the `<nav>` landmark's accessible name. Defaults to a localized "Breadcrumb". */
|
|
7
9
|
ariaLabel?: string;
|
|
@@ -18,4 +20,4 @@ export type BreadcrumbProps = {
|
|
|
18
20
|
*/
|
|
19
21
|
itemRender?: BreadcrumbItemRenderProp;
|
|
20
22
|
};
|
|
21
|
-
export declare function Breadcrumb({ items, linkComponent: LinkComponent, ariaLabel, separator, itemRender, }: BreadcrumbProps): import("react").JSX.Element;
|
|
23
|
+
export declare function Breadcrumb({ items, currentItem, linkComponent: LinkComponent, ariaLabel, separator, itemRender, }: BreadcrumbProps): import("react").JSX.Element;
|