@godxjp/ui 23.4.3 → 23.4.5

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.
package/README.md CHANGED
@@ -125,11 +125,25 @@ Then name the family directly after your own face:
125
125
  node_modules/@godxjp/ui/dist/styles/core.css` → `0` is the promise, so the fallbacks got their own
126
126
  entry rather than being folded in.
127
127
 
128
+ Japanese app that wants the bundled face without the per-screen round-trips? The fourth entry
129
+ replaces the 729 sliced faces with one merged file per weight, JIS X 0208 level 1:
130
+
131
+ ```css
132
+ @import "@godxjp/ui/styles/core-with-jis-level1"; /* core + fallbacks + 3 merged faces */
133
+ ```
134
+
135
+ **3 requests, ~1.53 MB, all on first paint** — against 150 requests and 1,772,728 bytes for a
136
+ 694-character Japanese screen on `styles`, spread across screens. It does not cover JIS level 2
137
+ (rare-surname kanji fall through to the platform face, so name one after ours) and it loses to the
138
+ slices below roughly 620 distinct characters. Full table and reasoning in
139
+ [docs/CUSTOMER-THEMING.md](docs/CUSTOMER-THEMING.md).
140
+
128
141
  > **Do not cherry-pick `*-layout.css` files.** Layers depend on each other (a
129
142
  > Select's rows, a menu's surface, a form's rhythm live in shared rules) and a
130
143
  > missing layer fails silently: menus render with no background, rows with no
131
- > height. `styles`, `styles/core` and `styles/core-with-fallbacks` are the three supported entries;
132
- > the runtime `visual-audit` flags a page whose layers are incomplete (`css-layers-missing`).
144
+ > height. `styles`, `styles/core`, `styles/core-with-fallbacks` and
145
+ > `styles/core-with-jis-level1` are the four supported entries; the runtime `visual-audit` flags a
146
+ > page whose layers are incomplete (`css-layers-missing`).
133
147
 
134
148
  ## Golden ratio (φ ≈ 1.618)
135
149
 
@@ -188,7 +188,7 @@ const BranchScopePicker = React.forwardRef(
188
188
  ),
189
189
  /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", "aria-live": "polite", children: t("dataEntry.branchScope.selectedCount", { count: branchIds.length }) })
190
190
  ] }),
191
- invalid && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-destructive text-xs", children: error })
191
+ invalid && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-error-strong text-xs", children: error })
192
192
  ] })
193
193
  );
194
194
  }
@@ -175,7 +175,7 @@ function FormField({
175
175
  t(`dataEntry.form.${validationStatus}`)
176
176
  ] }) : null,
177
177
  helperPlacement === "after" ? helperNode : null,
178
- error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-destructive text-xs", children: error }) : null
178
+ error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-error-strong text-xs", children: error }) : null
179
179
  ] })
180
180
  ]
181
181
  }
@@ -434,7 +434,7 @@ function Upload({
434
434
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
435
435
  hiddenInput,
436
436
  liveRegion,
437
- rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive", children: rejection }),
437
+ rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
438
438
  /* @__PURE__ */ jsxs(
439
439
  "div",
440
440
  {
@@ -483,7 +483,7 @@ function Upload({
483
483
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
484
484
  hiddenInput,
485
485
  liveRegion,
486
- rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive", children: rejection }),
486
+ rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
487
487
  /* @__PURE__ */ jsxs(
488
488
  Button,
489
489
  {
@@ -514,7 +514,7 @@ function Upload({
514
514
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-upload-grid", className), children: [
515
515
  hiddenInput,
516
516
  liveRegion,
517
- rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive", children: rejection }),
517
+ rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
518
518
  (showUploadList ? items : []).map((item2) => /* @__PURE__ */ jsx(
519
519
  UploadPictureCard,
520
520
  {
@@ -548,7 +548,7 @@ function Upload({
548
548
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
549
549
  hiddenInput,
550
550
  liveRegion,
551
- rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive", children: rejection }),
551
+ rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
552
552
  /* @__PURE__ */ jsx(Button, { type: "button", disabled, variant: triggerVariant, onClick: openPicker, children: children ?? t("dataEntry.upload.addImage") }),
553
553
  list
554
554
  ] });
@@ -558,7 +558,7 @@ function Upload({
558
558
  return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm ui-upload-picture", className), children: [
559
559
  hiddenInput,
560
560
  liveRegion,
561
- rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive", children: rejection }),
561
+ rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
562
562
  item2?.previewUrl && !item2.pendingDelete ? /* @__PURE__ */ jsxs("div", { className: "ui-upload-picture-frame", children: [
563
563
  /* @__PURE__ */ jsx("img", { src: item2.previewUrl, alt: "", className: "ui-upload-picture-img" }),
564
564
  item2.status === "uploading" && /* @__PURE__ */ jsx("div", { className: "ui-upload-overlay ui-upload-picture-overlay", children: t("dataEntry.upload.uploading") }),
@@ -650,7 +650,7 @@ function UploadDraftActions({
650
650
  const { state, undoRemove, undoReplace } = draft;
651
651
  if (state.canUndoRemove) {
652
652
  return /* @__PURE__ */ jsxs("div", { className: "ui-upload-draft-undo", children: [
653
- /* @__PURE__ */ jsx("span", { className: "text-destructive", children: t("dataEntry.upload.markedForDelete") }),
653
+ /* @__PURE__ */ jsx("span", { className: "text-error-strong", children: t("dataEntry.upload.markedForDelete") }),
654
654
  /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "outline", disabled, onClick: undoRemove, children: [
655
655
  /* @__PURE__ */ jsx(RotateCcw, { className: "ui-upload-draft-icon", "aria-hidden": "true" }),
656
656
  t("dataEntry.upload.undo")
@@ -690,7 +690,7 @@ function UploadPictureCard({
690
690
  }
691
691
  )
692
692
  ] }),
693
- item.status === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-destructive", children: item.error }),
693
+ item.status === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-error-strong", children: item.error }),
694
694
  /* @__PURE__ */ jsxs("div", { className: "ui-inline-xs", children: [
695
695
  onCancel && item.status === "uploading" && /* @__PURE__ */ jsx(
696
696
  Button,
@@ -760,7 +760,7 @@ function UploadFileList({
760
760
  /* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: item.name }),
761
761
  /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs", children: [
762
762
  formatBytes(item.size),
763
- item.status === "error" && item.error && /* @__PURE__ */ jsxs("span", { role: "alert", className: "text-destructive", children: [
763
+ item.status === "error" && item.error && /* @__PURE__ */ jsxs("span", { role: "alert", className: "text-error-strong", children: [
764
764
  " ",
765
765
  "\xB7 ",
766
766
  item.error
@@ -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
@@ -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",
@@ -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"] {
@@ -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 {
@@ -0,0 +1,3 @@
1
+
2
+ @import "./core-with-fallbacks.css";
3
+ @import "./jis-level1-fonts.css";
@@ -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
 
@@ -107,7 +107,7 @@
107
107
 
108
108
  .ui-dialog-step-up-error {
109
109
  font-size: var(--dialog-step-up-error-font-size);
110
- color: hsl(var(--destructive));
110
+ color: hsl(var(--text-error));
111
111
  }
112
112
 
113
113
  [data-slot="dialog-footer"] {
@@ -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 {
@@ -0,0 +1,24 @@
1
+
2
+ @font-face {
3
+ font-family: "Noto Sans JP";
4
+ font-style: normal;
5
+ font-weight: 400;
6
+ font-display: swap;
7
+ src: url("./fonts/noto-sans-jp-jis-level1-400.woff2") format("woff2");
8
+ }
9
+
10
+ @font-face {
11
+ font-family: "Noto Sans JP";
12
+ font-style: normal;
13
+ font-weight: 500;
14
+ font-display: swap;
15
+ src: url("./fonts/noto-sans-jp-jis-level1-500.woff2") format("woff2");
16
+ }
17
+
18
+ @font-face {
19
+ font-family: "Noto Sans JP";
20
+ font-style: normal;
21
+ font-weight: 700;
22
+ font-display: swap;
23
+ src: url("./fonts/noto-sans-jp-jis-level1-700.woff2") format("woff2");
24
+ }
@@ -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,12 @@
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
+
207
222
  .ui-tabs-tab-remove {
208
223
  display: inline-flex;
209
224
  flex: 0 0 auto;
@@ -223,7 +238,8 @@
223
238
  color: hsl(var(--accent-foreground));
224
239
  }
225
240
 
226
- .ui-tabs-tab-remove-icon {
241
+ .ui-tabs-tab-remove-icon,
242
+ .ui-tabs-tab-remove > :is(svg, img) {
227
243
  inline-size: var(--tabs-tab-remove-icon-size);
228
244
  block-size: var(--tabs-tab-remove-icon-size);
229
245
  }
@@ -374,7 +390,7 @@
374
390
  }
375
391
 
376
392
  .ui-dropdown-menu-item[data-variant="destructive"] {
377
- color: hsl(var(--destructive));
393
+ color: hsl(var(--text-error));
378
394
  }
379
395
 
380
396
  .ui-dropdown-menu-separator,
@@ -461,7 +477,7 @@
461
477
  }
462
478
 
463
479
  .ui-steps-inline-item[data-status="error"] .ui-steps-inline-control {
464
- color: hsl(var(--destructive));
480
+ color: hsl(var(--text-error));
465
481
  }
466
482
 
467
483
  .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control {
@@ -590,7 +606,7 @@
590
606
 
591
607
  .ui-dropdown-menu-item[data-variant="destructive"][data-highlighted] {
592
608
  background: hsl(var(--destructive) / 0.1);
593
- color: hsl(var(--destructive));
609
+ color: hsl(var(--text-error));
594
610
  }
595
611
 
596
612
  .ui-dropdown-menu-item[data-disabled],
@@ -709,7 +725,7 @@
709
725
  }
710
726
 
711
727
  .ui-steps-title[data-status="error"] {
712
- color: hsl(var(--destructive));
728
+ color: hsl(var(--text-error));
713
729
  }
714
730
 
715
731
  .ui-steps-subtitle {
@@ -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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Read this once; the audit enforces it. Everything else in `docs/` is for contributors.
4
4
 
5
- 1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all) or `@import "@godxjp/ui/styles/core-with-fallbacks"` (`core` + six `local()`-only metric-matched faces, still zero network bytes — for when you supply Noto Sans JP yourself). Never cherry-pick `*-layout.css`.
5
+ 1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled, 729 sliced woff2 faces, ~11.7 MB), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all), `@import "@godxjp/ui/styles/core-with-fallbacks"` (`core` + six `local()`-only metric-matched faces, still zero network bytes — for when you supply Noto Sans JP yourself) or `@import "@godxjp/ui/styles/core-with-jis-level1"` (`core-with-fallbacks` + Noto Sans JP merged to JIS X 0208 level 1: **3 requests, ~1.53 MB, once** instead of ~8 font requests on every navigation — for a Japanese app that wants the bundled face). Never cherry-pick `*-layout.css`.
6
6
  2. Every page is `<PageContainer title subtitle extra footer>`; its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
7
7
  3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
8
8
  4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`.
