@godxjp/ui 19.4.0 → 19.4.2

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 (65) hide show
  1. package/dist/components/data-display/badge.d.ts +1 -1
  2. package/dist/components/data-display/list-row.d.ts +2 -0
  3. package/dist/components/data-display/list-row.js +16 -12
  4. package/dist/components/data-display/popover.d.ts +1 -1
  5. package/dist/components/data-display/table.d.ts +1 -1
  6. package/dist/components/data-display/timeline-grid.js +5 -3
  7. package/dist/components/data-entry/checkbox.js +1 -1
  8. package/dist/components/data-entry/form-field.js +5 -5
  9. package/dist/components/data-entry/input.js +1 -1
  10. package/dist/components/data-entry/radio.js +1 -1
  11. package/dist/components/data-entry/search-input.d.ts +2 -18
  12. package/dist/components/data-entry/search-input.js +3 -0
  13. package/dist/components/data-entry/select.js +1 -1
  14. package/dist/components/general/button.d.ts +2 -0
  15. package/dist/components/general/button.js +5 -1
  16. package/dist/components/general/inert-background.js +2 -1
  17. package/dist/components/layout/app-shell.js +1 -1
  18. package/dist/components/layout/master-detail.d.ts +1 -1
  19. package/dist/components/layout/master-detail.js +47 -3
  20. package/dist/components/layout/mobile-shell.d.ts +1 -1
  21. package/dist/components/layout/responsive-grid.js +7 -4
  22. package/dist/components/navigation/app-setting-picker.js +10 -1
  23. package/dist/email/tokens.generated.d.ts +1 -1
  24. package/dist/email/tokens.generated.js +1 -1
  25. package/dist/props/components/data-display.prop.d.ts +1 -1
  26. package/dist/props/components/data-entry.prop.d.ts +10 -5
  27. package/dist/props/components/general.prop.d.ts +4 -0
  28. package/dist/props/components/layout.prop.d.ts +8 -2
  29. package/dist/props/registry.d.ts +2 -2
  30. package/dist/props/registry.js +2 -1
  31. package/dist/props/vocabulary/layout.prop.d.ts +1 -1
  32. package/dist/styles/control.css +29 -0
  33. package/dist/styles/data-display-layout.css +22 -1
  34. package/dist/styles/focus-ring.css +9 -0
  35. package/dist/styles/form-layout.css +6 -0
  36. package/dist/styles/layout.css +36 -1
  37. package/dist/styles/navigation-layout.css +2 -1
  38. package/dist/tokens/components/data-display.css +2 -1
  39. package/dist/tokens/foundation.css +2 -1
  40. package/docs/FRAME-A11Y-CI.md +99 -17
  41. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  42. package/docs/data-display/list-row.tsx +21 -0
  43. package/docs/data-display/popover.tsx +4 -1
  44. package/docs/data-entry/date-picker.tsx +2 -1
  45. package/docs/data-entry/form-field/index.tsx +2 -2
  46. package/docs/data-entry/search-input.tsx +1 -0
  47. package/docs/data-entry/select-matrix.tsx +2 -1
  48. package/docs/data-entry/select.tsx +3 -1
  49. package/docs/feedback/dialog.tsx +2 -1
  50. package/docs/feedback/sheet.tsx +2 -1
  51. package/docs/general/button/index.md +4 -0
  52. package/docs/layout/app-shell.tsx +4 -4
  53. package/docs/layout/master-detail.tsx +3 -0
  54. package/docs/layout/responsive-grid.tsx +21 -4
  55. package/docs/layout/sidebar.tsx +3 -3
  56. package/docs/layout/split-pane.tsx +4 -5
  57. package/docs/navigation/context-menu.tsx +6 -1
  58. package/docs/navigation/dropdown-menu.tsx +2 -1
  59. package/docs/navigation/tabs.tsx +2 -2
  60. package/docs/showcase/case6-agency-handy.tsx +2 -2
  61. package/docs/showcase/org-switcher.tsx +1 -1
  62. package/docs/showcase/settings-account-sections.tsx +2 -2
  63. package/docs/showcase/table-master-detail.tsx +1 -1
  64. package/docs/showcase/table-view-tabs.tsx +1 -1
  65. package/package.json +2 -2
@@ -19,7 +19,7 @@ export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
19
19
  * `<div>` would be invalid HTML: inside a `TabsTrigger`/`PopoverTrigger`/`Button` (all render a
20
20
  * `<button>`, whose content model is phrasing content only), inside a `<label>` or a `<p>`.
