@godxjp/ui 18.10.0 → 18.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/components/data-display/card.d.ts +27 -3
  2. package/dist/components/data-display/card.js +4 -1
  3. package/dist/components/data-display/data-table.d.ts +4 -1
  4. package/dist/components/data-display/data-table.js +33 -5
  5. package/dist/components/data-entry/command-palette.d.ts +42 -1
  6. package/dist/components/data-entry/command-palette.js +31 -2
  7. package/dist/components/data-entry/input-otp.d.ts +1 -1
  8. package/dist/components/data-entry/select.d.ts +7 -1
  9. package/dist/components/data-entry/select.js +46 -26
  10. package/dist/components/feedback/banner.js +5 -1
  11. package/dist/components/layout/page-container.d.ts +1 -1
  12. package/dist/components/layout/page-container.js +61 -41
  13. package/dist/components/layout/resizable.d.ts +1 -1
  14. package/dist/components/layout/resizable.js +26 -8
  15. package/dist/components/navigation/pagination.js +20 -1
  16. package/dist/components/navigation/steps.d.ts +2 -2
  17. package/dist/components/navigation/steps.js +4 -2
  18. package/dist/components/navigation/tabs.js +6 -1
  19. package/dist/components/ui/avatar.d.ts +1 -1
  20. package/dist/components/ui/avatar.js +2 -1
  21. package/dist/components/ui/input-otp.d.ts +15 -3
  22. package/dist/components/ui/input-otp.js +2 -1
  23. package/dist/email/tokens.generated.d.ts +2 -2
  24. package/dist/email/tokens.generated.js +2 -2
  25. package/dist/i18n/messages/en.json +3 -0
  26. package/dist/i18n/messages/ja.json +3 -0
  27. package/dist/i18n/messages/vi.json +3 -0
  28. package/dist/lib/field-a11y.d.ts +7 -0
  29. package/dist/lib/field-a11y.js +6 -0
  30. package/dist/lib/hooks.d.ts +15 -0
  31. package/dist/lib/hooks.js +37 -0
  32. package/dist/props/components/data-display.prop.d.ts +16 -0
  33. package/dist/props/components/data-entry.prop.d.ts +7 -0
  34. package/dist/props/components/layout.prop.d.ts +8 -0
  35. package/dist/props/components/navigation.prop.d.ts +9 -0
  36. package/dist/props/registry.d.ts +20 -1
  37. package/dist/props/registry.js +21 -1
  38. package/dist/styles/alert-layout.css +1 -1
  39. package/dist/styles/base.css +28 -0
  40. package/dist/styles/card-layout.css +49 -13
  41. package/dist/styles/control.css +17 -2
  42. package/dist/styles/data-display-layout.css +16 -0
  43. package/dist/styles/layout.css +16 -0
  44. package/dist/styles/navigation-layout.css +8 -3
  45. package/dist/styles/shell-layout.css +21 -1
  46. package/dist/styles/table-layout.css +53 -0
  47. package/dist/tokens/components/card.css +12 -0
  48. package/dist/tokens/components/control.css +16 -0
  49. package/dist/tokens/components/data-display.css +12 -0
  50. package/dist/tokens/components/email.css +8 -2
  51. package/dist/tokens/components/feedback.css +24 -0
  52. package/dist/tokens/components/navigation.css +9 -0
  53. package/dist/tokens/components/shell.css +27 -4
  54. package/dist/tokens/components/table.css +21 -0
  55. package/dist/tokens/semantic/layout.css +8 -0
  56. package/package.json +2 -2
@@ -3,9 +3,21 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import type { LucideIcon } from "lucide-react";
4
4
  import type { HeadingLevelProp } from "../../props/vocabulary/index.js";
5
5
  type CardSize = "md" | "compact";
6
- /** Semantic leading-edge accent stripe (border-inline-start; width via the
7
- * --card-accent-rail-width token, default 6px). */
6
+ /** Semantic accent tone. `accentPlacement` decides WHERE it is drawn — a leading-edge
7
+ * stripe (default) or the full perimeter. */
8
8
  type CardAccent = "primary" | "success" | "warning" | "info" | "attention" | "destructive";
