@godxjp/ui 32.2.2 → 32.3.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 (38) hide show
  1. package/README.md +2 -2
  2. package/agent/START-HERE.md +4 -4
  3. package/agent/components/Alert.json +1 -0
  4. package/agent/components/DatePicker.json +6 -0
  5. package/agent/components/EmptyState.json +7 -0
  6. package/agent/components/Form.json +1 -0
  7. package/agent/components/FormField.json +2 -1
  8. package/agent/components/MobileShell.json +1 -1
  9. package/agent/components/PageContainer.json +1 -1
  10. package/agent/components/Toaster.json +2 -1
  11. package/agent/components.json +21 -4
  12. package/agent/index.json +4 -4
  13. package/agent/llms.txt +5 -5
  14. package/agent/tokens.json +12 -0
  15. package/dist/components/data-display/empty-state.d.ts +1 -1
  16. package/dist/components/data-display/empty-state.js +2 -1
  17. package/dist/components/data-entry/calendar.js +16 -1
  18. package/dist/components/data-entry/date-picker.js +22 -7
  19. package/dist/components/data-entry/record-picker.js +22 -16
  20. package/dist/components/feedback/sonner.js +4 -1
  21. package/dist/components/layout/mobile-shell.js +19 -2
  22. package/dist/components/layout/page-container.js +4 -1
  23. package/dist/contracts/measurement.json +1 -1
  24. package/dist/i18n/messages/en.json +3 -1
  25. package/dist/i18n/messages/ja.json +3 -1
  26. package/dist/i18n/messages/vi.json +3 -1
  27. package/dist/lib/datetime/picker-format.d.ts +8 -1
  28. package/dist/lib/datetime/picker-format.js +16 -4
  29. package/dist/lib/toast-clearance.d.ts +25 -0
  30. package/dist/lib/toast-clearance.js +27 -0
  31. package/dist/props/components/data-display.prop.d.ts +7 -0
  32. package/dist/props/components/data-entry.prop.d.ts +12 -0
  33. package/dist/styles/control.css +57 -0
  34. package/dist/styles/focus-ring.css +1 -0
  35. package/dist/styles/layers.json +1 -1
  36. package/dist/styles.css +1 -1
  37. package/dist/tokens/components/feedback.css +4 -0
  38. package/package.json +2 -2
package/README.md CHANGED
@@ -18,7 +18,7 @@ npm i @godxjp/ui
18
18
 
19
19
  <!-- agent-catalog-counts:start -->
20
20
 
21
- The catalog — 161 components, 2193 tokens, 43 cardinal rules — is published in two forms from one
21
+ The catalog — 161 components, 2195 tokens, 43 cardinal rules — is published in two forms from one
22
22
  source. **Entry point for either: [`AGENTS.md`](AGENTS.md).**
23
23
  <!-- agent-catalog-counts:end -->
24
24
 
@@ -55,7 +55,7 @@ static files straight from this public repo — no hosting, no deploy step:
55
55
  | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 47 KB | all 161 as name + group + tagline — **fetch this first** |
56
56
  | [`agent/components/<Name>.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json) | 1 KB–37 KB | one file per component, each with its `importPath` — **this is the route to take** |
57
57
  | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.2 MB | all 161 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
58
- | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 845 KB | all 2193 design tokens, their values and why they exist |
58
+ | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 846 KB | all 2195 design tokens, their values and why they exist |
59
59
  | [`agent/vocabulary.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json) | 7 KB | the controlled prop vocabulary |
60
60
  | [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) | 21 KB | the 43 cardinal rules |
61
61
  <!-- prettier-ignore-end -->
@@ -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` 32.2.2.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 32.3.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.
@@ -56,10 +56,10 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
56
56
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
57
57
  3. `rules.json` — 43 cardinal rules. The ones about raw HTML and hardcoded colour are not
58
58
  style advice.
59
- 4. `tokens.json` — 2193 design tokens, each tagged with its `tier`. **If you were handed a
59
+ 4. `tokens.json` — 2195 design tokens, each tagged with its `tier`. **If you were handed a
60
60
  brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
61
61
  `--radius`, `--font-size-base` are the handful everything else derives from. The
62
- 1870 `component` entries are per-part knobs; reach for one only when a role is
62
+ 1872 `component` entries are per-part knobs; reach for one only when a role is
63
63
  right everywhere except one component.
64
64
  5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
65
65
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
@@ -145,7 +145,7 @@ has stopped following the brand.
145
145
  |---|---|---|---|
146
146
  | `foundation` | 211 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** Handed a brand colour, this is where it goes: `:root { --primary: <H> <S>% <L>%; }` (HSL components, no `hsl()` wrapper) |
147
147
  | `semantic` | 112 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
148
- | `component` | 1870 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
148
+ | `component` | 1872 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
149
149
 
150
150
  A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
151
151
  value, so the real default is computed where the element paints it. Set it and yours wins.
@@ -66,6 +66,7 @@
66
66
  "DON'T suppress the icon with `icon={false}` unless there is a deliberate design reason; the icon is the primary a11y cue for sighted users since the root already carries `role=\"alert\"` for screen readers.",
67
67
  "BANNER (`variant=\"banner\"`): place it FULL-BLEED at the top of the surface it warns about — directly under the Topbar inside AppShell's children for shell-wide attention (past-due subscription, active support session), or as the first child of a page. Its inline inset defaults to the page gutter (--space-page-active-x) and its geometry is owned by the `--banner-*` tokens; never fake a strip with `className` overrides. At most ONE per surface.",
68
68
  "CALLOUT (`variant=\"callout\"`): for content that is PART of the document being read — a docs admonition, a CMS note, a policy caveat. It renders `role=\"note\"` and never announces. Map Markdown `[!NOTE]`/`[!TIP]`/`[!IMPORTANT]`/`[!WARNING]`/`[!CAUTION]` straight onto `kind`. DON'T pass `onDismiss` (prose is not dismissed) and DON'T use a callout for something that just HAPPENED — that is the default or banner variant, which announce.",
69
+ "LIVE OR NOT (gh#1271): static, unchanging guidance → `variant=\"callout\"` (not a live region, `role=\"note\"`); `role=\"alert\"`/`role=\"status\"` (the default and banner variants) are for messages that appear in response to something — a failed save, a sync that finished. A hint that is on the page when it loads is not a status message (WCAG 4.1.3), so it must not announce on every render.",
69
70
  "DO NOT use `Alert` for transient ephemeral feedback (e.g. 'saved successfully'). Use `toast()` from sonner + `<Toaster>` for that. `Alert` is for persistent, page-scoped banners that stay visible until the user acts or dismisses."
