@godxjp/ui 28.5.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 (35) hide show
  1. package/dist/components/data-entry/field.d.ts +1 -1
  2. package/dist/components/data-entry/field.js +35 -4
  3. package/dist/components/data-entry/input.d.ts +1 -1
  4. package/dist/components/layout/index.d.ts +1 -1
  5. package/dist/components/layout/nav-list.d.ts +9 -1
  6. package/dist/components/layout/nav-list.js +22 -11
  7. package/dist/components/layout/sidebar.d.ts +16 -2
  8. package/dist/components/layout/sidebar.js +1 -0
  9. package/dist/components/ui/password-input.d.ts +1 -1
  10. package/dist/contracts/measurement.json +1 -1
  11. package/dist/lib/control-styles.d.ts +18 -3
  12. package/dist/lib/control-styles.js +1 -1
  13. package/dist/props/components/data-entry.prop.d.ts +36 -2
  14. package/dist/props/components/layout.prop.d.ts +16 -5
  15. package/dist/props/registry.d.ts +1 -1
  16. package/dist/props/registry.js +8 -1
  17. package/dist/styles/control.css +12 -0
  18. package/dist/styles/data-display-layout.css +25 -2
  19. package/dist/styles/dialog-layout.css +12 -0
  20. package/dist/styles/layout.css +1 -1
  21. package/dist/tokens/components/data-display.css +2 -1
  22. package/dist/tokens/components/feedback.css +2 -0
  23. package/docs/FRAME-COVERAGE-REPORT.md +1 -1
  24. package/docs/data-display/collapsible.tsx +5 -5
  25. package/docs/data-display/descriptions.tsx +188 -109
  26. package/docs/data-display/legend.tsx +17 -4
  27. package/docs/data-display/scroll-area.tsx +75 -5
  28. package/docs/data-display/stat-card.tsx +319 -64
  29. package/docs/data-entry/form.tsx +700 -6
  30. package/docs/data-entry/input.tsx +663 -53
  31. package/docs/data-entry/radio-group.tsx +216 -2
  32. package/docs/data-entry/switch.tsx +210 -4
  33. package/docs/data-entry/upload-crop-dialog.tsx +24 -25
  34. package/docs/layout/nav-list.tsx +556 -35
  35. package/package.json +7 -6
@@ -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
  }
@@ -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.5.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",
@@ -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;
@@ -289,9 +289,32 @@
289
289
  .ui-progress-ring-label {
290
290
  position: relative;
291
291
  color: hsl(var(--foreground));
292
- font-size: var(--progress-ring-label-font-size);
292
+ font-size: var(--progress-ring-label-font-size, calc(var(--progress-ring-size) * 0.28));
293
293
  font-variant-numeric: tabular-nums;
294
- line-height: 1;
294
+
295
+ line-height: var(--line-height-normal);
296
+
297
+ max-inline-size: calc(
298
+ var(--progress-ring-size) - 2 * var(--progress-ring-border-width) - var(--space-1)
299
+ );
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ white-space: nowrap;
303
+ }
304
+
305
+ .ui-disclosure-chevron {
306
+ transition: rotate var(--duration-fast) var(--ease-standard);
307
+ }
308
+
309
+ :is([data-state="open"], [aria-expanded="true"]) > .ui-disclosure-chevron,
310
+ :is([data-state="open"], [aria-expanded="true"]) .ui-disclosure-chevron {
311
+ rotate: 180deg;
312
+ }
313
+
314
+ @media (prefers-reduced-motion: reduce) {
315
+ .ui-disclosure-chevron {
316
+ transition: none;
317
+ }
295
318
  }
296
319
 
297
320
  .ui-legend {
@@ -146,6 +146,18 @@
146
146
  opacity: var(--dialog-close-rest-alpha);
147
147
  }
148
148
 
