@godxjp/ui 23.4.2 → 23.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +31 -2
  2. package/dist/components/data-entry/branch-scope-picker.js +1 -1
  3. package/dist/components/data-entry/date-picker.js +4 -16
  4. package/dist/components/data-entry/form-field.js +1 -1
  5. package/dist/components/data-entry/upload.js +8 -8
  6. package/dist/components/layout/draggable-panel.d.ts +3 -2
  7. package/dist/components/layout/draggable-panel.js +11 -2
  8. package/dist/components/layout/index.d.ts +1 -1
  9. package/dist/props/components/index.d.ts +1 -1
  10. package/dist/props/components/layout.prop.d.ts +13 -2
  11. package/dist/props/registry.d.ts +4 -0
  12. package/dist/props/registry.js +5 -0
  13. package/dist/styles/control.css +21 -0
  14. package/dist/styles/core-with-fallbacks.css +3 -0
  15. package/dist/styles/core-with-jis-level1.css +3 -0
  16. package/dist/styles/dialog-layout.css +1 -1
  17. package/dist/styles/font-fallbacks.css +83 -0
  18. package/dist/styles/fonts/noto-sans-jp-jis-level1-400.woff2 +0 -0
  19. package/dist/styles/fonts/noto-sans-jp-jis-level1-500.woff2 +0 -0
  20. package/dist/styles/fonts/noto-sans-jp-jis-level1-700.woff2 +0 -0
  21. package/dist/styles/fonts.css +1 -82
  22. package/dist/styles/jis-level1-fonts.css +24 -0
  23. package/dist/styles/layout.css +5 -0
  24. package/dist/tokens/components/draggable-panel.css +4 -0
  25. package/docs/CONSUMER-RULES.md +1 -1
  26. package/docs/CUSTOMER-THEMING.md +60 -2
  27. package/docs/DESIGN-AUTHORITY.md +4 -0
  28. package/docs/TOKENS.md +19 -1
  29. package/package.json +6 -2
  30. package/scripts/_agent-setup.mjs +28 -3
  31. package/scripts/cli.mjs +6 -2
  32. package/scripts/consumer-rule.md +95 -18
  33. package/scripts/guinea-pig-skill.md +38 -9
  34. package/scripts/ui-audit.mjs +18 -2
package/README.md CHANGED
@@ -110,11 +110,40 @@ The framework ships colors, the type scale, the wa-iro palette, and (opt-in) bun
110
110
  @import "@godxjp/ui/styles/core"; /* every component layer, no @font-face */
