@godxjp/ui 33.22.2 → 33.24.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 (65) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +4 -4
  3. package/agent/DESIGN.md +1 -1
  4. package/agent/START-HERE.md +7 -7
  5. package/agent/components/AgentBadge.json +43 -0
  6. package/agent/components/AppShell.json +6 -0
  7. package/agent/components/Avatar.json +7 -0
  8. package/agent/components/PageContainer.json +5 -0
  9. package/agent/components/PageErrorBoundary.json +48 -0
  10. package/agent/components/Toaster.json +1 -1
  11. package/agent/components/Toolbar.json +1 -1
  12. package/agent/components/Topbar.json +1 -1
  13. package/agent/components-index.json +10 -0
  14. package/agent/components.json +112 -3
  15. package/agent/index.json +7 -7
  16. package/agent/llms.txt +6 -6
  17. package/agent/tokens.json +84 -0
  18. package/dist/components/data-display/avatar.d.ts +1 -1
  19. package/dist/components/data-display/avatar.js +7 -1
  20. package/dist/components/data-display/badge.d.ts +11 -0
  21. package/dist/components/data-display/badge.js +20 -0
  22. package/dist/components/data-display/image.js +1 -1
  23. package/dist/components/data-display/index.d.ts +2 -2
  24. package/dist/components/data-display/index.js +2 -1
  25. package/dist/components/data-entry/record-picker.js +122 -98
  26. package/dist/components/data-entry/select.js +13 -3
  27. package/dist/components/layout/app-shell.d.ts +1 -1
  28. package/dist/components/layout/app-shell.js +3 -1
  29. package/dist/components/layout/index.d.ts +2 -0
  30. package/dist/components/layout/index.js +2 -0
  31. package/dist/components/layout/page-container.d.ts +1 -1
  32. package/dist/components/layout/page-container.js +6 -3
  33. package/dist/components/layout/page-error-boundary.d.ts +32 -0
  34. package/dist/components/layout/page-error-boundary.js +57 -0
  35. package/dist/components/layout/sidebar.d.ts +1 -1
  36. package/dist/components/layout/sidebar.js +100 -49
  37. package/dist/components/layout/topbar.js +108 -21
  38. package/dist/components/navigation/toolbar.js +2 -1
  39. package/dist/contracts/measurement.json +1 -1
  40. package/dist/i18n/messages/en.json +6 -0
  41. package/dist/i18n/messages/ja.json +6 -0
  42. package/dist/i18n/messages/vi.json +6 -0
  43. package/dist/props/components/data-display.prop.d.ts +27 -0
  44. package/dist/props/components/layout.prop.d.ts +45 -0
  45. package/dist/props/components/navigation.prop.d.ts +12 -1
  46. package/dist/props/registry.d.ts +50 -2
  47. package/dist/props/registry.js +72 -3
  48. package/dist/styles/alert-layout.css +4 -0
  49. package/dist/styles/badge-layout.css +14 -0
  50. package/dist/styles/data-display-layout.css +44 -0
  51. package/dist/styles/icon-layout.css +3 -0
  52. package/dist/styles/layers.json +22 -2
  53. package/dist/styles/layout.css +11 -0
  54. package/dist/styles/shell-layout.css +17 -1
  55. package/dist/styles/table-layout.css +1 -1
  56. package/dist/styles.css +1 -1
  57. package/dist/tokens/components/badge.css +3 -0
  58. package/dist/tokens/components/data-display.css +9 -0
  59. package/dist/tokens/components/shell.css +2 -0
  60. package/dist/tokens/foundation.css +8 -0
  61. package/docs/CONSUMER-RULES.md +1 -1
  62. package/package.json +2 -2
  63. package/scripts/consumer-rule.md +1 -1
  64. package/skills/godxjp-ui-navigation/SKILL.md +14 -9
  65. package/skills/godxjp-ui-navigation/references/principles.md +2 -2
package/agent/tokens.json CHANGED
@@ -185,6 +185,24 @@
185
185
  "tier": "foundation",
186
186
  "value": "48 8% 13%"
187
187
  },
