@godxjp/ui 33.12.8 → 33.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +8 -4
  2. package/agent/START-HERE.md +5 -5
  3. package/agent/components/BranchScopePicker.json +21 -1
  4. package/agent/components/Breadcrumb.json +8 -2
  5. package/agent/components/DataTable.json +2 -1
  6. package/agent/components/MobileShell.json +1 -0
  7. package/agent/components/PageContainer.json +6 -0
  8. package/agent/components/Persona.json +66 -0
  9. package/agent/components/Sidebar.json +1 -0
  10. package/agent/components/Table.json +1 -1
  11. package/agent/components-index.json +5 -0
  12. package/agent/components.json +106 -5
  13. package/agent/index.json +7 -7
  14. package/agent/llms.txt +6 -6
  15. package/agent/tokens.json +62 -2
  16. package/agent/vocabulary.json +13 -0
  17. package/dist/components/data-display/data-table.js +51 -10
  18. package/dist/components/data-display/index.d.ts +2 -0
  19. package/dist/components/data-display/index.js +2 -0
  20. package/dist/components/data-display/persona.d.ts +11 -0
  21. package/dist/components/data-display/persona.js +34 -0
  22. package/dist/components/data-entry/branch-scope-picker.d.ts +3 -0
  23. package/dist/components/data-entry/branch-scope-picker.js +65 -11
  24. package/dist/components/layout/breadcrumb-ellipsis.js +33 -1
  25. package/dist/components/layout/breadcrumb.d.ts +4 -2
  26. package/dist/components/layout/breadcrumb.js +13 -3
  27. package/dist/components/layout/page-container.d.ts +1 -1
  28. package/dist/components/layout/page-container.js +5 -3
  29. package/dist/contracts/measurement.json +1 -1
  30. package/dist/i18n/messages/en.json +2 -1
  31. package/dist/i18n/messages/ja.json +13 -12
  32. package/dist/i18n/messages/vi.json +2 -1
  33. package/dist/props/components/data-display.prop.d.ts +10 -0
  34. package/dist/props/components/data-entry.prop.d.ts +6 -0
  35. package/dist/props/components/layout.prop.d.ts +5 -1
  36. package/dist/props/registry.d.ts +24 -2
  37. package/dist/props/registry.js +26 -0
  38. package/dist/props/vocabulary/index.d.ts +1 -1
  39. package/dist/props/vocabulary/navigation.prop.d.ts +2 -0
  40. package/dist/styles/control.css +29 -3
  41. package/dist/styles/data-display-layout.css +58 -0
  42. package/dist/styles/layers.json +8 -2
  43. package/dist/styles/table-layout.css +32 -12
  44. package/dist/styles.css +1 -1
  45. package/dist/tokens/base.css +2 -0
  46. package/dist/tokens/components/persona.css +13 -0
  47. package/dist/tokens/components/table.css +1 -1
  48. package/docs/TOKENS.md +11 -0
  49. package/package.json +2 -2
  50. package/scripts/cli.mjs +8 -2
  51. package/scripts/visual-audit.mjs +119 -27
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
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.12.8.
3
+ > A Japanese-enterprise React design system: 170 components, 2251 design tokens,
4
+ > 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.14.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.12.8`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@33.14.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -16,10 +16,10 @@ that can only fetch URLs.
16
16
  ## Catalog
17
17
 
18
18
  - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 28 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
19
- - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 53 KB — all 169 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 53 KB — all 170 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
20
20
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–37 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
21
21
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.3 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
22
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 218 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1910 `component` knobs.
22
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 218 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1920 `component` knobs.
23
23
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
24
24
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 44 cardinal rules.
25
25
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 35 shapes that make generated UI look generated, each with its fix.
@@ -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.12.8/agent/...`. A catalog that does not match the installed
30
+ the tag: `.../godx-jp/godxjp-ui/v33.14.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
@@ -10073,6 +10073,66 @@
10073
10073
  "tier": "component",
10074
10074
  "value": "var(--space-2)"
10075
10075
  },
10076
+ {
10077
+ "description": "Default var(--space-2).",
10078
+ "name": "--persona-avatar-gap",
10079
+ "tier": "component",
10080
+ "value": "initial"
10081
+ },
10082
+ {
10083
+ "description": "Default zero, set at the call site.",
10084
+ "name": "--persona-label-gap",
10085
+ "tier": "component",
10086
+ "value": "initial"
10087
+ },
10088
+ {
10089
+ "description": "Default var(--space-1).",
10090
+ "name": "--persona-trailing-gap",
10091
+ "tier": "component",
10092
+ "value": "initial"
10093
+ },
10094
+ {
10095
+ "description": "Identity geometry. Initial spacing/type defaults resolve against the local density.",
10096
+ "name": "--persona-label-line-height",
10097
+ "tier": "component",
10098
+ "value": "1.5"
10099
+ },
10100
+ {
10101
+ "description": "Default var(--font-size-sm).",
10102
+ "name": "--persona-name-font-size",
10103
+ "tier": "component",
10104
+ "value": "initial"
10105
+ },
10106
+ {
10107
+ "description": "Default var(--font-size-xs).",
10108
+ "name": "--persona-name-font-size-sm",
10109
+ "tier": "component",
10110
+ "value": "initial"
10111
+ },
10112
+ {
10113
+ "description": "Default var(--font-weight-medium).",
10114
+ "name": "--persona-name-font-weight",
10115
+ "tier": "component",
10116
+ "value": "initial"
10117
+ },
10118
+ {
10119
+ "description": "Default var(--font-size-xs).",
10120
+ "name": "--persona-description-font-size",
10121
+ "tier": "component",
10122
+ "value": "initial"
10123
+ },
10124
+ {
10125
+ "description": "Role mirror: hsl(var(--foreground)).",
10126
+ "name": "--persona-name-color",
10127
+ "tier": "component",
10128
+ "value": "initial"
10129
+ },
10130
+ {
10131
+ "description": "Role mirror: hsl(var(--muted-foreground)).",
10132
+ "name": "--persona-description-color",
10133
+ "tier": "component",
10134
+ "value": "initial"
10135
+ },
10076
10136
  {
10077
10137
  "description": "Separator layout thickness; focus paint must not reserve extra space.",
10078
10138
  "name": "--resizable-handle-size",
@@ -12534,10 +12594,10 @@
12534
12594
  "value": "var(--space-stack-sm)"
12535
12595
  },
12536
12596
  {
12537
- "description": "Table component tokens: row height, cell padding.",
12597
+ "description": "default = Card inset, resolved at the row",
12538
12598
  "name": "--table-stacked-collection-card-padding-x",
12539
12599
  "tier": "component",
12540
- "value": "var(--space-inline-md)"
12600
+ "value": "initial"
12541
12601
  },
12542
12602
  {
12543
12603
  "description": "Table component tokens: row height, cell padding.",
@@ -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>",
@@ -80,6 +80,33 @@ import {
80
80
  } from "../../lib/control-styles.js";
81
81
  import { isDevelopment } from "../../lib/dev.js";
82
82
  import { pageSizeOptionsWith } from "../navigation/pagination-utils.js";
83
+ function recoverCellText(cell) {
84
+ if (!cell) return;
85
+ const part = truncatedPart(cell);
86
+ const value = part && part !== cell ? part : cell.querySelector(".ui-table-stacked-collection-value") ?? cell;
87
+ const selector = '[title]:not([title=""]), [aria-describedby]';
88
+ const carrier = value.closest(selector);
89
+ const recovery = carrier && carrier !== cell ? carrier : value.querySelector(selector);
90
+ if (recovery) {
91
+ if (cell.dataset.cellTitle !== void 0) {
92
+ cell.removeAttribute("title");
93
+ delete cell.dataset.cellTitle;
94
+ }
95
+ return;
96
+ }
97
+ if (!cell.title || cell.dataset.cellTitle !== void 0) {
98
+ const content = value.cloneNode(true);
99
+ content.querySelectorAll('[aria-hidden="true"], [hidden], .sr-only, [role="tooltip"]').forEach((node) => node.remove());
100
+ const text = (content.textContent ?? "").replace(/\s+/g, " ").trim();
101
+ if (text) {
102
+ if (cell.title !== text) cell.title = text;
103
+ cell.dataset.cellTitle = "";
104
+ } else {
105
+ cell.removeAttribute("title");
106
+ delete cell.dataset.cellTitle;
107
+ }
108
+ }
109
+ }
83
110
  function resolveColumnHideBelow(col) {
84
111
  if (col.hideBelow) return col.hideBelow;
85
112
  if (col.hiddenOnMobile) return "md";
@@ -913,27 +940,40 @@ DataTable.Content = function DataTableContent() {
913
940
  observer.disconnect();
914
941
  };
915
942
  }, [fixedSignature]);
916
- const hasEllipsis = !stacked && visibleColumns.some((col) => col.ellipsis);
943
+ const ellipsisEnabled = visibleColumns.some((col) => col.ellipsis);
944
+ const hasEllipsis = !stacked && ellipsisEnabled;
945
+ React.useLayoutEffect(() => {
946
+ const root = scrollRef.current;
947
+ if (!root || !ellipsisEnabled) return;
948
+ const sync = () => {
949
+ for (const cell of root.querySelectorAll(
950
+ "tbody td.ui-data-table-ellipsis"
951
+ ))
952
+ recoverCellText(cell);
953
+ };
954
+ sync();
955
+ const observer = new MutationObserver(sync);
956
+ observer.observe(root, {
957
+ subtree: true,
958
+ childList: true,
959
+ characterData: true,
960
+ attributes: true,
961
+ attributeFilter: ["title", "aria-describedby"]
962
+ });
963
+ return () => observer.disconnect();
964
+ });
917
965
  const markTruncated = React.useCallback(() => {
918
966
  const el = scrollRef.current;
919
967
  if (!el) return;
920
968
  for (const cell of el.querySelectorAll("tbody td.ui-data-table-ellipsis")) {
921
969
  if (cell.contains(document.activeElement)) continue;
922
- const ownTitle = cell.dataset.truncatedTitle !== void 0;
970
+ recoverCellText(cell);
923
971
  const part = truncatedPart(cell);
924
972
  if (part) {
925
973
  cell.dataset.truncated = "";
926
- if ((!cell.title || ownTitle) && (part === cell || !part.title)) {
927
- cell.title = (part.textContent ?? "").replace(/\s+/g, " ").trim();
928
- cell.dataset.truncatedTitle = "";
929
- }
930
974
  if (!cell.querySelector(TRUNCATION_FOCUSABLE)) cell.tabIndex = 0;
931
975
  } else if (cell.dataset.truncated !== void 0) {
932
976
  delete cell.dataset.truncated;
933
- if (ownTitle) {
934
- cell.removeAttribute("title");
935
- delete cell.dataset.truncatedTitle;
936
- }
937
977
  cell.removeAttribute("tabindex");
938
978
  }
939
979
  }
@@ -1380,6 +1420,7 @@ DataTable.Content = function DataTableContent() {
1380
1420
  return /* @__PURE__ */ jsx(
1381
1421
  TableCell,
1382
1422
  {
1423
+ ref: col.ellipsis ? recoverCellText : void 0,
1383
1424
  priority: col.priority,
1384
1425
  "data-align": col.align,
1385
1426
  title,
@@ -1,3 +1,5 @@
1
+ export { Persona } from "./persona.js";
2
+ export type { PersonaProp, PersonaProps } from "./persona.js";
1
3
  export { Badge, StatusBadge } from "./badge.js";
2
4
  export type { BadgeProps, BadgeTone } from "./badge.js";
3
5
  export { Legend } from "./legend.js";
@@ -1,3 +1,4 @@
1
+ import { Persona } from "./persona.js";
1
2
  import { Badge, StatusBadge } from "./badge.js";
2
3
  import { Legend } from "./legend.js";
3
4
  import { Swatch } from "./swatch.js";
@@ -78,6 +79,7 @@ export {
78
79
  Legend,
79
80
  ListRow,
80
81
  PermissionMatrix,
82
+ Persona,
81
83
  Popover,
82
84
  PopoverAnchor,
83
85
  PopoverContent,
@@ -0,0 +1,11 @@
1
+ import * as React from "react";
2
+ export type { PersonaProp, PersonaProp as PersonaProps, } from "../../props/components/data-display.prop.js";
3
+ /** A person's identity, independent of row chrome, navigation and account actions. */
4
+ export declare const Persona: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> & {
5
+ name: string;
6
+ description?: string;
7
+ avatarSrc?: string;
8
+ avatarFallback?: React.ReactNode;
9
+ trailing?: React.ReactNode;
10
+ size?: Extract<import("../../props/index.js").SizeProp, "sm" | "md">;
11
+ } & React.RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
6
+ const Persona = React.forwardRef(
7
+ ({ name, description, avatarSrc, avatarFallback, trailing, size = "md", className, ...props }, ref) => /* @__PURE__ */ jsxs(
8
+ "span",
9
+ {
10
+ ref,
11
+ "data-slot": "persona",
12
+ "data-size": size,
13
+ className: cn("ui-persona", className),
14
+ ...props,
15
+ children: [
16
+ /* @__PURE__ */ jsxs(Avatar, { size, "aria-hidden": "true", children: [
17
+ avatarSrc ? /* @__PURE__ */ jsx(AvatarImage, { src: avatarSrc, alt: "" }) : null,
18
+ /* @__PURE__ */ jsx(AvatarFallback, { children: avatarFallback ?? Array.from(name)[0] })
19
+ ] }),
20
+ /* @__PURE__ */ jsxs("span", { "data-slot": "persona-body", children: [
21
+ /* @__PURE__ */ jsxs("span", { "data-slot": "persona-name-line", children: [
22
+ /* @__PURE__ */ jsx("span", { "data-slot": "persona-name", dir: "auto", title: name, children: name }),
23
+ trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "persona-trailing", children: trailing }) : null
24
+ ] }),
25
+ description != null ? /* @__PURE__ */ jsx("span", { "data-slot": "persona-description", dir: "auto", title: description, children: description }) : null
26
+ ] })
27
+ ]
28
+ }
29
+ )
30
+ );
31
+ Persona.displayName = "Persona";
32
+ export {
33
+ Persona
34
+ };
@@ -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 = DEFAULT_VALUE,
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(defaultValue);
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
- value: "all",
133
- label: resolvedAllLabel,
134
- description: allLabel === void 0 ? t("dataEntry.branchScope.allDescription") : void 0
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 { cloneElement, useRef } from "react";
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;
@@ -11,6 +11,7 @@ import {
11
11
  } from "../navigation/dropdown-menu.js";
12
12
  function Breadcrumb({
13
13
  items,
14
+ currentItem = "last",
14
15
  linkComponent: LinkComponent = "a",
15
16
  ariaLabel,
16
17
  separator,
@@ -27,7 +28,15 @@ function Breadcrumb({
27
28
  "data-ellipsis": item.ellipsis ? "" : void 0,
28
29
  children: [
29
30
  index > 0 ? /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-separator", "aria-hidden": "true", children: separatorNode }) : null,
30
- renderBreadcrumbSegment({ item, index, isLast, items, itemRender, LinkComponent })
31
+ renderBreadcrumbSegment({
32
+ item,
33
+ index,
34
+ isLast,
35
+ isCurrent: isLast && currentItem === "last",
36
+ items,
37
+ itemRender,
38
+ LinkComponent
39
+ })
31
40
  ]
32
41
  },
33
42
  item.to ?? index
@@ -38,6 +47,7 @@ function renderBreadcrumbSegment({
38
47
  item,
39
48
  index,
40
49
  isLast,
50
+ isCurrent,
41
51
  items,
42
52
  itemRender,
43
53
  LinkComponent
@@ -62,10 +72,10 @@ function renderBreadcrumbSegment({
62
72
  )) })
63
73
  ] });
64
74
  }
65
- if (item.to && !isLast) {
75
+ if (item.to && !isCurrent) {
66
76
  return /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(LinkComponent, { href: item.to, to: item.to, className: "ui-breadcrumb-link", children: item.label }) });
67
77
  }
68
- return /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-current", "aria-current": isLast ? "page" : void 0, children: item.label }) });
78
+ return /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-current", "aria-current": isCurrent ? "page" : void 0, children: item.label }) });
69
79
  }
70
80
  export {
71
81
  Breadcrumb
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
2
2
  export type { PageContainerProp, PageContainerProp as PageContainerProps, PageContainerExtraProp, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
- declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, icon, cover, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
5
+ declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbCurrentItem, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, icon, cover, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
6
6
  export declare const PageContainer: typeof PageContainerRoot & {
7
7
  Inset: typeof PageContainerInset;
8
8
  };
@@ -42,6 +42,7 @@ function PageContainerRoot({
42
42
  toolbarPad,
43
43
  footerPad,
44
44
  breadcrumb,
45
+ breadcrumbCurrentItem = "last",
45
46
  breadcrumbLabel,
46
47
  breadcrumbAriaLabel,
47
48
  linkComponent: LinkComponent = "a",
@@ -111,6 +112,7 @@ function PageContainerRoot({
111
112
  className: "ui-breadcrumb",
112
113
  children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", "data-ellipsis": trailEllipsis(breadcrumb), children: breadcrumb.map((item, i) => {
113
114
  const isLast = i === breadcrumb.length - 1;
115
+ const isCurrent = isLast && breadcrumbCurrentItem === "last";
114
116
  return (
115
117
  // `--space-inline-xs` gap the previous `.ui-inline-xs` gave, and it brings
116
118
  // `.ui-breadcrumb-item svg { 0.75rem }` with it, so the separator glyph no
@@ -124,7 +126,7 @@ function PageContainerRoot({
124
126
  "data-ellipsis": item.ellipsis ? "" : void 0,
125
127
  children: [
126
128
  i > 0 ? /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-separator", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) }) : null,
127
- item.to && !isLast ? /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(
129
+ item.to && !isCurrent ? /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(
128
130
  LinkComponent,
129
131
  {
130
132
  href: item.to,
@@ -135,8 +137,8 @@ function PageContainerRoot({
135
137
  ) }) : /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(
136
138
  "span",
137
139
  {
138
- className: isLast ? "text-foreground" : "",
139
- "aria-current": isLast ? "page" : void 0,
140
+ className: isCurrent ? "text-foreground" : "",
141
+ "aria-current": isCurrent ? "page" : void 0,
140
142
  children: item.label
141
143
  }
142
144
  ) })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "33.12.8",
3
+ "version": "33.14.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -171,7 +171,8 @@
171
171
  "error": "Could not load branches",
172
172
  "denied": "You do not have permission to view branches",
173
173
  "loading": "Loading branches…",
174
- "noMatches": "No branches match your search"
174
+ "noMatches": "No branches match your search",
175
+ "choose": "Choose a branch"
175
176
  },
176
177
  "formErrors": {
177
178
  "title": "Please review the following errors"