@godxjp/ui 28.4.0 → 28.6.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 (49) hide show
  1. package/README.md +58 -0
  2. package/dist/app/theme-axes.js +6 -0
  3. package/dist/components/data-entry/field.d.ts +1 -1
  4. package/dist/components/data-entry/field.js +35 -4
  5. package/dist/components/data-entry/index.d.ts +1 -0
  6. package/dist/components/data-entry/index.js +2 -0
  7. package/dist/components/data-entry/input.d.ts +1 -1
  8. package/dist/components/layout/index.d.ts +1 -1
  9. package/dist/components/layout/nav-list.d.ts +9 -1
  10. package/dist/components/layout/nav-list.js +22 -11
  11. package/dist/components/layout/sidebar.d.ts +16 -2
  12. package/dist/components/layout/sidebar.js +1 -0
  13. package/dist/components/ui/password-input.d.ts +1 -1
  14. package/dist/contracts/measurement.json +1 -1
  15. package/dist/lib/control-styles.d.ts +18 -3
  16. package/dist/lib/control-styles.js +1 -1
  17. package/dist/props/components/data-entry.prop.d.ts +36 -2
  18. package/dist/props/components/layout.prop.d.ts +16 -5
  19. package/dist/props/registry.d.ts +1 -1
  20. package/dist/props/registry.js +8 -1
  21. package/dist/styles/base.css +5 -3
  22. package/dist/styles/card-layout.css +11 -6
  23. package/dist/styles/control.css +20 -8
  24. package/dist/styles/data-display-layout.css +51 -20
  25. package/dist/styles/data-entry-layout.css +3 -3
  26. package/dist/styles/dialog-layout.css +12 -0
  27. package/dist/styles/layout.css +1 -1
  28. package/dist/styles/table-layout.css +9 -6
  29. package/dist/styles/text-layout.css +1 -1
  30. package/dist/theme/famgia.service.css +2 -4
  31. package/dist/tokens/components/control.css +4 -3
  32. package/dist/tokens/components/data-display.css +2 -1
  33. package/dist/tokens/components/feedback.css +2 -0
  34. package/dist/tokens/derived.css +20 -0
  35. package/dist/tokens/foundation.css +6 -13
  36. package/docs/CUSTOMER-THEMING.md +82 -3
  37. package/docs/FRAME-COVERAGE-REPORT.md +2 -2
  38. package/docs/data-display/collapsible.tsx +5 -5
  39. package/docs/data-display/descriptions.tsx +188 -109
  40. package/docs/data-display/legend.tsx +17 -4
  41. package/docs/data-display/scroll-area.tsx +75 -5
  42. package/docs/data-display/stat-card.tsx +319 -64
  43. package/docs/data-entry/form.tsx +700 -6
  44. package/docs/data-entry/input.tsx +663 -53
  45. package/docs/data-entry/radio-group.tsx +216 -2
  46. package/docs/data-entry/switch.tsx +210 -4
  47. package/docs/data-entry/upload-crop-dialog.tsx +109 -0
  48. package/docs/layout/nav-list.tsx +556 -35
  49. package/package.json +99 -91
package/README.md CHANGED
@@ -12,6 +12,64 @@ npm i @godxjp/ui
12
12
 
13
13
  ---
14
14
 