111
111
  ```
112
112
 
113
+ Supplying Noto Sans JP yourself and want the cold-visit swap not to reflow the page? Take the
114
+ third entry instead — the same layers plus the six metric-matched fallback faces, whose `src` is
115
+ `local()`-only, so it costs **zero network bytes** over `core`:
116
+
117
+ ```css
118
+ @import "@godxjp/ui/styles/core-with-fallbacks"; /* core + 6 local()-only faces */
119
+ ```
120
+
121
+ Then name the family directly after your own face:
122
+ `--font-sans-base: "Noto Sans JP", "Noto Sans JP Fallback", system-ui, sans-serif;`
123
+
124
+ `core` stays at **zero** `@font-face` on purpose — `grep -c '@font-face'
125
+ node_modules/@godxjp/ui/dist/styles/core.css` → `0` is the promise, so the fallbacks got their own
126
+ entry rather than being folded in.
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
+
113
141
  > **Do not cherry-pick `*-layout.css` files.** Layers depend on each other (a
114
142
  > Select's rows, a menu's surface, a form's rhythm live in shared rules) and a
115
143
  > missing layer fails silently: menus render with no background, rows with no
116
- > height. `styles` and `styles/core` are the two supported entries; the runtime
117
- > `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`).
118
147
 
119
148
  ## Golden ratio (φ ≈ 1.618)
120
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
  }
@@ -441,14 +441,8 @@ function DatePicker(props) {
441
441
  event.currentTarget.closest("div")?.querySelector("input:not([type=hidden])")?.focus();
442
442
  clear();
443
443
  },
444
- className: range ? "text-muted-foreground hover:text-foreground shrink-0" : "ui-control-inline-affix-action",
445
- children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(
446
- X,
447
- {
448
- className: range ? "ui-month-picker-icon" : "ui-control-inline-affix-icon",
449
- "aria-hidden": "true"
450
- }
451
- )
444
+ className: "ui-control-inline-affix-action",
445
+ children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
452
446
  }
453
447
  ) : /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
454
448
  "button",
@@ -457,14 +451,8 @@ function DatePicker(props) {
457
451
  disabled: allDisabled,
458
452
  tabIndex: -1,
459
453
  "aria-label": triggerLabel ?? (isPeriod ? t("dataEntry.monthPicker.openGrid") : range ? t("dataEntry.dateRangePicker.openCalendar") : t("dataEntry.datePicker.openCalendar")) ?? "Open calendar",
460
- className: range ? "text-muted-foreground hover:text-foreground shrink-0" : "ui-control-inline-affix-action",
461
- children: /* @__PURE__ */ jsx(
462
- CalendarIcon,
463
- {
464
- className: range ? "ui-month-picker-icon" : "ui-control-inline-affix-icon",
465
- "aria-hidden": "true"
466
- }
467
- )
454
+ className: "ui-control-inline-affix-action",
455
+ children: /* @__PURE__ */ jsx(CalendarIcon, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
468
456
  }
469
457
  ) });
470
458
  const panel = /* @__PURE__ */ jsxs(
@@ -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
@@ -1,18 +1,19 @@
1
1
  import * as React from "react";
2
2
  import type { DragAxisProp, DraggablePanelPositionProp } from "../../props/components/layout.prop.js";
3
- export type { DraggablePanelProp, DraggablePanelProp as DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DragAxisProp, DragBoundsProp, } from "../../props/components/layout.prop.js";
3
+ export type { DraggablePanelProp, DraggablePanelProp as DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelLabels, DragAxisProp, DragBoundsProp, } from "../../props/components/layout.prop.js";
4
4
  export declare const DraggablePanel: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement>, "children" | "onDrag" | "title"> & {
5
5
  title: import("../../props/index.js").TitleProp;
6
6
  children?: import("../../props/index.js").ChildrenProp;
7
7
  extra?: import("../../props/index.js").ExtraProp;
8
8
  placement?: import("./draggable-panel.js").DraggablePanelPlacementProp;
9
- width?: Extract<import("../../props/index.js").SizeProp, "sm" | "md" | "lg">;
9
+ width?: Extract<import("../../props/index.js").SizeProp, "sm" | "md" | "lg"> | "xl";
10
10
  axis?: DragAxisProp;
11
11
  bounds?: import("./draggable-panel.js").DragBoundsProp;
12
12
  position?: DraggablePanelPositionProp;
13
13
  defaultPosition?: DraggablePanelPositionProp;
14
14
  onPositionChange?: (position: DraggablePanelPositionProp) => void;
15
15
  onClose?: () => void;
16
+ labels?: import("./draggable-panel.js").DraggablePanelLabels;
16
17
  disabled?: import("../../props/index.js").DisabledProp;
17
18
  className?: import("../../props/index.js").ClassNameProp;
18
19
  } & React.RefAttributes<HTMLElement>>;
@@ -51,6 +51,7 @@ const DraggablePanel = React.forwardRef(
51
51
  defaultPosition,
52
52
  onPositionChange,
53
53
  onClose,
54
+ labels,
54
55
  disabled = false,
55
56
  className,
56
57
  ...props
@@ -73,6 +74,14 @@ const DraggablePanel = React.forwardRef(
73
74
  [bounds, onPositionChange, position]
74
75
  );
75
76
  React.useEffect(() => () => stopDrag.current?.(), []);
77
+ React.useEffect(() => {
78
+ if (bounds !== "viewport" || typeof window === "undefined") return;
79
+ const onResize = () => {
80
+ commit(currentRef.current);
81
+ };
82
+ window.addEventListener("resize", onResize);
83
+ return () => window.removeEventListener("resize", onResize);
84
+ }, [bounds, commit]);
76
85
  const setPanel = React.useCallback(
77
86
  (node) => {
78
87
  panelRef.current = node;
@@ -156,7 +165,7 @@ const DraggablePanel = React.forwardRef(
156
165
  type: "button",
157
166
  "data-slot": "draggable-panel-handle",
158
167
  className: "ui-draggable-panel-handle ui-focus-ring",
159
- "aria-label": t("layout.draggablePanel.moveLabel"),
168
+ "aria-label": labels?.move ?? t("layout.draggablePanel.moveLabel"),
160
169
  "aria-disabled": movable ? void 0 : "true",
161
170
  onPointerDown: handlePointerDown,
162
171
  onKeyDown: handleKeyDown,
@@ -171,7 +180,7 @@ const DraggablePanel = React.forwardRef(
171
180
  type: "button",
172
181
  "data-slot": "draggable-panel-close",
173
182
  className: "ui-draggable-panel-close ui-focus-ring",
174
- "aria-label": t("feedback.alert.dismiss"),
183
+ "aria-label": labels?.close ?? t("feedback.alert.dismiss"),
175
184
  onClick: onClose,
176
185
  children: /* @__PURE__ */ jsx(X, { className: "ui-draggable-panel-close-icon", "aria-hidden": "true" })
177
186
  }
@@ -50,7 +50,7 @@ export type { MasterDetailProps } from "./master-detail.js";
50
50
  export { SplitPane } from "./split-pane.js";
51
51
  export type { SplitPaneProps } from "./split-pane.js";
52
52
  export { DraggablePanel } from "./draggable-panel.js";
53
- export type { DraggablePanelProp, DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DragAxisProp, DragBoundsProp, } from "./draggable-panel.js";
53
+ export type { DraggablePanelProp, DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelLabels, DragAxisProp, DragBoundsProp, } from "./draggable-panel.js";
54
54
  export { Separator } from "./separator.js";
55
55
  export type { SeparatorProp, SeparatorProps } from "./separator.js";
56
56
  export { AspectRatio } from "./aspect-ratio.js";
@@ -1,4 +1,4 @@
1
- export type { AppLauncherApp, AppLauncherGroup, AppLauncherLabels, AppLauncherProp, AppShellProp, AuthFooterProp, AuthIdentityProp, OrgSwitcherLabels, OrgSwitcherOrganization, OrgSwitcherProp, DragAxisProp, DragBoundsProp, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelProp, PageContainerProp, PageInsetProp, SidebarItemProp, SidebarProductProp, SidebarProp, SidebarSectionProp, TopbarProp, } from "./layout.prop.js";
1
+ export type { AppLauncherApp, AppLauncherGroup, AppLauncherLabels, AppLauncherProp, AppShellProp, AuthFooterProp, AuthIdentityProp, OrgSwitcherLabels, OrgSwitcherOrganization, OrgSwitcherProp, DragAxisProp, DragBoundsProp, DraggablePanelLabels, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelProp, PageContainerProp, PageInsetProp, SidebarItemProp, SidebarProductProp, SidebarProp, SidebarSectionProp, TopbarProp, } from "./layout.prop.js";
2
2
  export type { ButtonProp } from "./general.prop.js";
3
3
  export type { InputProp, TextareaProp, FormFieldProp, SearchInputProp, CheckboxProp, CheckboxGroupProp, ChoiceOptionProp, RadioProp, RadioGroupProp, SwitchProp, SliderProp, CalendarProp, DatePickerProp, TimePickerProp, ColorPickerProp, UploadProp, UploadFileItemProp, UploadVariantProp, TreeOptionProp, TreeFieldNamesProp, CascaderProp, TreeSelectProp, ShowCheckedStrategyProp, TransferProp, TransferItemProp, } from "./data-entry.prop.js";
4
4
  export type { AvatarProp, EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, QrCodeProp, } from "./data-display.prop.js";
@@ -1442,6 +1442,11 @@ export type DraggablePanelPositionProp = {
1442
1442
  x: number;
1443
1443
  y: number;
1444
1444
  };
1445
+ /** Optional overrides for title-bar control labels when `AppProvider` is not in scope (gh#606). */
1446
+ export type DraggablePanelLabels = {
1447
+ close?: string;
1448
+ move?: string;
1449
+ };
1445
1450
  /**
1446
1451
  * @see DraggablePanel — a floating surface the person using it can MOVE, so a docked assistant
1447
1452
  * stops covering the thing they are asking about.
@@ -1458,8 +1463,8 @@ export type DraggablePanelProp = Omit<React.HTMLAttributes<HTMLElement>, "title"
1458
1463
  extra?: ExtraProp;
1459
1464
  /** Resting corner before any movement. Default `bottom-end`. */
1460
1465
  placement?: DraggablePanelPlacementProp;
1461
- /** Panel width, from the token scale. Default `md`. */
1462
- width?: Extract<SizeProp, "sm" | "md" | "lg">;
1466
+ /** Panel width, from the token scale (`sm`–`xl`). Default `md`. */
1467
+ width?: Extract<SizeProp, "sm" | "md" | "lg"> | "xl";
1463
1468
  /** react-draggable `axis`. Default `both`. */
1464
1469
  axis?: DragAxisProp;
1465
1470
  /** react-draggable `bounds`. Default `viewport`. */
@@ -1476,6 +1481,12 @@ export type DraggablePanelProp = Omit<React.HTMLAttributes<HTMLElement>, "title"
1476
1481
  onPositionChange?: (position: DraggablePanelPositionProp) => void;
1477
1482
  /** Presence renders the close control in the title bar (antd Modal's `onCancel`). */
1478
1483
  onClose?: () => void;
1484
+ /**
1485
+ * Localized strings for the close and move controls. Each key wins over `t()` when set; omit to
1486
+ * keep the framework default. For script-injected embeds that cannot mount `AppProvider` without
1487
+ * restyling the host page — a scoped provider subtree is the long-term fix (gh#606).
1488
+ */
1489
+ labels?: DraggablePanelLabels;
1479
1490
  /** react-draggable `disabled` — the panel stays, the handle stops moving it. */
1480
1491
  disabled?: DisabledProp;
1481
1492
  className?: ClassNameProp;
@@ -2536,6 +2536,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
2536
2536
  readonly field: "onClose";
2537
2537
  readonly local: true;
2538
2538
  readonly reason: "Presence renders the title-bar close control — antd Modal's onCancel.";
2539
+ }, {
2540
+ readonly field: "labels";
2541
+ readonly local: true;
2542
+ readonly reason: "Optional close/move strings for embeds that cannot mount AppProvider without restyling the host page (gh#606).";
2539
2543
  }];
2540
2544
  };
2541
2545
  readonly ThumbnailSizeProp: {
@@ -2953,6 +2953,11 @@ const COMPONENT_PROP_REGISTRY = {
2953
2953
  field: "onClose",
2954
2954
  local: true,
2955
2955
  reason: "Presence renders the title-bar close control \u2014 antd Modal's onCancel."
2956
+ },
2957
+ {
2958
+ field: "labels",
2959
+ local: true,
2960
+ reason: "Optional close/move strings for embeds that cannot mount AppProvider without restyling the host page (gh#606)."
2956
2961
  }
2957
2962
  ]
2958
2963
  },
@@ -1355,6 +1355,27 @@
1355
1355
  block-size: 0.75rem;
1356
1356
  }
1357
1357
 
1358
+ .ui-number-input-step {
1359
+ position: relative;
1360
+ }
1361
+
1362
+ .ui-number-input-step::after {
1363
+ content: "";
1364
+ position: absolute;
1365
+ inset-inline-start: 50%;
1366
+ translate: -50% 0;
1367
+ inline-size: var(--touch-target-min);
1368
+ block-size: var(--touch-target-min);
1369
+ }
1370
+
1371
+ .ui-number-input-step-up::after {
1372
+ inset-block-end: 0;
1373
+ }
1374
+
1375
+ .ui-number-input-step-down::after {
1376
+ inset-block-start: 0;
1377
+ }
1378
+
1358
1379
  @media (pointer: coarse) {
1359
1380
  .ui-number-input:not([data-size="xs"]):not([data-size="sm"]) {
1360
1381
  --control-height: var(--number-input-touch-height);
@@ -0,0 +1,3 @@
1
+
2
+ @import "./core.css";
3
+ @import "./font-fallbacks.css";
@@ -0,0 +1,3 @@
1
+
2
+ @import "./core-with-fallbacks.css";
3
+ @import "./jis-level1-fonts.css";
@@ -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"] {
@@ -0,0 +1,83 @@
1
+
2
+ @font-face {
3
+ font-family: "Noto Sans JP Fallback";
4
+ font-weight: 400;
5
+ src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans");
6
+ size-adjust: 103.02%;
7
+ ascent-override: 112.6%;
8
+ descent-override: 27.96%;
9
+ line-gap-override: 0%;
10
+ unicode-range:
11
+ U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
12
+ U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
13
+ U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
14
+ }
15
+ @font-face {
16
+ font-family: "Noto Sans JP Fallback";
17
+ font-weight: 500;
18
+ src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans");
19
+ size-adjust: 105.5%;
20
+ ascent-override: 109.95%;
21
+ descent-override: 27.3%;
22
+ line-gap-override: 0%;
23
+ unicode-range:
24
+ U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
25
+ U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
26
+ U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
27
+ }
28
+ @font-face {
29
+ font-family: "Noto Sans JP Fallback";
30
+ font-weight: 700;
31
+ src:
32
+ local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold"),
33
+ local("LiberationSans-Bold");
34
+ size-adjust: 101.5%;
35
+ ascent-override: 114.29%;
36
+ descent-override: 28.37%;
37
+ line-gap-override: 0%;
38
+ unicode-range:
39
+ U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
40
+ U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
41
+ U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
42
+ }
43
+ @font-face {
44
+ font-family: "Noto Sans JP Fallback";
45
+ font-weight: 400;
46
+ src:
47
+ local("Hiragino Sans W3"), local("HiraginoSans-W3"), local("Hiragino Kaku Gothic ProN W3"),
48
+ local("HiraKakuProN-W3"), local("Yu Gothic Medium"), local("YuGothic-Medium"), local("Meiryo"),
49
+ local("Noto Sans CJK JP"), local("NotoSansCJKjp-Regular"), local("IPAGothic");
50
+ ascent-override: 116%;
51
+ descent-override: 28.8%;
52
+ line-gap-override: 0%;
53
+ unicode-range:
54
+ U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
55
+ }
56
+ @font-face {
57
+ font-family: "Noto Sans JP Fallback";
58
+ font-weight: 500;
59
+ src:
60
+ local("Hiragino Sans W5"), local("HiraginoSans-W5"), local("Hiragino Kaku Gothic ProN W3"),
61
+ local("HiraKakuProN-W3"), local("Yu Gothic Medium"), local("YuGothic-Medium"), local("Meiryo"),
62
+ local("Noto Sans CJK JP Medium"), local("NotoSansCJKjp-Medium"), local("Noto Sans CJK JP"),
63
+ local("NotoSansCJKjp-Regular"), local("IPAGothic");
64
+ ascent-override: 116%;
65
+ descent-override: 28.8%;
66
+ line-gap-override: 0%;
67
+ unicode-range:
68
+ U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
69
+ }
70
+ @font-face {
71
+ font-family: "Noto Sans JP Fallback";
72
+ font-weight: 700;
73
+ src:
74
+ local("Hiragino Sans W6"), local("HiraginoSans-W6"), local("Hiragino Kaku Gothic ProN W6"),
75
+ local("HiraKakuProN-W6"), local("Yu Gothic Bold"), local("YuGothic-Bold"), local("Meiryo Bold"),
76
+ local("Meiryo-Bold"), local("Noto Sans CJK JP Bold"), local("NotoSansCJKjp-Bold"),
77
+ local("IPAGothic");
78
+ ascent-override: 116%;
79
+ descent-override: 28.8%;
80
+ line-gap-override: 0%;
81
+ unicode-range:
82
+ U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
83
+ }
@@ -6,88 +6,7 @@
6
6
  @import "@fontsource/noto-sans-jp/500.css";
7
7
  @import "@fontsource/noto-sans-jp/700.css";
8
8
 
9
- @font-face {
10
- font-family: "Noto Sans JP Fallback";
11
- font-weight: 400;
12
- src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans");
13
- size-adjust: 103.02%;
14
- ascent-override: 112.6%;
15
- descent-override: 27.96%;
16
- line-gap-override: 0%;
17
- unicode-range:
18
- U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
19
- U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
20
- U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
21
- }
22
- @font-face {
23
- font-family: "Noto Sans JP Fallback";
24
- font-weight: 500;
25
- src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans");
26
- size-adjust: 105.5%;
27
- ascent-override: 109.95%;
28
- descent-override: 27.3%;
29
- line-gap-override: 0%;
30
- unicode-range:
31
- U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
32
- U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
33
- U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
34
- }
35
- @font-face {
36
- font-family: "Noto Sans JP Fallback";
37
- font-weight: 700;
38
- src:
39
- local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold"),
40
- local("LiberationSans-Bold");
41
- size-adjust: 101.5%;
42
- ascent-override: 114.29%;
43
- descent-override: 28.37%;
44
- line-gap-override: 0%;
45
- unicode-range:
46
- U+0000-02CC, U+02CE-02D7, U+02DA, U+02DC-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
47
- U+1D00-1DBF, U+1E00-1EFF, U+2000-206F, U+20A0-20C0, U+2113, U+2122, U+2191, U+2193, U+2212,
48
- U+2215, U+2C60-2C7F, U+A720-A7FF, U+FEFF, U+FFFD;
49
- }
50
- @font-face {
51
- font-family: "Noto Sans JP Fallback";
52
- font-weight: 400;
53
- src:
54
- local("Hiragino Sans W3"), local("HiraginoSans-W3"), local("Hiragino Kaku Gothic ProN W3"),
55
- local("HiraKakuProN-W3"), local("Yu Gothic Medium"), local("YuGothic-Medium"), local("Meiryo"),
56
- local("Noto Sans CJK JP"), local("NotoSansCJKjp-Regular"), local("IPAGothic");
57
- ascent-override: 116%;
58
- descent-override: 28.8%;
59
- line-gap-override: 0%;
60
- unicode-range:
61
- U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
62
- }
63
- @font-face {
64
- font-family: "Noto Sans JP Fallback";
65
- font-weight: 500;
66
- src:
67
- local("Hiragino Sans W5"), local("HiraginoSans-W5"), local("Hiragino Kaku Gothic ProN W3"),
68
- local("HiraKakuProN-W3"), local("Yu Gothic Medium"), local("YuGothic-Medium"), local("Meiryo"),
69
- local("Noto Sans CJK JP Medium"), local("NotoSansCJKjp-Medium"), local("Noto Sans CJK JP"),
70
- local("NotoSansCJKjp-Regular"), local("IPAGothic");
71
- ascent-override: 116%;
72
- descent-override: 28.8%;
73
- line-gap-override: 0%;
74
- unicode-range:
75
- U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
76
- }
77
- @font-face {
78
- font-family: "Noto Sans JP Fallback";
79
- font-weight: 700;
80
- src:
81
- local("Hiragino Sans W6"), local("HiraginoSans-W6"), local("Hiragino Kaku Gothic ProN W6"),
82
- local("HiraKakuProN-W6"), local("Yu Gothic Bold"), local("YuGothic-Bold"), local("Meiryo Bold"),
83
- local("Meiryo-Bold"), local("Noto Sans CJK JP Bold"), local("NotoSansCJKjp-Bold"),
84
- local("IPAGothic");
85
- ascent-override: 116%;
86
- descent-override: 28.8%;
87
- line-gap-override: 0%;
88
- unicode-range:
89
- U+3000-30FF, U+31F0-31FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FA1F;
90
- }
9
+ @import "./font-fallbacks.css";
91
10
 
92
11
  :root {
93
12
  --font-sans-base:
@@ -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
+ }
@@ -1799,6 +1799,7 @@
1799
1799
  flex-direction: column;
1800
1800
  inline-size: var(--draggable-panel-width);
1801
1801
  max-inline-size: calc(100% - var(--draggable-panel-inset) - var(--draggable-panel-inset));
1802
+ block-size: var(--draggable-panel-block-size);
1802
1803
  max-block-size: var(--draggable-panel-max-block-size);
1803
1804
  border: var(--draggable-panel-border-width) solid hsl(var(--border));
1804
1805
  border-radius: var(--draggable-panel-radius);
@@ -1818,6 +1819,10 @@
1818
1819
  inline-size: var(--draggable-panel-width-lg);
1819
1820
  }
1820
1821
 
1822
+ .ui-draggable-panel[data-width="xl"] {
1823
+ inline-size: var(--draggable-panel-width-xl);
1824
+ }
1825
+
1821
1826
  .ui-draggable-panel[data-placement^="top"] {
1822
1827
  inset-block-start: var(--draggable-panel-inset);
1823
1828
  }
@@ -12,6 +12,10 @@
12
12
  --draggable-panel-width-sm: 18rem;
13
13
  --draggable-panel-width: 22rem;
14
14
  --draggable-panel-width-lg: 28rem;
15
+
16
+ --draggable-panel-width-xl: var(--centered-shell-width-md);
17
+
18
+ --draggable-panel-block-size: auto;
15
19
  --draggable-panel-max-block-size: 70vh;
16
20
 
17
21
  --draggable-panel-radius: var(--radius-lg);
@@ -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) or `@import "@godxjp/ui/styles/core"` (no fonts). 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,14 +16,71 @@ That single import ships everything: colors, the bundled fonts (**Noto Sans JP**
16
16
 
17
17
  ---
18
18
 
19
- ## CSS entries — `styles` or `styles/core`, nothing smaller
19
+ ## CSS entries — `styles`, `styles/core`, `styles/core-with-fallbacks`, `styles/core-with-jis-level1`, nothing smaller
20
20
 
21
- `@godxjp/ui/styles` bundles every component's CSS plus the fonts. 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:
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
 
23
23
  ```css