21
21
  * It swaps the TAG only — the chip's own `inline-flex` box, icon and label are unchanged, and
22
- * both the icon `<svg>` and the label `<span>` are already phrasing content (gh#354).
22
+ * both the icon `<svg>` and the label `<span>` are already phrasing content.
23
23
  */
24
24
  as?: "div" | "span";
25
25
  /** Corner shape — `default` (badge radius) · `pill` (fully rounded) · `sharp` (square). */
@@ -15,6 +15,8 @@ export type ListRowDensity = ListRowDensityProp;
15
15
  export interface ListRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
16
16
  /** Render element — `div` (default) or `li` when the parent is a `<ul>`/`<ol>`. */
17
17
  as?: "div" | "li";
18
+ /** Use the supplied link as the entire row. Do not nest interactive trailing controls. */
19
+ asChild?: boolean;
18
20
  /** Leading slot — a decorative icon or an Avatar. Mark a purely decorative icon `aria-hidden`. */
19
21
  leading?: React.ReactNode;
20
22
  /** Primary line — rendered in medium weight. */
@@ -1,12 +1,15 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
4
5
  import { useTranslation } from "../../i18n/use-translation.js";
5
6
  import { cn } from "../../lib/utils.js";
6
7
  import { Text } from "../general/typography.js";
7
8
  const ListRow = React.forwardRef(
8
9
  ({
9
10
  as = "div",
11
+ asChild = false,
12
+ children,
10
13
  leading,
11
14
  title,
12
15
  description,
@@ -19,9 +22,18 @@ const ListRow = React.forwardRef(
19
22
  ...props
20
23
  }, ref) => {
21
24
  const { t } = useTranslation();
22
- const Comp = as;
25
+ const Comp = asChild ? Slot : as;
23
26
  const truncate = overflow !== "wrap";
24
- return /* @__PURE__ */ jsxs(
27
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
28
+ unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
29
+ leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
30
+ /* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
31
+ /* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
32
+ description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
33
+ ] }),
34
+ trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
35
+ ] });
36
+ return /* @__PURE__ */ jsx(
25
37
  Comp,
26
38
  {
27
39
  ref,
@@ -32,15 +44,7 @@ const ListRow = React.forwardRef(
32
44
  "data-unread": unread === true ? "" : void 0,
33
45
  className: cn("ui-list-row", className),
34
46
  ...props,
35
- children: [
36
- unread !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-indicator", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: unread ? t("dataDisplay.listRow.unread") : t("dataDisplay.listRow.read") }) }) : null,
37
- leading != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-leading", children: leading }) : null,
38
- /* @__PURE__ */ jsxs("span", { "data-slot": "list-row-body", children: [
39
- /* @__PURE__ */ jsx(Text, { weight: "medium", truncate, children: title }),
40
- description != null ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", truncate, children: description }) : null
41
- ] }),
42
- trailing != null ? /* @__PURE__ */ jsx("span", { "data-slot": "list-row-trailing", children: trailing }) : null
43
- ]
47
+ children: asChild && React.isValidElement(children) ? React.cloneElement(children, void 0, content) : content
44
48
  }
45
49
  );
46
50
  }
@@ -9,7 +9,7 @@ export declare function PopoverAnchor(props: React.ComponentProps<typeof Popover
9
9
  * edge and draw its own separators across the full width. The popover drops its own padding by
10
10
  * zeroing `--popover-space-inset` ON THE PANEL, so the inset stays one token (a service that
11
11
  * retunes `--popover-space-inset` still owns every padded popover) and no consumer has to reach
12
- * for a zero-padding utility, which no service theme can reach (gh#354).
12
+ * for a zero-padding utility, which no service theme can reach.
13
13
  */
14
14
  type PopoverContentFlush = {
15
15
  flush?: FlushProp;
@@ -65,7 +65,7 @@ export declare const TableHead: React.ForwardRefExoticComponent<React.ThHTMLAttr
65
65
  /**
66
66
  * The cell's CONTENT owns its inset — an expanded detail panel, a nested table, a full-bleed media
67
67
  * strip. The cell drops its own padding so the child reaches the cell edges; without it the only
68
- * route was a zero-padding utility at the call site, which no service theme can reach (gh#354).
68
+ * route was a zero-padding utility at the call site, which no service theme can reach.
69
69
  */
70
70
  type TableCellFlush = {
71
71
  flush?: FlushProp;
@@ -25,7 +25,9 @@ function resolveEvent(event) {
25
25
  if (startMin === null || endMin === null) return null;
26
26
  return { event, startMin, endMin: endMin > startMin ? endMin : endMin + MINUTES_PER_DAY };
27
27
  }
28
+ const EVENT_MIN_MINUTES = 36;
28
29
  function placeColumn(events) {
30
+ const drawnEnd = (e) => Math.max(e.endMin, e.startMin + EVENT_MIN_MINUTES);
29
31
  const sorted = [...events].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
30
32
  const placed = [];
31
33
  let cluster = [];
@@ -42,8 +44,8 @@ function placeColumn(events) {
42
44
  if (entry.startMin >= clusterEnd) closeCluster();
43
45
  let lane = laneEnds.findIndex((end) => end <= entry.startMin);
44
46
  if (lane === -1) lane = laneEnds.length;
45
- laneEnds[lane] = entry.endMin;
46
- clusterEnd = Math.max(clusterEnd, entry.endMin);
47
+ laneEnds[lane] = drawnEnd(entry);
48
+ clusterEnd = Math.max(clusterEnd, drawnEnd(entry));
47
49
  cluster.push({ ...entry, lane, lanes: 1 });
48
50
  }
49
51
  closeCluster();
@@ -93,7 +95,7 @@ const TimelineGrid = React.forwardRef(
93
95
  const axis = React.useMemo(() => resolveWindow(start, end, resolved), [start, end, resolved]);
94
96
  const span = axis.endMin - axis.startMin;
95
97
  const hours = span / 60;
96
- const step = Number.isFinite(interval) && interval > 0 ? interval : 1;
98
+ const step = Number.isFinite(interval) && interval > 0 ? Math.max(interval, 1 / 60) : 1;
97
99
  const byColumn = React.useMemo(() => {
98
100
  const buckets = columns.map(() => []);
99
101
  for (const entry of resolved) {
@@ -26,7 +26,7 @@ const CheckboxRoot = React.forwardRef(({ className, ...props }, ref) => {
26
26
  // service overriding --checkbox-checked-background got no fill change at all. Radix sets
27
27
  // `data-state`/`data-disabled` on the root, so the CSS rules match. Rendering is unchanged:
28
28
  // both knobs default to exactly the values these utilities hard-coded.
29
- "peer ui-checkbox aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
29
+ "peer ui-checkbox aria-invalid:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none",
30
30
  className
31
31
  ),
32
32
  ...props,
@@ -114,7 +114,7 @@ function FormField({
114
114
  asChild: true,
115
115
  id: labelId,
116
116
  className: "ui-inline-xs text-[length:var(--form-label-font-size)]",
117
- children: /* @__PURE__ */ jsxs(
117
+ children: /* @__PURE__ */ jsx(
118
118
  "span",
119
119
  {
120
120
  onClick: () => {
@@ -123,10 +123,10 @@ function FormField({
123
123
  const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
124
124
  (focusable ?? el).focus();
125
125
  },
126
- children: [
127
- /* @__PURE__ */ jsx("span", { children: label }),
128
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-destructive", children: "*" })
129
- ]
126
+ children: /* @__PURE__ */ jsxs("span", { children: [
127
+ label,
128
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ui-form-field-required", children: "*" })
129
+ ] })
130
130
  }
131
131
  )
132
132
  }
@@ -9,7 +9,7 @@ const inputBaseClass = [
9
9
  "ui-control ui-input border-input bg-background w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
10
10
  "selection:bg-primary selection:text-primary-foreground",
11
11
  "placeholder:text-muted-foreground",
12
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20"
12
+ "aria-invalid:border-destructive"
13
13
  ];
14
14
  const Input = React.forwardRef(
15
15
  ({
@@ -27,7 +27,7 @@ const RadioItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
27
27
  // and reads --disabled-opacity. The utility was layered after components, so it silently
28
28
  // outranked that token — a service theme's --disabled-opacity never reached a radio.
29
29
  // Byte-identical: --disabled-opacity defaults to 0.5.
30
- "ui-radio aria-invalid:border-destructive aria-invalid:ring-destructive/20 shrink-0 shadow-xs transition-shadow outline-none",
30
+ "ui-radio aria-invalid:border-destructive shrink-0 shadow-xs transition-shadow outline-none",
31
31
  className
32
32
  ),
33
33
  ...props,
@@ -1,20 +1,4 @@
1
1
  import * as React from "react";
2
- import { type FieldA11yProps } from "../../lib/field-a11y.js";
3
- interface SearchInputProps extends FieldA11yProps {
4
- value?: string;
5
- defaultValue?: string;
6
- placeholder?: string;
7
- debounce?: number;
8
- /** Fires on EVERY keystroke (immediate) — required to keep a controlled `value` responsive. */
9
- onValueChange?: (q: string) => void;
10
- /** Fires with the DEBOUNCED term. Optional — omit it when you drive filtering off `onValueChange`. */
11
- onSearch?: (q: string) => void;
12
- label?: React.ReactNode;
13
- ariaLabel?: string;
14
- className?: string;
15
- inputClassName?: string;
16
- id?: string;
17
- disabled?: boolean;
18
- }
2
+ import type { SearchInputProp } from "../../props/components/data-entry.prop.js";
3
+ export type SearchInputProps = SearchInputProp;
19
4
  export declare function SearchInput({ value: controlledValue, defaultValue, placeholder, debounce, onValueChange, onSearch, label, ariaLabel, className, inputClassName, id, disabled, ...ariaProps }: SearchInputProps): React.JSX.Element;
20
- export {};
@@ -37,7 +37,10 @@ function SearchInput({
37
37
  React.useEffect(() => {
38
38
  onSearchRef.current = onSearch;
39
39
  });
40
+ const lastSearch = React.useRef(debounced);
40
41
  React.useEffect(() => {
42
+ if (lastSearch.current === debounced) return;
43
+ lastSearch.current = debounced;
41
44
  onSearchRef.current?.(debounced);
42
45
  }, [debounced]);
43
46
  const setValue = (v) => {
@@ -99,7 +99,7 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", widt
99
99
  className: cn(
100
100
  controlTriggerClass,
101
101
  width === "auto" ? "w-auto" : "w-full",
102
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
102
+ "aria-invalid:border-destructive data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
103
103
  className
104
104
  ),
105
105
  ...props,
@@ -10,6 +10,8 @@ export declare const Button: React.ForwardRefExoticComponent<React.ButtonHTMLAtt
10
10
  size?: import("../../props/index.js").ButtonSizeProp;
11
11
  shape?: import("../../props/index.js").ShapeProp;
12
12
  fullWidth?: boolean;
13
+ wrap?: boolean;
14
+ align?: import("../../props/index.js").TextAlignProp;
13
15
  asChild?: import("../../props/index.js").AsChildProp;
14
16
  onClick?: import("../../props/index.js").OnClickProp;
15
17
  disabled?: import("../../props/index.js").DisabledProp;
@@ -47,6 +47,8 @@ const Button = React.forwardRef(
47
47
  size,
48
48
  shape,
49
49
  fullWidth = false,
50
+ wrap = false,
51
+ align = "center",
50
52
  asChild = false,
51
53
  loading = false,
52
54
  loadingText,
@@ -76,13 +78,15 @@ const Button = React.forwardRef(
76
78
  "data-size": size ?? "default",
77
79
  "data-shape": shape ?? "default",
78
80
  "data-full-width": fullWidth ? "" : void 0,
81
+ "data-wrap": wrap ? "" : void 0,
82
+ "data-align": align === "center" ? void 0 : align,
79
83
  "data-loading": isLoading ? "" : void 0,
80
84
  "aria-busy": isLoading || void 0,
81
85
  disabled: isLoading || disabled,
82
86
  type: asChild ? void 0 : type ?? "button",
83
87
  className: cn(
84
88
  fullWidth && "w-full",
85
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
89
+ "aria-invalid:border-destructive",
86
90
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
87
91
  buttonVariants({ variant, size, shape, className })
88
92
  ),
@@ -7,7 +7,8 @@ let mounted = 0;
7
7
  function sync() {
8
8
  for (const el of document.querySelectorAll("[data-aria-hidden]")) {
9
9
  if (owned.has(el) || el.hasAttribute("inert")) continue;
10
- if (el.querySelector(TABBABLE)) {
10
+ if (el.matches("[data-radix-focus-guard]")) continue;
11
+ if (el.matches(TABBABLE) || el.querySelector(TABBABLE)) {
11
12
  el.setAttribute("inert", "");
12
13
  owned.add(el);
13
14
  }
@@ -51,7 +51,7 @@ function AppShell({
51
51
  type: "button",
52
52
  variant: "ghost",
53
53
  size: "sm",
54
- className: "app-mobile-nav-trigger hidden max-[900px]:inline-flex",
54
+ className: "app-mobile-nav-trigger",
55
55
  "aria-label": t("layout.appShell.openNav"),
56
56
  "aria-haspopup": "dialog",
57
57
  children: /* @__PURE__ */ jsx(Menu, { className: "size-[var(--app-shell-mobile-nav-icon-size)]", "aria-hidden": "true" })
@@ -5,4 +5,4 @@ export type MasterDetailProps = MasterDetailProp;
5
5
  * drives. Either way `master` comes first in DOM order, so below the threshold the regions stack
6
6
  * list-then-detail.
7
7
  */
8
- export declare function MasterDetail({ master, children, rail, railWidth, masterViewport, collapseBelow, masterLabel, detailLabel, detailId, }: MasterDetailProps): import("react").JSX.Element;
8
+ export declare function MasterDetail({ master, mobilePane, detailBack, children, rail, railWidth, masterViewport, collapseBelow, masterLabel, detailLabel, detailId, }: MasterDetailProps): import("react").JSX.Element;
@@ -1,6 +1,10 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
4
  function MasterDetail({
3
5
  master,
6
+ mobilePane,
7
+ detailBack,
4
8
  children,
5
9
  rail = "detail",
6
10
  railWidth = "standard",
@@ -10,11 +14,45 @@ function MasterDetail({
10
14
  detailLabel,
11
15
  detailId
12
16
  }) {
17
+ const rootRef = useRef(null);
18
+ const thresholdRef = useRef(null);
19
+ const [compact, setCompact] = useState(false);
20
+ const navigable = mobilePane !== void 0;
21
+ useLayoutEffect(() => {
22
+ if (!navigable || !rootRef.current || !thresholdRef.current) return;
23
+ const root = rootRef.current;
24
+ const threshold = thresholdRef.current;
25
+ const measure = () => setCompact(root.getBoundingClientRect().width < threshold.getBoundingClientRect().width);
26
+ measure();
27
+ const observer = new ResizeObserver(measure);
28
+ observer.observe(root);
29
+ observer.observe(threshold);
30
+ return () => observer.disconnect();
31
+ }, [navigable, collapseBelow]);
32
+ const masterRef = useRef(null);
33
+ const detailRef = useRef(null);
34
+ useEffect(() => {
35
+ if (!mobilePane) return;
36
+ if (mobilePane === "detail" && masterRef.current?.getClientRects().length === 0) {
37
+ const heading = detailRef.current?.querySelector("h1, h2, h3");
38
+ if (heading) {
39
+ heading.tabIndex = -1;
40
+ heading.focus();
41
+ } else {
42
+ detailRef.current?.focus();
43
+ }
44
+ } else if (mobilePane === "master" && detailRef.current?.getClientRects().length === 0) {
45
+ masterRef.current?.querySelector("a[aria-current], a[href]")?.focus({ preventScroll: true });
46
+ }
47
+ }, [mobilePane, detailLabel, compact]);
13
48
  const bounded = masterViewport !== "auto";
14
49
  return /* @__PURE__ */ jsxs(
15
50
  "div",
16
51
  {
52
+ ref: rootRef,
17
53
  className: "ui-master-detail",
54
+ "data-mobile-compact": navigable && compact ? "" : void 0,
55
+ "data-mobile-pane": mobilePane,
18
56
  "data-rail": rail,
19
57
  "data-rail-width": railWidth,
20
58
  "data-master-viewport": masterViewport,
@@ -23,22 +61,28 @@ function MasterDetail({
23
61
  /* @__PURE__ */ jsx(
24
62
  "section",
25
63
  {
64
+ ref: masterRef,
26
65
  className: "ui-master-detail-master",
27
66
  "aria-label": masterLabel,
28
67
  tabIndex: bounded ? 0 : void 0,
29
68
  children: master
30
69
  }
31
70
  ),
32
- /* @__PURE__ */ jsx(
71
+ /* @__PURE__ */ jsxs(
33
72
  "section",
34
73
  {
74
+ ref: detailRef,
35
75
  className: "ui-master-detail-detail",
36
76
  id: detailId,
37
77
  "aria-label": detailLabel,
38
78
  tabIndex: -1,
39
- children
79
+ children: [
80
+ mobilePane && detailBack ? /* @__PURE__ */ jsx("div", { className: "ui-master-detail-back", children: detailBack }) : null,
81
+ children
82
+ ]
40
83
  }
41
- )
84
+ ),
85
+ navigable ? /* @__PURE__ */ jsx("span", { className: "ui-master-detail-measure", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { ref: thresholdRef }) }) : null
42
86
  ]
43
87
  }
44
88
  );
@@ -1,7 +1,7 @@
1
1
  import type { MobileShellProp } from "../../props/components/layout.prop.js";
2
2
  export type { MobileShellProp, MobileShellProp as MobileShellProps, } from "../../props/components/layout.prop.js";
3
3
  /**
4
- * MobileShell — the HANDHELD app shell (gh#354 §6): a status band, an app bar, ONE scroll region,
4
+ * MobileShell — the HANDHELD app shell: a status band, an app bar, ONE scroll region,
5
5
  * a sticky action bar and a bottom tab bar, in that fixed order.
6
6
  *
7
7
  * The fourth root shell. The other three cannot express a phone app: `AppShell` REQUIRES a sidebar
@@ -1,24 +1,27 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  const RESPONSIVE_GRID_PRESET_COLUMNS = {
4
- "pricing-plans": { sm: 3, md: 3, lg: 3 }
4
+ "pricing-plans": { base: 1, sm: 3, md: 3, lg: 3 }
5
5
  };
6
6
  function resolveColumns(columns) {
7
7
  if (typeof columns === "number") {
8
8
  return {
9
+ base: 1,
9
10
  sm: Math.min(columns, 2),
10
11
  md: Math.min(columns, 3),
11
12
  lg: columns
12
13
  };
13
14
  }
14
15
  return {
15
- sm: columns.sm ?? 1,
16
- md: columns.md ?? columns.sm ?? 1,
17
- lg: columns.lg ?? columns.md ?? columns.sm ?? 1
16
+ base: columns.base ?? 1,
17
+ sm: columns.sm ?? columns.base ?? 1,
18
+ md: columns.md ?? columns.sm ?? columns.base ?? 1,
19
+ lg: columns.lg ?? columns.md ?? columns.sm ?? columns.base ?? 1
18
20
  };
19
21
  }
20
22
  function toStyle(resolved) {
21
23
  return {
24
+ "--responsive-grid-base": resolved.base,
22
25
  "--responsive-grid-sm": resolved.sm,
23
26
  "--responsive-grid-md": resolved.md,
24
27
  "--responsive-grid-lg": resolved.lg
@@ -139,7 +139,16 @@ const AppSettingPicker = React.forwardRef(
139
139
  // (`data-[state=open]:border-ring`, from controlTriggerClass) and the
140
140
  // focus-visible ring are untouched, so keyboard and "is this open" affordance
141
141
  // still hold.
142
- "ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground"
142
+ // Bề ngang phải là UTILITY, không phải luật class. SelectTrigger phát `w-full`,
143
+ // mà utility nằm sau components trong thứ tự layer nên `inline-size` khai trong
144
+ // .ui-app-setting-picker-icon luôn thua. Trong một khe co theo nội dung của
145
+ // topbar, `width: 100%` co lại bằng chính nội dung, tức 18px thay vì ô vuông
146
+ // --control-height. Nhánh có nhãn ngay dưới đã phải tự vệ đúng như vậy bằng
147
+ // `w-auto`. Giá trị vẫn đọc token chứ không phải một con số.
148
+ cn(
149
+ "ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground",
150
+ "w-[length:var(--control-height)]"
151
+ )
143
152
  ) : (
144
153
  // Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
145
154
  // A form field that wants a full-width control passes
@@ -29,7 +29,7 @@ export declare const EMAIL_COLOR_SOURCE: {
29
29
  };
30
30
  readonly mutedForeground: {
31
31
  readonly cssVar: "--muted-foreground";
32
- readonly hsl: "44 5% 42%";
32
+ readonly hsl: "44 5% 39%";
33
33
  };
34
34
  readonly border: {
35
35
  readonly cssVar: "--border";
@@ -21,7 +21,7 @@ const EMAIL_COLOR_SOURCE = {
21
21
  },
22
22
  "mutedForeground": {
23
23
  "cssVar": "--muted-foreground",
24
- "hsl": "44 5% 42%"
24
+ "hsl": "44 5% 39%"
25
25
  },
26
26
  "border": {
27
27
  "cssVar": "--border",
@@ -140,7 +140,7 @@ export type BadgeProp = {
140
140
  /**
141
141
  * Render element — `div` (default) or `span` when the chip sits in a phrasing context where a
142
142
  * `<div>` is invalid HTML (inside a `<button>` rendered by TabsTrigger/PopoverTrigger/Button,
143
- * a `<label>`, a `<p>`). Swaps the tag only (gh#354).
143
+ * a `<label>`, a `<p>`). Swaps the tag only.
144
144
  */
145
145
  as?: "div" | "span";
146
146
  variant?: "default" | "secondary" | "outline";
@@ -8,7 +8,7 @@ import type { DateRange } from "react-day-picker";
8
8
  import type * as React from "react";
9
9
  import type { UploadFileItem } from "../../components/data-entry/upload-types.js";
10
10
  import type { FieldA11yProps } from "../../lib/field-a11y.js";
11
- import type { ClassNameProp, DisabledProp, EmptyMessageProp, ErrorBagProp, ErrorProp, HelperProp, IdProp, LabelProp, NameProp, OnChangeProp, OnValueChangeProp, OnSearchChangeProp, OpenProp, OnOpenChangeProp, PlaceholderProp, RequiredProp, ValueProp, DefaultValueProp, FormLayoutProp, WidthProp, BreakpointProp, DensityProp, SizeProp, TitleProp } from "../vocabulary/index.js";
11
+ import type { ClassNameProp, DisabledProp, EmptyMessageProp, ErrorBagProp, ErrorProp, HelperProp, IdProp, LabelProp, NameProp, OnValueChangeProp, OnSearchChangeProp, OpenProp, OnOpenChangeProp, PlaceholderProp, RequiredProp, ValueProp, DefaultValueProp, FormLayoutProp, WidthProp, BreakpointProp, DensityProp, SizeProp, TitleProp } from "../vocabulary/index.js";
12
12
  import type { ResponsiveGridColumnsProp } from "./layout.prop.js";
13
13
  /** One-outline-per-group appearance for the compound InputOTP control. */
14
14
  export type InputOTPGroupAppearanceProp = "slots" | "grouped";
@@ -214,12 +214,17 @@ export type FormErrorsProviderProp = {
214
214
  export type SearchInputProp = FieldA11yProps & {
215
215
  id?: IdProp;
216
216
  label?: LabelProp;
217
+ ariaLabel?: string;
217
218
  placeholder?: PlaceholderProp;
218
- value?: ValueProp;
219
- onChange?: OnChangeProp;
220
- onSearchChange?: OnSearchChangeProp;
221
- debounceMs?: number;
219
+ value?: string;
220
+ defaultValue?: string;
221
+ onValueChange?: (query: string) => void;
222
+ /** Emits changed, debounced queries; does not run for the initial value. */
223
+ onSearch?: (query: string) => void;
224
+ debounce?: number;
225
+ disabled?: DisabledProp;
222
226
  className?: ClassNameProp;
227
+ inputClassName?: ClassNameProp;
223
228
  };
224
229
  /** @see Checkbox — extends Radix checkbox root props. */
225
230
  export type CheckboxProp = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>;
@@ -43,6 +43,10 @@ export type ButtonProp = React.ButtonHTMLAttributes<HTMLButtonElement> & {
43
43
  /** Corner shape — `default` (control radius) · `pill` (fully rounded) · `sharp` (square). */
44
44
  shape?: ShapeProp;
45
45
  fullWidth?: boolean;
46
+ /** Allow a text button to grow vertically for multi-line labels. */
47
+ wrap?: boolean;
48
+ /** Logical content alignment, especially for full-width collection actions. */
49
+ align?: TextAlignProp;
46
50
  asChild?: AsChildProp;
47
51
  onClick?: OnClickProp;
48
52
  disabled?: DisabledProp;
@@ -107,7 +107,7 @@ export type FlexProp = React.HTMLAttributes<HTMLDivElement> & {
107
107
  * `<div>` would be invalid HTML: inside a `TabsTrigger`/`PopoverTrigger`/`Button` (all of which
108
108
  * render a `<button>`, whose content model is phrasing content only), inside a `<label>`, or
109
109
  * inside a `<p>`. Same closed shape as `ListRow`'s `as` — it swaps the TAG, nothing else: the
110
- * `.ui-flex` rules carry `display: flex`, so the box is identical either way (gh#354).
110
+ * `.ui-flex` rules carry `display: flex`, so the box is identical either way.
111
111
  */
112
112
  as?: "div" | "span";
113
113
  direction?: FlexDirectionProp;
@@ -127,7 +127,9 @@ export type FlexProp = React.HTMLAttributes<HTMLDivElement> & {
127
127
  */
128
128
  hideFrom?: BreakpointProp;
129
129
  };
130
+ /** Container column counts; omitted steps inherit from the previous step. Base defaults to 1. */
130
131
  export type ResponsiveGridColumnsProp = number | {
132
+ base?: number;
131
133
  sm?: number;
132
134
  md?: number;
133
135
  lg?: number;
@@ -147,6 +149,10 @@ export type MasterDetailRailProp = "master" | "detail";
147
149
  export type MasterDetailMasterViewportProp = "auto" | "compact" | "standard";
148
150
  /** @see MasterDetail */
149
151
  export type MasterDetailProp = {
152
+ /** Opt into one-pane navigation on mobile (below the token-owned collapse threshold); desktop retains both panes. */
153
+ mobilePane?: "master" | "detail";
154
+ /** Back link shown above detail only in mobile navigation mode. */
155
+ detailBack?: ReactNode;
150
156
  /** Selectable collection; always first in DOM order, so the stacked order stays list-then-detail. */
151
157
  master: ReactNode;
152
158
  /** Detail surface for the current selection. */
@@ -282,7 +288,7 @@ export type AuthShellProp = {
282
288
  * `AppShell` REQUIRES a sidebar (its bar is a grid area beside the nav rail), `AuthShell` is the
283
289
  * UNAUTHENTICATED root and centres a ~24rem card, and `CenteredShell` is a scrolling DOCUMENT —
284
290
  * its `main` scrolls the page, which is exactly what a handheld app must not do. Composing one out
285
- * of `Card` + `ui-card-inset*` (what docs/showcase/case6 did before gh#354) reproduces the look
291
+ * of `Card` + `ui-card-inset*` (what docs/showcase/case6 once did) reproduces the look
286
292
  * and none of the two behaviours that matter on a real device:
287
293
  *
288
294
  * 1. The shell is the only scroll container. The root is exactly one screen tall, so the DOCUMENT
@@ -930,7 +930,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
930
930
  readonly ButtonProp: {
931
931
  readonly group: "general";
932
932
  readonly file: "components/general.prop.ts";
933
- readonly vocabulary: readonly ["ButtonVariantProp", "SizeProp", "ShapeProp", "AsChildProp", "DisabledProp", "OnClickProp", "PendingProp"];
933
+ readonly vocabulary: readonly ["TextAlignProp", "ButtonVariantProp", "SizeProp", "ShapeProp", "AsChildProp", "DisabledProp", "OnClickProp", "PendingProp"];
934
934
  };
935
935
  readonly RevealProp: {
936
936
  readonly group: "general";
@@ -998,7 +998,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
998
998
  readonly SearchInputProp: {
999
999
  readonly group: "data-entry";
1000
1000
  readonly file: "components/data-entry.prop.ts";
1001
- readonly vocabulary: readonly ["LabelProp", "PlaceholderProp", "OnSearchChangeProp"];
1001
+ readonly vocabulary: readonly ["LabelProp", "PlaceholderProp", "DisabledProp", "ClassNameProp", "IdProp"];
1002
1002
  };
1003
1003
  readonly CheckboxProp: {
1004
1004
  readonly group: "data-entry";
@@ -1024,6 +1024,7 @@ const COMPONENT_PROP_REGISTRY = {
1024
1024
  group: "general",
1025
1025
  file: "components/general.prop.ts",
1026
1026
  vocabulary: [
1027
+ "TextAlignProp",
1027
1028
  "ButtonVariantProp",
1028
1029
  "SizeProp",
1029
1030
  "ShapeProp",
@@ -1147,7 +1148,7 @@ const COMPONENT_PROP_REGISTRY = {
1147
1148
  SearchInputProp: {
1148
1149
  group: "data-entry",
1149
1150
  file: "components/data-entry.prop.ts",
1150
- vocabulary: ["LabelProp", "PlaceholderProp", "OnSearchChangeProp"]
1151
+ vocabulary: ["LabelProp", "PlaceholderProp", "DisabledProp", "ClassNameProp", "IdProp"]
1151
1152
  },
1152
1153
  CheckboxProp: {
1153
1154
  group: "data-entry",
@@ -57,7 +57,7 @@ export type AuthShellPresetProp = "default" | "login" | "registration" | "device
57
57
  * `"none"` is a DELIBERATE zero, not the absence of a value: two lines that read as ONE block —
58
58
  * a name over its role, a weekday over its date, a tab bar with no seam between its triggers.
59
59
  * Without it those stacks had to carry `gap="xs"`, which is a visual change forced by a missing
60
- * step rather than by design (gh#354).
60
+ * step rather than by design.
61
61
  */
62
62
  export type GapProp = "none" | "xs" | "sm" | "md" | "lg" | "xl";
63
63
  /** DataTable row density subset. */