70
71
  ],
71
72
  "useCases": [
@@ -87,6 +87,11 @@
87
87
  "name": "showWeek",
88
88
  "type": "boolean"
89
89
  },
90
+ {
91
+ "description": "How the day panel's month caption is drawn (react-day-picker `captionLayout`, forwarded to Calendar). Default `label`. `dropdown` turns month and year into selects; without minDate/maxDate the year list spans 100 years back to the current year.",
92
+ "name": "captionLayout",
93
+ "type": "\"label\" | \"dropdown\" | \"dropdown-months\" | \"dropdown-years\""
94
+ },
90
95
  {
91
96
  "description": "Which period the PANEL opens on, independently of the value (antd `defaultPickerValue`), and — as in antd — RE-APPLIED every time the panel opens, not only at mount. Use it for 'open on the fiscal year's start month' or 'open on the month of the row being edited'.",
92
97
  "name": "defaultPickerValue",
@@ -266,6 +271,7 @@
266
271
  "DO test by filling the input directly: `await user.type(screen.getByRole('combobox'), '2026-04-15')`. With `range`, the two edges are named textboxes (From / To). The panel is secondary and not required for testing.",
267
272
  "DO use `minDate` / `maxDate` to restrict what is selectable — they are enforced on the panel AND on typed entry, so the keyboard is not a way around the rule the mouse obeys.",
268
273
  "DON'T place two DatePickers side by side to fake a from~to pair — that is `range`, which is one control, one shell and one value.",
274
+ "DO use captionLayout=\"dropdown\" for a birth date or any far-away date — the year select reaches 1999 in one pick instead of ~320 arrow clicks; also set `maxDate` when the date can lie in the future (the year list stops at the current year).",
269
275
  "DO use `defaultPickerValue` when the panel should open somewhere other than the value — it is the only way to say it, and it re-applies on every open.",
270
276
  "DON'T reach for `inputReadOnly` to switch the control off; it only sets the input's readonly attribute (the panel still opens). `disabled` is the inert one.",
271
277
  "DON'T hand-roll a date text input + calendar popover — this component IS that pattern at WAI-ARIA combobox spec level."
@@ -47,6 +47,12 @@
47
47
  "description": "Render the title as a non-heading element (p/div) instead of a heading — for a compact/section empty state inside a section that already owns its heading, so the message is not announced as a heading and cannot skip an outline level. Overrides titleLevel.",
48
48
  "name": "titleAs",
49
49
  "type": "\"h1\" | \"h2\" | \"h3\" | \"h4\" | \"p\" | \"div\""
50
+ },
51
+ {
52
+ "defaultValue": "false",
53
+ "description": "Announce the empty state as a status message (`role=\"status\"`). Default false since gh#1271: a static empty state (\"No invoices yet\") is content, not an update. Rule of thumb — on the page when it loads → leave it off; APPEARS because the user just searched or filtered and nothing matched → `live`.",
54
+ "name": "live",
55
+ "type": "boolean"
50
56
  }
51
57
  ],