@@ -16,7 +16,7 @@ That single import ships everything: colors, the bundled fonts (**Noto Sans JP**
16
16
 
17
17
  ---
18
18
 
19
- ## CSS entries — `styles`, `styles/core`, `styles/core-with-fallbacks`, nothing smaller
19
+ ## CSS entries — `styles`, `styles/core`, `styles/core-with-fallbacks`, `styles/core-with-jis-level1`, nothing smaller
20
20
 
21
21
  `@godxjp/ui/styles` bundles every component's CSS plus the fonts: **729 woff2 subsets, ~11.7 MB on disk** at the current @fontsource versions (issue #535 measured 737 files / 13 MB in a real consumer build). When you manage fonts yourself (next/font, a system stack, a browser extension that must not ship font files), load the same layers without the faces:
22
22
 
@@ -38,6 +38,49 @@ It declares the faces and nothing else — name the family yourself, directly af
38
38
  }
39
39
  ```
40
40
 
41
+ ### A Japanese app that wants the bundled face anyway
42
+
43
+ The slicing is what costs the round-trips: a browser cannot know which of the 729 faces it needs
44
+ until it has laid out and measured the text, so every new screen discovers a new handful. The
45
+ fourth entry replaces them with **one merged file per weight**, JIS X 0208 level 1 — 2965 kanji
46
+ plus kana, symbols, Cyrillic, Latin and Vietnamese, 3861 code points:
47
+
48
+ ```css
49
+ @import "@godxjp/ui/styles/core-with-jis-level1"; /* core + fallbacks + 3 merged faces */
50
+ ```
51
+
52
+ Measured against the sliced entry, Noto Sans JP only, weights 400/500/700:
53
+
54
+ | distinct Japanese characters on screen | `styles` | `core-with-jis-level1` |
55
+ | --------------------------------------------- | -------------------------: | -----------------------: |
56
+ | 448 — this package's own `ja` labels, no data | 99 requests · 1,051,268 B | 3 requests · 1,534,636 B |
57
+ | 694 — labels plus names, addresses, prose | 150 requests · 1,772,728 B | 3 requests · 1,534,636 B |
58
+ | 772 — a little more prose | 216 requests · 3,491,840 B | 3 requests · 1,534,636 B |
59
+
60
+ The left column grows with your content and is paid again on every screen that renders a character
61
+ no earlier screen did; the right column does not move. **Below roughly 620 distinct characters the
62
+ slices are fewer bytes** (in ~100 requests), so an app that renders less Japanese than this
63
+ package's own menu labels should stay on `styles`.
64
+
65
+ What is deliberately not in it: **JIS level 2** (rows 48–84), which would roughly double the bytes
66
+ to cover kanji that appear in rare surnames — those resolve from the platform Japanese face, so
67
+ name one after ours. **M PLUS 2**, which sits behind Noto Sans JP in every stack this package ships
68
+ and is therefore never downloaded today either; merging it would add 1,089,676 bytes for nothing.
69
+ And **no `unicode-range`** on the merged faces, because per-range discovery is the mechanism the
70
+ entry exists to remove — the cost of that is a Latin-only screen downloading its weight's ~500 KB
71
+ rather than the ~25 KB of Latin inside it.
72
+
73
+ Like `core-with-fallbacks`, it declares faces and does not set `--font-sans-base`; name a platform
74
+ Japanese face after ours so level 2 kanji have somewhere to land:
75
+
76
+ ```css
77
+ :root {
78
+ --font-sans-base:
79
+ "Noto Sans JP", "Noto Sans JP Fallback", "Hiragino Sans", "Yu Gothic Medium", Meiryo, system-ui,
80
+ sans-serif;
81
+ }
82
+ ```
83
+
41
84
  The per-layer files (`control`, `card-layout`, `navigation-layout`, …) are the package's internal structure, **not a public menu**. Layers share rules — a Select's rows and a menu's surface, a form's rhythm, a card's header type — so a page that loads a subset renders naked menus and unsized rows with no error. The runtime `visual-audit` reports it as `css-layers-missing`.
42
85
 
43
86
  ---
@@ -190,3 +233,4 @@ Scope the overrides under a tenant attribute instead of `:root`. The colour util
190
233
  Set `data-tenant` on the app root. Two CSS-inheritance caveats for the **scoped** case (a single `:root` brand theme is unaffected — there, overriding just `--radius` / `--shadow-color` cascades):
191
234
 
192
235
  - **Radius & shadow-tint don't cascade from a scoped anchor.** `--radius-{xs…2xl}`, `--card-radius`, `--control-radius` and the `--shadow-{xs…2xl}` ramp are computed at their declaring element, so a scoped `--radius` / `--shadow-color` override won't reach them. For a scoped re-theme, re-declare the derived tokens you need (e.g. `--card-radius: var(--radius)`, or set `--card-shadow` to a literal value).
236
+ ````
package/docs/TOKENS.md CHANGED
@@ -53,7 +53,7 @@ in the source.
53
53
  | Tier | Tokens | What it paints | Contrast bar |
54
54
  | -------- | ------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
55
55
  | **FILL** | `--success`, `--warning`, `--info`, `--destructive` | A solid chip, band or bar with a label ON it | AA **4.5:1** against its own `*-foreground` label |
56
- | **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label | AA **4.5:1** against the surface BEHIND it |
56
+ | **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label, a field's error line | AA **4.5:1** against the surface BEHIND it |
57
57
  | **MARK** | `--mark-success`, `--mark-warning`, `--mark-info`, `--mark-destructive`, `--mark-primary`, `--mark-attention` | A thin shape carrying meaning with nothing written on it — a `Card accent` rail, a `DataTable rowTone` rail | SC 1.4.11 **3:1** against the surface it sits on |
58
58
 
59
59
  The MARK tier exists because both rails were reading FILL, and two of them were effectively
@@ -94,6 +94,24 @@ Worst case anywhere on the two routes after the move: **4.52:1**. `.ui-progress-
94
94
  over-capacity) reads the same tokens as the slice, so a `tone="warning"` meter and a `warning`