149
+ :is([data-slot="dialog-close"], [data-slot="sheet-close"])::after {
150
+ content: "";
151
+ position: absolute;
152
+ inset-block-start: 50%;
153
+ inset-inline-start: 50%;
154
+ translate: -50% -50%;
155
+ min-inline-size: var(--dialog-close-size);
156
+ min-block-size: var(--dialog-close-size);
157
+ inline-size: 100%;
158
+ block-size: 100%;
159
+ }
160
+
149
161
  [data-slot="dialog-close"]:hover {
150
162
  opacity: 1;
151
163
  }
@@ -834,7 +834,7 @@
834
834
  .ui-page-header-extra {
835
835
  width: auto;
836
836
  flex-shrink: 1;
837
- min-inline-size: 0;
837
+ min-inline-size: auto;
838
838
  justify-content: flex-end;
839
839
  }
840
840
 
@@ -113,7 +113,8 @@
113
113
  --progress-ring-size: 2.75rem;
114
114
  --progress-ring-size-sm: 2rem;
115
115
  --progress-ring-border-width: var(--stroke-lg);
116
- --progress-ring-label-font-size: var(--font-size-xs);
116
+
117
+ --progress-ring-label-font-size: initial;
117
118
 
118
119
  --legend-gap: 0.875rem;
119
120
  --legend-item-gap: 0.3125rem;
@@ -43,6 +43,8 @@
43
43
 
44
44
  --dialog-close-rest-alpha: 0.7;
45
45
 
46
+ --dialog-close-size: var(--touch-target-min);
47
+
46
48
  --dialog-step-up-error-font-size: var(--font-size-sm);
47
49
 
48
50
  --alert-radius: var(--card-radius);
@@ -173,7 +173,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
173
173
  | Carousel | ✓ | N/A | N/A | · | · | · | · | · | · | · |
174
174
  | AppSettingPicker | ✓ | N/A | · | · | · | · | · | · | · | · |
175
175
  | AppSettingToggle | ✓ | N/A | · | · | · | · | · | · | · | · |
176
- | Field | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
176
+ | Field | ✓ | N/A | · | N/A | · | · | · | · | · | · |
177
177
  | LineChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
178
178
  | BarChart | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
179
179
  | CompactBarTrend | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
@@ -10,7 +10,7 @@ import {
10
10
  } from "@godxjp/ui/data-display";
11
11
  import { Button, Text } from "@godxjp/ui/general";
12
12
  import { Flex, PageContainer } from "@godxjp/ui/layout";
13
- import { ChevronsUpDown } from "lucide-react";
13
+ import { ChevronDown } from "lucide-react";
14
14
  import { useState } from "react";
15
15
 
16
16
  /**
@@ -43,7 +43,7 @@ export default function Demo() {
43
43
  aria-label="開閉"
44
44
  className="transition-transform data-[state=open]:rotate-180"
45
45
  >
46
- <ChevronsUpDown />
46
+ <ChevronDown className="ui-disclosure-chevron" />
47
47
  </Button>
48
48
  </CollapsibleTrigger>
49
49
  </Flex>
@@ -74,7 +74,7 @@ export default function Demo() {
74
74
  aria-label="開閉"
75
75
  className="transition-transform data-[state=open]:rotate-180"
76
76
  >
77
- <ChevronsUpDown />
77
+ <ChevronDown className="ui-disclosure-chevron" />
78
78
  </Button>
79
79
  </CollapsibleTrigger>
80
80
  </Flex>
@@ -112,7 +112,7 @@ export default function Demo() {
112
112
  aria-label="開閉"
113
113
  className="transition-transform data-[state=open]:rotate-180"
114
114
  >
115
- <ChevronsUpDown />
115
+ <ChevronDown className="ui-disclosure-chevron" />
116
116
  </Button>
117
117
  </CollapsibleTrigger>
118
118
  </Flex>
@@ -144,7 +144,7 @@ export default function Demo() {
144
144
  aria-label="開閉"
145
145
  className="transition-transform data-[state=open]:rotate-180"
146
146
  >
147
- <ChevronsUpDown />
147
+ <ChevronDown className="ui-disclosure-chevron" />
148
148
  </Button>
149
149
  </CollapsibleTrigger>
150
150
  </Flex>