52
58
  "related": [
@@ -59,6 +65,7 @@
59
65
  "storyPath": "data-display/empty-state.tsx",
60
66
  "tagline": "Centred empty placeholder with icon, title, description, and optional CTA.",
61
67
  "usage": [
68
+ "DO decide whether the empty state is a STATUS MESSAGE (gh#1271). It is not a live region by default: a static \"No records yet\" that is there when the page loads would otherwise be announced as an update on every render (WCAG 4.1.3 covers content that CHANGES). Pass `live` (→ `role=\"status\"`) only when it appears as the result of a user action — a search or filter that matched nothing: `<EmptyState live icon={SearchIcon} title=\"No matching invoices\" />`. Inside `DataTable` do neither — its empty cell already carries `aria-live=\"polite\"`.",
62
69
  "DO always pass `title` — it is the only required prop and renders a heading (`<h3>` by default); omitting it causes a blank silent render with no visible error.",
63
70
  "DO set `titleLevel` to match the page outline (page h1 → section h2 → nested h3) so the empty state does not trigger a heading-order violation. Choose the level for OUTLINE position, never for visual size — the size never changes with the level. When the empty state sits in a section that already has its own heading, use `titleAs=\"p\"` so the message is not a heading at all.",
64
71
  "DO use `tone=\"success\"` (or warning/destructive/info) for a semantic confirmation/alert zero-state — it recolours the icon medallion from the role token; do NOT hand-roll a `.ui-success-state` class that scopes `--empty-state-icon-*`.",
@@ -74,6 +74,7 @@
74
74
  "DO rely on mobile-first collapse: `layout='horizontal'` automatically stacks to vertical below `collapseBelow` (default `md`). Pass `collapseBelow={false}` only when a field MUST stay label-beside-control even on phones.",
75
75
  "DO use `columns` for multi-field forms (e.g. `columns={2}`) — it reuses ResponsiveGrid (1 column on small screens, more on md/lg). Span a wide field across columns with `<FormField colSpan={2}>`.",
76
76
  "ROW RHYTHM IS THE SAME ON EVERY PATH: a grid of FormFields hands its row spacing to the grid's own `row-gap` (--form-grid-row-gap, defaulting to --form-field-row-gap) instead of the per-field margin a stacked Form uses. So `columns={1}` — and any `columns={n}` form once a narrow container collapses it to one column — is pixel-identical to a Form with no `columns`, and a hand-written `<ResponsiveGrid columns={2}>` of FormFields inside a `CardContent` (what a form with several titled Card sections has to write) matches `columns={2}` exactly. Retune --form-field-row-gap to move every path together; --form-grid-row-gap / --form-grid-column-gap to retune the grid rows / gutter alone. Never add margin to a FormField to space a grid row: a per-item margin inside a grid misaligns row 1 and double-counts the track gap.",
77
+ "ACTION ROW in a horizontal form (gh#1271): the submit/cancel buttons go in `<FormField label={false}><Flex gap=\"sm\" wrap>…buttons…</Flex></FormField>` as the LAST field. A label-less field renders no label cell and its control is pinned to the control column (gh#709), so the first button's left edge is the inputs' left edge, it follows `labelWidth`, and it collapses with the fields below `collapseBelow`; `wrap` lets the row break onto a second line on a phone. Requires `labelWidth` on the Form — unset, each label column is as wide as its own text and nothing can align with it. DON'T use `staticText` for buttons (it is the read-only VALUE slot), DON'T push the row with a hand-written padding, and DON'T float it to the far end of the card, away from the column the eye just went down. This is the row for a form INSIDE a card or section; a form that is the whole page puts its primary + cancel in the PageContainer footer instead (docs/general/button/examples/form-actions.tsx).",
77
78
  "DON'T hand-roll a `<form>` + Flex stack for spacing — `<Form>` provides the vertical rhythm and the layout context FormField reads. Wire react-hook-form by spreading `onSubmit={handleSubmit(...)}` onto `<Form>`.",
78
79
  "SERVER ERROR BAG: pass `errors={form.errors}` (Inertia) ONCE on `<Form>`, give each field a `name`, and put `<FormErrors />` at the top of the form. A named field resolves its message from the bag automatically (no per-field `error={errors.x}`), and FormErrors catches validation errors on hidden/derived keys (`action_mode`, `page`, a source-record id) that no visible field could display — without it those submits fail SILENTLY.",
79
80
  "SIBLING FORMS: an edit screen split into several Card+Form sections shares ONE bag by wrapping the region in `<FormErrorsProvider errors={form.errors}>` instead of passing `errors` to each Form — the section Forms (without their own `errors`) join the shared registry, and one `<FormErrors />` anywhere in the region renders the unclaimed remainder."
@@ -26,7 +26,7 @@
26
26
  "type": "string"
27
27
  },
28
28
  {
29
- "description": "Field label above the control.",
29
+ "description": "Field label above the control. `false` renders no label row; in a horizontal form the content still sits in the control column — the action row (gh#1271).",
30
30
  "name": "label",
31
31
  "required": true,
32
32
  "type": "ReactNode"
@@ -112,6 +112,7 @@
112
112
  "tagline": "Wraps a control with label, helper, and error; injects the accessible name (aria-labelledby), description (aria-describedby) and validation (aria-errormessage/aria-invalid/aria-required) contract onto the child, which forwards it to its real semantic focus target. Reads the parent Form's layout (vertical/horizontal) — overridable per field.",
113
113
  "usage": [
114
114
  "DO pass the same string to both `id` on `<FormField>` and `id` on the child control — the component wires `<Label htmlFor={id}>`, and builds `{id}-helper` / `{id}-error` ids for `aria-describedby`. If the ids diverge the label click and screen-reader announcements break.",
115
+ "NO LABEL: `label={false}` renders no label row and reserves no label space; in a horizontal form the content still sits in the CONTROL column (gh#709). That is the canonical action row under a horizontal form (gh#1271): `<FormField label={false}><Flex gap=\"sm\" wrap><Button type=\"submit\">Save</Button><Button variant=\"outline\">Cancel</Button></Flex></FormField>`. A label-less field around a CONTROL must name the control itself (a `<Checkbox>label</Checkbox>`, an `aria-label`).",
115
116
  "DO pass a SINGLE React element as `children`. FormField calls `React.cloneElement` on it to inject `aria-describedby`, `aria-required`, and `aria-invalid` — if you pass a fragment or multiple nodes, cloneElement silently skips the injection and a11y attributes are lost.",
116
117
  "COMPOSITE CHILD: when the single child is a layout wrapper — a `Flex` holding a range from/to pair or a 年/月 input+select combo — the label still reaches every control inside. FormField publishes its label through FieldNameContext and each control's semantic focus target (Input's `<input>`, Select's `role=combobox` trigger, and everything composed on them) adopts it as a LAST-RESORT accessible name; a control's own `aria-label`/`aria-labelledby` always wins, so set a per-control `aria-label` (e.g. 開始日/終了日) when the two halves should announce distinct names. The wrapper itself renders as a named `role='group'` (see Flex).",
117
118
  "DO reach for `staticText` (not `children` with a bare string/span) for a read-only field mixed into an otherwise-editable Form — e.g. an immutable name/email row above an editable role Select on the same Members-edit card. It renders with the exact typography `Descriptions.Item`'s value uses, and — because it IS a FormField reading the same Form context — it lines up with every other field's label column, `labelAlign`, and row-to-row gap automatically. A bare string as `children` instead triggers the dev-mode 'expected a single React element child' warning and has no typography contract at all.",
@@ -21,7 +21,7 @@
21
21
  "type": "ReactNode"
22
22
  },
23
23
  {
24
- "description": "The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable with no `position: sticky` and no scroll-padding hack, and it takes the home-indicator inset whenever no tabBar follows it.",
24
+ "description": "The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable with no `position: sticky` and no scroll-padding hack, and it takes the home-indicator inset whenever no tabBar follows it. In a `height='viewport'` shell the action bar plus the tab bar publish their summed block size as `--toast-sticky-bar-height`, so the `Toaster` stack sits above the bottom chrome instead of over the primary verb (gh#1271).",
25
25
  "name": "actions",
26
26
  "type": "ReactNode"
27
27
  },
@@ -103,7 +103,7 @@
103
103
  },
104
104
  {
105
105
  "defaultValue": "false",
106
- "description": "Pin footer to viewport bottom on scroll — pairs with variant=\"narrow\".",
106
+ "description": "Pin footer to viewport bottom on scroll — pairs with variant=\"narrow\". While the pinned footer is VISIBLE (always, or once revealed under `footerReveal=\"onScroll\"`), it publishes its block size as `--toast-sticky-bar-height` on `<html>` and the `Toaster` stack rises above it, so a toast never covers the Save/Submit button that raised it (gh#1271). Cleared on unmount or while a reveal footer is parked below the fold.",
107
107
  "name": "stickyFooter",
108
108
  "type": "boolean"
109
109
  },
@@ -40,7 +40,8 @@
40
40
  "DO: Tune how long toasts stay with the `--toast-duration` token in the theme (default 4000ms) rather than a `duration` prop on every mount. Never truncate a toast's body — it wraps.",
41
41
  "DO: Let the wrapper handle theming — it uses `useDocumentTheme()` to sync with the document `dark` class and `prefers-color-scheme` automatically. Never pass a hardcoded `theme` prop unless you are deliberately overriding.",
42
42
  "DON'T: Use `Toaster` for persistent errors or blocking confirmations. Toasts auto-dismiss; they are not a substitute for `Alert` (inline persistent warnings) or `Dialog` (decisions requiring user input).",
43
- "DO: Pass `position` to relocate the stack if a persistent sidebar/footer would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs."
43
+ "DO: Pass `position` to relocate the stack if a persistent sidebar would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs.",
44
+ "DON'T: Hand-tune `offset` to dodge a sticky action bar. The bottom offset (desktop 24px, mobile `--toast-mobile-offset`) already adds `--toast-sticky-bar-height`, which a visible `PageContainer stickyFooter` or `MobileShell` action/tab bar publishes at runtime, so the stack clears it on its own (gh#1271). For bottom chrome the library does not render (an app's own fixed bar), set `--toast-offset-bottom` once in theme.css — it is added on desktop and mobile alike."
44
45
  ],