95
95
  slice on one screen stay the same colour.
96
96
 
97
+ **ERROR PROSE WAS THE THIRD PLACE THE SAME MISTAKE LIVED (gh#610).** `Alert`, `Text` and
98
+ `Heading` resolved their destructive ink through TEXT from the start, but the `role="alert"` line
99
+ the data-entry primitives render — `<FormField error>`, `Upload`'s rejections, the `Dialog` step-up
100
+ failure — was on `text-destructive`, the FILL utility. So the same `tone` was readable inside an
101
+ `Alert` and near-invisible one line below it, in the field that caused it. Measured in Chromium on
102
+ `/isolate/layout-auth-recovery-examples-mfa-challenge`:
103
+
104
+ | surface | ground | FILL (before) | TEXT (after) |
105
+ | --- | --- | --- | --- |
106
+ | `.ui-form-field-note[role="alert"]` | dark card | **2.95** | **5.52** |
107
+ | `.ui-form-field-note[role="alert"]` | light card | 6.16 | **7.21** |
108
+
109
+ Only the DARK branch failed, and the light one passing is why it survived: the fill is tuned for a
110
+ white label ON it, so it darkens on light grounds and lightens on dark ones — the opposite of what
111
+ ink needs. The guard is `error-text-tier.test.ts` (token ratios on `--background`/`--card`/
112
+ `--muted` in both themes, plus a source scan for the fill utility), and `check:contrast` gained the
113
+ two `ui-auth-shell` routes it had never loaded.
114
+
97
115
  **A theme that repoints `--secondary` owes `--progress-track-background`.** The track defaults to
98
116
  `hsl(var(--secondary))`, which is a pale neutral in the stock palette. `docs/showcase/acme-portal`
99
117
  repurposes `--secondary` as a navy *button* colour, so its bars were drawn on a near-black track
@@ -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
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "23.4.3",
4
- "godxUiMcp": "23.4.3",
3
+ "version": "23.4.5",
4
+ "godxUiMcp": "23.4.5",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -48,6 +48,7 @@
48
48
  "./styles": "./dist/styles/index.css",
49
49
  "./styles/core": "./dist/styles/core.css",
50
50
  "./styles/core-with-fallbacks": "./dist/styles/core-with-fallbacks.css",
51
+ "./styles/core-with-jis-level1": "./dist/styles/core-with-jis-level1.css",
51
52
  "./styles/*": "./dist/styles/*.css",
52
53
  "./theme/*": "./dist/theme/*",
53
54
  "./tokens": "./dist/tokens/base.css",
@@ -380,6 +381,7 @@
380
381
  "audit": "node scripts/ui-audit.mjs",
381
382
  "audit:examples": "node scripts/ui-audit.mjs docs",
382
383
  "build": "tsup && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs && node scripts/fix-esm-extensions.mjs && node scripts/add-use-client.mjs",
384
+ "build:jis-level1-fonts": "node scripts/build-jis-level1-fonts.mjs",
383
385
  "dev": "node scripts/dev.mjs",
384
386
  "prepublishOnly": "pnpm run build",
385
387
  "check:audit-sync": "node scripts/check-audit-sync.mjs",
@@ -12,12 +12,13 @@
12
12
  `docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
13
13
  `.claude/skills/godx-ui-guinea-pig/SKILL.md`.
14
14
 
15
- ## Nạp style: BA lối vào, và hai lối sau không chở font
15
+ ## Nạp style: BỐN lối vào, và ba lối sau đều không chở 729 lát font
16
16
 
17
17
  ```css
18
- @import "@godxjp/ui/styles"; /* mọi layer + Noto Sans JP / M PLUS 2 đóng gói sẵn */
18
+ @import "@godxjp/ui/styles"; /* mọi layer + 729 lát woff2 cắt theo unicode-range */
19
19
  @import "@godxjp/ui/styles/core"; /* CÙNG các layer ấy, KHÔNG một @font-face nào */
20
20
  @import "@godxjp/ui/styles/core-with-fallbacks"; /* core + 6 khối local()-only, vẫn 0 byte mạng */
21
+ @import "@godxjp/ui/styles/core-with-jis-level1"; /* + Noto Sans JP gộp JIS mức 1: 3 request, ~1,53 MB */
21
22
  ```
22
23
 
23
24
  Chọn `core` khi kho tự lo mặt chữ, hoặc khi không muốn chở font: `@fontsource` cắt
@@ -30,13 +31,31 @@ sổ swap không đội hình: nó chở đúng 6 `@font-face` metric-matched, `
30
31
  nên **không tải byte nào**. Nhớ tự xếp tên họ chữ ngay sau mặt chữ của bạn:
31
32
  `--font-sans-base: "Noto Sans JP", "Noto Sans JP Fallback", system-ui, sans-serif;`
32
33
 
34
+ Lối thứ tư dành cho **app tiếng Nhật vẫn muốn font kèm gói**, nhưng không muốn trả số
35
+ vòng tải mỗi lần chuyển màn: nó thay 729 lát bằng **một tệp gộp cho mỗi weight**, phủ
36
+ JIS X 0208 mức 1 (2965 kanji + kana + ký hiệu + Latin + tiếng Việt, 3861 code point).
37
+ Đo trên ba weight, chỉ Noto Sans JP:
38
+
39
+ | số ký tự Nhật khác nhau trên màn | `styles` | `core-with-jis-level1` |
40
+ | -------------------------------- | ---------------------------- | -------------------------- |
41
+ | 448 (nhãn `ja` của chính gói) | 99 request · 1.051.268 byte | 3 request · 1.534.636 byte |
42
+ | 694 (nhãn + tên, địa chỉ, câu) | 150 request · 1.772.728 byte | 3 request · 1.534.636 byte |
43
+ | 772 (thêm chút văn xuôi) | 216 request · 3.491.840 byte | 3 request · 1.534.636 byte |
44
+
45
+ Cột trái phình theo NỘI DUNG và phải trả lại ở mỗi màn có ký tự mới; cột phải không đổi.
46
+ **Dưới khoảng 620 ký tự khác nhau thì cắt lát ít byte hơn** (đổi bằng ~100 request), nên
47
+ app nào render ít chữ Nhật hơn cả menu của gói này thì cứ ở `styles`. Lối này KHÔNG chở
48
+ JIS mức 2 — kanji trong họ tên hiếm sẽ rơi xuống mặt chữ của hệ điều hành, nên hãy xếp
49
+ một mặt chữ Nhật của hệ ngay sau mặt chữ của gói. Nó cũng không đụng `--font-sans-base`,
50
+ y như lối thứ ba.
51
+
33
52
  `core` giữ `@font-face` = **0** và đó là lời hứa đo được —
34
53
  `grep -c '@font-face' node_modules/@godxjp/ui/dist/styles/core.css` → `0`. Vì vậy các
35
54
  fallback nằm ở entry riêng chứ không nhét vào `core`.
36
55
 
37
- Không lối nào trong ba là cherry-pick: cả ba đều được hỗ trợ và thứ tự layer vẫn nguyên
38
- vẹn. Cherry-pick từng layer riêng lẻ thì vẫn cấm — đó là thứ làm vỡ hợp đồng thứ tự,
39
- không phải việc chọn lối vào.
56
+ Không lối nào trong bốn là cherry-pick: cả bốn đều được hỗ trợ và thứ tự layer vẫn
57
+ nguyên vẹn. Cherry-pick từng layer riêng lẻ thì vẫn cấm — đó là thứ làm vỡ hợp đồng thứ
58
+ tự, không phải việc chọn lối vào.
40
59
 
41
60
  ## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
42
61
 
@@ -902,8 +902,24 @@ function jsxOpeningEnd(source, start) {
902
902
  * filters AND a table is a mixed body, where the padding is correct — the reporter hit that trap
903
903
  * with a `childElementCount === 1` test.
904
904
  */
905
+ /*
906
+ * The alternation is DERIVED from the stylesheet, not guessed. `SkeletonTable` was missing and the
907
+ * same mistake went through silently (gh#611) — which is the worse half, because `SkeletonTable`
908
+ * is the package's OWN stand-in for `DataTable`: a consumer writes the skeleton and the table side
909
+ * by side, one is flagged and the other is not.
910
+ *
911
+ * The list to match is exactly what `[data-slot="card-content"][data-flush]` special-cases in CSS,
912
+ * because a per-pair exception there IS the statement that the pair matters:
913
+ *
914
+ * .ui-data-table-root · .ui-data-table-scroll · .ui-data-table-toolbar → DataTable
915
+ * .ui-skeleton-table → SkeletonTable
916
+ *
917
+ * `Table` stays because it is the primitive `DataTable` is built from and a consumer can place it
918
+ * directly. Adding a component to that CSS block without adding it here reopens this issue, so the
919
+ * two are checked against each other in the rule's test.
920
+ */
905
921
  const CARD_TABLE_FLUSH = new RegExp(
906
- `<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|Table)\\b`,
922
+ `<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|SkeletonTable|Table)\\b`,
907
923
  "g",
908
924
  );
909
925