188
+ {
189
+ "description": "AGENT role — marks an AI agent (Avatar kind=\"agent\", AgentBadge) so it is never mistaken for a person. Violet, chosen apart from every status hue so \"AI\" never reads as a state. FILL #7c3aed under white ink measures 5.70:1; TEXT --text-agent #6d28d9 on white measures 7.10:1 (AA). #7c3aed",
190
+ "name": "--agent",
191
+ "tier": "foundation",
192
+ "value": "262 83% 58%"
193
+ },
194
+ {
195
+ "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
196
+ "name": "--agent-foreground",
197
+ "tier": "foundation",
198
+ "value": "0 0% 100%"
199
+ },
200
+ {
201
+ "description": "#6d28d9 — AA violet text on white",
202
+ "name": "--text-agent",
203
+ "tier": "foundation",
204
+ "value": "263 70% 50%"
205
+ },
188
206
  {
189
207
  "description": "IDENTITY role — the PRODUCT MARK colour (Logo `tone=\"success\"`, its wordmark, the email brand capsule). Neutral ink by default since v32 (gh#1220); the GoDX preset (`themes/godx.css`) sets it back to the violet of identity v2.3. It is deliberately distinct from two neighbours it keeps being confused with: · from --primary — a service re-theming its action colour must never recolour the mark. They hold the SAME VALUE by default and are still separate declarations; `brand-identity-role.test.ts` asserts on the DECLARATION, not the value, that this one never reads `var(--primary)` (gh#250). · from --success — 若竹 #68be8d is the STATUS green (badges, progress, \"saved\" text) and stays exactly as it is. The mark borrowed it once, rendering the identity at #69bf8e — ΔE76 ≈ 17.5 from the identity of the day, a plainly wrong brand colour (gh#250/#214). If the identity moves again, THIS COMMENT MOVES WITH IT. There is no Tailwind `bg-brand` utility on purpose: `--color-brand` is already taken by --text-brand, so identity surfaces read `hsl(var(--brand))` directly in component CSS. neutral ink. Same value as --primary by default, NOT an alias of it: re-theming the action colour must still leave the identity alone, which is what brand-identity-role.test.ts checks on the DECLARATION rather than the value.",
190
208
  "name": "--brand",
@@ -2309,6 +2327,18 @@
2309
2327
  "tier": "component",
2310
2328
  "value": "initial"
2311
2329
  },
2330
+ {
2331
+ "description": "AgentBadge — the violet outline and ink of the \"AI agent\" label. Role-mirrors, `initial`: hsl(var(--agent)) border and hsl(var(--text-agent)) ink at the call site.",
2332
+ "name": "--badge-agent-border",
2333
+ "tier": "component",
2334
+ "value": "initial"
2335
+ },
2336
+ {
2337
+ "description": "Badge component tokens.",
2338
+ "name": "--badge-agent-foreground",
2339
+ "tier": "component",
2340
+ "value": "initial"
2341
+ },
2312
2342
  {
2313
2343
  "description": "Leading/trailing glyph inside the pill. It was a bare `0.75rem` in badge-layout.css with no token at all, so a service could only resize it with `!important` or a forked stylesheet — the two routes the icon axis's tier 2 exists to abolish (gh#326). Same step, so nothing moves. Matches --badge-font-size's step by design: the glyph reads as a character in the label's run, so the two retune together. Deliberately NOT --scaling-multiplied — the literal it replaces did not track density.",
2314
2344
  "name": "--badge-icon-size",
@@ -5939,6 +5969,54 @@
5939
5969
  "tier": "component",
5940
5970
  "value": "initial"
5941
5971
  },
5972
+ {
5973
+ "description": "Avatar `kind=\"agent\"` — the \"AI\" tag pinned to the block-start/inline-end corner (presence owns block-end). The tag's text is aria-hidden; a localized sr-only \"AI agent\" carries the meaning, so the colour is never the only signal (WCAG 1.4.1). --avatar-agent-background / -foreground / -ring-color are ROLE-MIRROR knobs, `initial` here so the --agent / --agent-foreground / --background defaults resolve at the call site under a scoped [data-tenant] / .dark theme. Defaults = 2xs bold label · pill · violet --agent fill, white ink · a 2px --background separator ring · spilling --space-1 past the corner. default = var(--font-size-2xs) at the call site",
5974
+ "name": "--avatar-agent-font-size",
5975
+ "tier": "component",
5976
+ "value": "initial"
5977
+ },
5978
+ {
5979
+ "description": "default = var(--font-weight-bold) at the call site",
5980
+ "name": "--avatar-agent-font-weight",
5981
+ "tier": "component",
5982
+ "value": "initial"
5983
+ },
5984
+ {
5985
+ "description": "default = var(--space-1) at the call site",
5986
+ "name": "--avatar-agent-padding-inline",
5987
+ "tier": "component",
5988
+ "value": "initial"
5989
+ },
5990
+ {
5991
+ "description": "default = calc(var(--space-1) * -1) at the call site: the tag spills slightly past the mark",
5992
+ "name": "--avatar-agent-inset",
5993
+ "tier": "component",
5994
+ "value": "initial"
5995
+ },
5996
+ {
5997
+ "description": "default = var(--stroke-md) at the call site",
5998
+ "name": "--avatar-agent-ring-width",
5999
+ "tier": "component",
6000
+ "value": "initial"
6001
+ },
6002
+ {
6003
+ "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
6004
+ "name": "--avatar-agent-background",
6005
+ "tier": "component",
6006
+ "value": "initial"
6007
+ },
6008
+ {
6009
+ "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
6010
+ "name": "--avatar-agent-foreground",
6011
+ "tier": "component",
6012
+ "value": "initial"
6013
+ },
6014
+ {
6015
+ "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
6016
+ "name": "--avatar-agent-ring-color",
6017
+ "tier": "component",
6018
+ "value": "initial"
6019
+ },
5942
6020
  {
5943
6021
  "description": "Progress track + fill — `initial` so the role defaults re-resolve under a scoped theme. Track reads --secondary, the default (`primary`) fill reads the brand mark; a service re-tones once. Defaults = hsl(var(--secondary)) track · hsl(var(--mark-primary, var(--primary))) fill (gh#1308 — it was the success mark; status tones are opt-in and do not read this knob).",
5944
6022
  "name": "--progress-track-background",
@@ -16247,6 +16325,12 @@
16247
16325
  "tier": "component",
16248
16326
  "value": "var(--space-3)"
16249
16327
  },