45
46
  "useCases": [
46
47
  "After a successful form save (invoice, journal entry, vendor record) — show `toast.success(\"保存しました\")` to confirm without blocking navigation.",
@@ -748,7 +748,7 @@
748
748
  },
749
749
  {
750
750
  "defaultValue": "false",
751
- "description": "Pin footer to viewport bottom on scroll — pairs with variant=\"narrow\".",
751
+ "description": "Pin footer to viewport bottom on scroll — pairs with variant=\"narrow\". While the pinned footer is VISIBLE (always, or once revealed under `footerReveal=\"onScroll\"`), it publishes its block size as `--toast-sticky-bar-height` on `<html>` and the `Toaster` stack rises above it, so a toast never covers the Save/Submit button that raised it (gh#1271). Cleared on unmount or while a reveal footer is parked below the fold.",
752
752
  "name": "stickyFooter",
753
753
  "type": "boolean"
754
754
  },
@@ -1502,7 +1502,7 @@
1502
1502
  "type": "ReactNode"
1503
1503
  },
1504
1504
  {
1505
- "description": "The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable with no `position: sticky` and no scroll-padding hack, and it takes the home-indicator inset whenever no tabBar follows it.",
1505
+ "description": "The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable with no `position: sticky` and no scroll-padding hack, and it takes the home-indicator inset whenever no tabBar follows it. In a `height='viewport'` shell the action bar plus the tab bar publish their summed block size as `--toast-sticky-bar-height`, so the `Toaster` stack sits above the bottom chrome instead of over the primary verb (gh#1271).",
1506
1506
  "name": "actions",
1507
1507
  "type": "ReactNode"
1508
1508
  },
@@ -4656,6 +4656,12 @@
4656
4656
  "description": "Render the title as a non-heading element (p/div) instead of a heading — for a compact/section empty state inside a section that already owns its heading, so the message is not announced as a heading and cannot skip an outline level. Overrides titleLevel.",
4657
4657
  "name": "titleAs",
4658
4658
  "type": "\"h1\" | \"h2\" | \"h3\" | \"h4\" | \"p\" | \"div\""
4659
+ },
4660
+ {
4661
+ "defaultValue": "false",
4662
+ "description": "Announce the empty state as a status message (`role=\"status\"`). Default false since gh#1271: a static empty state (\"No invoices yet\") is content, not an update. Rule of thumb — on the page when it loads → leave it off; APPEARS because the user just searched or filtered and nothing matched → `live`.",
4663
+ "name": "live",
4664
+ "type": "boolean"
4659
4665
  }
4660
4666
  ],
