@godxjp/ui 18.11.1 → 18.12.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.
@@ -9,6 +9,14 @@ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
9
9
  * avoids a redundant NESTED scroll container and a duplicate keyboard focus stop for one table.
10
10
  */
11
11
  scrollable?: boolean;
12
+ /**
13
+ * Draw the full cell grid (gh#274): a 1px outer frame plus vertical rules between columns
14
+ * (horizontal row rules already come from TableRow). Reach for this whenever the table
15
+ * carries rowSpan/colSpan merged cells — without column rules the merge relationships are
16
+ * unreadable. Colour comes from the `--table-border-color` token (default `--border`).
17
+ * Default `false` emits nothing, keeping the plain table byte-identical.
18
+ */
19
+ bordered?: boolean;
12
20
  /**
13
21
  * Named collection contract. `"default"` (the default) emits no attribute and keeps the plain
14
22
  * table exactly as it is. `"action-collection"` is the canonical dense approval / action queue
@@ -42,6 +50,14 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
42
50
  * avoids a redundant NESTED scroll container and a duplicate keyboard focus stop for one table.
43
51
  */
44
52
  scrollable?: boolean;
53
+ /**
54
+ * Draw the full cell grid (gh#274): a 1px outer frame plus vertical rules between columns
55
+ * (horizontal row rules already come from TableRow). Reach for this whenever the table
56
+ * carries rowSpan/colSpan merged cells — without column rules the merge relationships are
57
+ * unreadable. Colour comes from the `--table-border-color` token (default `--border`).
58
+ * Default `false` emits nothing, keeping the plain table byte-identical.
59
+ */
60
+ bordered?: boolean;
45
61
  /**
46
62
  * Named collection contract. `"default"` (the default) emits no attribute and keeps the plain
47
63
  * table exactly as it is. `"action-collection"` is the canonical dense approval / action queue
@@ -3,7 +3,14 @@ import * as React from "react";
3
3
  import { tableHeadHeightClass } from "../../lib/control-styles.js";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  const Table = React.forwardRef(
6
- ({ className, scrollable = true, preset = "default", collapseBelow = "sm", ...props }, ref) => (
6
+ ({
7
+ className,
8
+ scrollable = true,
9
+ bordered = false,
10
+ preset = "default",
11
+ collapseBelow = "sm",
12
+ ...props
13
+ }, ref) => (
7
14
  // A table wider than its container scrolls horizontally in this wrapper; keep it
8
15
  // keyboard-reachable so it can be scrolled without a pointer (WCAG 2.1.1 / axe
9
16
  // scrollable-region-focusable). No landmark role — avoids landmark-unique collisions.
@@ -26,7 +33,7 @@ const Table = React.forwardRef(
26
33
  {
27
34
  ref,
28
35
  "data-slot": "table",
29
- className: cn("w-full caption-bottom text-sm", className),
36
+ className: cn("w-full caption-bottom text-sm", bordered && "ui-table-bordered", className),
30
37
  ...props
31
38
  }
32
39
  )
@@ -2,11 +2,12 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as SelectPrimitive from "@radix-ui/react-select";
5
- import { ChevronDown, ChevronUp } from "lucide-react";
5
+ import { ChevronDown, ChevronUp, X } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { controlTriggerClass } from "../../lib/control-styles.js";
8
8
  import { mergeAriaIds, omitFieldA11y, pickFieldA11y } from "../../lib/field-a11y.js";
9
9
  import { SearchSelect } from "./search-select.js";
10
+ import { useTranslation } from "../../i18n/use-translation.js";
10
11
  function isDataSelect(props) {
11
12
  return "options" in props || "loadOptions" in props;
12
13
  }
@@ -203,6 +204,7 @@ function DataSelect({
203
204
  const ariaProps = Object.fromEntries(
204
205
  Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
205
206
  );
207
+ const { t } = useTranslation();
206
208
  const searchable = showSearch ?? Boolean(loadOptions);
207
209
  const hasOptions = options.length > 0;
208
210
  if (searchable) {
@@ -255,7 +257,9 @@ function DataSelect({
255
257
  option.value
256
258
  );
257
259
  const isControlled = value !== void 0;
258
- return /* @__PURE__ */ jsxs(
260
+ const canClear = clearable !== false && isControlled && !disabled && !readOnly;
261
+ const showClear = canClear && Boolean(value);
262
+ const select = /* @__PURE__ */ jsxs(
259
263
  SelectPrimitive.Root,
260
264
  {
261
265
  "data-slot": "select",
@@ -268,7 +272,17 @@ function DataSelect({
268
272
  disabled: disabled || !hasOptions,
269
273
  name,
270
274
  children: [
271
- /* @__PURE__ */ jsx(SelectTrigger, { id, "data-testid": dataTestId, className, ...ariaProps, children: /* @__PURE__ */ jsx(SelectValue, { placeholder }) }),
275
+ /* @__PURE__ */ jsx(
276
+ SelectTrigger,
277
+ {
278
+ id,
279
+ "data-testid": dataTestId,
280
+ className: cn(showClear && "pe-9", canClear ? void 0 : className),
281
+ showIndicator: !showClear,
282
+ ...ariaProps,
283
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
284
+ }
285
+ ),
272
286
  /* @__PURE__ */ jsx(SelectContent, { children: groupDataOptions(options).map(
273
287
  (group) => group.heading ? /* @__PURE__ */ jsxs(SelectGroup, { children: [
274
288
  /* @__PURE__ */ jsx(SelectLabel, { children: group.heading }),
@@ -278,6 +292,21 @@ function DataSelect({
278
292
  ]
279
293
  }
280
294
  );
295
+ if (!canClear) return select;
296
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
297
+ select,
298
+ showClear ? /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 end-2 flex items-center", children: /* @__PURE__ */ jsx(
299
+ "button",
300
+ {
301
+ type: "button",
302
+ "aria-label": clearLabel ?? t("dataEntry.searchSelect.clear"),
303
+ "data-testid": dataTestId ? `${dataTestId}-clear` : void 0,
304
+ className: "flex size-6 items-center justify-center rounded-sm opacity-50 hover:opacity-100 focus-visible:opacity-100",
305
+ onClick: () => onValueChange?.("", void 0),
306
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
307
+ }
308
+ ) }) : null
309
+ ] });
281
310
  }
282
311
  export {
283
312
  Select,
@@ -1,7 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
- import type { ResponsiveGridColumnsProp } from "../../props/components/layout.prop.js";
2
+ import type { ResponsiveGridColumnsProp, ResponsiveGridPresetProp } from "../../props/components/layout.prop.js";
3
3
  export type ResponsiveGridProps = {
4
4
  columns?: ResponsiveGridColumnsProp;
5
+ /**
6
+ * Named column geometry for a recognised collection shape — see `ResponsiveGridPresetProp`.
7
+ * Wins over `columns` when both are set, so a caller migrating to a preset does not also need
8
+ * to strip its old `columns` prop.
9
+ */
10
+ preset?: ResponsiveGridPresetProp;
5
11
  children: ReactNode;
6
12
  };
7
- export declare function ResponsiveGrid({ columns, children }: ResponsiveGridProps): import("react").JSX.Element;
13
+ export declare function ResponsiveGrid({ columns, preset, children }: ResponsiveGridProps): import("react").JSX.Element;
@@ -1,20 +1,31 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ const RESPONSIVE_GRID_PRESET_COLUMNS = {
3
+ "pricing-plans": { sm: 3, md: 3, lg: 3 }
4
+ };
2
5
  function resolveColumns(columns) {
3
6
  if (typeof columns === "number") {
4
7
  return {
5
- "--responsive-grid-sm": Math.min(columns, 2),
6
- "--responsive-grid-md": Math.min(columns, 3),
7
- "--responsive-grid-lg": columns
8
+ sm: Math.min(columns, 2),
9
+ md: Math.min(columns, 3),
10
+ lg: columns
8
11
  };
9
12
  }
10
13
  return {
11
- "--responsive-grid-sm": columns.sm ?? 1,
12
- "--responsive-grid-md": columns.md ?? columns.sm ?? 1,
13
- "--responsive-grid-lg": columns.lg ?? columns.md ?? columns.sm ?? 1
14
+ sm: columns.sm ?? 1,
15
+ md: columns.md ?? columns.sm ?? 1,
16
+ lg: columns.lg ?? columns.md ?? columns.sm ?? 1
17
+ };
18
+ }
19
+ function toStyle(resolved) {
20
+ return {
21
+ "--responsive-grid-sm": resolved.sm,
22
+ "--responsive-grid-md": resolved.md,
23
+ "--responsive-grid-lg": resolved.lg
14
24
  };
15
25
  }
16
- function ResponsiveGrid({ columns = 4, children }) {
17
- return /* @__PURE__ */ jsx("div", { className: "ui-responsive-grid-scope", children: /* @__PURE__ */ jsx("div", { className: "ui-responsive-grid", style: resolveColumns(columns), children }) });
26
+ function ResponsiveGrid({ columns = 4, preset, children }) {
27
+ const resolved = preset ? RESPONSIVE_GRID_PRESET_COLUMNS[preset] : resolveColumns(columns);
28
+ return /* @__PURE__ */ jsx("div", { className: "ui-responsive-grid-scope", children: /* @__PURE__ */ jsx("div", { className: "ui-responsive-grid", style: toStyle(resolved), children }) });
18
29
  }
19
30
  export {
20
31
  ResponsiveGrid
@@ -136,6 +136,19 @@ export type ResponsiveGridColumnsProp = number | {
136
136
  md?: number;
137
137
  lg?: number;
138
138
  };
139
+ /**
140
+ * Named, package-owned column geometry for ResponsiveGrid — the semantic alternative to a
141
+ * consumer hand-rolling a `columns={{ sm, md, lg }}` breakpoint map for a recognised collection
142
+ * shape. Takes priority over `columns` when both are set (`columns` is then ignored, not merged).
143
+ *
144
+ * `pricing-plans` — the canonical billing/pricing-plan collection: 1 column until the `lg` step
145
+ * (container ≥ 64rem), then 3 columns from `lg` upward. Because ResponsiveGrid has no step above
146
+ * `lg`, this reads as exactly 3 columns at BOTH the 1024px and 1440px reference widths and 1
147
+ * column at 390px — the 3/3/1 contract requested for the billing plan catalog
148
+ * (dxs-platform/platform#333, tracked upstream at dxs-platform/pkg-ui#14). General-purpose beyond
149
+ * pricing: any 3-up desktop / 1-up mobile collection (no intermediate `md` step) can reuse it.
150
+ */
151
+ export type ResponsiveGridPresetProp = "pricing-plans";
139
152
  export type MasterDetailRailWidthProp = "compact" | "standard";
140
153
  export type MasterDetailRailProp = "master" | "detail";
141
154
  /**
@@ -532,6 +532,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
532
532
  readonly file: "components/layout.prop.ts";
533
533
  readonly vocabulary: readonly [];
534
534
  };
535
+ readonly ResponsiveGridPresetProp: {
536
+ readonly group: "layout";
537
+ readonly file: "components/layout.prop.ts";
538
+ readonly vocabulary: readonly [];
539
+ };
535
540
  readonly MasterDetailRailWidthProp: {
536
541
  readonly group: "layout";
537
542
  readonly file: "components/layout.prop.ts";
@@ -525,6 +525,7 @@ const COMPONENT_PROP_REGISTRY = {
525
525
  ]
526
526
  },
527
527
  ResponsiveGridColumnsProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
528
+ ResponsiveGridPresetProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
528
529
  MasterDetailRailWidthProp: {
529
530
  group: "layout",
530
531
  file: "components/layout.prop.ts",
@@ -78,14 +78,16 @@
78
78
  }
79
79
 
80
80
  /* `.app-main` carries tabindex="0" (axe scrollable-region-focusable, dbad118), so keyboard
81
- * focus can land on the region without a rule here the USER-AGENT default outline (blue
82
- * in Chrome) wraps the whole content area. Use the design-system ring instead; INSET because
83
- * `.app-main` is the shell's clip boundary (`contain: paint`) and an outset shadow would be
84
- * clipped by the grid. Never remove the indicator outright the region is focusable and
85
- * must show focus (WCAG 2.4.7, gh#271). */
81
+ * focus can land on the region. The UA default outline is suppressed and the indicator is
82
+ * token-gated (gh#276): `--region-focus-ring-width` defaults to 0 OFF because a
83
+ * control-strength frame around the whole content area reads as a glitch (`:focus-visible`
84
+ * promotes on any keypress after a click-focus, so mouse users see it constantly). A service
85
+ * that needs the visible keyboard-scroll affordance (WCAG 2.4.7) opts in via the token;
86
+ * INSET because `.app-main` is the shell's clip boundary (`contain: paint`). */
86
87
  .app-main:focus-visible {
87
88
  outline: none;
88
- box-shadow: inset 0 0 0 var(--focus-ring-width) hsl(var(--focus-ring-color, var(--ring)));
89
+ box-shadow: inset 0 0 0 var(--region-focus-ring-width)
90
+ var(--region-focus-ring-color, hsl(var(--focus-ring-color, var(--ring))));
89
91
  }
90
92
 
91
93
  .app-footer {
@@ -281,6 +281,19 @@
281
281
  var(--table-row-selected-background, hsl(var(--muted) / 0.3))
282
282
  );
283
283
  }
284
+
285
+ /* `<Table bordered>` — the full cell grid (gh#274). Horizontal row rules already come from
286
+ * TableRow's own border; here we add the outer frame and the vertical rules between columns,
287
+ * so rowSpan/colSpan merged cells read as merged (a merged group cell floating in whitespace
288
+ * is unreadable). Colour resolves the --table-border-color token at the CALL SITE so a scoped
289
+ * theme override of --border reaches it. */
290
+ .ui-table-bordered {
291
+ border: 1px solid var(--table-border-color, hsl(var(--border)));
292
+ }
293
+
294
+ .ui-table-bordered :is([data-slot="table-head"], [data-slot="table-cell"]):not(:last-child) {
295
+ border-inline-end: 1px solid var(--table-border-color, hsl(var(--border)));
296
+ }
284
297
  }
285
298
 
286
299
  @layer components {
@@ -18,6 +18,12 @@
18
18
  * --table-cell-space-x are density-scaled and re-declared inside a `.ui-density-*` subtree, and a
19
19
  * :root binding would freeze the footer at the :root density (docs/TOKENS.md — the :root freeze
20
20
  * rule). Defaults = var(--space-stack-sm) block · var(--table-cell-space-x) inline. */
21
+ /* Cell-grid rule colour for `<Table bordered>` (gh#274) — the outer frame and the vertical
22
+ * rules between columns. Declared `initial` so the default re-resolves to the LIVE --border
23
+ * role at the call site (a :root binding to hsl(var(--border)) freezes at the :root value and
24
+ * a scoped [data-tenant] override of --border would never reach it — docs/TOKENS.md).
25
+ * Default = hsl(var(--border)). */
26
+ --table-border-color: initial;
21
27
  --table-pagination-padding-y: initial;
22
28
  --table-pagination-padding-x: initial;
23
29
  /* DataTable surface — narrow-viewport legibility FLOOR (gh#253). Below the `sm` viewport step a
@@ -153,6 +153,17 @@
153
153
  * default re-resolves at the element that paints the ring while an explicit override still wins. */
154
154
  --focus-ring-color: initial; /* default = the live --ring role */
155
155
  --focus-ring-width: 2px;
156
+ /* REGION focus ring — the keyboard-scroll affordance on large scroll REGIONS (`.app-main`,
157
+ * gh#271/gh#276), separate from the control ring above. A control-strength 2px brand frame
158
+ * around the entire content area reads as a glitch to mouse-first users (`:focus-visible`
159
+ * promotes on any keypress after a click-focus), so the DEFAULT IS OFF — a deliberate,
160
+ * product-owner tradeoff against WCAG 2.4.7 for keyboard-scroll visibility. A service that
161
+ * needs the visible affordance opts in with ONE token:
162
+ * --region-focus-ring-width: var(--focus-ring-width);
163
+ * `--region-focus-ring-color` is declared `initial` (NOT a :root var binding — the freeze
164
+ * rule) and resolves to the live focus-ring hue at the element that paints it. */
165
+ --region-focus-ring-width: 0;
166
+ --region-focus-ring-color: initial; /* default = hsl(var(--focus-ring-color, var(--ring))) */
156
167
 
157
168
  /* GRADIENTS — opt-in decorative fills exposed at the token layer so a service can paint a hero
158
169
  * banner, a brand wash, or a radial brand glow purely by configuring tokens (cardinal rule #45).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.11.1",
4
- "godxUiMcp": "18.11.1",
3
+ "version": "18.12.2",
4
+ "godxUiMcp": "18.12.2",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -331,14 +331,15 @@
331
331
  "check:final-touch-rtl": "node scripts/check-final-touch-rtl.mjs",
332
332
  "check:frame-runtime": "pnpm check:data-entry-frame-runtime && pnpm check:data-entry-touch-aria && pnpm check:layout-nav-frames && pnpm check:provider-feedback-query-runtime && pnpm check:layout-nav-closure && pnpm check:final-touch-rtl && pnpm check:data-table-pagination-wrap && pnpm check:button-icon-xs",
333
333
  "pretest": "pnpm check:frame-coverage",
334
- "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
335
- "verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
334
+ "verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
335
+ "verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
336
336
  "verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage && pnpm check:frame-axe",
337
337
  "check:mcp-lockstep": "node scripts/check-release-lockstep.mjs",
338
338
  "check:release-plan": "node scripts/check-release-command-plan.mjs",
339
339
  "check:packed-public-contract": "node scripts/check-packed-public-contract.mjs",
340
340
  "check:mcp-pattern-imports": "node scripts/check-mcp-pattern-imports.mjs",
341
341
  "check:mcp-sync": "node scripts/check-mcp-sync.mjs",
342
+ "check:mcp-catalog-coverage": "node scripts/check-mcp-catalog-coverage.mjs",
342
343
  "check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
343
344
  "check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
344
345
  "gen:component-api-manifest": "node scripts/gen-component-api-manifest.mjs",