@godxjp/ui 19.4.2 → 19.5.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 (71) hide show
  1. package/dist/app/app-provider.js +10 -1
  2. package/dist/app/theme-axes.d.ts +18 -3
  3. package/dist/app/theme-axes.js +15 -3
  4. package/dist/components/data-display/badge.js +4 -1
  5. package/dist/components/data-display/data-table.js +9 -2
  6. package/dist/components/data-entry/checkbox.d.ts +12 -0
  7. package/dist/components/data-entry/checkbox.js +34 -3
  8. package/dist/components/data-entry/index.d.ts +2 -0
  9. package/dist/components/data-entry/index.js +2 -0
  10. package/dist/components/data-entry/segmented.d.ts +2 -0
  11. package/dist/components/data-entry/segmented.js +5 -0
  12. package/dist/components/data-entry/select.d.ts +2 -0
  13. package/dist/components/data-entry/select.js +8 -3
  14. package/dist/components/feedback/sheet.js +1 -1
  15. package/dist/components/feedback/sonner.d.ts +7 -1
  16. package/dist/components/feedback/sonner.js +27 -7
  17. package/dist/components/general/button.js +6 -1
  18. package/dist/components/general/inert-background.d.ts +8 -3
  19. package/dist/components/general/inert-background.js +26 -2
  20. package/dist/components/general/logo.d.ts +53 -0
  21. package/dist/components/general/logo.js +57 -3
  22. package/dist/components/layout/index.d.ts +4 -0
  23. package/dist/components/layout/index.js +4 -0
  24. package/dist/components/layout/nav-list.d.ts +26 -0
  25. package/dist/components/layout/nav-list.js +21 -0
  26. package/dist/components/layout/topbar-item.d.ts +17 -0
  27. package/dist/components/layout/topbar-item.js +21 -0
  28. package/dist/components/navigation/app-setting-picker.js +22 -5
  29. package/dist/components/navigation/context-menu.js +2 -2
  30. package/dist/components/navigation/dropdown-menu.js +2 -2
  31. package/dist/components/navigation/tabs.js +42 -10
  32. package/dist/components/ui/segmented.d.ts +48 -0
  33. package/dist/components/ui/segmented.js +48 -0
  34. package/dist/email/tokens.generated.d.ts +3 -3
  35. package/dist/email/tokens.generated.js +3 -3
  36. package/dist/i18n/messages/en.json +2 -1
  37. package/dist/i18n/messages/ja.json +2 -1
  38. package/dist/i18n/messages/vi.json +2 -1
  39. package/dist/props/components/app.prop.d.ts +9 -2
  40. package/dist/props/components/layout.prop.d.ts +48 -2
  41. package/dist/props/registry.d.ts +19 -0
  42. package/dist/props/registry.js +27 -0
  43. package/dist/props/vocabulary/shared.prop.d.ts +11 -2
  44. package/dist/styles/alert-layout.css +4 -0
  45. package/dist/styles/base.css +2 -0
  46. package/dist/styles/control.css +94 -26
  47. package/dist/styles/data-display-layout.css +2 -1
  48. package/dist/styles/dialog-layout.css +2 -1
  49. package/dist/styles/focus-ring.css +77 -24
  50. package/dist/styles/layout.css +2 -1
  51. package/dist/styles/logo-layout.css +56 -0
  52. package/dist/styles/navigation-layout.css +15 -0
  53. package/dist/styles/shell-layout.css +64 -10
  54. package/dist/tokens/antd.generated.css +40 -0
  55. package/dist/tokens/axes.css +6 -0
  56. package/dist/tokens/base.css +3 -0
  57. package/dist/tokens/components/control.css +5 -1
  58. package/dist/tokens/components/feedback.css +2 -0
  59. package/dist/tokens/components/legal-document.css +0 -2
  60. package/dist/tokens/components/logo.css +13 -0
  61. package/dist/tokens/components/navigation.css +2 -0
  62. package/dist/tokens/components/segmented.css +27 -0
  63. package/dist/tokens/components/shell.css +15 -2
  64. package/dist/tokens/foundation.css +19 -12
  65. package/docs/DESIGN-AUTHORITY.md +346 -0
  66. package/docs/FRAME-COVERAGE-REPORT.md +5 -2
  67. package/docs/README.md +14 -13
  68. package/docs/data-entry/segmented.tsx +124 -0
  69. package/docs/layout/nav-list.tsx +63 -0
  70. package/docs/layout/topbar-item.tsx +157 -0
  71. package/package.json +8 -2