16328
+ {
16329
+ "description": "The gap between the brand and the custom bar inside `.app-topbar-rail` (gh#1481). It was a literal `--space-3`, so a theme that tightens the bar for a 320px / 200%-text viewport could reach every other gap in the row but this one.",
16330
+ "name": "--app-shell-rail-gap",
16331
+ "tier": "component",
16332
+ "value": "var(--space-3)"
16333
+ },
16250
16334
  {
16251
16335
  "description": "HOW MUCH OF THE BAR THE BRAND MAY KEEP (gh#728). A wide bar is not the problem, so the cap is `none` there; below the `sm` step the brand keeps at most a square of the row it sits in, and everything past that is cropped from the inline-end — which is the half a lockup can lose, because a lockup leads with its mark in both directions. A theme that wants the wordmark to survive on a phone raises the compact cap; a theme with a very wide lockup lowers the other. Choosing WHAT the narrow bar shows, rather than how much of it, is `AppShell logoCompact`: `viewBox` is an attribute and no stylesheet can set one.",
16252
16336
  "name": "--app-shell-brand-max-inline-size",
@@ -3,7 +3,7 @@ import { type ImageLoadingStatus } from "../../lib/image-loading-status.js";
3
3
  import type { AvatarImageFitProp, AvatarProp } from "../../props/components/data-display.prop.js";
4
4
  export declare const Avatar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
5
5
  asChild?: boolean;
6
- } & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
6
+ } & Pick<AvatarProp, "appearance" | "kind" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
7
7
  export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & {
8
8
  asChild?: boolean;
9
9
  onLoadingStatusChange?: (status: ImageLoadingStatus) => void;
@@ -22,6 +22,7 @@ const Avatar = React.forwardRef(
22
22
  appearance = "default",
23
23
  presence,
24
24
  presenceLabel,
25
+ kind = "person",
25
26
  asChild = false,
26
27
  children,
27
28
  ...props
@@ -42,11 +43,16 @@ const Avatar = React.forwardRef(
42
43
  "data-size": size === "md" ? void 0 : size,
43
44
  "data-appearance": appearance === "tinted" ? "tinted" : void 0,
44
45
  "data-presence": presence,
46
+ "data-kind": kind === "agent" ? "agent" : void 0,
45
47
  className: cn("ui-avatar", className),
46
48
  ...props,
47
49
  children: [
48
50
  children,
49
- presence !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "avatar-presence", "data-presence": presence, children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: presenceLabel ?? t(`dataDisplay.avatar.presence.${presence}`) }) }) : null
51
+ presence !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "avatar-presence", "data-presence": presence, children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: presenceLabel ?? t(`dataDisplay.avatar.presence.${presence}`) }) }) : null,
52
+ kind === "agent" ? /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-agent", children: [
53
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "AI" }),
54
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.avatar.agent") })
55
+ ] }) : null
50
56
  ]
51
57
  }
52
58
  ) });
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
+ import type { AgentBadgeProp } from "../../props/components/data-display.prop.js";
3
4
  import type { ShapeProp, ToneProp } from "../../props/vocabulary/index.js";
4
5
  export type BadgeVariant = "default" | "secondary" | "outline" | "dashed";