24
24
  @import "@godxjp/ui/styles/core"; /* every component layer, no @font-face */
25
25
  ```
26
26
 
27
+ `core` carries **zero** `@font-face` — `grep -c '@font-face' node_modules/@godxjp/ui/dist/styles/core.css` → `0` — and that number is the point of it. If you supply Noto Sans JP yourself and also want the cold-visit swap to stop reflowing the page (issue #475), take the third entry: `core` plus the six metric-matched fallback faces, every one `local()`-only, so the extra cost over `core` is **zero network bytes**.
28
+
29
+ ```css
30
+ @import "@godxjp/ui/styles/core-with-fallbacks"; /* core + 6 local()-only faces */
31
+ ```
32
+
33
+ It declares the faces and nothing else — name the family yourself, directly after your own face:
34
+
35
+ ```css
36
+ :root {
37
+ --font-sans-base: "Noto Sans JP", "Noto Sans JP Fallback", system-ui, sans-serif;
38
+ }
39
+ ```
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
+
27
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`.
28
85
 
29
86
  ---
@@ -176,3 +233,4 @@ Scope the overrides under a tenant attribute instead of `:root`. The colour util
176
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):
177
234
 
178
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
+ ````
@@ -247,6 +247,10 @@ in page CSS.
247
247
  (gh#602, measured 1.00:1). This library adds `count` / `overflowCount` / `showZero` /
248
248
  `countLabel` on `SegmentedOption` with a DS-owned pill (`--segmented-count-*` tokens), same
249
249
  counter vocabulary as `Button` / `Toggle`.
250
+ - **`DraggablePanel.labels`.** antd's Draggable Modal demo has no i18n hook for the drag handle.
251
+ Embedded consumers cannot mount `AppProvider` without writing theme attributes on the host
252
+ `document.documentElement` (gh#606). `labels?: { close?: string; move?: string }` overrides `t()`
253
+ per control; a scoped `AppProvider` subtree remains the long-term fix.
250
254
  - **`Badge.onRemove` instead of antd `Tag`'s `closable` + `onClose`.** Semantics match antd's
251
255
  closable tag (a chip that draws its own × and fires one callback). The boolean is folded into
252
256
  prop presence (`onRemove` omitted ⇒ no ×), and the callback is named `onRemove` rather than
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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "23.4.2",
4
- "godxUiMcp": "23.4.2",
3
+ "version": "23.4.4",
4
+ "godxUiMcp": "23.4.4",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -46,6 +46,9 @@
46
46
  "import": "./dist/index.js"
47
47
  },
48
48
  "./styles": "./dist/styles/index.css",
49
+ "./styles/core": "./dist/styles/core.css",
50
+ "./styles/core-with-fallbacks": "./dist/styles/core-with-fallbacks.css",
51
+ "./styles/core-with-jis-level1": "./dist/styles/core-with-jis-level1.css",
49
52
  "./styles/*": "./dist/styles/*.css",
50
53
  "./theme/*": "./dist/theme/*",
51
54
  "./tokens": "./dist/tokens/base.css",
@@ -378,6 +381,7 @@
378
381
  "audit": "node scripts/ui-audit.mjs",
379
382
  "audit:examples": "node scripts/ui-audit.mjs docs",
380
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",
381
385
  "dev": "node scripts/dev.mjs",
382
386
  "prepublishOnly": "pnpm run build",
383
387
  "check:audit-sync": "node scripts/check-audit-sync.mjs",
@@ -125,6 +125,18 @@ export function stampedDigest(text) {
125
125
  return text?.match(DIGEST_RE)?.[1] ?? null;
126
126
  }
127
127
 
128
+ /**
129
+ * Refresh only the human-readable release marker when the managed body is already current.
130
+ *
131
+ * The writer keys off digest; `ui-audit` keys off version (godx-jp/id#513). When rule text is
132
+ * unchanged across releases both must still agree after sync — without rewriting the body or
133
+ * breaking idempotency when digest and version already match.
134
+ */
135
+ function resyncVersionStamp(text) {
136
+ if (stampedVersion(text) === KIT_VERSION) return null;
137
+ return text.replace(STAMP_RE, `<!-- godxjp-ui:version ${KIT_VERSION} -->`);
138
+ }
139
+
128
140
  /**
129
141
  * Replace the MANAGED region of a file and leave everything else alone.
130
142
  *
@@ -533,7 +545,14 @@ export function refreshGuineaPigSkill(root) {
533
545
  * to anyone reading it. Only the digest half is compared.
534
546
  */
535
547
  const stamp = `${KIT_VERSION}:${digestOf(base)}`;
536
- if (readFileSync(optin, "utf8").trim().split(":").pop() === digestOf(base)) return false;
548
+ const optinLine = readFileSync(optin, "utf8").trim();
549
+ const optinDigest = optinLine.split(":").pop();
550
+ if (optinDigest === digestOf(base)) {
551
+ if (optinLine.split(":")[0] === KIT_VERSION) return false;
552
+ // Base unchanged — only the release half of `<version>:<digest>` drifted (same split as above).
553
+ writeFileAtomic(optin, `${stamp}\n`);
554
+ return true;
555
+ }
537
556
 
538
557
  const current = readFileSync(target, "utf8");
539
558
  const marker = "\n---\n\n# 8. ";
@@ -581,8 +600,14 @@ export function ensureConsumerRules(root) {
581
600
  const managed = `${front}${body}`;
582
601
  const next = `${OWNED_STAMP(managed)}\n${managed}`;
583
602
 
584
- if (existsSync(target) && stampedDigest(readFileSync(target, "utf8")) === digestOf(managed)) {
585
- return false;
603
+ if (existsSync(target)) {
604
+ const current = readFileSync(target, "utf8");
605
+ if (stampedDigest(current) === digestOf(managed)) {
606
+ const resynced = resyncVersionStamp(current);
607
+ if (resynced == null) return false;
608
+ writeFileAtomic(target, resynced);
609
+ return uiDir;
610
+ }
586
611
  }
587
612
  mkdirSync(dir, { recursive: true });
588
613
  writeFileAtomic(target, next);
package/scripts/cli.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * @godxjp/ui CLI — `npx @godxjp/ui <command>`.
4
4
  * init-agent scaffold the agent forcing-kit (MCP + auto-audit hook + mandate)
5
+ * sync-rules refresh package-owned agent rules (same path as postinstall)
5
6
  * audit static UI audit (regex over source)
6
7
  * visual-audit runtime audit (Playwright + axe-core) against a running app
7
8
  */
@@ -12,6 +13,7 @@ import { fileURLToPath } from "node:url";
12
13
  const HERE = dirname(fileURLToPath(import.meta.url));
13
14
  const MAP = {
14
15
  "init-agent": "init-agent-kit.mjs",
16
+ "sync-rules": "postinstall.mjs",
15
17
  audit: "ui-audit.mjs",
16
18
  "visual-audit": "visual-audit.mjs",
17
19
  };
@@ -19,8 +21,10 @@ const MAP = {
19
21
  const [cmd, ...rest] = process.argv.slice(2);
20
22
  const script = MAP[cmd];
21
23
  if (!script) {
22
- console.error("usage: godxjp-ui <init-agent | audit | visual-audit> [args]");
24
+ console.error("usage: godxjp-ui <init-agent | sync-rules | audit | visual-audit> [args]");
23
25
  process.exit(1);
24
26
  }
25
- const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit" });
27
+ const env =
28
+ cmd === "sync-rules" ? { ...process.env, INIT_CWD: process.env.INIT_CWD ?? process.cwd() } : process.env;
29
+ const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit", env });
26
30
  process.exit(r.status ?? 0);
@@ -1,6 +1,9 @@
1
1
  # @godxjp/ui
2
2
 
3
- > **Tệp này do gói `@godxjp/ui` sở hữu bị GHI ĐÈ mỗi lần nâng cấp.**
3
+ > **Tệp này do gói `@godxjp/ui` sở hữu.** Nội dung được ghi lại khi luật trong gói đổi (thường
4
+ > qua postinstall sau `npm i`, trừ khi `.npmrc` có `ignore-scripts=true`). Khi đó chạy
5
+ > `npx @godxjp/ui sync-rules`. Dấu `<!-- godxjp-ui:version -->` luôn được đồng bộ với phiên bản
6
+ > gói đang cài — kể cả khi thân tệp không đổi giữa hai bản.
4
7
  > Đừng sửa ở đây — luật của riêng kho thuộc về một tệp khác trong `.ai/rules/`,
5
8
  > và index sẽ nạp cả hai. (Khác với `.claude/skills/.../SKILL.md`, nơi mục §8
6
9
  > trở đi là của kho và được giữ lại.)
@@ -9,11 +12,13 @@
9
12
  `docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
10
13
  `.claude/skills/godx-ui-guinea-pig/SKILL.md`.
11
14
 
12
- ## Nạp style: HAI lối vào, và lối thứ hai 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
13
16
 
14
17
  ```css
15
- @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 */
16
19
  @import "@godxjp/ui/styles/core"; /* CÙNG các layer ấy, KHÔNG một @font-face nào */
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 */
17
22
  ```
18
23
 
19
24
  Chọn `core` khi kho tự lo mặt chữ, hoặc khi không muốn chở font: `@fontsource` cắt
@@ -21,9 +26,36 @@ Noto Sans JP thành hàng trăm lát `unicode-range`, và trình duyệt chỉ b
21
26
  SAU khi đã dựng bố cục — một consumer đo được **737 lát / 13 MB**, gấp bảy lần toàn bộ
22
27
  JavaScript của họ, cộng ~8 vòng tải mỗi lần chuyển màn.
23
28
 
24
- `core` không phải cherry-pick: một trong hai lối vào được hỗ trợ, thứ tự layer
25
- vẫn 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
26
- thứ tự, không phải việc chọn lối vào.
29
+ Lối thứ ba dành cho kho **tự cấp Noto Sans JP** (next/font, self-host) vẫn muốn cửa
30
+ sổ swap không đội hình: chở đúng 6 `@font-face` metric-matched, `src` toàn `local()`
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:
32
+ `--font-sans-base: "Noto Sans JP", "Noto Sans JP Fallback", system-ui, sans-serif;`
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
+
52
+ `core` giữ `@font-face` = **0** và đó là lời hứa đo được —
53
+ `grep -c '@font-face' node_modules/@godxjp/ui/dist/styles/core.css` → `0`. Vì vậy các
54
+ fallback nằm ở entry riêng chứ không nhét vào `core`.
55
+
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.
27
59
 
28
60
  ## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
29
61
 
@@ -66,24 +98,69 @@ Slack thì đặt lại **một dòng**, không fork `.app-nav-rail`.
66
98
  Hỏi MCP `godxjp-ui` (`search_components`, `get_component`). Đo được trong một
67
99
  ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ khác —
68
100
 
69
- | Cần | Đã có |
70
- | ---------------------------------------- | --------------------- |
71
- | đường kẻ chạm mép Card | `<CardContent flush>` |
72
- | header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
73
- | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
74
- | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
75
- | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` |
101
+ | Cần | Đã có |
102
+ | ----------------------------------------------- | ---------------------------------------------------------------------------------- |
103
+ | đường kẻ chạm mép Card | `<CardContent flush>` |
104
+ | header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
105
+ | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
106
+ | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
107
+ | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
108
+ | chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
109
+ | dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
110
+ | tiêu đề / đoạn văn / liên kết trong văn bản | `<Title>` · `<Paragraph>` · `<Link>` (`Text` 33 prop) |
111
+ | nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
112
+ | khung xương của một FORM khi đang tải | `<SkeletonForm>` |
113
+ | ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn | `<Swatch>` |
114
+ | ảnh đại diện của một tệp / bản xem trước | `<Thumbnail>` |
115
+ | danh sách "có gì trong gói này" | `<FeatureList>` |
116
+ | panel kéo giãn được (chia đôi màn) | `<DraggablePanel>` |
117
+ | màn hội thoại AI | `<Welcome>` · `<Conversations>` · `<Attachments>` · `<ThoughtChain>` · `<Actions>` |
76
118
 
77
119
  Lỗi không phải "đoán sai tên prop" mà là **cho rằng nó không tồn tại nên không
78
120
  hỏi**.
79
121
 
80
- ## Catalog chở PROP, không chở LUẬT BỐ CỤC
122
+ ## Dialog AlertDialog MỘT họ `variant` là lối chuẩn
81
123
 
82
- `CardBar` trong manifest đúng một prop (`extra`) không dòng nào nói tự
83
- kẻ theo vị trí (đầu: kẻ dưới · cuối: kẻ trên · giữa: cả hai). Luật ấy chỉ nằm
84
- trong chú thích `node_modules/@godxjp/ui/src/styles/card-layout.css`.
124
+ Đừng với tay sang 12 export `AlertDialog*` nữa. Chúng **vẫn chạy y như cũ** (gỡ
125
+ breaking change) nhưng **LỐI CŨ**: đo được 26 export với **12 cặp trùng
126
+ tên** **0** phần chỉ `AlertDialog` mới có. antd — thẩm quyền bề mặt prop của
127
+ gói này — chỉ có MỘT `Modal`, và mức nguy hiểm ở đó là một PROP.
85
128
 
86
- **Làm bố cục trong một component của DS → mở tệp `*-layout.css` của nó ra đọc.**
129
+ ```tsx
130
+ <DialogContent variant="destructive"> {/* thay cho <AlertDialogContent> */}
131
+ ```
132
+
133
+ Một prop ấy quyết định ba thứ đi cùng nhau: `role="alertdialog"`, click ra ngoài
134
+ KHÔNG đóng, nút chính nhấn mạnh destructive (và ✕ mặc định tắt). Esc VẪN đóng —
135
+ y như `AlertDialogContent` trước nay. Đặt `variant` ở `DialogRoot` thì cả cây kế
136
+ thừa.
137
+
138
+ ## antd là CHUẨN — thiếu gì thì port 100%, đừng tự thiết kế
139
+
140
+ `docs/DESIGN-AUTHORITY.md` của gói: **nơi antd đặt tên cho một năng lực, gói này
141
+ lấy nguyên tên và nguyên ngữ nghĩa của antd.** Một năng lực còn thiếu được port
142
+ từ antd **100% TRƯỚC** — tên, prop, ngữ nghĩa — rồi mới cải tiến. Không thiết kế
143
+ lại trước, không port một nửa.
144
+
145
+ Nghĩa là với consumer: thấy thiếu prop thì **mở issue kèm tên antd của nó**
146
+ (`sorter`, `closable`, `okType`…), đừng đề xuất một cái tên mới và đừng tự vẽ
147
+ lại bằng class tiện ích. Ba trục cố ý lệch khỏi antd đều đã ghi lý do trong
148
+ DESIGN-AUTHORITY (logical thay cho `left/right`, từ vựng giá trị của gói này,
149
+ `density` thay cho `size`) — lệch thêm thì phải viết ra ở đó.
150
+
151
+ ## Catalog giờ chở CẢ luật bố cục — nhưng phải hỏi mới có
152
+
153
+ Mục này từng nói "catalog chở PROP, không chở LUẬT BỐ CỤC", với `CardBar` làm
154
+ bằng chứng: một prop (`extra`), không dòng nào nói nó tự kẻ theo vị trí. **Bằng
155
+ chứng ấy đã hết đúng.** `CardBar` nay có 6 prop, trong đó `border` (`"none" |
156
+ "block-start" | "block-end" | "both"`) ép được đường kẻ khi xếp chồng, và cả
157
+ `Card` lẫn `CardBar` trong catalog đều nói ra luật vị trí (đầu: kẻ dưới · cuối:
158
+ kẻ trên · giữa: cả hai).
159
+
160
+ Nên luật hiện hành là: **hỏi `get_component` trước** — nay nó thường trả lời cả
161
+ hình dạng lẫn luật. Còn khi `usage`/`description` im lặng về bố cục thì mới mở
162
+ `node_modules/@godxjp/ui/src/styles/*-layout.css` của component ấy ra đọc; chú
163
+ thích trong đó vẫn là bản đầy đủ nhất.
87
164
 
88
165
  ## Card không lồng Card
89
166
 
@@ -101,6 +101,28 @@ Thứ tự ưu tiên, chỉ tiến khi bước trước thật sự không diễ
101
101
  **dùng → ghép → thêm prop vào component đã có → tạo component mới.**
102
102
  Một prop nữa hơn một component nữa.
103
103
 
104
+ **Và TÊN không phải của bạn — nó là của antd.** `docs/DESIGN-AUTHORITY.md` (mục
105
+ "The PROP SURFACE of a component is antd's too"): **nơi antd đặt tên cho một năng
106
+ lực, kho này lấy nguyên tên và nguyên ngữ nghĩa của antd. Một năng lực còn thiếu
107
+ được port từ antd 100% TRƯỚC — tên, prop, ngữ nghĩa — rồi mới cải tiến. Không
108
+ thiết kế lại trước, và không port một nửa.**
109
+
110
+ Đây là luật mới nhất và là luật hay bị bỏ qua nhất, vì nó nghe như lời khuyên.
111
+ Cái giá của việc bỏ qua đã đo được: `DataTable` mọc `pin: "end"` nơi antd có
112
+ `fixed`, `sortable: true` nơi antd có `sorter`, và không có câu trả lời nào cho
113
+ filter/expandable — mỗi lần một người quyết một kiểu. Cùng hình dạng lỗi:
114
+ `Dialog` + `AlertDialog` từng là 26 export với **12 cặp trùng tên** và **0** phần
115
+ chỉ `AlertDialog` mới có, trong khi antd chỉ có MỘT `Modal` (nguy hiểm là một
116
+ PROP, `okType`). Nay là một họ, `variant` là lối chuẩn, 12 export cũ ở lại vì gỡ
117
+ là breaking. Và cả họ Ant Design X từng tới nửa vời — thiếu `Conversations`,
118
+ `Attachments`, `ThoughtChain`, `Welcome`, `Actions`, nay đã đủ.
119
+
120
+ Ba chỗ cố ý lệch khỏi antd (logical thay `left/right`, từ vựng giá trị của kho
121
+ này, `density` thay `size`) đều **ghi lý do trong DESIGN-AUTHORITY**. Lệch thêm
122
+ thì phải viết vào đó, không lệch lặng lẽ. Và đọc bề mặt antd từ **type đã cài ở
123
+ một checkout khác**, không từ trí nhớ — `antd` đã bị gỡ khỏi devDependencies của
124
+ kho này từ 20.0.0 và `check:no-antd-runtime` canh cho nó không quay lại.
125
+
104
126
  ### Bước 4 — Kiểm bằng tarball TRƯỚC khi phát hành
105
127
 
106
128
  Đây là bước làm cho "vừa làm vừa trải nghiệm" thành thật. Đừng phát hành rồi
@@ -303,15 +325,22 @@ không tồn tại nên không hỏi**.
303
325
  Trước khi viết bất kỳ bố cục nào: `search_components` + `get_component`. Rẻ hơn
304
326
  mọi lần sửa sau.
305
327
 
306
- **Nhưng catalog chở PROP, không chở LUẬT BỐ CỤC** — và đó một khoảng trống
307
- thật của catalog, không chỉ lỗi của người dùng nó. Ví dụ đo được: `CardBar`
308
- trong manifest có đúng một prop (`extra`), không dòng nào nói nó **tự lấy đường
309
- kẻ theo VỊ TRÍ** đầu thì kẻ dưới, cuối thì kẻ trên, ở giữa thì cả hai. Luật ấy
310
- chỉ nằm trong chú thích của `src/styles/card-layout.css`, cùng chỗ định nghĩa hai
311
- nhịp `section` (header phẳng) `band` (header có kẻ).
312
-
313
- Nên khi làm bố cục bên trong một component của DS: **mở tệp `*-layout.css` của
314
- ra đọc**. Một agent hỏi MCP đúng cách vẫn sẽ không biết những luật này.
328
+ **Catalog từng chở PROP không chở LUẬT BỐ CỤC — và dụ ấy nay đã được vá,
329
+ nên đừng trích nữa.** Bản trước của mục này viết: `CardBar` trong manifest có
330
+ đúng một prop (`extra`), không dòng nào nói nó tự lấy đường kẻ theo VỊ TRÍ. Đo
331
+ lại hôm nay: `CardBar` **6 prop**, trong đó `border` (`"none" | "block-start"
332
+ | "block-end" | "both"`) ép được đường kẻ khi xếp chồng, và luật vị trí (đầu: kẻ
333
+ dưới · cuối: kẻ trên · giữa: cả hai) nằm **trong chính catalog** — ở `usage` của
334
+ `CardBar` lẫn `description` của `Card`.
335
+
336
+ Bài học còn lại vẫn thật, chỉ đổi hình: một luật bố cục **có thể** chỉ sống
337
+ trong chú thích CSS, và không có cổng nào bắt nó phải lên catalog. Nên:
338
+
339
+ 1. Hỏi `get_component` trước — nay nó thường trả lời cả hình dạng lẫn luật.
340
+ 2. `usage`/`description` im lặng về bố cục → mở `src/styles/*-layout.css` của
341
+ component ấy ra đọc (chỗ định nghĩa hai nhịp `section` và `band` chẳng hạn).
342
+ 3. Là chuột bạch, khi bước 2 phải dùng tới, đó là **khoảng trống catalog** — đưa
343
+ luật ấy lên `usage` trong cùng PR, theo §4. Đó là cách `CardBar` được vá.
315
344
 
316
345
  ## 6. Thứ KHÔNG đẩy lên DS
317
346
 
@@ -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
 
@@ -1006,7 +1022,7 @@ function staleOwnedRules() {
1006
1022
  `This file is written by @godxjp/ui and says version ${stamped}, but the installed package ` +
1007
1023
  `is ${installed}. Its rules describe a different library than the one you are building ` +
1008
1024
  "against — most likely because `ignore-scripts=true` kept our postinstall from running.",
1009
- replacement: 'INIT_CWD="$PWD" node node_modules/@godxjp/ui/scripts/postinstall.mjs',
1025
+ replacement: "npx @godxjp/ui sync-rules",
1010
1026
  snippet: `<!-- godxjp-ui:version ${stamped} --> vs installed ${installed}`,
1011
1027
  };
1012
1028
  }