15
+ ## Using this from an AI agent
16
+
17
+ The catalog — 165 components, 1616 tokens, 47 cardinal rules — is published in two forms from one
18
+ source. **Entry point for either: [`AGENTS.md`](AGENTS.md).**
19
+
20
+ ### If your agent can run a process
21
+
22
+ Claude Code · Codex CLI · Cursor · any MCP client:
23
+
24
+ ```bash
25
+ npx @godxjp/ui sync-rules
26
+ ```
27
+
28
+ Writes `.mcp.json`, `CLAUDE.md` and `.ai/rules/` into the consumer repo and wires
29
+ `@godxjp/ui-mcp`. Prefer this: it is searchable, it drills down instead of dumping, and it is
30
+ **locked to the version on disk** — the failure it prevents is an agent being told a prop does not
31
+ exist because the catalog was two minors behind (gh#789).
32
+
33
+ ### If your agent can only fetch URLs
34
+
35
+ ChatGPT on the web, Claude.ai, or any client without a local process. The same data is served as
36
+ static files straight from this public repo — no hosting, no deploy step:
37
+
38
+ | file | size | what it is |
39
+ | ----------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
40
+ | [`agent/START-HERE.md`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md) | 6 KB | **read first** — self-contained: the four rules, the token override model, a page that passes review |
41
+ | [`agent/llms.txt`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/llms.txt) | 2 KB | the [llms.txt](https://llmstxt.org/) entry point |
42
+ | [`agent/index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json) | 2 KB | manifest: version, counts, every file's URL |
43
+ | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 42 KB | all 165 as name + group + tagline — **fetch this first** |
44
+ | [`agent/components/<Name>.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json) | 1–32 KB | one file per component, each with its `importPath` — **this is the route to take** |
45
+ | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.1 MB | all 165 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
46
+ | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 0.6 MB | every design token, its value and why it exists |
47
+ | [`agent/vocabulary.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json) | 7 KB | the controlled prop vocabulary |
48
+ | [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) | 17 KB | the 47 cardinal rules |
49
+
50
+ Pasteable bootstrap:
51
+
52
+ > Read https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md and follow it.
53
+ > Then fetch .../agent/components-index.json to choose components, and
54
+ > .../agent/components/<Name>.json for the props of each one you chose. Tell me which catalog
55
+ > version you read.
56
+
57
+ **Pin to the version you installed.** Every URL above tracks `main`. Swap `main` for the matching
58
+ tag — `https://raw.githubusercontent.com/godx-jp/godxjp-ui/v28.4.0/agent/...` — because a catalog
59
+ newer than your package describes props you do not have, an older one hides props you do, and
60
+ neither failure announces itself. Pinned URLs only resolve for releases whose tag contains
61
+ `agent/`; on a 404 the release predates this catalog, so read `main` and compare
62
+ `index.json` → `version` against what you installed.
63
+
64
+ ### Why static files and not a hosted MCP endpoint
65
+
66
+ GitHub serves files; it does not run servers. MCP over the network needs a process handling POST
67
+ requests, which no amount of Pages or raw hosting provides. A hosted HTTP MCP would work and would
68
+ need real hosting, ops and an auth decision — so the static lane ships first, carrying the same
69
+ data at no operational cost. Regenerate both from the one source with `pnpm gen:agent-catalog`.
70
+
71
+ ---
72
+
15
73
  ## Role & boundary — read this first
16
74
 
17
75
  This package is **the single source of UI truth**. It is shared, versioned infrastructure, which means two things are non-negotiable:
@@ -68,6 +68,12 @@ function applyPrimaryColor(root, color, foreground) {
68
68
  "--primary-active": "initial",
69
69
  "--primary-border": "initial",
70
70
  "--control-outline": "initial",
71
+ /* The brand TEXT roles derive from the seed too (gh#664), so they are reset for the same
72
+ * reason: an ancestor theme that pinned a link colour for the previous brand would otherwise
73
+ * outrank the seed just set, and a re-tinted product would keep the old brand's links. */
74
+ "--text-link": "initial",
75
+ "--text-brand": "initial",
76
+ "--text-primary": "initial",
71
77
  "--primary-hover-channels": `var(--primary-hover-${polarity}-channels)`,
72
78
  "--primary-active-channels": `var(--primary-active-${polarity}-channels)`,
73
79
  "--ring": hsl(rgb)
@@ -2,4 +2,4 @@ import * as React from "react";
2
2
  import type { FieldProp } from "../../props/components/data-entry.prop.js";
3
3
  export type { FieldProp, FieldProp as FieldProps } from "../../props/components/data-entry.prop.js";
4
4
  /** Label + optional description beside a checkbox/radio/switch control. */
5
- export declare function Field({ id, label, description, className, children }: FieldProp): React.JSX.Element;
5
+ export declare function Field({ id, label, labelAddon, description, error, className, children, }: FieldProp): React.JSX.Element;
@@ -1,12 +1,43 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
2
4
  import { cn } from "../../lib/utils.js";
5
+ import { mergeAriaIds } from "../../lib/field-a11y.js";
3
6
  import { Label } from "./label.js";
4
- function Field({ id, label, description, className, children }) {
7
+ function Field({
8
+ id,
9
+ label,
10
+ labelAddon,
11
+ description,
12
+ error,
13
+ className,
14
+ children
15
+ }) {
16
+ const descriptionId = description ? `${id}-description` : void 0;
17
+ const errorId = error ? `${id}-error` : void 0;
18
+ const childProps = React.isValidElement(children) ? children.props : void 0;
19
+ const control = React.isValidElement(children) ? React.cloneElement(children, {
20
+ "aria-describedby": mergeAriaIds(
21
+ childProps?.["aria-describedby"],
22
+ descriptionId,
23
+ errorId
24
+ ),
25
+ "aria-errormessage": mergeAriaIds(
26
+ childProps?.["aria-errormessage"],
27
+ errorId
28
+ ),
29
+ "aria-invalid": error ? true : childProps?.["aria-invalid"]
30
+ }) : children;
31
+ const labelNode = /* @__PURE__ */ jsx(Label, { htmlFor: id, className: "ui-choice-label", children: label });
5
32
  return /* @__PURE__ */ jsxs("div", { className: cn("ui-choice-field", className), children: [
6
- /* @__PURE__ */ jsx("div", { className: "ui-choice-control", children }),
33
+ /* @__PURE__ */ jsx("div", { className: "ui-choice-control", children: control }),
7
34
  /* @__PURE__ */ jsxs("div", { className: "ui-choice-content", children: [
8
- /* @__PURE__ */ jsx(Label, { htmlFor: id, className: "ui-choice-label", children: label }),
9
- description ? /* @__PURE__ */ jsx("p", { className: "ui-choice-description", children: description }) : null
35
+ labelAddon != null ? /* @__PURE__ */ jsxs("div", { className: "ui-choice-label-row", children: [
36
+ labelNode,
37
+ labelAddon
38
+ ] }) : labelNode,
39
+ description ? /* @__PURE__ */ jsx("p", { id: descriptionId, className: "ui-choice-description", children: description }) : null,
40
+ error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-choice-error", children: error }) : null
10
41
  ] })
11
42
  ] });
12
43
  }
@@ -42,6 +42,7 @@ export type { SelectProp, SelectProp as SelectProps } from "./select.js";
42
42
  export type { SearchSelectOptionProp as SelectOption, SearchSelectLoadParamsProp as SelectLoadParams, SearchSelectLoadResultProp as SelectLoadResult, } from "./search-select.js";
43
43
  export { UPLOAD_LIST_IGNORE, Upload, collectUploadCommitActions, createUploadItem, useUploadDraft, } from "./upload.js";
44
44
  export type { UploadProps, UploadFileItem, UploadVariant, UploadCommitAction } from "./upload.js";
45
+ export { UploadCropDialog } from "./upload-crop-dialog.js";
45
46
  export { Cascader } from "./cascader.js";
46
47
  export type { CascaderProps, TreeOption, TreeFieldNames } from "./cascader.js";
47
48
  export { TreeSelect, SHOW_CHILD, SHOW_PARENT, SHOW_ALL } from "./tree-select.js";
@@ -39,6 +39,7 @@ import {
39
39
  createUploadItem,
40
40
  useUploadDraft
41
41
  } from "./upload.js";
42
+ import { UploadCropDialog } from "./upload-crop-dialog.js";
42
43
  import { Cascader } from "./cascader.js";
43
44
  import { TreeSelect, SHOW_CHILD, SHOW_PARENT, SHOW_ALL } from "./tree-select.js";
44
45
  import { Transfer } from "./transfer.js";
@@ -127,6 +128,7 @@ export {
127
128
  TreeSelect,
128
129
  UPLOAD_LIST_IGNORE,
129
130
  Upload,
131
+ UploadCropDialog,
130
132
  collectUploadCommitActions,
131
133
  createUploadItem,
132
134
  dateMatchModifiers,
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  export type { InputProp, InputProp as InputProps } from "../../props/components/data-entry.prop.js";
3
3
  export declare const Input: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "prefix" | "size"> & {
4
4
  onValueChange?: (value: string) => void;
5
- size?: "sm" | "md" | "lg";
5
+ size?: "xs" | "sm" | "md" | "lg";
6
6
  status?: import("../../props/index.js").ControlStatusProp;
7
7
  variant?: import("../../props/index.js").ControlVariantProp;
8
8
  allowClear?: import("../../props/index.js").AllowClearProp;
@@ -38,7 +38,7 @@ export type { ErrorSurfaceModeProp, ErrorSurfaceStatusProp } from "../../props/v
38
38
  export { Breadcrumb } from "./breadcrumb.js";
39
39
  export type { BreadcrumbProps } from "./breadcrumb.js";
40
40
  export { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
41
- export type { SidebarItemData, SidebarLinkComponentProp, SidebarLinkProp, SidebarProductProp as SidebarProduct, SidebarProp, SidebarProp as SidebarProps, SidebarRenderItemProp, SidebarSectionProp, } from "../../props/components/layout.prop.js";
41
+ export type { SidebarItemData, SidebarItemProp, SidebarLinkComponentProp, SidebarLinkProp, SidebarProductProp as SidebarProduct, SidebarProp, SidebarProp as SidebarProps, SidebarRenderItemProp, SidebarSectionProp, } from "../../props/components/layout.prop.js";
42
42
  export { Topbar } from "./topbar.js";
43
43
  export type { TopbarProp, TopbarProps } from "./topbar.js";
44
44
  export { TopbarItem } from "./topbar-item.js";
@@ -16,9 +16,17 @@ export type NavListProps = NavListProp;
16
16
  * consumer that already learnt `SidebarItemProp` / `linkComponent` for the rail knows this too.
17
17
  * Only the container differs — a `<nav>` landmark instead of the shell's grid area, and no
18
18
  * collapsed rail (a page-level nav has no rail to collapse into).
19
+ *
20
+ * `item.children` is part of that shared row shape, and it is rendered through the rail's OWN
21
+ * `NavGroup` (gh#815). It used to be dropped: `items` mapped straight to `SidebarItem`, so a
22
+ * nested group type-checked, rendered as a single flat row, and its children vanished with no
23
+ * error and no warning. A grouped settings nav — アカウント / セキュリティ / 通知 / 請求 — is the
24
+ * canonical use of this component, and it took one `NavList` per group wrapped in
25
+ * `SidebarSection`, i.e. N `<nav>` landmarks where the page has one navigation. Now it is one
26
+ * `NavList`, one landmark, and the group opens itself whenever the route lands on a descendant.
19
27
  */
20
28
  export declare const NavList: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> & {
21
- items: import("../../props/index.js").SidebarItemProp[];
29
+ items: import("./sidebar.js").SidebarItemProp[];
22
30
  activeId?: string;
23
31
  label: string;
24
32
  linkComponent?: import("./sidebar.js").SidebarLinkComponentProp;
@@ -2,18 +2,29 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../lib/utils.js";
5
- import { SidebarItem } from "./sidebar.js";
5
+ import { NavGroup, SidebarItem } from "./sidebar.js";
6
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
- )) })
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(
8
+ (item) => item.children && item.children.length > 0 ? /* @__PURE__ */ jsx(
9
+ NavGroup,
10
+ {
11
+ item,
12
+ activeId: activeId ?? "",
13
+ linkComponent,
14
+ onSelect
15
+ },
16
+ item.id
17
+ ) : /* @__PURE__ */ jsx(
18
+ SidebarItem,
19
+ {
20
+ item,
21
+ active: item.id === activeId,
22
+ linkComponent,
23
+ onActivate: onSelect
24
+ },
25
+ item.id
26
+ )
27
+ ) })
17
28
  );
18
29
  NavList.displayName = "NavList";
19
30
  export {
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
- import type { SidebarItemData, SidebarLinkComponentProp, SidebarProp, SidebarRenderItemProp } from "../../props/components/layout.prop.js";
3
- export type { SidebarItemData, SidebarLinkComponentProp, SidebarLinkProp, SidebarProductProp as SidebarProduct, SidebarProp, SidebarProp as SidebarProps, SidebarRenderItemProp, } from "../../props/components/layout.prop.js";
2
+ import type { SidebarItemData, SidebarItemProp, SidebarLinkComponentProp, SidebarProp, SidebarRenderItemProp } from "../../props/components/layout.prop.js";
3
+ export type { SidebarItemData, SidebarItemProp, SidebarLinkComponentProp, SidebarLinkProp, SidebarProductProp as SidebarProduct, SidebarProp, SidebarProp as SidebarProps, SidebarRenderItemProp, } from "../../props/components/layout.prop.js";
4
4
  type RenderItem = (item: SidebarItemData, rowProps: SidebarRenderItemProp) => React.ReactNode;
5
5
  type SidebarHeaderProps = React.HTMLAttributes<HTMLDivElement>;
6
6
  type SidebarSectionProps = {
@@ -28,6 +28,20 @@ type SidebarItemProps = {
28
28
  export declare function SidebarHeader({ children, className, ...props }: SidebarHeaderProps): React.JSX.Element;
29
29
  export declare function SidebarSection({ label, collapsed, children, className, ...props }: SidebarSectionProps & React.HTMLAttributes<HTMLDivElement>): React.JSX.Element;
30
30
  export declare function SidebarItem({ item, active, sub, onActivate, linkComponent: LinkComponent, asChild, renderItem, children, ...props }: SidebarItemProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick">): React.JSX.Element;
31
+ type RowProps = {
32
+ item: SidebarItemProp;
33
+ activeId: string;
34
+ onSelect?: (id: string) => void;
35
+ sub?: boolean;
36
+ linkComponent?: SidebarLinkComponentProp;
37
+ renderItem?: RenderItem;
38
+ };
39
+ /**
40
+ * The collapsible submenu group of a nested `SidebarItemProp`. Exported (not from the package —
41
+ * only within `src/components/layout`) so `NavList` renders nested items through the SAME group as
42
+ * the rail instead of dropping `item.children` on the floor (gh#815).
43
+ */
44
+ export declare function NavGroup({ item, activeId, onSelect, linkComponent, renderItem }: RowProps): React.JSX.Element;
31
45
  export { createSidebarLink } from "./sidebar-link.js";
32
46
  /**
33
47
  * Sidebar — data-driven vertical nav rail. Use {@link createSidebarLink} to adapt a router `Link`,
@@ -396,6 +396,7 @@ function Sidebar({
396
396
  ] });
397
397
  }
398
398
  export {
399
+ NavGroup,
399
400
  Sidebar,
400
401
  SidebarHeader,
401
402
  SidebarItem,
@@ -18,7 +18,7 @@ export type PasswordInputProps = Omit<React.ComponentPropsWithoutRef<typeof Inpu
18
18
  };
19
19
  export declare const PasswordInput: React.ForwardRefExoticComponent<Omit<Omit<Omit<React.InputHTMLAttributes<HTMLInputElement>, "prefix" | "size"> & {
20
20
  onValueChange?: (value: string) => void;
21
- size?: "sm" | "md" | "lg";
21
+ size?: "xs" | "sm" | "md" | "lg";
22
22
  status?: import("../../props/index.js").ControlStatusProp;
23
23
  variant?: import("../../props/index.js").ControlVariantProp;
24
24
  allowClear?: import("../../props/index.js").AllowClearProp;
@@ -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": "28.4.0",
3
+ "version": "28.6.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -30,11 +30,26 @@ export declare const controlOpenRingClass = "ui-control-trigger";
30
30
  * `--control-bounded-width` in gh#375). A control that wants the antd `variant` × `status` matrix
31
31
  * therefore composes THIS class plus `ui-control-surface`, which supplies the identical resting
32
32
  * border and fill from `--control-surface-*`.
33
+ *
34
+ * TRUNCATION HAS TO READ AS TRUNCATION (gh#813). `line-clamp-1` clips the value and — inside the
35
+ * trigger's `whitespace-nowrap` — never reaches a second line, so its own ellipsis never engages:
36
+ * `YYYY-MM-DD(年-月-日)` in a 101px value box showed `YYYY-MM-DD(全`, a format string cut through
37
+ * the middle of a glyph, with nothing to say it had been cut. Two declarations because the two
38
+ * engines ellipsize by different mechanisms, both measured on
39
+ * `/isolate/navigation-app-setting-picker` (dateFormat, ja, 1280×1000, value box 101px / text
40
+ * 160px):
41
+ *
42
+ * text-ellipsis Blink honours `text-overflow` on the clamped box → `YYYY-MM-DD…`.
43
+ * Gecko ignores it there (still `YYYY-MM-DD(全`).
44
+ * whitespace-normal lets `-webkit-line-clamp` clamp a real line, which is the ellipsis Gecko
45
+ * DOES paint → `YYYY-MM-D…` in Firefox, `YYYY-MM-DD…` in Chromium.
46
+ *
47
+ * The clamp keeps it to one line, so a wrappable value cannot grow the control.
33
48
  */
34
- export declare const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1";
35
- export declare const controlTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 border-input bg-background";
49
+ export declare const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis";
50
+ export declare const controlTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis border-input bg-background";
36
51
  /** `controlTriggerBaseClass` + the token-driven surface — the select-family trigger. */
37
- export declare const controlSurfaceTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 ui-control-surface";
52
+ export declare const controlSurfaceTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis ui-control-surface";
38
53
  export declare const controlIconClass = "size-[length:var(--control-height)] shrink-0";
39
54
  export declare const controlIconSmClass = "size-[calc(var(--control-height)-0.5rem)] shrink-0";
40
55
  /** Leading/affix icon inside an input row (search, command) — sized to `--control-icon-size`. */
@@ -2,7 +2,7 @@ const controlMultilineClass = "ui-control-multiline aria-invalid:border-destruct
2
2
  const controlMultilineGhostClass = "ui-control-multiline data-[status=error]:border-destructive data-[status=warning]:border-warning w-full min-h-0 border-0 bg-transparent shadow-none placeholder:text-muted-foreground focus-visible:ring-0";
3
3
  const controlMultilineFilledClass = "ui-control-multiline ui-control--filled aria-invalid:border-destructive data-[status=error]:border-destructive data-[status=warning]:border-warning w-full rounded-[var(--control-radius)] ring-offset-background placeholder:text-muted-foreground";
4
4
  const controlOpenRingClass = "ui-control-trigger";
5
- const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1";
5
+ const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis";
6
6
  const controlTriggerClass = `${controlTriggerBaseClass} border-input bg-background`;
7
7
  const controlSurfaceTriggerClass = `${controlTriggerBaseClass} ui-control-surface`;
8
8
  const controlIconClass = "size-[length:var(--control-height)] shrink-0";
@@ -126,8 +126,18 @@ export type CommandProp = {
126
126
  };
127
127
  export type InputProp = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "prefix"> & {
128
128
  onValueChange?: (value: string) => void;
129
- /** Control height tier: `md` (default), `sm` or `lg` — the same tiers as SelectTrigger. */
130
- size?: "sm" | "md" | "lg";
129
+ /**
130
+ * Control height tier: `md` (default), plus `xs`, `sm` and `lg` — the same tiers as
131
+ * SelectTrigger and NumberInput.
132
+ *
133
+ * `xs` was missing from this union alone. Everything underneath it already worked: `Input`
134
+ * renders `ui-control` and emits `data-size={size}`, and `.ui-control[data-size="xs"]`
135
+ * (src/styles/control.css:1674) binds `--control-height-xs`. That rule was added for the
136
+ * select family — its comment says the token "existed with nothing reading it" — and this
137
+ * union was never widened to match, so the most-used control in the package was the one
138
+ * control missing the bottom rung, for a reason no user could have guessed from behaviour.
139
+ */
140
+ size?: "xs" | "sm" | "md" | "lg";
131
141
  /** Validation state the field paints — antd `status`. `error` also reports `aria-invalid`. */
132
142
  status?: ControlStatusProp;
133
143
  /** Chrome level — antd `variant`. Default `outlined`. */
@@ -547,7 +557,31 @@ export type SwitchProp = Omit<React.ComponentPropsWithoutRef<"button">, "checked
547
557
  export type FieldProp = {
548
558
  id: IdProp;
549
559
  label: LabelProp;
560
+ /**
561
+ * Optional content rendered BESIDE the label and OUTSIDE the `<label>` element — a help
562
+ * button, a Tooltip trigger, a status chip, a short text action. `FormField.labelAddon` with
563
+ * the same semantics, and it has to live outside the label for a reason this row has that a
564
+ * stacked field does not: `Field`'s label is a real `<label htmlFor>` pointing at a
565
+ * checkbox / radio / switch, so a browser forwards a click anywhere inside it to that
566
+ * control. An interactive addon passed through `label` therefore TOGGLES the control when
567
+ * pressed (gh#812). A non-interactive mark (a `Badge as="span"`) is still fine inside
568
+ * `label`; anything pressable belongs here.
569
+ */
570
+ labelAddon?: React.ReactNode;
571
+ /**
572
+ * Muted hint under the label. Announced with the control (`aria-describedby`) rather than
573
+ * left as decoration, and it COMPOSES with `error` — `.ui-choice-content` is already a
574
+ * vertical stack, so the two lines sit under each other exactly as they do on `FormField`.
575
+ */
550
576
  description?: React.ReactNode;
577
+ /**
578
+ * Validation message under the description (`role="alert"`). Presence of a message IS the
579
+ * invalid state: the control is cloned with `aria-invalid`, and the message id reaches it
580
+ * through `aria-errormessage` AND `aria-describedby` (see field.tsx — react-aria's hidden
581
+ * input drops the former). This is the slot that lets a validated boolean stay in `Field`:
582
+ * `FormField` must not wrap a bare `Switch`, so before this there was nowhere to put it.
583
+ */
584
+ error?: ErrorProp;
551
585
  className?: ClassNameProp;
552
586
  children: React.ReactNode;
553
587
  };
@@ -932,11 +932,14 @@ export type SidebarItemProp = {
932
932
  id: string;
933
933
  label: string;
934
934
  /**
935
- * Leading 16px glyph — REQUIRED: the collapsed rail is icon-only and the expanded rail aligns
936
- * every label to the icon column. Its colour is themeable separately from the label via
935
+ * Leading 16px glyph. STRONGLY RECOMMENDED on the rail — the collapsed rail is icon-only, so a
936
+ * row without one reads as a hole there — but OPTIONAL at the type level (gh#815): the runtime
937
+ * has always rendered an empty `.sb-icon` box for a row whose data carries no glyph, precisely
938
+ * so the label column still aligns, and `NavList` (which has no collapsed rail) routinely mixes
939
+ * rows with and without one. Its colour is themeable separately from the label via
937
940
  * `--sidebar-nav-icon-foreground` (see {@link SidebarProp}).
938
941
  */
939
- icon: ComponentType<SVGProps<SVGSVGElement>>;
942
+ icon?: ComponentType<SVGProps<SVGSVGElement>>;
940
943
  /**
941
944
  * Count/status affix rendered in the row's `.sb-badge` pill. Pass the CONTENT ONLY — a number, a
942
945
  * string, `"9+"`.
@@ -968,7 +971,11 @@ export type SidebarItemProp = {
968
971
  * right-click / open-in-new-tab / middle-click all work.
969
972
  */
970
973
  href?: string;
971
- /** Nested rows — renders a collapsible submenu group (the parent reads active when any child is). */
974
+ /**
975
+ * Nested rows — renders a collapsible submenu group (the parent reads active when any child is).
976
+ * Honoured by BOTH `Sidebar` and `NavList` (gh#815): `NavList` used to map `items` straight to a
977
+ * row, so a nested group type-checked, rendered as one flat row and lost its children silently.
978
+ */
972
979
  children?: SidebarItemProp[];
973
980
  };
974
981
  /**
@@ -979,7 +986,11 @@ export type SidebarItemProp = {
979
986
  * learn a second one, and a fix to the row reaches both.
980
987
  */
981
988
  export type NavListProp = Omit<React.HTMLAttributes<HTMLElement>, "onSelect"> & {
982
- /** Rows, in reading order. `icon` is required by `SidebarItemProp` — the label aligns to it. */
989
+ /**
990
+ * Rows, in reading order. An item carrying `children` renders as a collapsible GROUP — the same
991
+ * `.sb-nav-group` the rail draws, opened by the route whenever a descendant is active — so a
992
+ * grouped settings nav is one `NavList` and one `<nav>` landmark, not one per group (gh#815).
993
+ */
983
994
  items: SidebarItemProp[];
984
995
  /** `SidebarItemProp.id` of the current route; that row gets `aria-current="page"`. */
985
996
  activeId?: string;
@@ -1470,7 +1470,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
1470
1470
  readonly FieldProp: {
1471
1471
  readonly group: "data-entry";
1472
1472
  readonly file: "components/data-entry.prop.ts";
1473
- readonly vocabulary: readonly ["IdProp", "LabelProp", "DescriptionProp", "ClassNameProp", "ChildrenProp"];
1473
+ readonly vocabulary: readonly ["IdProp", "LabelProp", "DescriptionProp", "ErrorProp", "ClassNameProp", "ChildrenProp"];
1474
1474
  };
1475
1475
  readonly SliderProp: {
1476
1476
  readonly group: "data-entry";
@@ -1689,7 +1689,14 @@ const COMPONENT_PROP_REGISTRY = {
1689
1689
  FieldProp: {
1690
1690
  group: "data-entry",
1691
1691
  file: "components/data-entry.prop.ts",
1692
- vocabulary: ["IdProp", "LabelProp", "DescriptionProp", "ClassNameProp", "ChildrenProp"]
1692
+ vocabulary: [
1693
+ "IdProp",
1694
+ "LabelProp",
1695
+ "DescriptionProp",
1696
+ "ErrorProp",
1697
+ "ClassNameProp",
1698
+ "ChildrenProp"
1699
+ ]
1693
1700
  },
1694
1701
  SliderProp: {
1695
1702
  group: "data-entry",
@@ -74,7 +74,9 @@
74
74
  --color-attention: hsl(var(--attention));
75
75
  --color-attention-foreground: hsl(var(--attention-foreground));
76
76
 
77
- --color-primary-strong: hsl(var(--text-primary));
77
+ --color-primary-strong: hsl(
78
+ var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels))
79
+ );
78
80
  --color-success-strong: hsl(var(--text-success));
79
81
  --color-warning-strong: hsl(var(--text-warning));
80
82
  --color-info-strong: hsl(var(--text-info));
@@ -97,8 +99,8 @@
97
99
 
98
100
  --color-foreground-tertiary: hsl(var(--text-tertiary));
99
101
  --color-foreground-disabled: hsl(var(--text-disabled));
100
- --color-link: hsl(var(--text-link));
101
- --color-brand: hsl(var(--text-brand));
102
+ --color-link: hsl(var(--text-link, from hsl(var(--primary)) var(--text-link-channels)));
103
+ --color-brand: hsl(var(--text-brand, from hsl(var(--primary)) var(--text-brand-channels)));
102
104
 
103
105
  --color-primary-hover: hsl(
104
106
  var(--primary-hover, from hsl(var(--primary)) var(--primary-hover-channels))
@@ -55,27 +55,32 @@
55
55
  }
56
56
 
57
57
  [data-slot="card"][data-accent="primary"] {
58
- --card-accent-color: hsl(var(--mark-primary));
58
+ --card-accent-color: hsl(
59
+ var(
60
+ --mark-primary,
61
+ var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels))
62
+ )
63
+ );
59
64
  }
60
65
 
61
66
  [data-slot="card"][data-accent="success"] {
62
- --card-accent-color: hsl(var(--mark-success));
67
+ --card-accent-color: hsl(var(--mark-success, var(--text-success)));
63
68
  }
64
69
 
65
70
  [data-slot="card"][data-accent="warning"] {
66
- --card-accent-color: hsl(var(--mark-warning));
71
+ --card-accent-color: hsl(var(--mark-warning, var(--text-warning)));
67
72
  }
68
73
 
69
74
  [data-slot="card"][data-accent="info"] {
70
- --card-accent-color: hsl(var(--mark-info));
75
+ --card-accent-color: hsl(var(--mark-info, var(--text-info)));
71
76
  }
72
77
 
73
78
  [data-slot="card"][data-accent="attention"] {
74
- --card-accent-color: hsl(var(--mark-attention));
79
+ --card-accent-color: hsl(var(--mark-attention, var(--attention)));
75
80
  }
76
81
 
77
82
  [data-slot="card"][data-accent="destructive"] {
78
- --card-accent-color: hsl(var(--mark-destructive));
83
+ --card-accent-color: hsl(var(--mark-destructive, var(--text-error)));
79
84
  }
80
85
 
81
86
  [data-slot="card"][data-accent] {
@@ -435,11 +435,23 @@
435
435
  font-weight: var(--font-weight-normal);
436
436
  }
437
437
 
438
+ .ui-choice-label-row {
439
+ display: flex;
440
+ flex-wrap: wrap;
441
+ align-items: center;
442
+ gap: var(--control-label-space-gap);
443
+ }
444
+
438
445
  .ui-choice-description {
439
446
  color: hsl(var(--muted-foreground));
440
447
  font-size: var(--choice-description-font-size);
441
448
  }
442
449
 
450
+ .ui-choice-error {
451
+ color: hsl(var(--text-error));
452
+ font-size: var(--choice-description-font-size);
453
+ }
454
+
443
455
  .ui-label {
444
456
  display: flex;
445
457
  align-items: center;
@@ -999,7 +1011,7 @@
999
1011
  .ui-otp-container:has(.ui-otp-input[data-status="warning"]) .ui-otp-slot,
1000
1012
  .ui-otp-container:has(.ui-otp-input[data-status="warning"])
1001
1013
  .ui-otp-group[data-appearance="grouped"] {
1002
- border-color: var(--control-status-warning-border-color);
1014
+ border-color: var(--control-status-warning-border-color, hsl(var(--text-warning)));
1003
1015
  }
1004
1016
 
1005
1017
  .ui-otp-container:has(.ui-otp-input[readonly]) .ui-otp-slot {
@@ -1371,10 +1383,10 @@
1371
1383
  }
1372
1384
 
1373
1385
  .ui-control-surface[data-status="warning"] {
1374
- border-color: var(--control-status-warning-border-color);
1386
+ border-color: var(--control-status-warning-border-color, hsl(var(--text-warning)));
1375
1387
  --focus-ring-color: var(--warning);
1376
1388
  --focus-outline-color: var(--warning);
1377
- --focus-ring-glow-color: var(--control-status-warning-outline-color);
1389
+ --focus-ring-glow-color: var(--control-status-warning-outline-color, var(--text-warning));
1378
1390
  }
1379
1391
 
1380
1392
  .ui-number-input[data-size="sm"] {
@@ -2442,7 +2454,7 @@
2442
2454
 
2443
2455
  .ui-calendar .ui-calendar-day.day-today:not(.day-selected) .ui-calendar-day-button {
2444
2456
  box-shadow: inset 0 0 0 1px hsl(var(--primary));
2445
- color: hsl(var(--text-primary));
2457
+ color: hsl(var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels)));
2446
2458
  font-weight: var(--font-weight-medium);
2447
2459
  }
2448
2460
 
@@ -2471,7 +2483,7 @@
2471
2483
 
2472
2484
  .ui-calendar .ui-calendar-day.day-today:not(:has(button)) {
2473
2485
  box-shadow: inset 0 0 0 1px hsl(var(--primary));
2474
- color: hsl(var(--text-primary));
2486
+ color: hsl(var(--text-primary, from hsl(var(--primary)) var(--text-primary-channels)));
2475
2487
  font-weight: var(--font-weight-medium);
2476
2488
  border-radius: var(--calendar-day-radius);
2477
2489
  }
@@ -2806,8 +2818,8 @@
2806
2818
 
2807
2819
  .ui-control[data-status="warning"],
2808
2820
  .ui-control-multiline[data-status="warning"] {
2809
- --focus-ring-color: var(--control-status-warning-outline-color);
2810
- --focus-outline-color: var(--control-status-warning-outline-color);
2821
+ --focus-ring-color: var(--control-status-warning-outline-color, var(--text-warning));
2822
+ --focus-outline-color: var(--control-status-warning-outline-color, var(--text-warning));
2811
2823
  --focus-ring-glow-color: var(--control-status-warning-glow-color);
2812
2824
  --focus-ring-glow-alpha: var(--control-status-warning-glow-alpha);
2813
2825
  }
@@ -2833,7 +2845,7 @@
2833
2845
  }
2834
2846
 
2835
2847
  .ui-control-count[data-exceeded="true"] {
2836
- color: hsl(var(--control-count-exceeded-color));
2848
+ color: hsl(var(--control-count-exceeded-color, var(--text-error)));
2837
2849
  }
2838
2850
 
2839
2851
  .ui-textarea-count {