9
+ /**
10
+ * Where the semantic `accent` tone is drawn.
11
+ *
12
+ * - `"edge"` (default) — the leading-edge stripe on `border-inline-start` only, at the
13
+ * `--card-accent-rail-width` measure (6px). The classic "this row needs a look" rail.
14
+ * - `"perimeter"` — a full attention border around the WHOLE card in the same semantic
15
+ * tone, measured by `--card-accent-perimeter-width` + `--card-accent-perimeter-ring-width`.
16
+ * This is what `variant="featured"` does, except the tone is yours instead of `--primary`,
17
+ * so a card can shout "action required" (`accent="attention"`) or "this failed"
18
+ * (`accent="destructive"`) without borrowing the brand colour.
19
+ */
20
+ type CardAccentPlacement = "edge" | "perimeter";
9
21
  /** Surface fill — plain card, muted band, borderless outline, or emphasized featured ring. */
10
22
  type CardVariant = "default" | "muted" | "outline" | "featured";
11
23
  /** Padding density — base 16px · tight 12px · cozy 20px. */
@@ -16,6 +28,11 @@ declare const cardVariants: (props?: ({
16
28
  export type CardProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof cardVariants> & {
17
29
  size?: CardSize;
18
30
  accent?: CardAccent;
31
+ /**
32
+ * Where `accent` is drawn — `"edge"` (default, the leading-edge stripe) or `"perimeter"`
33
+ * (a full attention border in the accent tone). Inert without `accent`.
34
+ */
35
+ accentPlacement?: CardAccentPlacement;
19
36
  variant?: CardVariant;
20
37
  density?: CardDensity;
21
38
  };
@@ -24,6 +41,11 @@ export declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<
24
41
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & {
25
42
  size?: CardSize;
26
43
  accent?: CardAccent;
44
+ /**
45
+ * Where `accent` is drawn — `"edge"` (default, the leading-edge stripe) or `"perimeter"`
46
+ * (a full attention border in the accent tone). Inert without `accent`.
47
+ */
48
+ accentPlacement?: CardAccentPlacement;
27
49
  variant?: CardVariant;
28
50
  density?: CardDensity;
29
51
  } & React.RefAttributes<HTMLDivElement>>;
@@ -136,9 +158,11 @@ export type StatCardProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<
136
158
  inverse?: boolean;
137
159
  /** Semantic leading-edge rail (Card accent) — flags a KPI needing attention. */
138
160
  accent?: CardAccent;
161
+ /** Where `accent` is drawn — `"edge"` (default rail) or `"perimeter"` (full attention border). */
162
+ accentPlacement?: CardAccentPlacement;
139
163
  };
140
164
  /** KPI / stat tile — token-driven layout aligned to dashboard KPI cards. */
141
- export declare function StatCard({ label, value, hint, icon: Icon, delta, layout, align, inverse, accent, className, size, ...props }: StatCardProps): React.JSX.Element;
165
+ export declare function StatCard({ label, value, hint, icon: Icon, delta, layout, align, inverse, accent, accentPlacement, className, size, ...props }: StatCardProps): React.JSX.Element;
142
166
  /** Header actions slot — pair with `CardHeader className="flex flex-row …"`. */
143
167
  export declare const CardAction: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
144
168
  export {};
@@ -12,7 +12,7 @@ const cardVariants = cva("group/card", {
12
12
  defaultVariants: { size: "md" }
13
13
  });
14
14
  const Card = React.forwardRef(
15
- ({ className, size = "md", accent, variant, density, ...props }, ref) => /* @__PURE__ */ jsx(
15
+ ({ className, size = "md", accent, accentPlacement, variant, density, ...props }, ref) => /* @__PURE__ */ jsx(
16
16
  "div",
17
17
  {
18
18
  ref,
@@ -20,6 +20,7 @@ const Card = React.forwardRef(
20
20
  "data-slot": "card",
21
21
  "data-size": size === "compact" ? "compact" : void 0,
22
22
  "data-accent": accent,
23
+ "data-accent-placement": accentPlacement === "perimeter" ? "perimeter" : void 0,
23
24
  "data-variant": variant && variant !== "default" ? variant : void 0,
24
25
  "data-density": density,
25
26
  ...props
@@ -108,6 +109,7 @@ function StatCard({
108
109
  align = "start",
109
110
  inverse = false,
110
111
  accent,
112
+ accentPlacement,
111
113
  className,
112
114
  size = "compact",
113
115
  ...props
@@ -118,6 +120,7 @@ function StatCard({
118
120
  {
119
121
  size: size ?? "compact",
120
122
  accent,
123
+ accentPlacement,
121
124
  className: cn("ui-stat-card", className),
122
125
  "data-stat-card": "",
123
126
  "data-stat-layout": layout,
@@ -41,7 +41,10 @@ interface DataTableProps<T> {
41
41
  onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
42
42
  /**
43
43
  * Manual (server) flags. Default `false` so the simple `data`+`columns` case
44
- * sorts / filters / paginates in-browser with no extra wiring. Set the
44
+ * sorts / filters in-browser with no extra wiring. Client pagination slices
45
+ * rows ONLY when engaged — a numbered `<DataTable.Pagination>` child is
46
+ * composed, or `pagination`/`onPaginationChange` is supplied; a plain table
47
+ * with neither renders every row (no silent 10-row cap, gh#270). Set the
45
48
  * relevant flag `true` and drive the matching state from your query for
46
49
  * server-side sort / filter / pagination.
47
50
  */
@@ -178,6 +178,10 @@ function DataTable({
178
178
  }
179
179
  };
180
180
  const tanstackColumns = React.useMemo(() => toTanstackColumns(columns), [columns]);
181
+ const hasNumberedPager = React.Children.toArray(children).some(
182
+ (c) => React.isValidElement(c) && c.type.displayName === "DataTable.Pagination" && typeof c.props.onChange !== "function"
183
+ );
184
+ const paginationEngaged = manualPagination || controlledPagination !== void 0 || onPaginationChange !== void 0 || hasNumberedPager;
181
185
  const table = useReactTable({
182
186
  data,
183
187
  columns: tanstackColumns,
@@ -185,7 +189,7 @@ function DataTable({
185
189
  getCoreRowModel: getCoreRowModel(),
186
190
  ...manualSorting ? {} : { getSortedRowModel: getSortedRowModel() },
187
191
  ...manualFiltering ? {} : { getFilteredRowModel: getFilteredRowModel() },
188
- ...manualPagination ? {} : { getPaginationRowModel: getPaginationRowModel() },
192
+ ...manualPagination || !paginationEngaged ? {} : { getPaginationRowModel: getPaginationRowModel() },
189
193
  manualSorting,
190
194
  manualFiltering,
191
195
  manualPagination,
@@ -419,6 +423,26 @@ DataTable.Content = function DataTableContent() {
419
423
  );
420
424
  }
421
425
  }, [missingHeaderNames]);
426
+ const scrollRef = React.useRef(null);
427
+ const [hasOverflowEnd, setHasOverflowEnd] = React.useState(false);
428
+ React.useEffect(() => {
429
+ const el = scrollRef.current;
430
+ if (!el) return;
431
+ const update = () => {
432
+ const overflowing = el.scrollWidth - el.clientWidth > 1;
433
+ const atEnd = Math.abs(el.scrollLeft) + el.clientWidth >= el.scrollWidth - 1;
434
+ setHasOverflowEnd(overflowing && !atEnd);
435
+ };
436
+ update();
437
+ el.addEventListener("scroll", update, { passive: true });
438
+ const observer = new ResizeObserver(update);
439
+ observer.observe(el);
440
+ if (el.firstElementChild) observer.observe(el.firstElementChild);
441
+ return () => {
442
+ el.removeEventListener("scroll", update);
443
+ observer.disconnect();
444
+ };
445
+ }, []);
422
446
  const activeSort = sort ?? sortingStateToSort(table.getState().sorting);
423
447
  const isControlledSort = sort !== void 0 || !!onSortChange;
424
448
  const onHeaderClick = (col) => {
@@ -440,7 +464,12 @@ DataTable.Content = function DataTableContent() {
440
464
  return /* @__PURE__ */ jsx(
441
465
  "div",
442
466
  {
443
- className: cn("ui-data-table-scroll", hasPinEnd && "ui-data-table-has-pin-end"),
467
+ ref: scrollRef,
468
+ className: cn(
469
+ "ui-data-table-scroll",
470
+ hasPinEnd && "ui-data-table-has-pin-end",
471
+ hasOverflowEnd && "ui-data-table-has-overflow-end"
472
+ ),
444
473
  "aria-busy": loading,
445
474
  tabIndex: presetAttr ? void 0 : 0,
446
475
  children: /* @__PURE__ */ jsx(
@@ -636,9 +665,8 @@ DataTable.Content = function DataTableContent() {
636
665
  ),
637
666
  children: col.render ? col.render(original) : (() => {
638
667
  const v = original[col.key];
639
- if (v == null) return "\u2014";
640
- if (typeof v === "string" || typeof v === "number") return String(v);
641
- return "\u2014";
668
+ const text = v == null || !(typeof v === "string" || typeof v === "number") ? "\u2014" : String(v);
669
+ return /* @__PURE__ */ jsx("span", { "data-slot": "table-cell-text", children: text });
642
670
  })()
643
671
  },
644
672
  col.key
@@ -29,9 +29,50 @@ export type CommandPaletteProps = {
29
29
  open?: boolean;
30
30
  defaultOpen?: boolean;
31
31
  onOpenChange?: (open: boolean) => void;
32
+ /**
33
+ * Controlled search-box query. Pairs with `onSearchChange`; leave it out for the uncontrolled
34
+ * palette (seeded by `defaultSearch`). Same idiom as `SearchSelect`'s `search`.
35
+ */
36
+ search?: string;
37
+ /** Initial uncontrolled query, and the value the palette resets to when it closes. Default `""`. */
38
+ defaultSearch?: string;
39
+ /**
40
+ * Fires on every keystroke with the current query — the seam a server-backed group needs to run
41
+ * search-as-you-type. Also fires with `defaultSearch` when the palette closes.
42
+ */
43
+ onSearchChange?: (query: string) => void;
44
+ /**
45
+ * Whether the palette filters `groups` itself (cmdk's client-side fuzzy match). Set `false` when
46
+ * the QUERY is answered by a server and `groups` already holds the matches — otherwise every row
47
+ * is scored a second time against the same string and late-arriving matches are dropped.
48
+ *
49
+ * It also decides who owns the empty node; see the component doc block. Default `true`.
50
+ */
51
+ shouldFilter?: boolean;
32
52
  loading?: boolean;
33
53
  error?: React.ReactNode;
34
54
  trigger?: React.ReactNode;
35
55
  shortcut?: boolean;
36
56
  };
37
- export declare function CommandPalette({ groups, labels, onSelect, open: controlledOpen, defaultOpen, onOpenChange, loading, error, trigger, shortcut, }: CommandPaletteProps): React.JSX.Element;
57
+ /**
58
+ * CommandPalette — the ⌘K search dialog.
59
+ *
60
+ * ## The empty-state contract (gh#412)
61
+ *
62
+ * `labels.empty` renders under exactly one rule, and which mechanism decides follows `shouldFilter`:
63
+ *
64
+ * - **`shouldFilter` (default, client-side)** — cmdk owns it: the palette holds items, the query
65
+ * matches none of them. Unchanged.
66
+ * - **`shouldFilter={false}` (server-side)** — the PALETTE owns it, derived synchronously from
67
+ * props: the empty node renders when `groups` carries no items, and never while `loading` or
68
+ * `error` is set. cmdk's own empty node cannot be trusted here, because it is driven by a
69
+ * scheduled count of the items that have REGISTERED in the DOM, not by what the consumer knows —
70
+ * so an async group that populates a frame late flips it on and off and any assertion over it
71
+ * races. Reading `groups` instead makes "did we render the empty state?" a pure function of the
72
+ * props at that render, which is what a contract test can assert.
73
+ *
74
+ * The practical consequence for search-as-you-type: hold `loading` true for the whole in-flight
75
+ * window. While it is true the palette shows `labels.loading` and never claims "no results" —
76
+ * a request that has not answered yet is not an empty result.
77
+ */
78
+ export declare function CommandPalette({ groups, labels, onSelect, open: controlledOpen, defaultOpen, onOpenChange, search: controlledSearch, defaultSearch, onSearchChange, shouldFilter, loading, error, trigger, shortcut, }: CommandPaletteProps): React.JSX.Element;
@@ -19,14 +19,20 @@ function CommandPalette({
19
19
  open: controlledOpen,
20
20
  defaultOpen = false,
21
21
  onOpenChange,
22
+ search: controlledSearch,
23
+ defaultSearch = "",
24
+ onSearchChange,
25
+ shouldFilter = true,
22
26
  loading = false,
23
27
  error,
24
28
  trigger,
25
29
  shortcut = true
26
30
  }) {
27
31
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
32
+ const [uncontrolledSearch, setUncontrolledSearch] = React.useState(defaultSearch);
28
33
  const shortcutRestoreFocusRef = React.useRef(null);
29
34
  const open = controlledOpen ?? uncontrolledOpen;
35
+ const search = controlledSearch ?? uncontrolledSearch;
30
36
  const setOpen = React.useCallback(
31
37
  (next) => {
32
38
  if (controlledOpen === void 0) {
@@ -36,6 +42,22 @@ function CommandPalette({
36
42
  },
37
43
  [controlledOpen, onOpenChange]
38
44
  );
45
+ const setSearch = React.useCallback(
46
+ (next) => {
47
+ if (controlledSearch === void 0) {
48
+ setUncontrolledSearch(next);
49
+ }
50
+ onSearchChange?.(next);
51
+ },
52
+ [controlledSearch, onSearchChange]
53
+ );
54
+ const previousOpenRef = React.useRef(open);
55
+ React.useEffect(() => {
56
+ const wasOpen = previousOpenRef.current;
57
+ previousOpenRef.current = open;
58
+ if (!wasOpen || open || search === defaultSearch) return;
59
+ setSearch(defaultSearch);
60
+ }, [open, search, defaultSearch, setSearch]);
39
61
  React.useEffect(() => {
40
62
  if (!shortcut) return;
41
63
  const handleKeyDown = (event) => {
@@ -50,6 +72,7 @@ function CommandPalette({
50
72
  document.addEventListener("keydown", handleKeyDown);
51
73
  return () => document.removeEventListener("keydown", handleKeyDown);
52
74
  }, [open, setOpen, shortcut]);
75
+ const hasItems = React.useMemo(() => groups.some((group) => group.items.length > 0), [groups]);
53
76
  const triggerNode = trigger ?? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ui-command-palette-trigger", children: [
54
77
  /* @__PURE__ */ jsx(Search, { "aria-hidden": "true" }),
55
78
  /* @__PURE__ */ jsx("span", { children: labels.open }),
@@ -79,17 +102,23 @@ function CommandPalette({
79
102
  children: [
80
103
  /* @__PURE__ */ jsx(Dialog.Title, { className: "sr-only", children: labels.title }),
81
104
  /* @__PURE__ */ jsx(Dialog.Description, { id: "ui-command-palette-description", className: "sr-only", children: labels.description }),
82
- /* @__PURE__ */ jsxs(Command, { label: labels.title, children: [
105
+ /* @__PURE__ */ jsxs(Command, { label: labels.title, shouldFilter, children: [
83
106
  /* @__PURE__ */ jsx(
84
107
  CommandInput,
85
108
  {
86
109
  autoFocus: true,
110
+ value: search,
111
+ onValueChange: setSearch,
87
112
  placeholder: labels.placeholder,
88
113
  "aria-label": labels.placeholder
89
114
  }
90
115
  ),
91
116
  /* @__PURE__ */ jsx(CommandList, { "aria-busy": loading, children: loading ? /* @__PURE__ */ jsx("div", { className: "ui-command-palette-state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsx("div", { className: "ui-command-palette-state", role: "alert", children: error }) : /* @__PURE__ */ jsxs(Fragment, { children: [
92
- /* @__PURE__ */ jsx(CommandEmpty, { children: labels.empty }),
117
+ shouldFilter ? /* @__PURE__ */ jsx(CommandEmpty, { children: labels.empty }) : hasItems ? null : (
118
+ // Same node cmdk would have rendered (class + role), decided from `groups`
119
+ // instead of from its scheduled DOM-registration count — see the doc block.
120
+ /* @__PURE__ */ jsx("div", { className: "ui-command-empty", role: "presentation", children: labels.empty })
121
+ ),
93
122
  groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsxs(
94
123
  CommandItem,
95
124
  {
@@ -1,2 +1,2 @@
1
1
  export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "../ui/input-otp.js";
2
- export type { InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProps, } from "../ui/input-otp.js";
2
+ export type { InputOTPAlignProp, InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProps, } from "../ui/input-otp.js";
@@ -1,7 +1,13 @@
1
1
  import * as React from "react";
2
2
  import * as SelectPrimitive from "@radix-ui/react-select";
3
+ import type { FieldA11yProps } from "../../lib/field-a11y.js";
3
4
  import type { SelectDataProp } from "../../props/components/data-entry.prop.js";
4
- export type SelectProp = SelectDataProp | React.ComponentProps<typeof SelectPrimitive.Root>;
5
+ /** Compound-API props: the Radix root's own props PLUS the FormField field-a11y contract, which
6
+ * this component re-routes to the trigger (see {@link SelectFieldA11yContext}). */
7
+ export type SelectCompoundProp = React.ComponentProps<typeof SelectPrimitive.Root> & FieldA11yProps & {
8
+ id?: string;
9
+ };
10
+ export type SelectProp = SelectDataProp | SelectCompoundProp;
5
11
  /**
6
12
  * Select — one component for every single-select. Use the compound API for full control
7
13
  * (`<Select><SelectTrigger/><SelectContent><SelectItem/></SelectContent></Select>`), OR the
@@ -5,15 +5,22 @@ import * as SelectPrimitive from "@radix-ui/react-select";
5
5
  import { ChevronDown, ChevronUp } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { controlTriggerClass } from "../../lib/control-styles.js";
8
+ import { mergeAriaIds, omitFieldA11y, pickFieldA11y } from "../../lib/field-a11y.js";
8
9
  import { SearchSelect } from "./search-select.js";
9
10
  function isDataSelect(props) {
10
11
  return "options" in props || "loadOptions" in props;
11
12
  }
13
+ const SelectFieldA11yContext = React.createContext(null);
12
14
  function Select(props) {
13
15
  if (isDataSelect(props)) {
14
16
  return /* @__PURE__ */ jsx(DataSelect, { ...props });
15
17
  }
16
- return /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
18
+ return /* @__PURE__ */ jsx(CompoundSelect, { ...props });
19
+ }
20
+ function CompoundSelect({ id, ...props }) {
21
+ const fieldA11y = pickFieldA11y(props);
22
+ const rootProps = omitFieldA11y(props);
23
+ return /* @__PURE__ */ jsx(SelectFieldA11yContext.Provider, { value: { ...fieldA11y, id }, children: /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...rootProps }) });
17
24
  }
18
25
  function SelectGroup(props) {
19
26
  return /* @__PURE__ */ jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
@@ -21,31 +28,44 @@ function SelectGroup(props) {
21
28
  function SelectValue(props) {
22
29
  return /* @__PURE__ */ jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
23
30
  }
24
- const SelectTrigger = React.forwardRef(({ className, children, size = "md", showIndicator = true, ...props }, ref) => /* @__PURE__ */ jsxs(
25
- SelectPrimitive.Trigger,
26
- {
27
- ref,
28
- "data-slot": "select-trigger",
29
- "data-size": size,
30
- className: cn(
31
- controlTriggerClass,
32
- "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground w-full gap-2 whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
33
- className
34
- ),
35
- ...props,
36
- children: [
37
- children,
38
- showIndicator ? /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
39
- ChevronDown,
40
- {
41
- "data-slot": "select-chevron",
42
- className: "size-4 shrink-0 opacity-50",
43
- "aria-hidden": "true"
44
- }
45
- ) }) : null
46
- ]
47
- }
48
- ));
31
+ const SelectTrigger = React.forwardRef(({ className, children, size = "md", showIndicator = true, ...props }, ref) => {
32
+ const field = React.useContext(SelectFieldA11yContext);
33
+ const ownsName = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
34
+ const fieldA11y = field ? {
35
+ id: props.id ?? field.id,
36
+ ...ownsName ? {} : { "aria-label": field["aria-label"], "aria-labelledby": field["aria-labelledby"] },
37
+ "aria-describedby": mergeAriaIds(props["aria-describedby"], field["aria-describedby"]),
38
+ "aria-errormessage": mergeAriaIds(props["aria-errormessage"], field["aria-errormessage"]),
39
+ "aria-required": props["aria-required"] ?? field["aria-required"],
40
+ "aria-invalid": props["aria-invalid"] ?? field["aria-invalid"]
41
+ } : void 0;
42
+ return /* @__PURE__ */ jsxs(
43
+ SelectPrimitive.Trigger,
44
+ {
45
+ ref,
46
+ "data-slot": "select-trigger",
47
+ "data-size": size,
48
+ className: cn(
49
+ controlTriggerClass,
50
+ "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground w-full gap-2 whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
51
+ className
52
+ ),
53
+ ...props,
54
+ ...fieldA11y,
55
+ children: [
56
+ children,
57
+ showIndicator ? /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
58
+ ChevronDown,
59
+ {
60
+ "data-slot": "select-chevron",
61
+ className: "size-4 shrink-0 opacity-50",
62
+ "aria-hidden": "true"
63
+ }
64
+ ) }) : null
65
+ ]
66
+ }
67
+ );
68
+ });
49
69
  SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
50
70
  const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
51
71
  SelectPrimitive.ScrollUpButton,
@@ -2,7 +2,11 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { AlertActions, AlertBase, AlertContent, AlertDescription, AlertTitle } from "./alert.js";
5
- const BannerBase = React.forwardRef((props, ref) => /* @__PURE__ */ jsx(AlertBase, { ref, variant: "banner", ...props }));
5
+ const BannerBase = React.forwardRef((props, ref) => (
6
+ // `variant` sits AFTER the spread: the type already excludes it, and the runtime half of the
7
+ // same guarantee means a spread of leftover Alert props can never silently downgrade the strip.
8
+ /* @__PURE__ */ jsx(AlertBase, { ref, ...props, variant: "banner" })
9
+ ));
6
10
  BannerBase.displayName = "Banner";
7
11
  const Banner = Object.assign(BannerBase, {
8
12
  Title: AlertTitle,
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
2
2
  export type { PageContainerProp, PageContainerProp as PageContainerProps, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
- declare function PageContainerRoot({ title, subtitle, status, extra, footer, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, density, variant, preset, headerLayout, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
5
+ declare function PageContainerRoot({ title, subtitle, status, extra, footer, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, density, variant, preset, headerLayout, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
6
6
  export declare const PageContainer: typeof PageContainerRoot & {
7
7
  Inset: typeof PageContainerInset;
8
8
  };
@@ -43,6 +43,7 @@ function PageContainerRoot({
43
43
  breadcrumbLabel,
44
44
  breadcrumbAriaLabel,
45
45
  linkComponent: LinkComponent = "a",
46
+ headerLoading = false,
46
47
  density,
47
48
  variant = "default",
48
49
  preset = "default",
@@ -75,47 +76,66 @@ function PageContainerRoot({
75
76
  className
76
77
  ),
77
78
  children: [
78
- /* @__PURE__ */ jsxs("header", { ref: headerRef, className: "ui-page-header", "data-layout": headerLayout, children: [
79
- breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx(
80
- "nav",
81
- {
82
- "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
83
- className: "ui-breadcrumb",
84
- children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
85
- const isLast = i === breadcrumb.length - 1;
86
- return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
87
- item.to && !isLast ? /* @__PURE__ */ jsx(
88
- LinkComponent,
89
- {
90
- href: item.to,
91
- to: item.to,
92
- className: "hover:text-foreground hover:underline",
93
- children: item.label
94
- }
95
- ) : /* @__PURE__ */ jsx(
96
- "span",
97
- {
98
- className: isLast ? "text-foreground" : "",
99
- "aria-current": isLast ? "page" : void 0,
100
- children: item.label
101
- }
102
- ),
103
- !isLast && /* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": "true" })
104
- ] }, i);
105
- }) })
106
- }
107
- ),
108
- /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
109
- /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading min-w-0", children: [
110
- status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
111
- /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
112
- /* @__PURE__ */ jsx("div", { className: "ui-page-header-status", children: status })
113
- ] }) : /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
114
- subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
115
- ] }),
116
- extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
117
- ] })
118
- ] }),
79
+ /* @__PURE__ */ jsxs(
80
+ "header",
81
+ {
82
+ ref: headerRef,
83
+ className: "ui-page-header",
84
+ "data-layout": headerLayout,
85
+ "aria-busy": headerLoading ? "true" : void 0,
86
+ children: [
87
+ breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx(
88
+ "nav",
89
+ {
90
+ "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
91
+ className: "ui-breadcrumb",
92
+ children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
93
+ const isLast = i === breadcrumb.length - 1;
94
+ return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
95
+ item.to && !isLast ? /* @__PURE__ */ jsx(
96
+ LinkComponent,
97
+ {
98
+ href: item.to,
99
+ to: item.to,
100
+ className: "hover:text-foreground hover:underline",
101
+ children: item.label
102
+ }
103
+ ) : /* @__PURE__ */ jsx(
104
+ "span",
105
+ {
106
+ className: isLast ? "text-foreground" : "",
107
+ "aria-current": isLast ? "page" : void 0,
108
+ children: item.label
109
+ }
110
+ ),
111
+ !isLast && /* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": "true" })
112
+ ] }, i);
113
+ }) })
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
117
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading min-w-0", children: [
118
+ headerLoading ? /* @__PURE__ */ jsx("h1", { className: "ui-page-title ui-skeleton-block ui-page-title-placeholder", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("layout.pageHeader.loading") }) }) : status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
119
+ /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
120
+ /* @__PURE__ */ jsx("div", { className: "ui-page-header-status", children: status })
121
+ ] }) : /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
122
+ headerLoading ? (
123
+ // Decorative only — the pending state is already announced once by the heading above,
124
+ // so a second live placeholder here would double-announce it.
125
+ /* @__PURE__ */ jsx(
126
+ "p",
127
+ {
128
+ className: "ui-page-subtitle ui-skeleton-block ui-page-subtitle-placeholder",
129
+ "aria-hidden": "true"
130
+ }
131
+ )
132
+ ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
133
+ ] }),
134
+ extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
135
+ ] })
136
+ ]
137
+ }
138
+ ),
119
139
  children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
120
140
  footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", children: footer })
121
141
  ]
@@ -5,7 +5,7 @@ export declare const ResizablePanelGroup: {
5
5
  displayName: string;
6
6
  };
7
7
  export declare const ResizablePanel: {
8
- ({ className, ...props }: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Panel>): React.JSX.Element;
8
+ ({ className, elementRef, ...props }: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Panel>): React.JSX.Element;
9
9
  displayName: string;
10
10
  };
11
11
  export declare const ResizableHandle: {
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import * as ResizablePrimitive from "react-resizable-panels";
4
5
  import { cn } from "../../lib/utils.js";
6
+ import { useScrollableRegionTabIndex } from "../../lib/hooks.js";
5
7
  const ResizablePanelGroup = ({
6
8
  className,
7
9
  ...props
@@ -16,15 +18,31 @@ const ResizablePanelGroup = ({
16
18
  ResizablePanelGroup.displayName = "ResizablePanelGroup";
17
19
  const ResizablePanel = ({
18
20
  className,
21
+ elementRef,
19
22
  ...props
20
- }) => /* @__PURE__ */ jsx(
21
- ResizablePrimitive.Panel,
22
- {
23
- "data-slot": "resizable-panel",
24
- className: cn("ui-resizable-panel", className),
25
- ...props
26
- }
27
- );
23
+ }) => {
24
+ const [scrollRegion, setScrollRegion] = React.useState(null);
25
+ const setRootElement = React.useCallback(
26
+ (node) => {
27
+ setScrollRegion(
28
+ node?.firstElementChild instanceof HTMLElement ? node.firstElementChild : null
29
+ );
30
+ if (typeof elementRef === "function") elementRef(node);
31
+ else if (elementRef) elementRef.current = node;
32
+ },
33
+ [elementRef]
34
+ );
35
+ useScrollableRegionTabIndex(scrollRegion);
36
+ return /* @__PURE__ */ jsx(
37
+ ResizablePrimitive.Panel,
38
+ {
39
+ "data-slot": "resizable-panel",
40
+ className: cn("ui-resizable-panel", className),
41
+ elementRef: setRootElement,
42
+ ...props
43
+ }
44
+ );
45
+ };
28
46
  ResizablePanel.displayName = "ResizablePanel";
29
47
  const ResizableHandle = ({
30
48
  className,