@godxjp/ui 23.4.4 → 23.4.6

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.
@@ -73,6 +73,7 @@ import {
73
73
  tableCellPaddingClass,
74
74
  tableRowHeightClass
75
75
  } from "../../lib/control-styles.js";
76
+ import { isDevelopment } from "../../lib/dev.js";
76
77
  function resolveColumnHideBelow(col) {
77
78
  if (col.hideBelow) return col.hideBelow;
78
79
  if (col.hiddenOnMobile) return "md";
@@ -796,7 +797,7 @@ DataTable.Content = function DataTableContent() {
796
797
  const fixedTableLayout = scroll?.x !== void 0 || visibleColumns.some((col) => col.ellipsis || columnWidth(col.width).style !== void 0);
797
798
  const missingHeaderNames = visibleColumns.filter((col) => isEmptyHeader(col.header) && !col.ariaLabel).map((col) => col.key).join("|");
798
799
  React.useEffect(() => {
799
- if (process.env.NODE_ENV === "production" || !missingHeaderNames) return;
800
+ if (!isDevelopment() || !missingHeaderNames) return;
800
801
  for (const key of missingHeaderNames.split("|")) {
801
802
  console.warn(
802
803
  `[DataTable] Column "${key}" renders a <th> with no visible or accessible text. Give it a visible \`header\`, or set \`ariaLabel\` (e.g. "Actions"/"Select") so screen readers can announce the column and axe reports no empty-table-header violation.`
@@ -10,6 +10,7 @@ import { FieldIdentityContext, FieldNameContext, mergeAriaIds } from "../../lib/
10
10
  import { useOptionalAppContext } from "../../app/app-provider.js";
11
11
  import { useFormLayout } from "./form.js";
12
12
  import { firstBagMessage, useClaimErrorKey, useFormErrorsRegistry } from "./form-errors.js";
13
+ import { isDevelopment } from "../../lib/dev.js";
13
14
  const toCssLength = (v) => typeof v === "number" ? `${v}px` : v;
14
15
  const FOCUSABLE_SELECTOR = 'input:not([type="hidden"]), select, textarea, button, [tabindex]:not([tabindex="-1"])';
15
16
  function FormField({
@@ -55,7 +56,7 @@ function FormField({
55
56
  const fieldKey = field ?? name ?? id;
56
57
  const emitFieldNames = useOptionalAppContext()?.emitFieldNames ?? false;
57
58
  const isStatic = staticText !== void 0;
58
- if (!isStatic && typeof process !== "undefined" && process.env?.NODE_ENV !== "production" && !React.isValidElement(children)) {
59
+ if (!isStatic && isDevelopment() && !React.isValidElement(children)) {
59
60
  console.warn(
60
61
  "FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control. Pass plain text via `staticText` instead of `children` for a read-only value row."
61
62
  );
@@ -8,6 +8,7 @@ import { numberFormat } from "../../lib/intl-cache.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
10
10
  import { Button } from "./button.js";
11
+ import { isDevelopment } from "../../lib/dev.js";
11
12
  const GroupContext = React.createContext(null);
12
13
  const DEFAULT_ICON = /* @__PURE__ */ jsx(FileText, { "aria-hidden": "true" });
13
14
  const BUTTON_VARIANT = {
@@ -67,7 +68,7 @@ const FloatButtonRoot = React.forwardRef(function FloatButton({
67
68
  const mergedContent = isRenderable(content) ? content : description;
68
69
  const hasContent = isRenderable(mergedContent);
69
70
  const mergedIcon = !hasContent && icon == null ? DEFAULT_ICON : icon;
70
- if (process.env.NODE_ENV !== "production" && mergedShape === "circle" && hasContent) {
71
+ if (isDevelopment() && mergedShape === "circle" && hasContent) {
71
72
  console.warn(
72
73
  "[@godxjp/ui] FloatButton: `content` is supported only when `shape` is `square` \u2014 a circle has no room for a line of text."
73
74
  );
@@ -153,7 +154,7 @@ function FloatButtonGroup({
153
154
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
154
155
  const isControlled = open != null;
155
156
  const isOpen = isControlled ? open : uncontrolledOpen;
156
- if (process.env.NODE_ENV !== "production" && isControlled && !trigger) {
157
+ if (isDevelopment() && isControlled && !trigger) {
157
158
  console.warn(
158
159
  "[@godxjp/ui] FloatButton.Group: `open` needs to be used together with `trigger`."
159
160
  );
@@ -6,6 +6,7 @@ import { cn } from "../../lib/utils.js";
6
6
  import { useTranslation } from "../../i18n/use-translation.js";
7
7
  import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
8
8
  import { Textarea } from "../data-entry/textarea.js";
9
+ import { isDevelopment } from "../../lib/dev.js";
9
10
  const COPIED_RESET_MS = 3e3;
10
11
  const DECORATION_ELEMENTS = [
11
12
  ["strong", "strong"],
@@ -409,7 +410,7 @@ const TextBase = React.forwardRef((props, ref) => {
409
410
  const ellipsisRowCount = ellipsis ? ellipsisRows(ellipsis) : 0;
410
411
  const effectiveClamp = ellipsisRowCount > 1 ? ellipsisRowCount : ellipsis ? void 0 : clampLines;
411
412
  const truncating = (truncate === true || ellipsisRowCount === 1) && effectiveClamp === void 0;
412
- if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
413
+ if (isDevelopment()) {
413
414
  if (clamp !== void 0 && clampLines === void 0) {
414
415
  console.warn(`Text: \`clamp\` must be a finite number \u2265 1 (got ${String(clamp)}); ignored.`);
415
416
  }
@@ -7,6 +7,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
7
7
  import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../feedback/sheet.js";
8
8
  import { NavSurfaceProvider } from "./nav-surface.js";
9
9
  import { TopbarItem } from "./topbar-item.js";
10
+ import { isDevelopment } from "../../lib/dev.js";
10
11
  function useAppShellNavigationMode() {
11
12
  return useMediaQuery("(width <= 56.25rem)") ? "drawer" : "docked";
12
13
  }
@@ -59,7 +60,7 @@ function AppShell({
59
60
  }
60
61
  };
61
62
  const hasTopbarContent = topbar !== void 0 || topbarLeft !== void 0 || topbarRight !== void 0 || logo !== void 0;
62
- if (process.env.NODE_ENV !== "production" && topbar !== void 0) {
63
+ if (isDevelopment() && topbar !== void 0) {
63
64
  const ignored = [
64
65
  topbarLeft !== void 0 && "topbarLeft",
65
66
  topbarRight !== void 0 && "topbarRight"
@@ -9,6 +9,7 @@ import { Progress } from "../data-display/progress.js";
9
9
  import { Text } from "../general/typography.js";
10
10
  import { CenteredShell } from "./centered-shell.js";
11
11
  import { dateTimeFormat, numberFormat } from "../../lib/intl-cache.js";
12
+ import { isDevelopment } from "../../lib/dev.js";
12
13
  const STATUS_META = {
13
14
  400: { icon: TriangleAlert, tone: "warning" },
14
15
  403: { icon: ShieldAlert, tone: "warning" },
@@ -23,7 +24,7 @@ function singleAction(action) {
23
24
  const flattened = React.Children.toArray(action).flatMap(
24
25
  (child) => React.isValidElement(child) && child.type === React.Fragment ? React.Children.toArray(child.props.children) : [child]
25
26
  );
26
- if (flattened.length > 1 && process.env.NODE_ENV !== "production") {
27
+ if (flattened.length > 1 && isDevelopment()) {
27
28
  console.error(
28
29
  "[@godxjp/ui] ErrorSurface: `action` takes EXACTLY ONE recovery action. The extra elements were dropped. Put support contact or secondary guidance in `description`."
29
30
  );
@@ -166,6 +166,12 @@ function Tabs({
166
166
  const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
167
167
  const card = variant === "card" || variant === "editable-card";
168
168
  const sizeTriggerClassName = triggerSizeClassName(size);
169
+ const CARD_FACE = {
170
+ top: "rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] data-[state=active]:border-b-background",
171
+ bottom: "rounded-[0_0_var(--tabs-card-radius)_var(--tabs-card-radius)] data-[state=active]:border-t-background",
172
+ start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-background",
173
+ end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-background"
174
+ };
169
175
  const list = items ? /* @__PURE__ */ jsx(
170
176
  TabsList,
171
177
  {
@@ -239,11 +245,21 @@ function Tabs({
239
245
  // pixel and `justify-content` has nothing left to centre — measured, the leading and
240
246
  // trailing gaps were both 0px with `centered` on and off.
241
247
  centered && "flex-none",
242
- // CARD face: a real boundary on every side, rounded on the leading block edge only.
243
- // The ACTIVE face's block-end edge is the SURFACE colour, never `transparent`: the
244
- // rail is an inset shadow at the strip's padding-box edge, so a see-through border
245
- // would let that 1px of rail run straight across the tab it is joined to.
246
- card && "data-[state=active]:border-b-background rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))]"
248
+ // The merged edge is the SURFACE colour, never `transparent`: the rail is an inset
249
+ // shadow at the strip's padding-box edge, so a see-through border would let that 1px
250
+ // of rail run straight across the tab it is joined to.
251
+ // CARD face: a real boundary on every side, opened toward the panel by
252
+ // `CARD_FACE`. `flex-none` is antd's geometry, not a preference — its
253
+ // `nav-list` is a plain `display: flex` with no `flex-grow` on the tab, so a
254
+ // card tab is CONTENT-WIDTH. The base trigger is `flex-1`, which made every
255
+ // card tab stretch to an equal share of the strip: measured, three tabs in a
256
+ // 942px strip came out 311px each for labels needing ~70px, and with
257
+ // `justify-center` the label and its × floated in the middle of that box —
258
+ // the × ended up 90px from the tab's own trailing edge (179px on a two-tab
259
+ // strip). antd's is 8px away. This is also why the face read as a free-standing
260
+ // bordered box rather than a tab.
261
+ card && "flex-none border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))]",
262
+ card && CARD_FACE[placement]
247
263
  ),
248
264
  children: [
249
265
  item.icon ? /* @__PURE__ */ jsx(
@@ -463,7 +479,7 @@ const TabsTrigger = React.forwardRef(
463
479
  // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
464
480
  // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
465
481
  // border, focused is the 2px ring plus its halo outside it.
466
- "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
482
+ "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
467
483
  // The tier the root was given. Without this the compound form ignored `size` outright.
468
484
  triggerSizeClassName(size),
469
485
  className
@@ -5,6 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { Input } from "../data-entry/input.js";
8
+ import { isDevelopment } from "../../lib/dev.js";
8
9
  const PasswordInput = React.forwardRef(
9
10
  ({ className, visibilityToggle = true, iconRender, suffix, trailingIcon, allowClear, ...props }, ref) => {
10
11
  const { t } = useTranslation();
@@ -19,7 +20,7 @@ const PasswordInput = React.forwardRef(
19
20
  onVisibleChange?.(next);
20
21
  };
21
22
  const toggleOwnsTrailing = showToggle;
22
- if (toggleOwnsTrailing && process.env?.NODE_ENV !== "production" && (suffix != null || trailingIcon != null || allowClear)) {
23
+ if (toggleOwnsTrailing && isDevelopment() && (suffix != null || trailingIcon != null || allowClear)) {
23
24
  console.warn(
24
25
  "[godxjp-ui] PasswordInput: the reveal toggle owns the trailing slot, so `suffix` / `trailingIcon` / `allowClear` are ignored. Use `leadingIcon` (or `prefix`), or pass `visibilityToggle={false}` to hand the slot back."
25
26
  );
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { Separator as AriaSeparator } from "react-aria-components";
4
4
  import { padStepToken } from "../../lib/variants.js";
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { isDevelopment } from "../../lib/dev.js";
6
7
  const Separator = React.forwardRef(
7
8
  ({
8
9
  className,
@@ -20,7 +21,7 @@ const Separator = React.forwardRef(
20
21
  }, ref) => {
21
22
  const hasLabel = typeof label === "string" && label.trim() !== "";
22
23
  const labelled = hasLabel && orientation === "horizontal";
23
- if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
24
+ if (isDevelopment()) {
24
25
  if (hasLabel && orientation === "vertical") {
25
26
  console.warn(
26
27
  'Separator: `label` is rendered on orientation="horizontal" only; it was ignored. Use a horizontal rule for a day divider / unread watermark.'
@@ -33,7 +33,7 @@ export declare const EMAIL_COLOR_SOURCE: {
33
33
  };
34
34
  readonly border: {
35
35
  readonly cssVar: "--border";
36
- readonly hsl: "30 7% 83%";
36
+ readonly hsl: "30 7% 93%";
37
37
  };
38
38
  readonly primary: {
39
39
  readonly cssVar: "--primary";
@@ -92,7 +92,7 @@ export declare const EMAIL_COLOR_SOURCE_DARK: {
92
92
  };
93
93
  readonly border: {
94
94
  readonly cssVar: "--border";
95
- readonly hsl: "45 6% 22%";
95
+ readonly hsl: "45 6% 19%";
96
96
  };
97
97
  readonly primary: {
98
98
  readonly cssVar: "--primary";
@@ -25,7 +25,7 @@ const EMAIL_COLOR_SOURCE = {
25
25
  },
26
26
  "border": {
27
27
  "cssVar": "--border",
28
- "hsl": "30 7% 83%"
28
+ "hsl": "30 7% 93%"
29
29
  },
30
30
  "primary": {
31
31
  "cssVar": "--primary",
@@ -83,7 +83,7 @@ const EMAIL_COLOR_SOURCE_DARK = {
83
83
  },
84
84
  "border": {
85
85
  "cssVar": "--border",
86
- "hsl": "45 6% 22%"
86
+ "hsl": "45 6% 19%"
87
87
  },
88
88
  "primary": {
89
89
  "cssVar": "--primary",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `isDevelopment()` — the ONE place this package is allowed to ask whether it is in a dev build.
3
+ *
4
+ * WHY THIS EXISTS, and it is a crash rather than a tidiness question. Eight shipped modules read
5
+ * `process.env.NODE_ENV` to decide whether to emit a development warning. Three of them guarded
6
+ * the read with `typeof process !== "undefined"`; five did not, and `process` is not a browser
7
+ * global. Nothing in the build replaces it either — `tsup.config.ts` declares no `define`, and
8
+ * `grep -o 'process\.env' dist` on the published tarball of 23.4.4 returns 9 hits, raw.
9
+ *
10
+ * Measured, not theorised: this repo's own preview app (Vite 8, dev mode) renders
11
+ * `/isolate/data-entry-password-strength` as **"Preview render failed — process is not defined"**,
12
+ * six DOM nodes and nothing else. `PasswordInput` is one of the five unguarded modules and that
13
+ * page renders it. A bundler that happens to define `process.env.NODE_ENV` papers over it, which
14
+ * is exactly why it survived: most consumers' toolchains do, and the ones that do not get a blank
15
+ * screen rather than a warning.
16
+ *
17
+ * The helper reads the guard ONCE so the two halves cannot drift apart again, and it fails SAFE:
18
+ * where `process` is absent we cannot tell dev from production, so the answer is `false` and the
19
+ * warnings go quiet. A missing warning is a cost; a component that throws while rendering is not
20
+ * a cost, it is a broken page.
21
+ *
22
+ * A new bare `process.env` read in `src/components/**` is a CI failure — see
23
+ * `src/lib/__tests__/dev-guard.test.ts`.
24
+ *
25
+ * TWO THINGS ABOUT THE SHAPE BELOW, and the first one is why the three "guarded" modules were
26
+ * only half guarded.
27
+ *
28
+ * NO OPTIONAL CHAINING ON `process.env`. It does not survive the build. Written as
29
+ * `process.env?.NODE_ENV`, the transformed output measured in this repo's own vitest pipeline is
30
+ *
31
+ * return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
32
+ *
33
+ * — the `?.` is GONE, because a bundler `define` matches the text `process.env` and substitutes
34
+ * through it, discarding the chain. So `?.` is not a guard here; it reads like one, which is
35
+ * worse than nothing. `typeof process` does survive (measured in the same dump), and a truthiness
36
+ * test on `process.env` survives as a truthiness test whatever it is replaced with.
37
+ *
38
+ * THE LITERAL IS KEPT ON THE LAST LINE ON PURPOSE. `process.env.NODE_ENV` is the exact text every
39
+ * bundler's `define` looks for, and that substitution is what lets a consumer's production build
40
+ * fold these branches away and drop the warning strings entirely. Reading the global indirectly
41
+ * (`globalThis.process`) would be equally crash-proof and would cost every consumer that
42
+ * dead-code elimination, so the order of the checks does the work instead: the guard runs first,
43
+ * and the replaceable literal is only reached once `env` is known to exist.
44
+ */
45
+ export declare function isDevelopment(): boolean;
@@ -0,0 +1,7 @@
1
+ function isDevelopment() {
2
+ if (typeof process === "undefined" || !process.env) return false;
3
+ return process.env.NODE_ENV !== "production";
4
+ }
5
+ export {
6
+ isDevelopment
7
+ };
@@ -76,19 +76,19 @@
76
76
  }
77
77
 
78
78
  [data-slot="alert-icon"][data-tone="destructive"] {
79
- color: hsl(var(--destructive));
79
+ color: hsl(var(--text-error));
80
80
  }
81
81
 
82
82
  [data-slot="alert-icon"][data-tone="warning"] {
83
- color: hsl(var(--warning));
83
+ color: hsl(var(--text-warning));
84
84
  }
85
85
 
86
86
  [data-slot="alert-icon"][data-tone="success"] {
87
- color: hsl(var(--success));
87
+ color: hsl(var(--text-success));
88
88
  }
89
89
 
90
90
  [data-slot="alert-icon"][data-tone="info"] {
91
- color: hsl(var(--info));
91
+ color: hsl(var(--text-info));
92
92
  }
93
93
 
94
94
  [data-slot="alert-body"] {
@@ -27,6 +27,10 @@
27
27
  color: hsl(var(--muted-foreground));
28
28
  }
29
29
 
30
+ .ui-chart .recharts-legend-item-text {
31
+ color: hsl(var(--muted-foreground)) !important;
32
+ }
33
+
30
34
  .ui-chart .recharts-default-tooltip {
31
35
  border-radius: var(--radius);
32
36
  border: 1px solid hsl(var(--border)) !important;
@@ -1053,6 +1053,7 @@
1053
1053
  width: 1.25rem;
1054
1054
  height: 1.25rem;
1055
1055
  }
1056
+
1056
1057
  .ui-rating-star-filled {
1057
1058
  color: hsl(var(--warning));
1058
1059
  }
@@ -1572,7 +1573,7 @@
1572
1573
  }
1573
1574
 
1574
1575
  .ui-search-select-placeholder[data-tone="destructive"] {
1575
- color: hsl(var(--destructive));
1576
+ color: hsl(var(--text-error));
1576
1577
  }
1577
1578
 
1578
1579
  .ui-search-select-footer {
@@ -87,15 +87,15 @@
87
87
  }
88
88
 
89
89
  .ui-password-strength-segment[data-tone="destructive"] {
90
- background: hsl(var(--destructive));
90
+ background: hsl(var(--mark-destructive));
91
91
  }
92
92
 
93
93
  .ui-password-strength-segment[data-tone="warning"] {
94
- background: hsl(var(--warning));
94
+ background: hsl(var(--mark-warning));
95
95
  }
96
96
 
97
97
  .ui-password-strength-segment[data-tone="success"] {
98
- background: hsl(var(--success));
98
+ background: hsl(var(--mark-success));
99
99
  }
100
100
 
101
101
  .ui-password-strength-meta {
@@ -133,11 +133,11 @@
133
133
  }
134
134
 
135
135
  .ui-password-strength-checklist-item[data-state="passed"] svg {
136
- color: hsl(var(--success));
136
+ color: hsl(var(--text-success));
137
137
  }
138
138
 
139
139
  .ui-password-strength-checklist-item[data-state="failed"] svg {
140
- color: hsl(var(--destructive));
140
+ color: hsl(var(--text-error));
141
141
  }
142
142
  }
143
143
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  .ui-form-field-required {
10
10
  margin-inline-start: var(--space-inline-xs);
11
- color: hsl(var(--destructive));
11
+ color: hsl(var(--text-error));
12
12
  }
13
13
 
14
14
  .ui-form {
@@ -1247,7 +1247,7 @@
1247
1247
  }
1248
1248
 
1249
1249
  .ui-filter-bar-error {
1250
- color: hsl(var(--destructive));
1250
+ color: hsl(var(--text-error));
1251
1251
  }
1252
1252
 
1253
1253
  .ui-legal-document-scope {
@@ -76,16 +76,16 @@
76
76
  color: hsl(var(--primary));
77
77
  }
78
78
  .ui-activity[data-tone="success"] .ui-activity-mark {
79
- color: hsl(var(--success));
79
+ color: hsl(var(--text-success));
80
80
  }
81
81
  .ui-activity[data-tone="warning"] .ui-activity-mark {
82
- color: hsl(var(--warning));
82
+ color: hsl(var(--text-warning));
83
83
  }
84
84
  .ui-activity[data-tone="destructive"] .ui-activity-mark {
85
- color: hsl(var(--destructive));
85
+ color: hsl(var(--text-error));
86
86
  }
87
87
  .ui-activity[data-tone="info"] .ui-activity-mark {
88
- color: hsl(var(--info));
88
+ color: hsl(var(--text-info));
89
89
  }
90
90
 
91
91
  .ui-activity-dot {
@@ -60,7 +60,16 @@
60
60
 
61
61
  [data-slot="tabs-list"][data-variant="line"][data-orientation="horizontal"]
62
62
  [data-slot="tabs-trigger"]::after {
63
- inset-block-end: calc(-1 * var(--tabs-indicator-offset));
63
+ inset-block-end: calc(
64
+ -1 *
65
+ (
66
+ var(--tabs-indicator-offset) + var(--tabs-list-line-space-inset) +
67
+ var(
68
+ --tabs-list-focus-ring-space-inset,
69
+ calc(var(--focus-ring-width) + var(--focus-ring-glow-width))
70
+ )
71
+ )
72
+ );
64
73
  block-size: var(--tabs-indicator-size);
65
74
  inline-size: calc(100% - 2 * var(--tabs-indicator-inset));
66
75
  inset-inline-start: var(--tabs-indicator-inset);
@@ -204,6 +213,38 @@
204
213
  box-shadow: inset 0 calc(-1 * var(--tabs-card-rail-border-width)) 0 hsl(var(--border));
205
214
  }
206
215
 
216
+ [data-slot="tabs"][data-variant="card"][data-placement="bottom"] [data-slot="tabs-list"],
217
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="bottom"]
218
+ [data-slot="tabs-list"] {
219
+ box-shadow: inset 0 var(--tabs-card-rail-border-width) 0 hsl(var(--border));
220
+ }
221
+
222
+ [data-slot="tabs"][data-variant="card"][data-placement="start"] [data-slot="tabs-list"],
223
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="start"]
224
+ [data-slot="tabs-list"] {
225
+ box-shadow: inset calc(-1 * var(--tabs-card-rail-border-width)) 0 0 hsl(var(--border));
226
+ }
227
+
228
+ [data-slot="tabs"][data-variant="card"][data-placement="end"] [data-slot="tabs-list"],
229
+ [data-slot="tabs"][data-variant="editable-card"][data-placement="end"]
230
+ [data-slot="tabs-list"] {
231
+ box-shadow: inset var(--tabs-card-rail-border-width) 0 0 hsl(var(--border));
232
+ }
233
+
234
+ [dir="rtl"] [data-slot="tabs"][data-variant="card"][data-placement="start"]
235
+ [data-slot="tabs-list"],
236
+ [dir="rtl"] [data-slot="tabs"][data-variant="editable-card"][data-placement="start"]
237
+ [data-slot="tabs-list"] {
238
+ box-shadow: inset var(--tabs-card-rail-border-width) 0 0 hsl(var(--border));
239
+ }
240
+
241
+ [dir="rtl"] [data-slot="tabs"][data-variant="card"][data-placement="end"]
242
+ [data-slot="tabs-list"],
243
+ [dir="rtl"] [data-slot="tabs"][data-variant="editable-card"][data-placement="end"]
244
+ [data-slot="tabs-list"] {
245
+ box-shadow: inset calc(-1 * var(--tabs-card-rail-border-width)) 0 0 hsl(var(--border));
246
+ }
247
+
207
248
  .ui-tabs-tab-remove {
208
249
  display: inline-flex;
209
250
  flex: 0 0 auto;
@@ -223,7 +264,8 @@
223
264
  color: hsl(var(--accent-foreground));
224
265
  }
225
266
 
226
- .ui-tabs-tab-remove-icon {
267
+ .ui-tabs-tab-remove-icon,
268
+ .ui-tabs-tab-remove > :is(svg, img) {
227
269
  inline-size: var(--tabs-tab-remove-icon-size);
228
270
  block-size: var(--tabs-tab-remove-icon-size);
229
271
  }
@@ -374,7 +416,7 @@
374
416
  }
375
417
 
376
418
  .ui-dropdown-menu-item[data-variant="destructive"] {
377
- color: hsl(var(--destructive));
419
+ color: hsl(var(--text-error));
378
420
  }
379
421
 
380
422
  .ui-dropdown-menu-separator,
@@ -461,7 +503,7 @@
461
503
  }
462
504
 
463
505
  .ui-steps-inline-item[data-status="error"] .ui-steps-inline-control {
464
- color: hsl(var(--destructive));
506
+ color: hsl(var(--text-error));
465
507
  }
466
508
 
467
509
  .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control {
@@ -590,7 +632,7 @@
590
632
 
591
633
  .ui-dropdown-menu-item[data-variant="destructive"][data-highlighted] {
592
634
  background: hsl(var(--destructive) / 0.1);
593
- color: hsl(var(--destructive));
635
+ color: hsl(var(--text-error));
594
636
  }
595
637
 
596
638
  .ui-dropdown-menu-item[data-disabled],
@@ -709,7 +751,7 @@
709
751
  }
710
752
 
711
753
  .ui-steps-title[data-status="error"] {
712
- color: hsl(var(--destructive));
754
+ color: hsl(var(--text-error));
713
755
  }
714
756
 
715
757
  .ui-steps-subtitle {
@@ -121,6 +121,7 @@
121
121
  }
122
122
 
123
123
  .ui-data-table-sort-button {
124
+ position: relative;
124
125
  display: inline-flex;
125
126
  align-items: center;
126
127
  gap: var(--space-inline-xs);
@@ -132,6 +133,18 @@
132
133
  outline: none;
133
134
  }
134
135
 
136
+ .ui-data-table-sort-button::after {
137
+ content: "";
138
+ position: absolute;
139
+ inset-block-start: 50%;
140
+ inset-inline-start: 50%;
141
+ translate: -50% -50%;
142
+ min-inline-size: var(--touch-target-min);
143
+ min-block-size: var(--touch-target-min);
144
+ inline-size: 100%;
145
+ block-size: 100%;
146
+ }
147
+
135
148
  .ui-data-table-bulk {
136
149
  display: flex;
137
150
  align-items: center;
@@ -20,7 +20,8 @@
20
20
  --accent-foreground: 48 8% 13%;
21
21
  --destructive: 357 64% 44%;
22
22
  --destructive-foreground: 60 33% 99%;
23
- --border: 30 7% 83%;
23
+
24
+ --border: 30 7% 93%;
24
25
 
25
26
  --input: 30 7% 53%;
26
27
 
@@ -331,7 +332,8 @@
331
332
 
332
333
  --destructive: 357 55% 48%;
333
334
  --destructive-foreground: 0 0% 100%;
334
- --border: 45 6% 22%;
335
+
336
+ --border: 45 6% 19%;
335
337
 
336
338
  --input: 45 6% 47%;
337
339
 
@@ -11,7 +11,9 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
11
11
  */
12
12
  export default function Demo() {
13
13
  const [signupPassword, setSignupPassword] = useState("");
14
- const [resetPassword, setResetPassword] = useState("");
14
+ /* Seeded so the PASSED state is on screen without typing: the checklist's success icon is
15
+ * otherwise only reachable by interaction, which means no sweep can measure it (gh#612). */
16
+ const [resetPassword, setResetPassword] = useState("Passw0rdSeed");
15
17
 
16
18
  return (
17
19
  <PageContainer
@@ -300,6 +300,38 @@ export default function Demo() {
300
300
  </DropdownMenu>
301
301
  </CardContent>
302
302
  </Card>
303
+ {/* defaultOpen — OPEN ON MOUNT, on purpose, and this is the only example on the page that
304
+ is. A menu paints nothing until it is opened, so for as long as this page has existed
305
+ no sweep has ever measured a single menu pixel: that is how
306
+ `.ui-dropdown-menu-item[data-variant="destructive"]` — the label on a Delete row, i.e.
307
+ prose — kept the destructive FILL tier at 2.95:1 on dark (gh#612). Same reasoning as
308
+ `?toast=` in check-contrast's route list: a surface that needs an interaction is a
309
+ surface no gate sees. */}
310
+ <Card>
311
+ <CardHeader>
312
+ <CardTitle level={2}>defaultOpen · 計測できる状態で開いておく</CardTitle>
313
+ <CardDescription>
314
+ antd の open/defaultOpen。ここだけはマウント時から開いたままにしてある。
315
+ 閉じたメニューは 1 ピクセルも描かないので、どのスイープにも測れない。
316
+ destructive の行が読める色で描かれているかは、開いていて初めて測れる。
317
+ </CardDescription>
318
+ </CardHeader>
319
+ <CardContent>
320
+ <DropdownMenu defaultOpen>
321
+ <DropdownMenuTrigger asChild>
322
+ <Button variant="outline" size="sm">
323
+ 仕訳アクション
324
+ </Button>
325
+ </DropdownMenuTrigger>
326
+ <DropdownMenuContent align="start">
327
+ <DropdownMenuItem>編集</DropdownMenuItem>
328
+ <DropdownMenuItem>複製</DropdownMenuItem>
329
+ <DropdownMenuSeparator />
330
+ <DropdownMenuItem variant="destructive">削除</DropdownMenuItem>
331
+ </DropdownMenuContent>
332
+ </DropdownMenu>
333
+ </CardContent>
334
+ </Card>
303
335
  </Flex>
304
336
  </PageContainer>
305
337
  );
@@ -128,6 +128,26 @@ export default function Demo() {
128
128
  />
129
129
  </Flex>
130
130
 
131
+ {/* error — the ONE sentence that says why a filter set was refused, and the reason this
132
+ page has an example at all: `.ui-filter-bar-error` was painted with the destructive
133
+ FILL tier (2.95:1 on dark, gh#612) and NO route rendered it, so no sweep could ever
134
+ have caught that. Static, not behind a submit: a surface that needs an interaction is
135
+ a surface `check:contrast` does not measure. */}
136
+ <Flex direction="col" gap="xs">
137
+ <Text size="xs" tone="muted">
138
+ error · role=&quot;alert&quot; line under the bar, painted from the TEXT tier
139
+ </Text>
140
+ <FilterBar
141
+ search={{
142
+ value: modelQuery,
143
+ onValueChange: setModelQuery,
144
+ placeholder: "氏名・メールで検索",
145
+ ariaLabel: "メンバーを検索",
146
+ }}
147
+ error="期間の開始日が終了日より後です。条件を修正してください。"
148
+ />
149
+ </Flex>
150
+
131
151
  {/* overflow="wrap" (default) — stacked below 640px, wrapping rows above. */}
132
152
  <Flex direction="col" gap="xs">
133
153
  <Text size="xs" tone="muted">
@@ -27,7 +27,11 @@ const onboardingSteps = [
27
27
  export default function Demo() {
28
28
  const [approvalCurrent, setApprovalCurrent] = useState(1);
29
29
  const [wizardCurrent, setWizardCurrent] = useState(0);
30
- const [hasError, setHasError] = useState(false);
30
+ /* Seeded TRUE so `status="error"` paints on mount. The toggle below is still the point of the
31
+ * demo, but starting from `false` meant the error state was reachable only by clicking —
32
+ * and a surface that needs an interaction is a surface no sweep measures (gh#612), which is
33
+ * how `.ui-steps-title[data-status="error"]` kept the destructive FILL tier unnoticed. */
34
+ const [hasError, setHasError] = useState(true);
31
35
 
32
36
  return (
33
37
  <PageContainer
@@ -256,6 +260,15 @@ export default function Demo() {
256
260
  value={1}
257
261
  items={[{ title: "コード入力" }, { title: "確認" }, { title: "完了" }]}
258
262
  />
263
+ {/* status="error" on the INLINE form too. The vertical/horizontal error title had a
264
+ route; `.ui-steps-inline-item[data-status="error"] .ui-steps-inline-control` did
265
+ not, and it reads the same tone token (gh#612). */}
266
+ <Steps
267
+ type="inline"
268
+ status="error"
269
+ value={1}
270
+ items={[{ title: "コード入力" }, { title: "確認" }, { title: "完了" }]}
271
+ />
259
272
  </Flex>
260
273
  </CardContent>
261
274
  </Card>
@@ -1,6 +1,6 @@
1
1
  import { useState } from "react";
2
2
 
3
- import { Trash2 } from "lucide-react";
3
+ import { CircleX } from "lucide-react";
4
4
 
5
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
6
6
  import { Button, Text } from "@godxjp/ui/general";
@@ -547,7 +547,7 @@ export default function Demo() {
547
547
  id="antd-remove-icon"
548
548
  variant="editable-card"
549
549
  defaultValue="je-0042"
550
- closeIcon={<Trash2 aria-hidden="true" />}
550
+ closeIcon={<CircleX aria-hidden="true" />}
551
551
  onEdit={() => undefined}
552
552
  items={[
553
553
  {
@@ -600,6 +600,26 @@ export default function Demo() {
600
600
  size="lg"
601
601
  items={journalItems}
602
602
  />
603
+ {/* CARD on the vertical axis. `card` + `bottom` above had shipped upside down, and
604
+ `start`/`end` had no card example at all — so the rail, which is drawn with a
605
+ box-shadow and therefore cannot be logical, had nothing to be measured against
606
+ in either direction. */}
607
+ <Tabs
608
+ id="antd-card-start"
609
+ defaultValue="pending"
610
+ variant="card"
611
+ tabPlacement="start"
612
+ size="sm"
613
+ items={journalItems}
614
+ />
615
+ <Tabs
616
+ id="antd-card-end"
617
+ defaultValue="pending"
618
+ variant="card"
619
+ tabPlacement="end"
620
+ size="sm"
621
+ items={journalItems}
622
+ />
603
623
  </Flex>
604
624
  </CardContent>
605
625
  </Card>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "23.4.4",
4
- "godxUiMcp": "23.4.4",
3
+ "version": "23.4.6",
4
+ "godxUiMcp": "23.4.6",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",