@godxjp/ui 31.23.0 → 31.24.1

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.
@@ -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` 31.23.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 31.24.1.** 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` — 50 cardinal rules. The ones about raw HTML and hardcoded colour are not
58
58
  style advice.
59
- 4. `tokens.json` — 2129 design tokens, each tagged with its `tier`. **If you were handed a
59
+ 4. `tokens.json` — 2131 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
- 1814 `component` entries are per-part knobs; reach for one only when a role is
62
+ 1816 `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` | 104 | 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` | 1814 | 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` | 1816 | 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.
@@ -105,6 +105,11 @@
105
105
  "name": "mobileNavLabel",
106
106
  "type": "string"
107
107
  },
108
+ {
109
+ "description": "Visible text beside the drawer trigger's glyph (e.g. 'Pages'); it becomes the trigger's accessible name instead of the localized 'Open navigation'. The drawer closes on a link, a Sidebar row or a menu item, or an explicit close (Esc, overlay, close button, SheetClose) — other buttons such as a tree's expand toggle leave it open (gh#1133).",
110
+ "name": "mobileNavTriggerLabel",
111
+ "type": "string"
112
+ },
108
113
  {
109
114
  "description": "Controlled open state of the mobile drawer. Omit for AppShell-owned state.",
110
115
  "name": "mobileNavOpen",
@@ -125,6 +125,11 @@
125
125
  "name": "headerLoading",
126
126
  "type": "boolean"
127
127
  },
128
+ {
129
+ "description": "Ref to the page's <h1>; passing it also makes the heading programmatically focusable (tabIndex -1, outside the tab order, no focus ring) so a router can focus the new page title after client-side navigation (gh#1135).",
130
+ "name": "titleRef",
131
+ "type": "React.Ref<HTMLHeadingElement>"
132
+ },
128
133
  {
129
134
  "description": "Link component used for breadcrumb / header links (e.g. an Inertia or React Router `Link`). Defaults to a native `<a>`.",
130
135
  "name": "linkComponent",
@@ -25,6 +25,11 @@
25
25
  "name": "width",
26
26
  "type": "number | string"
27
27
  },
28
+ {
29
+ "description": "On SheetContent (Radix name, same as DialogContent): fires on every close before focus returns to the trigger. Call event.preventDefault() and focus your own target (e.g. the new page's heading after a drawer navigation) to send focus there instead (gh#1134).",
30
+ "name": "onCloseAutoFocus",
31
+ "type": "(event: Event) => void"
32
+ },
28
33
  {
29
34
  "defaultValue": "\"side\"",
30
35
  "description": "On SheetContent: the responsive drawer / detail-panel contract. \"side\" (default) always renders the physical `side` you named. \"auto\" renders the desktop side panel above --sheet-responsive-breakpoint-width (48rem/768px) and the mobile BOTTOM sheet at/below it, capped by --sheet-bottom-max-height (85dvh). \"bottom\" pins the bottom-sheet presentation.",
@@ -9,6 +9,11 @@
9
9
  "name": "ariaLabel",
10
10
  "type": "string"
11
11
  },
12
+ {
13
+ "description": "Ref to the navigation's scroll container (`.sb-nav-scroll`, the overflow-y element) — persist and restore its scrollTop across navigations or remounts (gh#1136).",
14
+ "name": "scrollRef",
15
+ "type": "React.Ref<HTMLElement>"
16
+ },
12
17
  {
13
18
  "description": "The id of the currently active nav item. For group items, the parent is automatically highlighted when any descendant id matches.",
14
19
  "name": "activeId",
@@ -737,6 +737,11 @@
737
737
  "name": "headerLoading",
738
738
  "type": "boolean"
739
739
  },
740
+ {
741
+ "description": "Ref to the page's <h1>; passing it also makes the heading programmatically focusable (tabIndex -1, outside the tab order, no focus ring) so a router can focus the new page title after client-side navigation (gh#1135).",
742
+ "name": "titleRef",
743
+ "type": "React.Ref<HTMLHeadingElement>"
744
+ },
740
745
  {
741
746
  "description": "Link component used for breadcrumb / header links (e.g. an Inertia or React Router `Link`). Defaults to a native `<a>`.",
742
747
  "name": "linkComponent",
@@ -1191,6 +1196,11 @@
1191
1196
  "name": "mobileNavLabel",
1192
1197
  "type": "string"
1193
1198
  },