5
6
  /**
@@ -100,4 +101,14 @@ export declare function Badge({ as: Element, className, variant, shape, tone, co
100
101
  * general-purpose primitive.
101
102
  */
102
103
  export declare const StatusBadge: typeof Badge;
104
+ /** @see AgentBadgeProp */
105
+ export type AgentBadgeProps = AgentBadgeProp;
106
+ /**
107
+ * The "AI agent" label beside a NAME — the text-row half of telling an AI agent apart from a
108
+ * person; `<Avatar kind="agent">` is the mark half. A list that shows names uses both; a place with
109
+ * only a mark (an assignee icon column, a dense cell) uses the Avatar alone. Violet outline on a
110
+ * transparent ground (`--badge-agent-*`, reading the `--agent` role), pill, never wraps. The label
111
+ * is real text, so it is announced as written and never colour-only.
112
+ */
113
+ export declare function AgentBadge({ as: Element, className, children, ...props }: AgentBadgeProps): React.JSX.Element;
103
114
  export {};
@@ -193,7 +193,27 @@ function Badge({
193
193
  ] });
194
194
  }
195
195
  const StatusBadge = Badge;
196
+ function AgentBadge({
197
+ as: Element = "span",
198
+ className,
199
+ children,
200
+ ...props
201
+ }) {
202
+ const { t } = useTranslation();
203
+ return /* @__PURE__ */ jsx(
204
+ Element,
205
+ {
206
+ "data-slot": "badge",
207
+ "data-agent": "",
208
+ "data-shape": "pill",
209
+ className: cn(badgeVariants({ variant: "tinted", shape: "pill" }), className),
210
+ ...props,
211
+ children: /* @__PURE__ */ jsx("span", { "data-slot": "badge-label", children: children ?? t("dataDisplay.avatar.agent") })
212
+ }
213
+ );
214
+ }
196
215
  export {
216
+ AgentBadge,
197
217
  Badge,
198
218
  StatusBadge
199
219
  };
