@godxjp/ui 18.8.0 → 18.9.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.
@@ -636,9 +636,8 @@ DataTable.Content = function DataTableContent() {
636
636
  ),
637
637
  children: col.render ? col.render(original) : (() => {
638
638
  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";
639
+ const text = v == null || !(typeof v === "string" || typeof v === "number") ? "\u2014" : String(v);
640
+ return /* @__PURE__ */ jsx("span", { "data-slot": "table-cell-text", children: text });
642
641
  })()
643
642
  },
644
643
  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,10 +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: [
83
- /* @__PURE__ */ jsx(CommandInput, { autoFocus: true, placeholder: labels.placeholder, "aria-label": labels.placeholder }),
105
+ /* @__PURE__ */ jsxs(Command, { label: labels.title, shouldFilter, children: [
106
+ /* @__PURE__ */ jsx(
107
+ CommandInput,
108
+ {
109
+ autoFocus: true,
110
+ value: search,
111
+ onValueChange: setSearch,
112
+ placeholder: labels.placeholder,
113
+ "aria-label": labels.placeholder
114
+ }
115
+ ),
84
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: [
85
- /* @__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
+ ),
86
122
  groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsxs(
87
123
  CommandItem,
88
124
  {
@@ -1547,7 +1547,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
1547
1547
  readonly CommandPaletteProp: {
1548
1548
  readonly group: "data-entry";
1549
1549
  readonly file: "components/data-entry/command-palette.tsx";
1550
- readonly vocabulary: readonly ["OpenProp", "DefaultOpenProp", "OnOpenChangeProp"];
1550
+ readonly vocabulary: readonly ["OpenProp", "DefaultOpenProp", "OnOpenChangeProp", "OnSearchChangeProp"];
1551
1551
  };
1552
1552
  readonly TwoFactorSetupProp: {
1553
1553
  readonly group: "feedback";
@@ -1697,7 +1697,7 @@ const COMPONENT_PROP_REGISTRY = {
1697
1697
  CommandPaletteProp: {
1698
1698
  group: "data-entry",
1699
1699
  file: "components/data-entry/command-palette.tsx",
1700
- vocabulary: ["OpenProp", "DefaultOpenProp", "OnOpenChangeProp"]
1700
+ vocabulary: ["OpenProp", "DefaultOpenProp", "OnOpenChangeProp", "OnSearchChangeProp"]
1701
1701
  },
1702
1702
  TwoFactorSetupProp: {
1703
1703
  group: "feedback",
@@ -28,6 +28,34 @@
28
28
  * ───────────────────────────────────────────────────────────────────────── */
29
29
  @import "tailwindcss";
30
30
 
31
+ /* ── THE LAYER CONTRACT (gh#412) ───────────────────────────────────────────
32
+ * `@import "tailwindcss"` establishes `theme, base, components, utilities`. Everything this
33
+ * package styles lives in `@layer components`, which is EARLIER than `utilities` — so a Tailwind
34
+ * utility a component itself emits silently outranks the package rule that is supposed to own the
35
+ * same property, no matter how specific that rule is. That is not a specificity bug you can fix
36
+ * with another selector; layer order beats specificity outright.
37
+ *
38
+ * It shipped as a real defect: `<table class="… text-sm">` (utilities) vs the collection preset's
39
+ * `font-size: var(--table-action-collection-font-size-compact)` (components) — the compact type
40
+ * tier never applied at ANY width, so the documented token was dead and a 5–6 character Japanese
41
+ * label could not fit the narrow-frame column measure (WCAG 2.2 SC 1.4.10, and only ever visible
42
+ * in Japanese). Two independent consumers reported it.
43
+ *
44
+ * `godxjp-ui-responsive` is declared HERE, after Tailwind, so it is the LAST layer and outranks
45
+ * `utilities`. It is reserved for one thing: RESPONSIVE RE-POINTS — the container/media blocks
46
+ * that must win over a component's own static utility for the layout contract to hold. Nothing
47
+ * static belongs in it; a rule that does not sit inside a `@container`/`@media` query goes in
48
+ * `@layer components` like everything else.
49
+ *
50
+ * What this layer canNOT do — and the rule consumers must follow: an UNLAYERED consumer rule
51
+ * outranks EVERY layer, including this one. So app CSS must theme this package through TOKENS on
52
+ * a wrapper, never by writing its own selectors against `[data-slot]` / `[data-priority]`
53
+ * elements. A consumer that does write such a rule must put it in a layer
54
+ * (`@layer components { … }`), or it will silently kill the responsive re-points the package
55
+ * makes — a column measured to 0px and wrapped one character per line is what that looks like.
56
+ * See docs/TOKENS.md · "The layer contract". */
57
+ @layer godxjp-ui-responsive;
58
+
31
59
  /* Per-language font wiring — token-only, NO hardcoded faces. Each locale reads
32
60
  * an OPTIONAL per-lang slot token (empty by default) and falls back to the
33
61
  * base sans. Consumers switch a locale's face by setting its slot, e.g.
@@ -307,7 +307,18 @@
307
307
  --table-action-collection-column-width: var(--table-action-collection-actions-width);
308
308
  text-align: end;
309
309
  }
310
+ }
310
311
 
312
+ /* The compact tier lives in `godxjp-ui-responsive` — the LAST layer, declared after Tailwind in
313
+ * styles/base.css — not in `@layer components`. `<table>` carries `text-sm`, a Tailwind utility,
314
+ * and `utilities` outranks `components` by LAYER ORDER, so a `font-size:` re-point written here as
315
+ * a component rule can never apply however specific it is: that is exactly why
316
+ * `--table-action-collection-font-size-compact` was dead at every width (gh#412), leaving the
317
+ * narrow frame at 14px where a 5–6 character Japanese label overflows its column measure. The
318
+ * column measures move with it so the whole compact tier is decided in ONE place, and so a
319
+ * consumer utility on the table can't half-collapse it either. See styles/base.css · THE LAYER
320
+ * CONTRACT. */
321
+ @layer godxjp-ui-responsive {
311
322
  /* Below the step the SOURCE measures are re-pointed to their compact tier (and the type / row
312
323
  * density with them); nothing else moves. One block per canonical step — a media/container
313
324
  * query cannot read a `var()`, so the tokenized scale is written out (sm 40rem · md 48rem ·
@@ -324,6 +335,10 @@
324
335
  --table-cell-space-x: var(--table-action-collection-cell-space-x-compact);
325
336
  --table-cell-padding-y: var(--table-action-collection-cell-padding-y-compact);
326
337
  font-size: var(--table-action-collection-font-size-compact);
338
+ /* The legibility floor. Default `0` keeps the table fitted to its container exactly as
339
+ * before; a consumer whose column count exceeds the priority budget raises it and the
340
+ * surrounding scroll region takes the overflow instead of the cells. */
341
+ min-inline-size: var(--table-action-collection-min-inline-size-compact);
327
342
  }
328
343
 
329
344
  /* A nowrap child (a Badge pill, a chip) would otherwise bleed into the next column at the
@@ -345,6 +360,10 @@
345
360
  --table-cell-space-x: var(--table-action-collection-cell-space-x-compact);
346
361
  --table-cell-padding-y: var(--table-action-collection-cell-padding-y-compact);
347
362
  font-size: var(--table-action-collection-font-size-compact);
363
+ /* The legibility floor. Default `0` keeps the table fitted to its container exactly as
364
+ * before; a consumer whose column count exceeds the priority budget raises it and the
365
+ * surrounding scroll region takes the overflow instead of the cells. */
366
+ min-inline-size: var(--table-action-collection-min-inline-size-compact);
348
367
  }
349
368
 
350
369
  /* A nowrap child (a Badge pill, a chip) would otherwise bleed into the next column at the
@@ -366,6 +385,10 @@
366
385
  --table-cell-space-x: var(--table-action-collection-cell-space-x-compact);
367
386
  --table-cell-padding-y: var(--table-action-collection-cell-padding-y-compact);
368
387
  font-size: var(--table-action-collection-font-size-compact);
388
+ /* The legibility floor. Default `0` keeps the table fitted to its container exactly as
389
+ * before; a consumer whose column count exceeds the priority budget raises it and the
390
+ * surrounding scroll region takes the overflow instead of the cells. */
391
+ min-inline-size: var(--table-action-collection-min-inline-size-compact);
369
392
  }
370
393
 
371
394
  /* A nowrap child (a Badge pill, a chip) would otherwise bleed into the next column at the
@@ -387,6 +410,10 @@
387
410
  --table-cell-space-x: var(--table-action-collection-cell-space-x-compact);
388
411
  --table-cell-padding-y: var(--table-action-collection-cell-padding-y-compact);
389
412
  font-size: var(--table-action-collection-font-size-compact);
413
+ /* The legibility floor. Default `0` keeps the table fitted to its container exactly as
414
+ * before; a consumer whose column count exceeds the priority budget raises it and the
415
+ * surrounding scroll region takes the overflow instead of the cells. */
416
+ min-inline-size: var(--table-action-collection-min-inline-size-compact);
390
417
  }
391
418
 
392
419
  /* A nowrap child (a Badge pill, a chip) would otherwise bleed into the next column at the
@@ -68,4 +68,25 @@
68
68
  --table-action-collection-cell-space-x-compact: var(--space-2);
69
69
  --table-action-collection-cell-padding-y-compact: var(--space-2);
70
70
  --table-action-collection-row-height-compact: var(--table-row-height-compact);
71
+ /* Compact-tier LEGIBILITY FLOOR — the measure below which the preset stops fitting the table
72
+ * to its container and lets the scroll region it already owns take over.
73
+ *
74
+ * The percentage budget above is sized for ONE column per priority tier plus one free-text
75
+ * column. A queue that repeats a tier — two `secondary` columns, three `meta` columns, or an
76
+ * unmarked column beside them — asks for more than 100%, and under `table-layout: fixed` the
77
+ * surplus is taken out of the columns rather than out of the table: measured on a seven-column
78
+ * Japanese admin queue at 390, every column landed at 44–59px and CJK labels wrapped at ONE to
79
+ * TWO characters per line (dxs-platform/platform#680). That is a WCAG 2.2 SC 1.4.10 Reflow (AA)
80
+ * failure, and it is the failure mode this floor removes.
81
+ *
82
+ * Scrolling is the correct escape, not a concession: SC 1.4.10 exempts "parts of the content
83
+ * which require two-dimensional layout for usage or meaning", and its own note names data
84
+ * tables as the example. A table that scrolls horizontally inside its card conforms; a table
85
+ * whose cells are one character wide does not.
86
+ *
87
+ * Default `0` so nothing changes for any existing consumer: a queue that fits its priority
88
+ * budget keeps fitting. A consumer whose queue carries more columns than the budget sets this
89
+ * to the measure at which its narrowest column is still readable (roughly 5rem per column for
90
+ * Japanese at the compact type tier) and the table scrolls instead of crushing. */
91
+ --table-action-collection-min-inline-size-compact: 0;
71
92
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.8.0",
4
- "godxUiMcp": "18.8.0",
3
+ "version": "18.9.0",
4
+ "godxUiMcp": "18.9.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.29.1",
7
7
  "pnpm": {