1199
+ {
1200
+ "description": "Visible text beside the drawer trigger's glyph (e.g. 'Pages'); it becomes the trigger's accessible name instead of the localized 'Open navigation'. The drawer closes on a link, a Sidebar row or a menu item, or an explicit close (Esc, overlay, close button, SheetClose) — other buttons such as a tree's expand toggle leave it open (gh#1133).",
1201
+ "name": "mobileNavTriggerLabel",
1202
+ "type": "string"
1203
+ },
1194
1204
  {
1195
1205
  "description": "Controlled open state of the mobile drawer. Omit for AppShell-owned state.",
1196
1206
  "name": "mobileNavOpen",
@@ -1501,6 +1511,11 @@
1501
1511
  "name": "ariaLabel",
1502
1512
  "type": "string"
1503
1513
  },
1514
+ {
1515
+ "description": "Ref to the navigation's scroll container (`.sb-nav-scroll`, the overflow-y element) — persist and restore its scrollTop across navigations or remounts (gh#1136).",
1516
+ "name": "scrollRef",
1517
+ "type": "React.Ref<HTMLElement>"
1518
+ },
1504
1519
  {
1505
1520
  "description": "The id of the currently active nav item. For group items, the parent is automatically highlighted when any descendant id matches.",
1506
1521
  "name": "activeId",
@@ -7801,6 +7816,11 @@
7801
7816
  "name": "width",
7802
7817
  "type": "number | string"
7803
7818
  },
7819
+ {
7820
+ "description": "On SheetContent (Radix name, same as DialogContent): fires on every close before focus returns to the trigger. Call event.preventDefault() and focus your own target (e.g. the new page's heading after a drawer navigation) to send focus there instead (gh#1134).",
7821
+ "name": "onCloseAutoFocus",
7822
+ "type": "(event: Event) => void"
7823
+ },
7804
7824
  {
7805
7825
  "defaultValue": "\"side\"",
7806
7826
  "description": "On SheetContent: the responsive drawer / detail-panel contract. \"side\" (default) always renders the physical `side` you named. \"auto\" renders the desktop side panel above --sheet-responsive-breakpoint-width (48rem/768px) and the mobile BOTTOM sheet at/below it, capped by --sheet-bottom-max-height (85dvh). \"bottom\" pins the bottom-sheet presentation.",
package/agent/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "components": 181,
5
5
  "patterns": 21,
6
6
  "rules": 50,
7
- "tokens": 2129,
7
+ "tokens": 2131,
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/v31.23.0/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.24.1/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": 1814,
58
+ "component": 1816,
59
59
  "foundation": 211,
60
60
  "semantic": 104
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": "31.23.0"
65
+ "version": "31.24.1"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 181 components, 2129 design tokens,
4
- > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.23.0.
3
+ > A Japanese-enterprise React design system: 181 components, 2131 design tokens,
4
+ > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.24.1.
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@31.23.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@31.24.1`). 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): 50 KB — all 181 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–36 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), 104 `semantic` roles, 1814 `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), 104 `semantic` roles, 1816 `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): 50 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/v31.23.0/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v31.24.1/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
@@ -7229,6 +7229,12 @@
7229
7229
  "tier": "component",
7230
7230
  "value": "16px"
7231
7231
  },
7232
+ {
7233
+ "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.",
7234
+ "name": "--dialog-close-size",
7235
+ "tier": "component",
7236
+ "value": "var(--band-height-xl)"
7237
+ },
7232
7238
  {
7233
7239
  "description": "Lightweight row surfaces and hover actions; theme overrides remain scoped component knobs.",
7234
7240
  "name": "--flex-surface-background",
@@ -11429,6 +11435,12 @@
11429
11435
  "tier": "component",
11430
11436
  "value": "var(--band-height-xl)"
11431
11437
  },
11438
+ {
11439
+ "description": "And for the Sidebar's own rows (gh#1138): measured 32px under a finger while the Tree rows and SearchInput beside them had already grown to 44 with the control ladder.",
11440
+ "name": "--sidebar-nav-item-height",
11441
+ "tier": "component",
11442
+ "value": "var(--band-height-xl)"
11443
+ },
11432
11444
  {
11433
11445
  "description": "MobileShell (gh#354 §6) — the handheld app shell: a status band, an app bar, ONE scroll region, a sticky action bar and a bottom tab bar. Two facts here cannot be reached by composition, which is why they belong to the shell and not to the page: 1. The shell is the only scroll container. The root is exactly one viewport tall (`--mobile-shell-block-size`), so the document itself never scrolls and the chrome bands never leave the screen — the reason a composed `Card` + `overflow-y-auto` stack drifts on a real phone, where the URL bar collapses under the page. 2. Every band absorbs the device safe-area insets, so a notch never covers the app bar and the home indicator never covers the primary verb. The safe-area knobs are `env()` values and are therefore ZERO on every surface with no insets (desktop, jsdom, the docs frames) — the geometry below is unchanged there. The INLINE knob takes `max()` of BOTH physical insets on purpose: `env(safe-area-inset-left)` is physical, so binding it to the inline START would be wrong under `dir=\"rtl\"`. A symmetric inset is correct in both writing directions and costs at most a few px on the non-notch side in landscape. A service retunes the page gutter, the three band heights and the block padding from its theme; nothing here is reachable only through a consumer selector.",