@@ -454,7 +454,7 @@ const ImageRoot = React.forwardRef(function Image({
454
454
  const setImgRef = React.useCallback(
455
455
  (node) => {
456
456
  imgRef.current = node;
457
- if (node?.complete && node.naturalWidth > 0) setStatus("loaded");
457
+ if (node?.complete) setStatus(node.naturalWidth > 0 ? "loaded" : "error");
458
458
  if (typeof ref === "function") ref(node);
459
459
  else if (ref) ref.current = node;
460
460
  },
@@ -1,7 +1,7 @@
1
1
  export { Persona } from "./persona.js";
2
2
  export type { PersonaProp, PersonaProps } from "./persona.js";
3
- export { Badge, StatusBadge } from "./badge.js";
4
- export type { BadgeProps, BadgeTone } from "./badge.js";
3
+ export { AgentBadge, Badge, StatusBadge } from "./badge.js";
4
+ export type { AgentBadgeProps, BadgeProps, BadgeTone } from "./badge.js";
5
5
  export { Legend } from "./legend.js";
6
6
  export type { LegendItemProp, LegendProp, LegendProps } from "./legend.js";
7
7
  export { Swatch } from "./swatch.js";
@@ -1,5 +1,5 @@
1
1
  import { Persona } from "./persona.js";
2
- import { Badge, StatusBadge } from "./badge.js";
2
+ import { AgentBadge, Badge, StatusBadge } from "./badge.js";
3
3
  import { Legend } from "./legend.js";
4
4
  import { Swatch } from "./swatch.js";
5
5
  import { FeatureList } from "./feature-list.js";
@@ -60,6 +60,7 @@ export {
60
60
  AccordionContent,
61
61
  AccordionItem,
62
62
  AccordionTrigger,
63
+ AgentBadge,
63
64
  Avatar,
64
65
  AvatarFallback,
65
66
  AvatarImage,
@@ -21,6 +21,7 @@ import {
21
21
  import { Command, CommandGroup } from "./command.js";
22
22
  import { Input } from "./input.js";
23
23
  import { isImeComposing } from "../../lib/ime.js";
24
+ import { FieldNameContext } from "../../lib/field-a11y.js";
24
25
  import { Select } from "./select.js";
25
26
  function selectLoadOptionsFor(loadOptions) {
26
27
  if (!loadOptions) return void 0;
@@ -35,103 +36,53 @@ function selectLoadOptionsFor(loadOptions) {
35
36
  return { options: r.options, hasMore: r.nextCursor !== void 0 };
36
37
  };
37
38
  }
38
- const RecordPicker = React.forwardRef(
39
- function RecordPicker2({
40
- mode = "single",
41
- shape = "auto",
42
- value,
43
- defaultValue,
44
- onValueChange,
45
- options,
46
- loadOptions,
47
- count,
48
- threshold = 10,
49
- filters,
50
- selectedOptions,
51
- emptyOption,
52
- placeholder,
53
- dialogTitle,
54
- notFoundContent,
55
- loadMoreLabel,
56
- disabled = false,
57
- size,
58
- className,
59
- "data-field": fieldName,
60
- ...props
61
- }, ref) {
62
- const { t } = useTranslation();
63
- const isMultiple = mode === "multiple";
64
- const controlled = value !== void 0;
65
- const [internal, setInternal] = React.useState(
66
- () => toArray(defaultValue ?? (isMultiple ? [] : null))
67
- );
68
- const selected = controlled ? toArray(value) : internal;
69
- const commit = React.useCallback(
70
- (next) => {
71
- if (!controlled) setInternal(next);
72
- onValueChange?.(isMultiple ? next : next[0] ?? null);
73
- },
74
- [controlled, isMultiple, onValueChange]
75
- );
76
- const selectLoadOptions = React.useMemo(() => selectLoadOptionsFor(loadOptions), [loadOptions]);
77
- const staticOptions = React.useMemo(
78
- () => options ? normalizeSelectOptions(options) : [],
79
- [options]
80
- );
81
- if (shape === "inline") {
82
- return /* @__PURE__ */ jsx(
83
- InlinePicker,
84
- {
85
- ref,
86
- isMultiple,
87
- selected,
88
- commit,
89
- staticOptions,
90
- loadOptions,
91
- filters,
92
- selectedOptions,
93
- emptyOption,
94
- placeholder,
95
- dialogTitle,
96
- notFoundContent,
97
- loadMoreLabel,
98
- disabled,
99
- size,
100
- className,
101
- "data-field": fieldName,
102
- ...props
103
- }
104
- );
105
- }
106
- const total = count ?? (options ? staticOptions.length : void 0);
107
- const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
108
- if (!asDialog) {
109
- return /* @__PURE__ */ jsx(
110
- Select,
111
- {
112
- mode: isMultiple ? "multiple" : void 0,
113
- loadOptions: selectLoadOptions,
114
- id: props.id,
115
- "data-field": fieldName,
116
- "aria-describedby": props["aria-describedby"],
117
- labelInValue: false,
118
- showSearch: true,
119
- options: withEmptyOption(options, emptyOption),
120
- value: isMultiple ? selected : selected[0] ?? void 0,
121
- onValueChange: ((next) => commit(toArray(next))),
122
- placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
123
- notFoundContent,
124
- disabled,
125
- size,
126
- className
127
- }
128
- );
129
- }
39
+ const RecordPicker = React.forwardRef(function RecordPicker2({
40
+ mode = "single",
41
+ shape = "auto",
42
+ value,
43
+ defaultValue,
44
+ onValueChange,
45
+ options,
46
+ loadOptions,
47
+ count,
48
+ threshold = 10,
49
+ filters,
50
+ selectedOptions,
51
+ emptyOption,
52
+ placeholder,
53
+ dialogTitle,
54
+ notFoundContent,
55
+ loadMoreLabel,
56
+ disabled = false,
57
+ size,
58
+ className,
59
+ "data-field": fieldName,
60
+ ...props
61
+ }, ref) {
62
+ const { t } = useTranslation();
63
+ const isMultiple = mode === "multiple";
64
+ const controlled = value !== void 0;
65
+ const [internal, setInternal] = React.useState(
66
+ () => toArray(defaultValue ?? (isMultiple ? [] : null))
67
+ );
68
+ const selected = controlled ? toArray(value) : internal;
69
+ const commit = React.useCallback(
70
+ (next) => {
71
+ if (!controlled) setInternal(next);
72
+ onValueChange?.(isMultiple ? next : next[0] ?? null);
73
+ },
74
+ [controlled, isMultiple, onValueChange]
75
+ );
76
+ const selectLoadOptions = React.useMemo(() => selectLoadOptionsFor(loadOptions), [loadOptions]);
77
+ const staticOptions = React.useMemo(
78
+ () => options ? normalizeSelectOptions(options) : [],
79
+ [options]
80
+ );
81
+ if (shape === "inline") {
130
82
  return /* @__PURE__ */ jsx(
131
- DialogPicker,
83
+ InlinePicker,
132
84
  {
133
85
  ref,
134
- "data-field": fieldName,
135
86
  isMultiple,
136
87
  selected,
137
88
  commit,
@@ -147,11 +98,59 @@ const RecordPicker = React.forwardRef(
147
98
  disabled,
148
99
  size,
149
100
  className,
101
+ "data-field": fieldName,
150
102
  ...props
151
103
  }
152
104
  );
153
105
  }
154
- );
106
+ const total = count ?? (options ? staticOptions.length : void 0);
107
+ const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
108
+ if (!asDialog) {
109
+ return /* @__PURE__ */ jsx(
110
+ Select,
111
+ {
112
+ mode: isMultiple ? "multiple" : void 0,
113
+ loadOptions: selectLoadOptions,
114
+ id: props.id,
115
+ "data-field": fieldName,
116
+ "aria-describedby": props["aria-describedby"],
117
+ labelInValue: false,
118
+ showSearch: true,
119
+ options: withEmptyOption(options, emptyOption),
120
+ value: isMultiple ? selected : selected[0] ?? void 0,
121
+ onValueChange: ((next) => commit(toArray(next))),
122
+ placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
123
+ notFoundContent,
124
+ disabled,
125
+ size,
126
+ className
127
+ }
128
+ );
129
+ }
130
+ return /* @__PURE__ */ jsx(
131
+ DialogPicker,
132
+ {
133
+ ref,
134
+ "data-field": fieldName,
135
+ isMultiple,
136
+ selected,
137
+ commit,
138
+ staticOptions,
139
+ loadOptions,
140
+ filters,
141
+ selectedOptions,
142
+ emptyOption,
143
+ placeholder,
144
+ dialogTitle,
145
+ notFoundContent,
146
+ loadMoreLabel,
147
+ disabled,
148
+ size,
149
+ className,
150
+ ...props
151
+ }
152
+ );
153
+ });
155
154
  const INLINE_SUGGESTION_LIMIT = 8;
156
155
  function toArray(v) {
157
156
  if (v === null || v === void 0) return [];
@@ -179,10 +178,30 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
179
178
  className,
180
179
  id,
181
180
  "data-field": fieldName,
182
- "aria-describedby": describedBy
181
+ "aria-describedby": describedBy,
182
+ "aria-label": ariaLabel,
183
+ "aria-labelledby": ariaLabelledBy,
184
+ "aria-errormessage": ariaErrorMessage,
185
+ "aria-invalid": ariaInvalid,
186
+ "aria-required": ariaRequired
183
187
  }, ref) {
184
188
  const { t } = useTranslation();
185
189
  const [query, setQuery] = React.useState("");
190
+ const inputRef = React.useRef(null);
191
+ const [hasNativeLabel, setHasNativeLabel] = React.useState(false);
192
+ React.useLayoutEffect(() => {
193
+ setHasNativeLabel((inputRef.current?.labels?.length ?? 0) > 0);
194
+ }, [id]);
195
+ const enclosingField = React.useContext(FieldNameContext);
196
+ const fallbackName = ariaLabel === void 0 && ariaLabelledBy === void 0 && !enclosingField && !hasNativeLabel ? placeholder ?? t("dataEntry.recordPicker.search") : void 0;
197
+ const setInputRef = React.useCallback(
198
+ (node) => {
199
+ inputRef.current = node;
200
+ if (typeof ref === "function") ref(node);
201
+ else if (ref) ref.current = node;
202
+ },
203
+ [ref]
204
+ );
186
205
  const [rows, setRows] = React.useState(staticOptions);
187
206
  const [openSuggest, setOpenSuggest] = React.useState(false);
188
207
  const [dialogQuery, setDialogQuery] = React.useState(null);
@@ -214,7 +233,7 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
214
233
  /* @__PURE__ */ jsx(
215
234
  Input,
216
235
  {
217
- ref,
236
+ ref: setInputRef,
218
237
  type: "search",
219
238
  value: query,
220
239
  disabled,
@@ -232,7 +251,12 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
232
251
  className: "ui-record-picker-inline-input",
233
252
  id,
234
253
  "data-field": fieldName,
235
- "aria-describedby": describedBy
254
+ "aria-describedby": describedBy,
255
+ "aria-label": ariaLabel ?? fallbackName,
256
+ "aria-labelledby": ariaLabelledBy,
257
+ "aria-errormessage": ariaErrorMessage,
258
+ "aria-invalid": ariaInvalid,
259
+ "aria-required": ariaRequired
236
260
  }
237
261
  ),
238
262
  /* @__PURE__ */ jsxs(
@@ -13,7 +13,8 @@ import {
13
13
  Select as AriaSelect,
14
14
  SelectStateContext,
15
15
  SelectValue as AriaSelectValue,
16
- Separator as AriaSeparator
16
+ Separator as AriaSeparator,
17
+ Text as AriaText
17
18
  } from "react-aria-components";
18
19
  import { ChevronDown, Loader2, X } from "lucide-react";
19
20
  import { cn } from "../../lib/utils.js";
@@ -613,6 +614,14 @@ function withOptionIcon(option, label) {
613
614
  /* @__PURE__ */ jsx("span", { className: "ui-select-option-label", children: label })
614
615
  ] });
615
616
  }
617
+ function optionRow(option) {
618
+ if (!option.sublabel) return withOptionIcon(option, option.label);
619
+ return /* @__PURE__ */ jsxs("span", { className: "ui-select-option-body", children: [
620
+ option.icon ? /* @__PURE__ */ jsx("span", { className: "ui-select-option-icon", "aria-hidden": "true", children: option.icon }) : null,
621
+ /* @__PURE__ */ jsx(AriaText, { slot: "label", className: "ui-select-option-label", children: option.label }),
622
+ /* @__PURE__ */ jsx(AriaText, { slot: "description", className: "ui-search-select-option-sublabel", children: option.sublabel })
623
+ ] });
624
+ }
616
625
  function DataSelectLabel({
617
626
  labelRender,
618
627
  options
@@ -729,7 +738,7 @@ function DataSelect(props) {
729
738
  disabled: option.disabled,
730
739
  "data-testid": optionTestId(option.value),
731
740
  selectedIcon: menuItemSelectedIcon,
732
- children: optionRender ? optionRender(option, { index }) : renderOption ? renderOption(option) : withOptionIcon(option, option.label)
741
+ children: optionRender ? optionRender(option, { index }) : renderOption ? renderOption(option) : optionRow(option)
733
742
  },
734
743
  option.value
735
744
  );
@@ -784,8 +793,9 @@ function DataSelect(props) {
784
793
  ...ariaProps,
785
794
  children: [
786
795
  prefix === void 0 || prefix === null ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-prefix", className: "ui-select-prefix", children: prefix }),
787
- /* @__PURE__ */ jsx(SelectValue, { placeholder: placeholder ?? t("dataEntry.searchSelect.placeholder"), children: labelRender ? /* @__PURE__ */ jsx(DataSelectLabel, { labelRender, options }) : options.some((option) => option.icon) ? (
796
+ /* @__PURE__ */ jsx(SelectValue, { placeholder: placeholder ?? t("dataEntry.searchSelect.placeholder"), children: labelRender ? /* @__PURE__ */ jsx(DataSelectLabel, { labelRender, options }) : options.some((option) => option.icon || option.sublabel) ? (
788
797
  // gh#1444 — the selected option's icon leads the trigger too, as in the search panel.
798
+ // gh#1495 — and a sublabel stays in the row: the trigger names the pick by its label.
789
799
  /* @__PURE__ */ jsx(
790
800
  DataSelectLabel,
791
801
  {
@@ -3,4 +3,4 @@ import type { AppShellProp } from "../../props/components/layout.prop.js";
3
3
  export type { AppShellProp, AppShellProp as AppShellProps, } from "../../props/components/layout.prop.js";
4
4
  /** Same rail boundary as shell-layout.css; SSR starts in docked mode. */
5
5
  export declare function useAppShellNavigationMode(): "drawer" | "docked";
6
- export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, logoCompact, logoCompactBelow, breadcrumb, footer, children, sidebarCollapsed, responsiveNavigation, topbarSpan, navRail, navRailPosition, navRailEnd, navRailLabel, mobileTabBar, mobileTabBarLabel, mobileNav, mobileNavLabel, mobileNavTriggerLabel, mobileNavOnCloseAutoFocus, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
6
+ export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, logoCompact, logoCompactBelow, breadcrumb, footer, children, errorBoundary, sidebarCollapsed, responsiveNavigation, topbarSpan, navRail, navRailPosition, navRailEnd, navRailLabel, mobileTabBar, mobileTabBarLabel, mobileNav, mobileNavLabel, mobileNavTriggerLabel, mobileNavOnCloseAutoFocus, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
@@ -10,6 +10,7 @@ import { ShellTabBar } from "./mobile-shell.js";
10
10
  import { useToastStickyBarClearance } from "../../lib/toast-clearance.js";
11
11
  import { NavSurfaceProvider } from "./nav-surface.js";
12
12
  import { TopbarItem } from "./topbar-item.js";
13
+ import { PageErrorBoundary } from "./page-error-boundary.js";
13
14
  import { isDevelopment } from "../../lib/dev.js";
14
15
  function useAppShellNavigationMode() {
15
16
  return useMediaQuery("(width <= 56.25rem)") ? "drawer" : "docked";
@@ -25,6 +26,7 @@ function AppShell({
25
26
  breadcrumb,
26
27
  footer,
27
28
  children,
29
+ errorBoundary = true,
28
30
  sidebarCollapsed = false,
29
31
  responsiveNavigation = "drawer",
30
32
  topbarSpan = "content",
@@ -201,7 +203,7 @@ function AppShell({
201
203
  tabIndex: 0,
202
204
  children: [
203
205
  breadcrumb !== void 0 && /* @__PURE__ */ jsx("div", { className: "app-breadcrumb", children: breadcrumb }),
204
- children,
206
+ errorBoundary === false ? children : /* @__PURE__ */ jsx(PageErrorBoundary, { ...errorBoundary === true ? {} : errorBoundary, children }),
205
207
  showTabBar && footerRegion
206
208
  ]
207
209
  }
@@ -31,6 +31,8 @@ export { CenteredShell } from "./centered-shell.js";
31
31
  export type { CenteredShellProp, CenteredShellProps, CenteredShellAuthProp, CenteredShellPageProp, } from "./centered-shell.js";
32
32
  export type { CenteredShellWidthProp, CenteredShellAlignProp } from "../../props/vocabulary/index.js";
33
33
  export { ErrorSurface } from "./error-surface.js";
34
+ export { PageErrorBoundary } from "./page-error-boundary.js";
35
+ export type { PageErrorBoundaryFallbackProp, PageErrorBoundaryProp, PageErrorBoundaryProps, } from "./page-error-boundary.js";
34
36
  export type { ErrorSurfaceMaintenanceProp, ErrorSurfaceProp, ErrorSurfaceProps, } from "./error-surface.js";
35
37
  export type { ErrorSurfaceModeProp, ErrorSurfaceStatusProp } from "../../props/vocabulary/index.js";
36
38
  export { Breadcrumb } from "./breadcrumb.js";
@@ -14,6 +14,7 @@ import { AuthStack } from "./auth-stack.js";
14
14
  import { MobileShell } from "./mobile-shell.js";
15
15
  import { CenteredShell } from "./centered-shell.js";
16
16
  import { ErrorSurface } from "./error-surface.js";
17
+ import { PageErrorBoundary } from "./page-error-boundary.js";
17
18
  import { Breadcrumb } from "./breadcrumb.js";
18
19
  import { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
19
20
  import { Topbar } from "./topbar.js";
@@ -141,6 +142,7 @@ export {
141
142
  NavList,
142
143
  OrgSwitcher,
143
144
  PageContainer,
145
+ PageErrorBoundary,
144
146
  PriceSummary,
145
147
  ResizableHandle,
146
148
  ResizablePanel,
@@ -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, 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;
5
+ declare function PageContainerRoot({ title, subtitle, status, extra, help, 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
  };
@@ -37,6 +37,7 @@ function PageContainerRoot({
37
37
  subtitle,
38
38
  status,
39
39
  extra,
40
+ help,
40
41
  toolbar,
41
42
  footer,
42
43
  toolbarPad,
@@ -68,7 +69,8 @@ function PageContainerRoot({
68
69
  const hasHeading = headerLoading || hasTitle || hasIcon || status != null || !!subtitle;
69
70
  const hasBreadcrumb = !!breadcrumb && breadcrumb.length > 0;
70
71
  const hasEyecatch = isValidElement(cover) && cover.props.variant === "eyecatch";
71
- const hasHeader = hasHeading || hasBreadcrumb || hasEyecatch || extraStart != null || extraEnd != null;
72
+ const hasHelp = help != null && help !== false;
73
+ const hasHeader = hasHeading || hasBreadcrumb || hasEyecatch || extraStart != null || extraEnd != null || hasHelp;
72
74
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll" && hasHeader;
73
75
  const { headerRef, revealed } = useFooterReveal(reveal);
74
76
  const footerRef = useRef(null);
@@ -177,9 +179,10 @@ function PageContainerRoot({
177
179
  )
178
180
  ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
179
181
  ] }),
180
- (extraStart != null || extraEnd != null) && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-extra", children: [
182
+ (extraStart != null || extraEnd != null || hasHelp) && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-extra", children: [
181
183
  extraStart,
182
- extraEnd
184
+ extraEnd,
185
+ hasHelp ? /* @__PURE__ */ jsx("div", { className: "ui-page-header-help", "data-slot": "page-header-help", children: help }) : null
183
186
  ] })
184
187
  ] })
185
188
  ]