4661
4667
  "related": [
@@ -4668,6 +4674,7 @@
4668
4674
  "storyPath": "data-display/empty-state.tsx",
4669
4675
  "tagline": "Centred empty placeholder with icon, title, description, and optional CTA.",
4670
4676
  "usage": [
4677
+ "DO decide whether the empty state is a STATUS MESSAGE (gh#1271). It is not a live region by default: a static \"No records yet\" that is there when the page loads would otherwise be announced as an update on every render (WCAG 4.1.3 covers content that CHANGES). Pass `live` (→ `role=\"status\"`) only when it appears as the result of a user action — a search or filter that matched nothing: `<EmptyState live icon={SearchIcon} title=\"No matching invoices\" />`. Inside `DataTable` do neither — its empty cell already carries `aria-live=\"polite\"`.",
4671
4678
  "DO always pass `title` — it is the only required prop and renders a heading (`<h3>` by default); omitting it causes a blank silent render with no visible error.",
4672
4679
  "DO set `titleLevel` to match the page outline (page h1 → section h2 → nested h3) so the empty state does not trigger a heading-order violation. Choose the level for OUTLINE position, never for visual size — the size never changes with the level. When the empty state sits in a section that already has its own heading, use `titleAs=\"p\"` so the message is not a heading at all.",
4673
4680
  "DO use `tone=\"success\"` (or warning/destructive/info) for a semantic confirmation/alert zero-state — it recolours the icon medallion from the role token; do NOT hand-roll a `.ui-success-state` class that scopes `--empty-state-icon-*`.",
@@ -5695,6 +5702,7 @@
5695
5702
  "DO rely on mobile-first collapse: `layout='horizontal'` automatically stacks to vertical below `collapseBelow` (default `md`). Pass `collapseBelow={false}` only when a field MUST stay label-beside-control even on phones.",
5696
5703
  "DO use `columns` for multi-field forms (e.g. `columns={2}`) — it reuses ResponsiveGrid (1 column on small screens, more on md/lg). Span a wide field across columns with `<FormField colSpan={2}>`.",
5697
5704
  "ROW RHYTHM IS THE SAME ON EVERY PATH: a grid of FormFields hands its row spacing to the grid's own `row-gap` (--form-grid-row-gap, defaulting to --form-field-row-gap) instead of the per-field margin a stacked Form uses. So `columns={1}` — and any `columns={n}` form once a narrow container collapses it to one column — is pixel-identical to a Form with no `columns`, and a hand-written `<ResponsiveGrid columns={2}>` of FormFields inside a `CardContent` (what a form with several titled Card sections has to write) matches `columns={2}` exactly. Retune --form-field-row-gap to move every path together; --form-grid-row-gap / --form-grid-column-gap to retune the grid rows / gutter alone. Never add margin to a FormField to space a grid row: a per-item margin inside a grid misaligns row 1 and double-counts the track gap.",
5705
+ "ACTION ROW in a horizontal form (gh#1271): the submit/cancel buttons go in `<FormField label={false}><Flex gap=\"sm\" wrap>…buttons…</Flex></FormField>` as the LAST field. A label-less field renders no label cell and its control is pinned to the control column (gh#709), so the first button's left edge is the inputs' left edge, it follows `labelWidth`, and it collapses with the fields below `collapseBelow`; `wrap` lets the row break onto a second line on a phone. Requires `labelWidth` on the Form — unset, each label column is as wide as its own text and nothing can align with it. DON'T use `staticText` for buttons (it is the read-only VALUE slot), DON'T push the row with a hand-written padding, and DON'T float it to the far end of the card, away from the column the eye just went down. This is the row for a form INSIDE a card or section; a form that is the whole page puts its primary + cancel in the PageContainer footer instead (docs/general/button/examples/form-actions.tsx).",
5698
5706
  "DON'T hand-roll a `<form>` + Flex stack for spacing — `<Form>` provides the vertical rhythm and the layout context FormField reads. Wire react-hook-form by spreading `onSubmit={handleSubmit(...)}` onto `<Form>`.",
5699
5707
  "SERVER ERROR BAG: pass `errors={form.errors}` (Inertia) ONCE on `<Form>`, give each field a `name`, and put `<FormErrors />` at the top of the form. A named field resolves its message from the bag automatically (no per-field `error={errors.x}`), and FormErrors catches validation errors on hidden/derived keys (`action_mode`, `page`, a source-record id) that no visible field could display — without it those submits fail SILENTLY.",
5700
5708
  "SIBLING FORMS: an edit screen split into several Card+Form sections shares ONE bag by wrapping the region in `<FormErrorsProvider errors={form.errors}>` instead of passing `errors` to each Form — the section Forms (without their own `errors`) join the shared registry, and one `<FormErrors />` anywhere in the region renders the unclaimed remainder."
@@ -5734,7 +5742,7 @@
5734
5742
  "type": "string"
5735
5743
  },
5736
5744
  {
5737
- "description": "Field label above the control.",
5745
+ "description": "Field label above the control. `false` renders no label row; in a horizontal form the content still sits in the control column — the action row (gh#1271).",
5738
5746
  "name": "label",
5739
5747
  "required": true,
5740
5748
  "type": "ReactNode"
@@ -5820,6 +5828,7 @@
5820
5828
  "tagline": "Wraps a control with label, helper, and error; injects the accessible name (aria-labelledby), description (aria-describedby) and validation (aria-errormessage/aria-invalid/aria-required) contract onto the child, which forwards it to its real semantic focus target. Reads the parent Form's layout (vertical/horizontal) — overridable per field.",
5821
5829
  "usage": [
5822
5830
  "DO pass the same string to both `id` on `<FormField>` and `id` on the child control — the component wires `<Label htmlFor={id}>`, and builds `{id}-helper` / `{id}-error` ids for `aria-describedby`. If the ids diverge the label click and screen-reader announcements break.",
5831
+ "NO LABEL: `label={false}` renders no label row and reserves no label space; in a horizontal form the content still sits in the CONTROL column (gh#709). That is the canonical action row under a horizontal form (gh#1271): `<FormField label={false}><Flex gap=\"sm\" wrap><Button type=\"submit\">Save</Button><Button variant=\"outline\">Cancel</Button></Flex></FormField>`. A label-less field around a CONTROL must name the control itself (a `<Checkbox>label</Checkbox>`, an `aria-label`).",
5823
5832
  "DO pass a SINGLE React element as `children`. FormField calls `React.cloneElement` on it to inject `aria-describedby`, `aria-required`, and `aria-invalid` — if you pass a fragment or multiple nodes, cloneElement silently skips the injection and a11y attributes are lost.",
5824
5833
  "COMPOSITE CHILD: when the single child is a layout wrapper — a `Flex` holding a range from/to pair or a 年/月 input+select combo — the label still reaches every control inside. FormField publishes its label through FieldNameContext and each control's semantic focus target (Input's `<input>`, Select's `role=combobox` trigger, and everything composed on them) adopts it as a LAST-RESORT accessible name; a control's own `aria-label`/`aria-labelledby` always wins, so set a per-control `aria-label` (e.g. 開始日/終了日) when the two halves should announce distinct names. The wrapper itself renders as a named `role='group'` (see Flex).",
5825
5834
  "DO reach for `staticText` (not `children` with a bare string/span) for a read-only field mixed into an otherwise-editable Form — e.g. an immutable name/email row above an editable role Select on the same Members-edit card. It renders with the exact typography `Descriptions.Item`'s value uses, and — because it IS a FormField reading the same Form context — it lines up with every other field's label column, `labelAlign`, and row-to-row gap automatically. A bare string as `children` instead triggers the dev-mode 'expected a single React element child' warning and has no typography contract at all.",
@@ -7344,6 +7353,11 @@
7344
7353
  "name": "showWeek",
7345
7354
  "type": "boolean"
7346
7355
  },
7356
+ {
7357
+ "description": "How the day panel's month caption is drawn (react-day-picker `captionLayout`, forwarded to Calendar). Default `label`. `dropdown` turns month and year into selects; without minDate/maxDate the year list spans 100 years back to the current year.",
7358
+ "name": "captionLayout",
7359
+ "type": "\"label\" | \"dropdown\" | \"dropdown-months\" | \"dropdown-years\""
7360
+ },
7347
7361
  {
7348
7362
  "description": "Which period the PANEL opens on, independently of the value (antd `defaultPickerValue`), and — as in antd — RE-APPLIED every time the panel opens, not only at mount. Use it for 'open on the fiscal year's start month' or 'open on the month of the row being edited'.",
7349
7363
  "name": "defaultPickerValue",
@@ -7523,6 +7537,7 @@
7523
7537
  "DO test by filling the input directly: `await user.type(screen.getByRole('combobox'), '2026-04-15')`. With `range`, the two edges are named textboxes (From / To). The panel is secondary and not required for testing.",
7524
7538
  "DO use `minDate` / `maxDate` to restrict what is selectable — they are enforced on the panel AND on typed entry, so the keyboard is not a way around the rule the mouse obeys.",
7525
7539
  "DON'T place two DatePickers side by side to fake a from~to pair — that is `range`, which is one control, one shell and one value.",
7540
+ "DO use captionLayout=\"dropdown\" for a birth date or any far-away date — the year select reaches 1999 in one pick instead of ~320 arrow clicks; also set `maxDate` when the date can lie in the future (the year list stops at the current year).",
7526
7541
  "DO use `defaultPickerValue` when the panel should open somewhere other than the value — it is the only way to say it, and it re-applies on every open.",
7527
7542
  "DON'T reach for `inputReadOnly` to switch the control off; it only sets the input's readonly attribute (the panel still opens). `disabled` is the inert one.",
7528
7543
  "DON'T hand-roll a date text input + calendar popover — this component IS that pattern at WAI-ARIA combobox spec level."
@@ -7891,6 +7906,7 @@
7891
7906
  "DON'T suppress the icon with `icon={false}` unless there is a deliberate design reason; the icon is the primary a11y cue for sighted users since the root already carries `role=\"alert\"` for screen readers.",
7892
7907
  "BANNER (`variant=\"banner\"`): place it FULL-BLEED at the top of the surface it warns about — directly under the Topbar inside AppShell's children for shell-wide attention (past-due subscription, active support session), or as the first child of a page. Its inline inset defaults to the page gutter (--space-page-active-x) and its geometry is owned by the `--banner-*` tokens; never fake a strip with `className` overrides. At most ONE per surface.",
7893
7908
  "CALLOUT (`variant=\"callout\"`): for content that is PART of the document being read — a docs admonition, a CMS note, a policy caveat. It renders `role=\"note\"` and never announces. Map Markdown `[!NOTE]`/`[!TIP]`/`[!IMPORTANT]`/`[!WARNING]`/`[!CAUTION]` straight onto `kind`. DON'T pass `onDismiss` (prose is not dismissed) and DON'T use a callout for something that just HAPPENED — that is the default or banner variant, which announce.",
7909
+ "LIVE OR NOT (gh#1271): static, unchanging guidance → `variant=\"callout\"` (not a live region, `role=\"note\"`); `role=\"alert\"`/`role=\"status\"` (the default and banner variants) are for messages that appear in response to something — a failed save, a sync that finished. A hint that is on the page when it loads is not a status message (WCAG 4.1.3), so it must not announce on every render.",
7894
7910
  "DO NOT use `Alert` for transient ephemeral feedback (e.g. 'saved successfully'). Use `toast()` from sonner + `<Toaster>` for that. `Alert` is for persistent, page-scoped banners that stay visible until the user acts or dismisses."
7895
7911
  ],
7896
7912
  "useCases": [
@@ -7946,7 +7962,8 @@
7946
7962
  "DO: Tune how long toasts stay with the `--toast-duration` token in the theme (default 4000ms) rather than a `duration` prop on every mount. Never truncate a toast's body — it wraps.",
7947
7963
  "DO: Let the wrapper handle theming — it uses `useDocumentTheme()` to sync with the document `dark` class and `prefers-color-scheme` automatically. Never pass a hardcoded `theme` prop unless you are deliberately overriding.",
7948
7964
  "DON'T: Use `Toaster` for persistent errors or blocking confirmations. Toasts auto-dismiss; they are not a substitute for `Alert` (inline persistent warnings) or `Dialog` (decisions requiring user input).",
7949
- "DO: Pass `position` to relocate the stack if a persistent sidebar/footer would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs."
7965
+ "DO: Pass `position` to relocate the stack if a persistent sidebar would obscure the default `bottom-right`. The wrapper already sets a safe `mobileOffset`; don't add redundant mobile offsets unless your layout differs.",
7966
+ "DON'T: Hand-tune `offset` to dodge a sticky action bar. The bottom offset (desktop 24px, mobile `--toast-mobile-offset`) already adds `--toast-sticky-bar-height`, which a visible `PageContainer stickyFooter` or `MobileShell` action/tab bar publishes at runtime, so the stack clears it on its own (gh#1271). For bottom chrome the library does not render (an app's own fixed bar), set `--toast-offset-bottom` once in theme.css — it is added on desktop and mobile alike."
7950
7967
  ],
7951
7968
  "useCases": [
7952
7969
  "After a successful form save (invoice, journal entry, vendor record) — show `toast.success(\"保存しました\")` to confirm without blocking navigation.",
package/agent/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "components": 161,
5
5
  "patterns": 24,
6
6
  "rules": 43,
7
- "tokens": 2193,
7
+ "tokens": 2195,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "files": [
@@ -48,19 +48,19 @@
48
48
  "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.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.2.2/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.3.0/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
55
  "tokenTiers": {
56
56
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
57
57
  "counts": {
58
- "component": 1870,
58
+ "component": 1872,
59
59
  "foundation": 211,
60
60
  "semantic": 112
61
61
  },
62
62
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
63
63
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
64
64
  },
65
- "version": "32.2.2"
65
+ "version": "32.3.0"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 161 components, 2193 design tokens,
4
- > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.2.2.
3
+ > A Japanese-enterprise React design system: 161 components, 2195 design tokens,
4
+ > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.3.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@32.2.2`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@32.3.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -18,7 +18,7 @@ that can only fetch URLs.
18
18
  - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 47 KB — all 161 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
19
  - [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.
20
20
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
21
- - [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` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 112 `semantic` roles, 1870 `component` knobs.
21
+ - [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` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 112 `semantic` roles, 1872 `component` knobs.
22
22
  - [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.
23
23
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 43 cardinal rules.
24
24
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v32.2.2/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v32.3.0/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  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
@@ -7541,6 +7541,18 @@
7541
7541
  "tier": "component",
7542
7542
  "value": "16px"
7543
7543
  },
7544
+ {
7545
+ "description": "Extra clearance the toast stack keeps above the viewport's bottom edge, ADDED to the gutter on both desktop (sonner's 24px) and mobile (`--toast-mobile-offset`) — gh#1271. `0px` = the stack sits where it always did. A service whose layout pins its own bottom chrome (a fixed bar the library does not render) sets it once in theme.css. Never unitless `0`: it is a `calc()` term, and a bare number there invalidates the whole offset.",
7546
+ "name": "--toast-offset-bottom",
7547
+ "tier": "component",
7548
+ "value": "0px"
7549
+ },
7550
+ {
7551
+ "description": "RUNTIME-PUBLISHED, not a theme knob (gh#1271). A mounted, visible sticky action bar — `PageContainer stickyFooter`, or `MobileShell actions`/`tabBar` in a viewport shell — writes its block size here inline on <html> (`src/lib/toast-clearance.ts`), and the Toaster adds it to its bottom offset so the stack never covers the primary action. Removed again when the bar unmounts or is hidden, which falls back to this `0px`. Set `--toast-offset-bottom` instead of this one: the runtime value overwrites any theme value.",
7552
+ "name": "--toast-sticky-bar-height",
7553
+ "tier": "component",
7554
+ "value": "0px"
7555
+ },
7544
7556
  {
7545
7557
  "description": "Rule #24 companion for the overlay ✕ (gh#1138): the 24px floor is the WCAG 2.5.8 AA minimum for a mouse; under a finger the target lifts to the 44px tap floor the control ladder already uses. Only the hit area grows (the `::after` above the glyph) — the 16px paint stays put.",
7546
7558
  "name": "--dialog-close-size",
@@ -1,3 +1,3 @@
1
1
  import type { EmptyStateProp } from "../../props/components/data-display.prop.js";
2
2
  export type { EmptyStateProp, EmptyStateProp as EmptyStateProps, } from "../../props/components/data-display.prop.js";
3
- export declare function EmptyState({ icon: Icon, title, description, action, variant, tone, titleLevel, titleAs, className, }: EmptyStateProp): import("react").JSX.Element;
3
+ export declare function EmptyState({ icon: Icon, title, description, action, variant, tone, titleLevel, titleAs, live, className, }: EmptyStateProp): import("react").JSX.Element;
@@ -10,6 +10,7 @@ function EmptyState({
10
10
  tone = "muted",
11
11
  titleLevel = 3,
12
12
  titleAs,
13
+ live = false,
13
14
  className
14
15
  }) {
15
16
  const TitleTag = titleAs ?? `h${titleLevel}`;
@@ -19,7 +20,7 @@ function EmptyState({
19
20
  "data-slot": "empty-state",
20
21
  "data-variant": variant,
21
22
  "data-tone": tone,
22
- role: "status",
23
+ role: live ? "status" : void 0,
23
24
  className: cn("ui-empty-state", className),
24
25
  children: [
25
26
  Icon && variant !== "compact" && /* @__PURE__ */ jsx("div", { className: "ui-empty-state-icon", children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" }) }),
@@ -87,7 +87,11 @@ function Calendar({
87
87
  * navigation" while the two buttons INSIDE that container said 前の月へ and 次の月へ. Nothing
88
88
  * failed and nothing warned; the only reader affected was the one using a screen reader.
89
89
  */
90
- labelNav: labels?.labelNav ?? (() => t("dataEntry.calendar.nav", { label: ariaLabel ?? t("dataEntry.calendar.name") }))
90
+ labelNav: labels?.labelNav ?? (() => t("dataEntry.calendar.nav", { label: ariaLabel ?? t("dataEntry.calendar.name") })),
91
+ // The two caption selects (`captionLayout="dropdown"`) are named the same way — react-day-
92
+ // picker's defaults are English literals whatever the locale (gh#1271).
93
+ labelMonthDropdown: labels?.labelMonthDropdown ?? (() => t("dataEntry.calendar.monthDropdown") ?? "Month"),
94
+ labelYearDropdown: labels?.labelYearDropdown ?? (() => t("dataEntry.calendar.yearDropdown") ?? "Year")
91
95
  },
92
96
  className: cn("ui-calendar", className),
93
97
  "data-width": width === "full" ? "full" : void 0,
@@ -97,6 +101,17 @@ function Calendar({
97
101
  month: cn("ui-calendar-month", classNames?.month),
98
102
  month_caption: cn("ui-calendar-caption", classNames?.month_caption),
99
103
  caption_label: cn("ui-calendar-caption-label", classNames?.caption_label),
104
+ /*
105
+ * `captionLayout="dropdown"` slots (gh#1271). Naming a slot REPLACES react-day-picker's own
106
+ * class, so with these set nothing of `.rdp-dropdown*` applies and control.css owns the whole
107
+ * look: a native `<select>` laid invisibly over a label + chevron that reads as a compact
108
+ * control. Unnamed, the selects fell back to the vendor sheet under our caption rules.
109
+ */
110
+ dropdowns: cn("ui-calendar-dropdowns", classNames?.dropdowns),
111
+ dropdown_root: cn("ui-calendar-dropdown-root", classNames?.dropdown_root),
112
+ dropdown: cn("ui-calendar-dropdown", classNames?.dropdown),
113
+ months_dropdown: cn("ui-calendar-months-dropdown", classNames?.months_dropdown),
114
+ years_dropdown: cn("ui-calendar-years-dropdown", classNames?.years_dropdown),
100
115
  // z-10 is load-bearing: `month` below is position:relative and later in the DOM, so
101
116
  // without a z-index it paints OVER these buttons — they render but never receive the
102
117
  // click (the month caption swallows it) and the calendar looks frozen.
@@ -77,6 +77,7 @@ function DatePicker(props) {
77
77
  placement = "bottom-start",
78
78
  renderExtraFooter,
79
79
  showSummary = true,
80
+ captionLayout = "label",
80
81
  size,
81
82
  status,
82
83
  variant,
@@ -114,7 +115,7 @@ function DatePicker(props) {
114
115
  const allowed = (date) => pickerDateAllowed(date, minimum, maximum, disabledDate);
115
116
  const display = (date) => format || showTime ? formatPickerDate(date, format, locale, Boolean(showTime)) : toIsoPeriod(date, picker);
116
117
  const displayAll = (date) => Array.isArray(date) ? date.map((day) => display(day)).join(", ") : display(date);
117
- const parse = (text2) => parsePickerDate(text2, format, parseFormat, Boolean(showTime)) ?? parseIsoPeriod(text2, picker);
118
+ const parse = (text2, strict = false) => parsePickerDate(text2, format, parseFormat, Boolean(showTime), strict) ?? parseIsoPeriod(text2, picker);
118
119
  const periodDate = (date) => pickerPeriodStart(date, picker, dayPickerLocale);
119
120
  const fieldA11y = range ? pickGroupFieldA11y(ariaProps) : pickFieldA11y(ariaProps);
120
121
  const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
@@ -241,13 +242,13 @@ function DatePicker(props) {
241
242
  choose(void 0);
242
243
  return;
243
244
  }
244
- const parsed = parse(raw);
245
+ const parsed = parse(raw, true);
245
246
  if (parsed && allowed(parsed)) choose(parsed);
246
247
  };
247
- const commitEdge = (edge, raw) => {
248
+ const commitEdge = (edge, raw, strict = false) => {
248
249
  if (edgeDisabled(edge)) return;
249
250
  const trimmed = raw.trim();
250
- const parsed = parse(trimmed);
251
+ const parsed = parse(trimmed, strict);
251
252
  if (trimmed && (!parsed || !allowed(parsed))) return;
252
253
  if (!trimmed && !allowEmpty[edge === "from" ? 0 : 1]) return;
253
254
  const base = working;
@@ -274,11 +275,20 @@ function DatePicker(props) {
274
275
  const next = periodDate(parsed);
275
276
  emit(next);
276
277
  setText(display(parsed));
278
+ } else if (!preserveInvalidOnBlur) {
279
+ setText(displayAll(committedSingle));
277
280
  }
278
281
  setOpen(false);
279
282
  };
280
283
  const enterEdge = (edge) => () => {
281
- commitEdge(edge, edge === "from" ? fromText : toText);
284
+ const raw = edge === "from" ? fromText : toText;
285
+ const parsed = parse(raw);
286
+ commitEdge(edge, raw);
287
+ if (raw.trim() && (!parsed || !allowed(parsed)) && !preserveInvalidOnBlur) {
288
+ const fallback = display(edge === "from" ? committedRange?.from : committedRange?.to);
289
+ if (edge === "from") setFromText(fallback);
290
+ else setToText(fallback);
291
+ }
282
292
  setOpen(false);
283
293
  };
284
294
  const displayIsCanonical = !format && !showTime && !multiple && !needConfirm && !preserveInvalidOnBlur && picker === "date";
@@ -426,6 +436,7 @@ function DatePicker(props) {
426
436
  endMonth: maximum,
427
437
  showToday,
428
438
  showClose,
439
+ captionLayout,
429
440
  onClose: () => setOpen(false)
430
441
  };
431
442
  const renderDayPanel = () => range ? /* @__PURE__ */ jsx(
@@ -608,13 +619,15 @@ function DatePicker(props) {
608
619
  onChange: (event) => {
609
620
  if (edge === "from") setFromText(event.target.value);
610
621
  else setToText(event.target.value);
611
- commitEdge(edge, event.target.value);
622
+ commitEdge(edge, event.target.value, true);
612
623
  },
613
624
  onBlur: (event) => {
614
- if (preserveInvalidOnBlur) return;
615
625
  const parsed = parse(event.target.value);
616
626
  const accepted = parsed && allowed(parsed) ? parsed : void 0;
617
627
  const fallback = edge === "from" ? committedRange?.from : committedRange?.to;
628
+ if (accepted && +periodDate(accepted) !== +(fallback ?? NaN))
629
+ commitEdge(edge, event.target.value);
630
+ if (preserveInvalidOnBlur) return;
618
631
  const next = accepted ? display(accepted) : display(fallback);
619
632
  if (edge === "from") setFromText(next);
620
633
  else setToText(next);
@@ -704,6 +717,8 @@ function DatePicker(props) {
704
717
  onBlur: (event) => {
705
718
  const parsed = parse(event.target.value);
706
719
  const accepted = parsed && allowed(parsed) && timeAllowed(parsed) ? parsed : void 0;
720
+ if (accepted && !multiple && !needConfirm && +periodDate(accepted) !== +(committedSingle ?? NaN))
721
+ emit(periodDate(accepted));
707
722
  if (!preserveInvalidOnBlur)
708
723
  setText(accepted ? display(accepted) : displayAll(committedSingle));
709
724
  }
@@ -486,25 +486,31 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
486
486
  ))
487
487
  }
488
488
  ) : null,
489
- /* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? /* @__PURE__ */ jsx(
490
- EmptyState,
491
- {
492
- variant: "compact",
493
- tone: "destructive",
494
- title: t("dataEntry.recordPicker.error"),
495
- action: /* @__PURE__ */ jsx(
496
- Button,
497
- {
498
- variant: "outline",
499
- size: "sm",
500
- onClick: () => void load(query, filterValues),
501
- children: t("common.retry")
502
- }
503
- )
504
- }
489
+ /* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? (
490
+ // `live` on both outcomes below: each answers the user's own query (gh#1271 —
491
+ // EmptyState is no longer a live region by default).
492
+ /* @__PURE__ */ jsx(
493
+ EmptyState,
494
+ {
495
+ live: true,
496
+ variant: "compact",
497
+ tone: "destructive",
498
+ title: t("dataEntry.recordPicker.error"),
499
+ action: /* @__PURE__ */ jsx(
500
+ Button,
501
+ {
502
+ variant: "outline",
503
+ size: "sm",
504
+ onClick: () => void load(query, filterValues),
505
+ children: t("common.retry")
506
+ }
507
+ )
508
+ }
509
+ )
505
510
  ) : status === "loading" ? /* @__PURE__ */ jsx("div", { role: "status", className: "ui-record-picker-status", children: t("dataEntry.recordPicker.loading") }) : visible.length === 0 ? /* @__PURE__ */ jsx(
506
511
  EmptyState,
507
512
  {
513
+ live: true,
508
514
  variant: "compact",
509
515
  title: notFoundContent ?? t("dataEntry.recordPicker.empty")
510
516
  }
@@ -99,8 +99,11 @@ function Toaster({ style, richColors = true, toastOptions, ...props }) {
99
99
  ...style
100
100
  },
101
101
  position: "bottom-right",
102
+ offset: {
103
+ bottom: "calc(24px + var(--toast-offset-bottom) + var(--toast-sticky-bar-height))"
104
+ },
102
105
  mobileOffset: {
103
- bottom: "var(--toast-mobile-offset)",
106
+ bottom: "calc(var(--toast-mobile-offset) + var(--toast-offset-bottom) + var(--toast-sticky-bar-height))",
104
107
  right: "var(--toast-mobile-offset)"
105
108
  },
106
109
  duration: tokenDuration,