11434
11446
  "name": "--mobile-shell-block-size",
@@ -76,8 +76,14 @@ export interface SheetContentProps extends React.ComponentPropsWithRef<"section"
76
76
  * physical `side` the consumer named, at every viewport.
77
77
  */
78
78
  responsive?: SheetResponsiveProp;
79
+ /**
80
+ * Radix's name, same contract as `DialogContent`: fires on every close, before focus returns to
81
+ * the trigger. Call `event.preventDefault()` and focus your own target — the page heading after a
82
+ * navigation, say — to send focus there instead.
83
+ */
84
+ onCloseAutoFocus?: (event: Event) => void;
79
85
  }
80
- export declare function SheetContent({ side, className, children, showCloseButton, overlayClassName, forceMount: _forceMount, width, responsive, style, ref, ...props }: SheetContentProps): React.JSX.Element | null;
86
+ export declare function SheetContent({ side, className, children, showCloseButton, overlayClassName, forceMount: _forceMount, width, responsive, onCloseAutoFocus, style, ref, ...props }: SheetContentProps): React.JSX.Element | null;
81
87
  export declare namespace SheetContent {
82
88
  var displayName: string;
83
89
  }
@@ -118,6 +118,7 @@ function SheetContent({
118
118
  forceMount: _forceMount,
119
119
  width,
120
120
  responsive = "side",
121
+ onCloseAutoFocus,
121
122
  style,
122
123
  ref,
123
124
  ...props
@@ -136,7 +137,7 @@ function SheetContent({
136
137
  const dataState = state.isOpen ? "open" : "closed";
137
138
  const modal = React.useContext(SheetModalContext);
138
139
  const { contentRef, isMounted } = useNonModalPortal(state.isOpen, !modal);
139
- useOverlayCloseFocus(state.isOpen, void 0, modal ? void 0 : contentRef);
140
+ useOverlayCloseFocus(state.isOpen, onCloseAutoFocus, modal ? void 0 : contentRef);
140
141
  const overlayPortalContainer = useOverlayPortalContainer();
141
142
  const dialog = /* @__PURE__ */ jsx(
142
143
  RacDialog,
@@ -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, mobileNav, mobileNavLabel, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
6
+ export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, logoCompact, logoCompactBelow, breadcrumb, footer, children, sidebarCollapsed, responsiveNavigation, topbarSpan, navRail, navRailPosition, navRailEnd, navRailLabel, mobileNav, mobileNavLabel, mobileNavTriggerLabel, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
@@ -31,6 +31,7 @@ function AppShell({
31
31
  navRailLabel,
32
32
  mobileNav,
33
33
  mobileNavLabel,
34
+ mobileNavTriggerLabel,
34
35
  mobileNavOpen,
35
36
  onMobileNavOpenChange
36
37
  }) {
@@ -52,7 +53,7 @@ function AppShell({
52
53
  const setDrawerOpen = onMobileNavOpenChange ?? setUncontrolledOpen;
53
54
  const handleDrawerClick = (event) => {
54
55
  const target = event.target;
55
- const hit = target.closest("a[href], button, [role='menuitem']");
56
+ const hit = target.closest("a[href], .sb-nav-item, [role='menuitem']");
56
57
  if (!hit) {
57
58
  return;
58
59
  }
@@ -103,14 +104,17 @@ function AppShell({
103
104
  );
104
105
  const bar = !hasTopbarContent && !hasDrawer ? null : /* @__PURE__ */ jsxs("header", { className: "app-topbar ui-scale-fixed", "aria-label": t("layout.appShell.headerLabel"), children: [
105
106
  hasDrawer && /* @__PURE__ */ jsxs(Sheet, { open: drawerOpen, onOpenChange: setDrawerOpen, children: [
106
- /* @__PURE__ */ jsx(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
107
+ /* @__PURE__ */ jsx(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
107
108
  TopbarItem,
108
109
  {
109
110
  type: "button",
110
111
  className: "app-mobile-nav-trigger",
111
- "aria-label": t("layout.appShell.openNav"),
112
+ "aria-label": mobileNavTriggerLabel === void 0 ? t("layout.appShell.openNav") : void 0,
112
113
  "aria-haspopup": "dialog",
113
- children: /* @__PURE__ */ jsx(Menu, { className: "size-[var(--app-shell-mobile-nav-icon-size)]", "aria-hidden": "true" })
114
+ children: [
115
+ /* @__PURE__ */ jsx(Menu, { className: "size-[var(--app-shell-mobile-nav-icon-size)]", "aria-hidden": "true" }),
116
+ mobileNavTriggerLabel
117
+ ]
114
118
  }
115
119
  ) }),
116
120
  /* @__PURE__ */ jsxs(
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
2
2
  export type { PageContainerProp, PageContainerProp as PageContainerProps, PageContainerExtraProp, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
- declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, density, variant, preset, headerLayout, headerScale, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
5
+ declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, 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
  };
@@ -45,6 +45,7 @@ function PageContainerRoot({
45
45
  breadcrumbAriaLabel,
46
46
  linkComponent: LinkComponent = "a",
47
47
  headerLoading = false,
48
+ titleRef,
48
49
  density,
49
50
  variant = "default",
50
51
  preset = "default",
@@ -61,6 +62,7 @@ function PageContainerRoot({
61
62
  const { headerRef, revealed } = useFooterReveal(reveal);
62
63
  const { t } = useTranslation();
63
64
  const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
65
+ const titleFocus = titleRef === void 0 ? void 0 : { ref: titleRef, tabIndex: -1 };
64
66
  return /* @__PURE__ */ jsxs(
65
67
  "div",
66
68
  {
@@ -134,10 +136,17 @@ function PageContainerRoot({
134
136
  ),
135
137
  /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
136
138
  /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading", children: [
137
- headerLoading ? /* @__PURE__ */ jsx("h1", { className: "ui-page-title ui-skeleton-block ui-page-title-placeholder", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("layout.pageHeader.loading") }) }) : status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
138
- /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
139
+ headerLoading ? /* @__PURE__ */ jsx(
140
+ "h1",
141
+ {
142
+ ...titleFocus,
143
+ className: "ui-page-title ui-skeleton-block ui-page-title-placeholder",
144
+ children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("layout.pageHeader.loading") })
145
+ }
146
+ ) : status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
147
+ /* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }),
139
148
  /* @__PURE__ */ jsx("div", { className: "ui-page-header-status", children: status })
140
- ] }) : /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
149
+ ] }) : /* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }),
141
150
  headerLoading ? (
142
151
  // Decorative only — the pending state is already announced once by the heading above,
143
152
  // so a second live placeholder here would double-announce it.
@@ -47,4 +47,4 @@ export { createSidebarLink } from "./sidebar-link.js";
47
47
  * Sidebar — data-driven vertical nav rail. Use {@link createSidebarLink} to adapt a router `Link`,
48
48
  * or `SidebarItem asChild` when composing rows by hand.
49
49
  */
50
- export declare function Sidebar({ ariaLabel: ariaLabelCamel, activeId, onSelect, sections, product, onProductClick, brand, collapsed, children, linkComponent, renderItem, footer, "aria-label": ariaLabel, }: SidebarProp): React.JSX.Element;
50
+ export declare function Sidebar({ ariaLabel: ariaLabelCamel, scrollRef, activeId, onSelect, sections, product, onProductClick, brand, collapsed, children, linkComponent, renderItem, footer, "aria-label": ariaLabel, }: SidebarProp): React.JSX.Element;
@@ -294,6 +294,7 @@ function CollapsedRow({ item, activeId, onSelect, linkComponent: LinkComponent }
294
294
  import { createSidebarLink } from "./sidebar-link.js";
295
295
  function Sidebar({
296
296
  ariaLabel: ariaLabelCamel,
297
+ scrollRef,
297
298
  activeId,
298
299
  onSelect,
299
300
  sections,
@@ -354,6 +355,7 @@ function Sidebar({
354
355
  /* @__PURE__ */ jsx(
355
356
  "nav",
356
357
  {
358
+ ref: scrollRef,
357
359
  className: "sb-nav-scroll",
358
360
  "aria-label": ariaLabel ?? ariaLabelCamel ?? t("layout.sidebar.ariaLabel"),
359
361
  children: children ?? resolvedSections.map((section, sectionIndex) => /* @__PURE__ */ jsx(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "31.23.0",
3
+ "version": "31.24.1",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -58,6 +58,12 @@ export type PageContainerProp = {
58
58
  * violation) so the page's heading outline never disappears mid-load.
59
59
  */
60
60
  headerLoading?: boolean;
61
+ /**
62
+ * Ref to the page's `<h1>`. Passing it also makes the heading programmatically focusable
63
+ * (`tabIndex={-1}`, outside the tab order, no focus ring), so a router can move focus to the new
64
+ * page's title after a client-side navigation — the pattern screen readers need to announce it.
65
+ */
66
+ titleRef?: React.Ref<HTMLHeadingElement>;
61
67
  /** @see PageContainerExtraProp */
62
68
  extra?: PageContainerExtraProp;
63
69
  /**
@@ -577,6 +583,14 @@ export type AppShellProp = {
577
583
  mobileNav?: ReactNode;
578
584
  /** Accessible title for the mobile navigation drawer. Defaults to the localized "Menu". */
579
585
  mobileNavLabel?: string;
586
+ /**
587
+ * Visible text beside the drawer trigger's glyph (e.g. "Pages"). It becomes the trigger's
588
+ * accessible name, replacing the localized "Open navigation" — omit it for the glyph-only
589
+ * trigger. The drawer closes when a link, a Sidebar row or a menu item inside it is activated,
590
+ * or on an explicit close (Esc, overlay, close button, `SheetClose`); other controls — a tree's
591
+ * expand toggle — leave it open.
592
+ */
593
+ mobileNavTriggerLabel?: string;
580
594
  /** Controlled open state of the mobile drawer. Omit for AppShell-owned (uncontrolled) state. */
581
595
  mobileNavOpen?: boolean;
582
596
  /** Change handler for the mobile drawer open state (pairs with `mobileNavOpen`). */
@@ -1371,6 +1385,11 @@ export type AppLauncherProp = {
1371
1385
  export type SidebarProp = {
1372
1386
  /** Accessible navigation landmark name; make it unique when multiple sidebars share a document. */
1373
1387
  ariaLabel?: string;
1388
+ /**
1389
+ * Ref to the navigation's scroll container (the element with `overflow-y: auto`), to persist
1390
+ * and restore its `scrollTop` across navigations or remounts.
1391
+ */
1392
+ scrollRef?: React.Ref<HTMLElement>;
1374
1393
  activeId: string;
1375
1394
  onSelect?: (id: string) => void;
1376
1395
  sections?: SidebarSectionProp[];
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "31.23.0",
2
+ "version": "31.24.1",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",
@@ -896,6 +896,10 @@
896
896
  min-inline-size: 0;
897
897
  }
898
898
 
899
+ .ui-page-title[tabindex="-1"]:focus {
900
+ outline: none;
901
+ }
902
+
899
903
  .ui-page-header-title-row > .ui-page-title {
900
904
  min-inline-size: 0;
901
905
  }
@@ -174,3 +174,9 @@
174
174
 
175
175
  --toast-mobile-offset: 16px;
176
176
  }
177
+
178
+ @media (pointer: coarse) {
179
+ :root {
180
+ --dialog-close-size: var(--band-height-xl);
181
+ }
182
+ }
@@ -441,6 +441,8 @@
441
441
 
442
442
  --app-shell-nav-rail-width: 3rem;
443
443
  --app-shell-nav-rail-item-size: var(--band-height-xl);
444
+
445
+ --sidebar-nav-item-height: var(--band-height-xl);
444
446
  }
445
447
  }
446
448
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "31.23.0",
4
- "godxUiMcp": "31.23.0",
3
+ "version": "31.24.1",
4
+ "godxUiMcp": "31.24.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",