@@ -1,6 +1,7 @@
1
1
  import { PageContainer } from "./page-container.js";
2
2
  import { Flex } from "./flex.js";
3
3
  import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
4
+ import { NavList } from "./nav-list.js";
4
5
  import { AppShell } from "./app-shell.js";
5
6
  import { OrgSwitcher } from "./org-switcher.js";
6
7
  import { AuthShell } from "./auth-shell.js";
@@ -16,6 +17,7 @@ import { ErrorSurface } from "./error-surface.js";
16
17
  import { Breadcrumb } from "./breadcrumb.js";
17
18
  import { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
18
19
  import { Topbar } from "./topbar.js";
20
+ import { TopbarItem } from "./topbar-item.js";
19
21
  import { ResponsiveGrid } from "./responsive-grid.js";
20
22
  import { MasterDetail } from "./master-detail.js";
21
23
  import { SplitPane } from "./split-pane.js";
@@ -40,6 +42,7 @@ export {
40
42
  LegalDocumentShell,
41
43
  MasterDetail,
42
44
  MobileShell,
45
+ NavList,
43
46
  OrgSwitcher,
44
47
  PageContainer,
45
48
  ResizableHandle,
@@ -54,5 +57,6 @@ export {
54
57
  SidebarSection,
55
58
  SplitPane,
56
59
  Topbar,
60
+ TopbarItem,
57
61
  createSidebarLink
58
62
  };
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import type { NavListProp } from "../../props/components/layout.prop.js";
3
+ export type { NavListProp };
4
+ export type NavListProps = NavListProp;
5
+ /**
6
+ * NavList — a vertical route navigation that is NOT shell furniture (gh#374).
7
+ *
8
+ * `Sidebar` owns the app's primary rail and renders into `AppShell`'s grid, so it cannot be nested
9
+ * in a page. The settings-nav shape — a column of links beside the pane they drive, one of them
10
+ * current — had no primitive, which left every consumer hand-rolling rows out of `Button`s with the
11
+ * current one encoded as a variant swap: no `aria-current`, no icon column, a different invention
12
+ * per app.
13
+ *
14
+ * This is the SAME row as the rail, deliberately: it composes `SidebarItem`, so icon slot, label,
15
+ * badge, active tokens and `aria-current="page"` are one implementation shared by both, and a
16
+ * consumer that already learnt `SidebarItemProp` / `linkComponent` for the rail knows this too.
17
+ * Only the container differs — a `<nav>` landmark instead of the shell's grid area, and no
18
+ * collapsed rail (a page-level nav has no rail to collapse into).
19
+ */
20
+ export declare const NavList: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> & {
21
+ items: import("../../props/index.js").SidebarItemProp[];
22
+ activeId?: string;
23
+ label: string;
24
+ linkComponent?: import("./sidebar.js").SidebarLinkComponentProp;
25
+ onSelect?: (id: string) => void;
26
+ } & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { SidebarItem } from "./sidebar.js";
6
+ const NavList = React.forwardRef(
7
+ ({ items, activeId, label, linkComponent, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsx("nav", { ref, "aria-label": label, className: cn("ui-nav-list", className), ...props, children: items.map((item) => /* @__PURE__ */ jsx(
8
+ SidebarItem,
9
+ {
10
+ item,
11
+ active: item.id === activeId,
12
+ linkComponent,
13
+ onActivate: onSelect
14
+ },
15
+ item.id
16
+ )) })
17
+ );
18
+ NavList.displayName = "NavList";
19
+ export {
20
+ NavList
21
+ };
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ export type { TopbarItemProp, TopbarItemProp as TopbarItemProps, } from "../../props/components/layout.prop.js";
3
+ /**
4
+ * TopbarItem — one interactive cell of a {@link Topbar} slot, shaped like part of the bar.
5
+ *
6
+ * Full bar height, the bar's own hover surface, and the focus mark hosted INSIDE the cell (a
7
+ * full-bleed cell has nothing outside itself to ring, and the bar clips its own overflow). All of
8
+ * that lives in `.ui-topbar-item`; this component only picks the element and the classes.
9
+ *
10
+ * `ui-focus-ring` is the ONE focus source (styles/focus-ring.css) — the cell hosts whatever that
11
+ * file resolves, including nothing at all while the `--focus-outline` switch ships off.
12
+ */
13
+ export declare const TopbarItem: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
14
+ asChild?: boolean;
15
+ children?: React.ReactNode;
16
+ className?: import("../../props/index.js").ClassNameProp;
17
+ } & React.RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cn } from "../../lib/utils.js";
6
+ const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, className, type, ...props }, ref) {
7
+ const Comp = asChild ? Slot : "button";
8
+ return /* @__PURE__ */ jsx(
9
+ Comp,
10
+ {
11
+ ref,
12
+ "data-slot": "topbar-item",
13
+ className: cn("ui-topbar-item ui-focus-ring", className),
14
+ type: asChild ? void 0 : type ?? "button",
15
+ ...props
16
+ }
17
+ );
18
+ });
19
+ export {
20
+ TopbarItem
21
+ };
@@ -135,19 +135,36 @@ const AppSettingPicker = React.forwardRef(
135
135
  // sits among ghost icon buttons (the topbar's sidebar-toggle/notifications/account
136
136
  // triggers), so it drops controlTriggerClass's form-input chrome (border/bg/shadow)
137
137
  // at rest and adopts the same ghost hover — a resting border here read as visually
138
- // inconsistent next to its borderless topbar siblings. The open-state ring
139
- // (`data-[state=open]:border-ring`, from controlTriggerClass) and the
140
- // focus-visible ring are untouched, so keyboard and "is this open" affordance
141
- // still hold.
138
+ // inconsistent next to its borderless topbar siblings. The open-state and
139
+ // focus-visible rings are untouched, so keyboard and "is this open" affordance
140
+ // still hold: both come from styles/focus-ring.css, which also excludes this
141
+ // variant from the bordered-field rebind precisely BECAUSE it has no resting
142
+ // border to recolour, so it keeps the opaque ring form.
142
143
  // Bề ngang phải là UTILITY, không phải luật class. SelectTrigger phát `w-full`,
143
144
  // mà utility nằm sau components trong thứ tự layer nên `inline-size` khai trong
144
145
  // .ui-app-setting-picker-icon luôn thua. Trong một khe co theo nội dung của
145
146
  // topbar, `width: 100%` co lại bằng chính nội dung, tức 18px thay vì ô vuông
146
147
  // --control-height. Nhánh có nhãn ngay dưới đã phải tự vệ đúng như vậy bằng
147
148
  // `w-auto`. Giá trị vẫn đọc token chứ không phải một con số.
149
+ // `justify-content` đi cùng lý do đó: SelectTrigger phát `justify-between`,
150
+ // với một con duy nhất thì nó dồn glyph sát viền trái (khe 1px/15px trong ô
151
+ // 32px). `justify-content: center` khai trong .ui-app-setting-picker-icon
152
+ // cũng nằm ở @layer components nên cũng thua. Cả hai trục của cái hộp này
153
+ // phải do lớp utility sở hữu, không được để sót nửa nào lại trong luật class.
154
+ // `shrink-0` là NỬA CÒN LẠI của trục ngang, không phải trang trí. `w-*` chỉ khai
155
+ // kích thước MONG MUỐN; trong một flex row chật, `flex-shrink: 1` mặc định vẫn
156
+ // ép nó nhỏ hơn. Đo thật trong Chromium ở chính frame tài liệu của component:
157
+ // `.ui-topbar-end` rộng 82px, gap 8px, hai trigger cùng co → ô vuông ra
158
+ // 21,28 × 32px thay vì 32 × 32. Đây không phải gh#366 quay lại: `inline-size`
159
+ // vẫn giải đúng --control-height; cái hỏng là co giãn. Một tap target vuông
160
+ // biến thành hình chữ nhật 21px mỗi khi topbar chật là lỗi thật, và nó kéo
161
+ // theo cả vùng chạm. Nhánh CÓ NHÃN cố tình KHÔNG nhận `shrink-0`: nó rộng theo
162
+ // nội dung và mang sẵn `max-w-full` để được phép thu lại.
163
+ // Vẫn phải là utility vì cùng lý do với `w-*`: luật trong @layer components
164
+ // thua lớp utilities mà SelectTrigger phát ra.
148
165
  cn(
149
166
  "ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground",
150
- "w-[length:var(--control-height)]"
167
+ "w-[length:var(--control-height)] shrink-0 justify-center"
151
168
  )
152
169
  ) : (
153
170
  // Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
@@ -11,11 +11,11 @@ function ContextMenuTrigger(props) {
11
11
  }
12
12
  const ContextMenuPortal = ContextMenuPrimitive.Portal;
13
13
  const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => {
14
- useInertHiddenBackground();
14
+ const contentRef = useInertHiddenBackground(ref);
15
15
  return /* @__PURE__ */ jsx(ContextMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
16
16
  ContextMenuPrimitive.Content,
17
17
  {
18
- ref,
18
+ ref: contentRef,
19
19
  "data-slot": "context-menu-content",
20
20
  className: cn("ui-context-menu-content", className),
21
21
  ...props
@@ -24,11 +24,11 @@ function DropdownMenuSub(props) {
24
24
  return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
25
25
  }
26
26
  const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => {
27
- useInertHiddenBackground();
27
+ const contentRef = useInertHiddenBackground(ref);
28
28
  return /* @__PURE__ */ jsx(DropdownMenuPortal, { children: /* @__PURE__ */ jsx(
29
29
  DropdownMenuPrimitive.Content,
30
30
  {
31
- ref,
31
+ ref: contentRef,
32
32
  "data-slot": "dropdown-menu-content",
33
33
  sideOffset,
34
34
  className: cn(
@@ -47,9 +47,28 @@ function Tabs({
47
47
  "data-slot": "tabs-list",
48
48
  variant: variant === "line" ? "line" : "default",
49
49
  className: cn(
50
- // The padding override is an arbitrary value reading the knob (never `p-0`): it must
51
- // stay a UTILITY so tailwind-merge still drops the strip's own padding step, and a
52
- variant === "line" && "h-auto w-full justify-start border-b p-[var(--tabs-list-line-space-inset)]",
50
+ // The inset override is an arbitrary value reading the knob (never `p-0`): it must
51
+ // stay a UTILITY so tailwind-merge still drops the strip's own padding step.
52
+ //
53
+ // IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
54
+ // block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
55
+ // inset is 0px, so a focused trigger had NO block headroom at all — measured in
56
+ // Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
57
+ // come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
58
+ // to `auto` per spec (a second scroll container, not a ring), and Chromium honours
59
+ // `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
60
+ // the ring's outer reach and an equal negative block margin hands it straight back —
61
+ // the ring paints inside the scrollport and the strip's OUTER box is unchanged
62
+ // (measured: 38px before and after). Both values read the same token the ring is
63
+ // sized from, so the two can never drift apart.
64
+ //
65
+ // The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
66
+ // border box) now sits the ring's reach below the triggers instead of flush. That is
67
+ // where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
68
+ // the same block inset — so the two construction paths now agree. A service that
69
+ // wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
70
+ // which exists for exactly that.
71
+ variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto w-full justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]",
53
72
  variant === "card" && "w-full justify-start",
54
73
  listClassName
55
74
  ),
@@ -134,12 +153,25 @@ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
134
153
  ref,
135
154
  "data-slot": "tabs-trigger",
136
155
  className: cn(
137
- // The SELECTED-state ring (`ring-1 ring-primary/25`) is scoped to the default/card lists
138
- // it competes with (and at equal specificity overrides) the `:focus-visible` ring. The
139
- // focus-visible ring/outline below is deliberately left unscoped: every variant keeps a
140
- // visible keyboard focus indicator (WCAG 2.4.7). The line indicator itself lives in
141
- // src/styles/navigation-layout.css so it reads the --tabs-indicator-* tokens.
142
- "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:ring-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=default]/tabs-list:data-[state=active]:ring-1 group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
156
+ // SELECTED IS A BORDER TINT, NOT A RING — and that is a WCAG fix, not a style preference.
157
+ // It used to be `ring-1 ring-primary/25`, which writes `--tw-ring-shadow` in the UTILITIES
158
+ // layer and therefore overwrote the focus ring that focus-ring.css feeds from `components`.
159
+ // Measured: a focused ACTIVE tab painted `oklab( / 0.25) 0 0 0 1px` and nothing else — a
160
+ // 1px indicator at 25% alpha, failing both clauses of SC 2.4.13 (2px perimeter, ≥3:1),
161
+ // while every other control in the library carried a full 2px ring. The trigger already
162
+ // owns `border border-transparent`, so tinting that border reproduces the selected hairline
163
+ // at the same colour and width with no layout change, and leaves `--tw-ring-shadow` free for
164
+ // the focus ring. `shadow-sm` stays: its composite READS `--tw-ring-shadow`, so the lift and
165
+ // the ring coexist.
166
+ //
167
+ // No `focus-visible:outline-1` either. It was the fallback that survived the clobber — a 1px
168
+ // currentColor line squeezed between the border and the ring once both paint. The ring is
169
+ // the indicator now; two marks for one state is what this pass exists to remove.
170
+ //
171
+ // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
172
+ // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
173
+ // border, focused is the 2px ring plus its halo outside it.
174
+ "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
143
175
  className
144
176
  ),
145
177
  ...props
@@ -151,7 +183,7 @@ const TabsContent = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
151
183
  {
152
184
  ref,
153
185
  "data-slot": "tabs-content",
154
- className: cn("focus-visible:ring-ring flex-1 outline-none focus-visible:ring-2", className),
186
+ className: cn("ui-focus-ring flex-1 outline-none", className),
155
187
  ...props
156
188
  }
157
189
  ));
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ /** One choice in a {@link Segmented}. */
3
+ export type SegmentedOption = {
4
+ /** Wire value — what `onValueChange` reports and what a form submits. */
5
+ value: string;
6
+ /** Visible label. It is also the item's accessible name, so it is required. */
7
+ label: React.ReactNode;
8
+ /** Optional leading glyph, rendered `aria-hidden` beside the label. */
9
+ icon?: React.ReactNode;
10
+ /** Disable this one choice; the rest of the group stays operable. */
11
+ disabled?: boolean;
12
+ };
13
+ export type SegmentedProp = {
14
+ /** The closed set of choices, in reading order. */
15
+ options: readonly SegmentedOption[];
16
+ /** Controlled selection. */
17
+ value?: string;
18
+ /** Uncontrolled initial selection. */
19
+ defaultValue?: string;
20
+ onValueChange?: (value: string) => void;
21
+ /** Disable the whole group. */
22
+ disabled?: boolean;
23
+ /** Form field name — submits the selected value with the form. */
24
+ name?: string;
25
+ id?: string;
26
+ className?: string;
27
+ /** Accessible name of the group. Required unless a visible label points at `id`. */
28
+ "aria-label"?: string;
29
+ "aria-labelledby"?: string;
30
+ };
31
+ export type SegmentedProps = SegmentedProp;
32
+ /**
33
+ * Segmented — one-of-N from a small, closed, always-visible set. antd's `Segmented`
34
+ * (docs/DESIGN-AUTHORITY.md names Ant Design the taxonomy authority) drawn on Radix's RadioGroup,
35
+ * which is this repo's authority for behaviour primitives.
36
+ *
37
+ * WHY NOT `ToggleGroup`. A ToggleGroup is a row of PRESSED buttons: `aria-pressed`, independently
38
+ * togglable, and — even at `type="single"` — deselectable, so "no theme at all" is a state the
39
+ * markup permits. A segmented control is a radio group: exactly one member is always chosen, the
40
+ * arrow keys move between members rather than Tab, and a screen reader must say "Light, radio
41
+ * button, 1 of 3, selected" and not "Light, toggle button, pressed". WAI-ARIA APG owns that
42
+ * distinction and it is not a skin.
43
+ *
44
+ * The primitive gives roving tabindex, arrow-key traversal (RTL-aware), the radiogroup/radio roles
45
+ * and the hidden input a native form submit needs. This file adds the antd geometry and nothing
46
+ * else — the focus mark comes from `ui-focus-ring`, the ONE source in styles/focus-ring.css.
47
+ */
48
+ export declare const Segmented: React.ForwardRefExoticComponent<SegmentedProp & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
+ import { cn } from "../../lib/utils.js";
6
+ const Segmented = React.forwardRef(function Segmented2({ options, value, defaultValue, onValueChange, disabled, name, id, className, ...props }, ref) {
7
+ return /* @__PURE__ */ jsx(
8
+ RadioGroupPrimitive.Root,
9
+ {
10
+ ref,
11
+ id,
12
+ "data-slot": "segmented",
13
+ className: cn("ui-segmented", className),
14
+ orientation: "horizontal",
15
+ value,
16
+ defaultValue,
17
+ onValueChange,
18
+ disabled,
19
+ name,
20
+ ...props,
21
+ children: options.map((option) => /* @__PURE__ */ jsxs(
22
+ RadioGroupPrimitive.Item,
23
+ {
24
+ value: option.value,
25
+ disabled: option.disabled,
26
+ "data-slot": "segmented-item",
27
+ className: "ui-segmented-item ui-focus-ring",
28
+ children: [
29
+ option.icon == null ? null : /* @__PURE__ */ jsx(
30
+ "span",
31
+ {
32
+ "data-slot": "segmented-item-icon",
33
+ className: "ui-segmented-item-icon",
34
+ "aria-hidden": "true",
35
+ children: option.icon
36
+ }
37
+ ),
38
+ /* @__PURE__ */ jsx("span", { "data-slot": "segmented-item-label", className: "ui-segmented-item-label", children: option.label })
39
+ ]
40
+ },
41
+ option.value
42
+ ))
43
+ }
44
+ );
45
+ });
46
+ export {
47
+ Segmented
48
+ };
@@ -44,8 +44,8 @@ export declare const EMAIL_COLOR_SOURCE: {
44
44
  readonly hsl: "60 33% 99%";
45
45
  };
46
46
  readonly focus: {
47
- readonly cssVar: "--ring";
48
- readonly hsl: "204 100% 39%";
47
+ readonly cssVar: "--primary";
48
+ readonly hsl: "204 100% 37%";
49
49
  };
50
50
  readonly brand: {
51
51
  readonly cssVar: "--brand";
@@ -103,7 +103,7 @@ export declare const EMAIL_COLOR_SOURCE_DARK: {
103
103
  readonly hsl: "48 9% 9%";
104
104
  };
105
105
  readonly focus: {
106
- readonly cssVar: "--ring";
106
+ readonly cssVar: "--primary";
107
107
  readonly hsl: "204 90% 60%";
108
108
  };
109
109
  readonly brand: {
@@ -36,8 +36,8 @@ const EMAIL_COLOR_SOURCE = {
36
36
  "hsl": "60 33% 99%"
37
37
  },
38
38
  "focus": {
39
- "cssVar": "--ring",
40
- "hsl": "204 100% 39%"
39
+ "cssVar": "--primary",
40
+ "hsl": "204 100% 37%"
41
41
  },
42
42
  "brand": {
43
43
  "cssVar": "--brand",
@@ -94,7 +94,7 @@ const EMAIL_COLOR_SOURCE_DARK = {
94
94
  "hsl": "48 9% 9%"
95
95
  },
96
96
  "focus": {
97
- "cssVar": "--ring",
97
+ "cssVar": "--primary",
98
98
  "hsl": "204 90% 60%"
99
99
  },
100
100
  "brand": {
@@ -336,7 +336,8 @@
336
336
  "themePicker": {
337
337
  "ariaLabel": "Theme",
338
338
  "light": "Light",
339
- "dark": "Dark"
339
+ "dark": "Dark",
340
+ "system": "System"
340
341
  },
341
342
  "densityPicker": {
342
343
  "ariaLabel": "Density",
@@ -324,7 +324,8 @@
324
324
  "themePicker": {
325
325
  "ariaLabel": "テーマ",
326
326
  "light": "ライト",
327
- "dark": "ダーク"
327
+ "dark": "ダーク",
328
+ "system": "システム"
328
329
  },
329
330
  "densityPicker": {
330
331
  "ariaLabel": "密度",
@@ -324,7 +324,8 @@
324
324
  "themePicker": {
325
325
  "ariaLabel": "Giao diện",
326
326
  "light": "Sáng",
327
- "dark": "Tối"
327
+ "dark": "Tối",
328
+ "system": "Hệ thống"
328
329
  },
329
330
  "densityPicker": {
330
331
  "ariaLabel": "Mật độ",
@@ -28,7 +28,11 @@ export type AppProviderProp = {
28
28
  storageKey?: string;
29
29
  /** Persist user choices. Default: true. */
30
30
  persist?: boolean;
31
- /** Initial light/dark theme — written to `<html data-theme>`. Default: `"light"`. */
31
+ /**
32
+ * Initial theme choice. `"light"` / `"dark"` are written straight to `<html data-theme>`;
33
+ * `"system"` defers to `prefers-color-scheme` and is re-resolved whenever the OS changes.
34
+ * Default: `"light"`.
35
+ */
32
36
  theme?: AppTheme;
33
37
  /**
34
38
  * Initial brand palette preset — written to `<html data-brand>`. OPT-IN: omit
@@ -101,7 +105,10 @@ export type AppContextValue = {
101
105
  requestHeaders: AppRequestHeaders;
102
106
  /** Configured timezone list; `undefined` → full IANA in the timezone-picker recipe. */
103
107
  timezoneOptions?: readonly AppTimezone[];
104
- /** Current theme axes (mirror `<html data-*>` / inline `--scaling`). */
108
+ /**
109
+ * Current theme axes (mirror `<html data-*>` / inline `--scaling`). `theme` is the user's
110
+ * CHOICE — it can be `"system"`, which `<html data-theme>` never is.
111
+ */
105
112
  theme: AppTheme;
106
113
  brand: AppBrand | null;
107
114
  density: AppDensity;
@@ -631,6 +631,25 @@ export type SidebarItemProp = {
631
631
  /** Nested rows — renders a collapsible submenu group (the parent reads active when any child is). */
632
632
  children?: SidebarItemProp[];
633
633
  };
634
+ /**
635
+ * A vertical route navigation that lives INSIDE a page, not in the shell (gh#374).
636
+ *
637
+ * Reuses `SidebarItemProp` and `SidebarLinkComponentProp` verbatim rather than minting a parallel
638
+ * vocabulary: the row is the same row, so a service that learnt the rail's item shape does not
639
+ * learn a second one, and a fix to the row reaches both.
640
+ */
641
+ export type NavListProp = Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> & {
642
+ /** Rows, in reading order. `icon` is required by `SidebarItemProp` — the label aligns to it. */
643
+ items: SidebarItemProp[];
644
+ /** `SidebarItemProp.id` of the current route; that row gets `aria-current="page"`. */
645
+ activeId?: string;
646
+ /** Accessible name for the `<nav>` landmark. Required: a page may hold more than one. */
647
+ label: string;
648
+ /** Router link component — same contract as `Sidebar.linkComponent`. */
649
+ linkComponent?: SidebarLinkComponentProp;
650
+ /** Reports the activated row's id, for consumers driving navigation themselves. */
651
+ onSelect?: (id: string) => void;
652
+ };
634
653
  /** @see Sidebar */
635
654
  export type SidebarItemData = SidebarItemProp;
636
655
  /**
@@ -654,8 +673,15 @@ export type SidebarLinkProp = {
654
673
  "aria-label"?: string;
655
674
  /** `"menuitem"` inside the collapsed rail's portaled flyout menu; absent for ordinary rows. */
656
675
  role?: "menuitem";
657
- /** Reports selection to `Sidebar.onSelect` after the router link runs its own handler. */
658
- onClick?: (event: React.MouseEvent<HTMLElement>) => void;
676
+ /**
677
+ * Reports selection to `Sidebar.onSelect` after the router link runs its own handler.
678
+ *
679
+ * Typed on `Element`, NOT `HTMLElement`. Handler parameters are contravariant, so a link
680
+ * component that accepts the wider `MouseEvent<Element>` — Inertia's `<Link>`, react-router's,
681
+ * TanStack's — cannot receive a prop narrowed to `HTMLElement`, and the anchor-safe contract
682
+ * this type promises would only hold behind a cast at every call site.
683
+ */
684
+ onClick?: React.MouseEventHandler<Element>;
659
685
  };
660
686
  /**
661
687
  * A framework router link component driven by {@link SidebarLinkProp} — Inertia's `<Link href>`,
@@ -809,6 +835,26 @@ export type TopbarProp = Omit<React.HTMLAttributes<HTMLDivElement>, "children">
809
835
  /** Escape hatch — render fully custom bar content instead of the three slots. */
810
836
  children?: ReactNode;
811
837
  };
838
+ /**
839
+ * @see TopbarItem — ONE interactive cell of a {@link TopbarProp} slot: the account button, a
840
+ * settings trigger, a notifications bell.
841
+ *
842
+ * It exists because the alternative is a `Button`, and a Button in a bar is a control that landed
843
+ * in the bar rather than a part of it — a pill of its own height floating in a taller strip, with
844
+ * its own hover surface and its own focus ring drawn around that pill. Fluent, SLDS, Atlassian and
845
+ * antd's ProLayout all draw a top-bar trigger the other way: the cell is as tall as the bar, its
846
+ * hover is the bar's own surface, and the focus mark is hosted INSIDE the cell because a
847
+ * full-bleed cell has nothing outside itself to ring.
848
+ */
849
+ export type TopbarItemProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
850
+ /**
851
+ * Render the bar-cell shape ONTO the child instead of emitting a `<button>` — for a router link,
852
+ * or for a menu/popover trigger that needs to own the element itself.
853
+ */
854
+ asChild?: boolean;
855
+ children?: ReactNode;
856
+ className?: ClassNameProp;
857
+ };
812
858
  /**
813
859
  * @see LegalDocumentShell — one entry of the table of contents + the matching document section.
814
860
  * `id` is the REAL anchor target (`href="#{id}"`, `<section id>`), so it must be unique on the page
@@ -927,6 +927,20 @@ export declare const COMPONENT_PROP_REGISTRY: {
927
927
  readonly file: "components/layout.prop.ts";
928
928
  readonly vocabulary: readonly ["ChildrenProp"];
929
929
  };
930
+ readonly TopbarItemProp: {
931
+ readonly group: "layout";
932
+ readonly file: "components/layout.prop.ts";
933
+ readonly vocabulary: readonly ["ChildrenProp", "ClassNameProp", {
934
+ readonly field: "asChild";
935
+ readonly local: true;
936
+ readonly reason: "Radix Slot passthrough — merges onto the child";
937
+ }];
938
+ };
939
+ readonly NavListProp: {
940
+ readonly group: "layout";
941
+ readonly file: "components/layout.prop.ts";
942
+ readonly vocabulary: readonly ["IdProp", "ClassNameProp"];
943
+ };
930
944
  readonly ButtonProp: {
931
945
  readonly group: "general";
932
946
  readonly file: "components/general.prop.ts";
@@ -1734,6 +1748,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1734
1748
  readonly file: "components/ui/rating.tsx";
1735
1749
  readonly vocabulary: readonly ["ValueProp", "OnValueChangeProp", "DisabledProp", "ClassNameProp"];
1736
1750
  };
1751
+ readonly SegmentedProp: {
1752
+ readonly group: "data-entry";
1753
+ readonly file: "components/ui/segmented.tsx";
1754
+ readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "DisabledProp", "NameProp", "IdProp", "ClassNameProp"];
1755
+ };
1737
1756
  readonly TagInputProp: {
1738
1757
  readonly group: "data-entry";
1739
1758
  readonly file: "components/ui/tag-input.tsx";
@@ -1020,6 +1020,20 @@ const COMPONENT_PROP_REGISTRY = {
1020
1020
  file: "components/layout.prop.ts",
1021
1021
  vocabulary: ["ChildrenProp"]
1022
1022
  },
1023
+ TopbarItemProp: {
1024
+ group: "layout",
1025
+ file: "components/layout.prop.ts",
1026
+ vocabulary: [
1027
+ "ChildrenProp",
1028
+ "ClassNameProp",
1029
+ { field: "asChild", local: true, reason: "Radix Slot passthrough \u2014 merges onto the child" }
1030
+ ]
1031
+ },
1032
+ NavListProp: {
1033
+ group: "layout",
1034
+ file: "components/layout.prop.ts",
1035
+ vocabulary: ["IdProp", "ClassNameProp"]
1036
+ },
1023
1037
  ButtonProp: {
1024
1038
  group: "general",
1025
1039
  file: "components/general.prop.ts",
@@ -1938,6 +1952,19 @@ const COMPONENT_PROP_REGISTRY = {
1938
1952
  file: "components/ui/rating.tsx",
1939
1953
  vocabulary: ["ValueProp", "OnValueChangeProp", "DisabledProp", "ClassNameProp"]
1940
1954
  },
1955
+ SegmentedProp: {
1956
+ group: "data-entry",
1957
+ file: "components/ui/segmented.tsx",
1958
+ vocabulary: [
1959
+ "ValueProp",
1960
+ "DefaultValueProp",
1961
+ "OnValueChangeProp",
1962
+ "DisabledProp",
1963
+ "NameProp",
1964
+ "IdProp",
1965
+ "ClassNameProp"
1966
+ ]
1967
+ },
1941
1968
  TagInputProp: {
1942
1969
  group: "data-entry",
1943
1970
  file: "components/ui/tag-input.tsx",
@@ -62,5 +62,14 @@ export type FlushProp = boolean;
62
62
  * px; a `string` is any CSS length (`"32rem"`, `"90vw"`, `"50%"`).
63
63
  */
64
64
  export type WidthProp = number | string;
65
- /** How a control sizes on the inline axis: fill its column, or hug its label. */
66
- export type ControlWidthProp = "full" | "auto";
65
+ /**
66
+ * How a control sizes on the inline axis: fill its column, hug its label, or sit at a bounded
67
+ * width the theme owns.
68
+ *
69
+ * `bounded` exists because neither of the other two is right for a control whose VALUE varies in
70
+ * length — an organization switcher in a shell top bar is the canonical case. `full` swallows the
71
+ * bar; `auto` makes the bar reflow every time the selected value changes length. `bounded` reads
72
+ * `--control-bounded-width`, so the width is a knob rather than geometry hand-written at the call
73
+ * site (gh#375).
74
+ */
75
+ export type ControlWidthProp = "full" | "auto" | "bounded";