@godxjp/ui 23.4.0 → 23.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,8 +73,21 @@ export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
73
73
  */
74
74
  tabular?: boolean;
75
75
  children?: React.ReactNode;
76
+ /**
77
+ * antd `Tag` `closable` + `onClose` — one callback when the user activates the × the chip draws.
78
+ * Named `onRemove` (not `onClose`) because this DS already uses `onClose` on overlays and
79
+ * `onValueChange` on fields; the chip remover is not a dismiss surface. Omitting it is antd
80
+ * `closable={false}`: no × is rendered.
81
+ *
82
+ * The × accessible name is built from the visible label (`children`, or the resolved `status`
83
+ * label) via `navigation.filterBar.removeFilter` — pass a string `children` (or plain text
84
+ * label) so the name quotes the chip, e.g. `getByRole('button', { name: /期限: 今週/ })`.
85
+ */
86
+ onRemove?: () => void;
87
+ /** Disables only the × when `onRemove` is set (FilterBar bar/chip disabled). Label stays visible. */
88
+ removeDisabled?: boolean;
76
89
  }
77
- export declare function Badge({ as: Element, className, variant, shape, tone, color, icon, status, tabular, style, children, ...props }: BadgeProps): React.JSX.Element;
90
+ export declare function Badge({ as: Element, className, variant, shape, tone, color, icon, status, tabular, style, children, onRemove, removeDisabled, ...props }: BadgeProps): React.JSX.Element;
78
91
  /**
79
92
  * Status-aware badge with the shared domain-status-to-tone mapping. `Badge` remains the
80
93
  * general-purpose primitive.
@@ -9,6 +9,7 @@ import {
9
9
  Pause,
10
10
  Play,
11
11
  Trash2,
12
+ X,
12
13
  XCircle
13
14
  } from "lucide-react";
14
15
  import { useTranslation } from "../../i18n/use-translation.js";
@@ -100,6 +101,8 @@ function Badge({
100
101
  tabular,
101
102
  style,
102
103
  children,
104
+ onRemove,
105
+ removeDisabled,
103
106
  ...props
104
107
  }) {
105
108
  const { t } = useTranslation();
@@ -108,6 +111,7 @@ function Badge({
108
111
  const ResolvedIcon = icon === void 0 ? statusDef?.icon : icon;
109
112
  const resolvedChildren = children ?? (status ? status in STATUS_MAP ? t(`status.${status}`) : status : void 0);
110
113
  const tinted = color != null && color !== "";
114
+ const removeLabel = typeof children === "string" ? children : typeof resolvedChildren === "string" ? resolvedChildren : void 0;
111
115
  return /* @__PURE__ */ jsxs(
112
116
  Element,
113
117
  {
@@ -116,6 +120,7 @@ function Badge({
116
120
  "data-tinted": tinted ? "" : void 0,
117
121
  "data-shape": shape ?? "default",
118
122
  "data-tabular": tabular ? "" : void 0,
123
+ "data-removable": onRemove ? "" : void 0,
119
124
  className: cn(
120
125
  badgeVariants({
121
126
  variant: tinted ? "tinted" : variant ?? "default",
@@ -128,7 +133,22 @@ function Badge({
128
133
  ...props,
129
134
  children: [
130
135
  ResolvedIcon ? /* @__PURE__ */ jsx(ResolvedIcon, { "data-slot": "badge-icon", "aria-hidden": "true" }) : null,
131
- resolvedChildren != null ? /* @__PURE__ */ jsx("span", { "data-slot": "badge-label", children: resolvedChildren }) : null
136
+ resolvedChildren != null ? /* @__PURE__ */ jsx("span", { "data-slot": "badge-label", children: resolvedChildren }) : null,
137
+ onRemove ? /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ "data-slot": "badge-remove",
142
+ className: "ui-control-inline-affix-action ui-badge-remove",
143
+ "aria-label": removeLabel != null ? t("navigation.filterBar.removeFilter", { label: removeLabel }) : t("common.delete"),
144
+ onClick: (event) => {
145
+ event.stopPropagation();
146
+ onRemove();
147
+ },
148
+ disabled: removeDisabled,
149
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
150
+ }
151
+ ) : null
132
152
  ]
133
153
  }
134
154
  );
@@ -4,10 +4,12 @@ import type { BreakpointProp, ColumnDefProp, DensityProp, OnColumnFilterChangePr
4
4
  export type Density = DensityProp;
5
5
  /**
6
6
  * Lean column definition — the simple, common-case column API. `render` shapes a cell; `sortable`
7
- * opts the column into the sort cycle; `align` / `width` / `pin` / `hiddenOnMobile` / `priority`
8
- * tune layout; `enableHiding` (default true) lists the column in DataTable.ViewOptions.
7
+ * opts the column into the sort cycle; `align` / `width` / `pin` / `hideBelow` / `hiddenOnMobile` /
8
+ * `priority` tune layout; `enableHiding` (default true) lists the column in DataTable.ViewOptions.
9
9
  */
10
10
  export type ColumnDef<T> = ColumnDefProp<T>;
11
+ /** Same resolution DataTable stamps as `data-hide-below` — `hiddenOnMobile: true` → `md`. */
12
+ export declare function resolveColumnHideBelow(col: Pick<ColumnDef<unknown>, "hideBelow" | "hiddenOnMobile">): BreakpointProp | undefined;
11
13
  interface DataTableProps<T> {
12
14
  data: T[];
13
15
  columns: ColumnDef<T>[];
@@ -73,6 +73,15 @@ import {
73
73
  tableCellPaddingClass,
74
74
  tableRowHeightClass
75
75
  } from "../../lib/control-styles.js";
76
+ function resolveColumnHideBelow(col) {
77
+ if (col.hideBelow) return col.hideBelow;
78
+ if (col.hiddenOnMobile) return "md";
79
+ return void 0;
80
+ }
81
+ function columnHideBelowProps(col) {
82
+ const step = resolveColumnHideBelow(col);
83
+ return step ? { "data-hide-below": step } : {};
84
+ }
76
85
  function dataTableFeatures() {
77
86
  return tableFeatures({
78
87
  rowSortingFeature,
@@ -861,7 +870,6 @@ DataTable.Content = function DataTableContent() {
861
870
  columnWidth(col.width).className,
862
871
  col.align === "right" && "text-end",
863
872
  col.align === "center" && "text-center",
864
- col.hiddenOnMobile && "hidden md:table-cell",
865
873
  col.ellipsis && "ui-data-table-ellipsis",
866
874
  edge === "end" && "ui-data-table-pin-end",
867
875
  edge === "start" && "ui-data-table-pin-start"
@@ -991,6 +999,7 @@ DataTable.Content = function DataTableContent() {
991
999
  "data-align": col.headerAlign ?? col.align,
992
1000
  "aria-sort": isSortable ? activeDirection ? activeDirection === "asc" ? "ascending" : "descending" : "none" : void 0,
993
1001
  ...fixedCellProps(col.key, fixedEdge(col)),
1002
+ ...columnHideBelowProps(col),
994
1003
  style: columnCellStyle(col),
995
1004
  className: cn(
996
1005
  columnCellClass(col),
@@ -1029,6 +1038,7 @@ DataTable.Content = function DataTableContent() {
1029
1038
  {
1030
1039
  priority: col.priority,
1031
1040
  "data-align": col.align,
1041
+ ...columnHideBelowProps(col),
1032
1042
  style: columnCellStyle(col),
1033
1043
  className: cn(cellPadding, columnCellClass(col)),
1034
1044
  children: /* @__PURE__ */ jsx(
@@ -1252,6 +1262,7 @@ DataTable.Content = function DataTableContent() {
1252
1262
  "data-align": col.align,
1253
1263
  title,
1254
1264
  ...fixedCellProps(col.key, fixedEdge(col)),
1265
+ ...columnHideBelowProps(col),
1255
1266
  style: columnCellStyle(col),
1256
1267
  className: cn(cellPadding, columnCellClass(col)),
1257
1268
  children: rendered
@@ -1410,5 +1421,6 @@ DataTable.RowActions = function DataTableRowActions({ ariaLabel, children }) {
1410
1421
  DataTable.RowActions.displayName = "DataTable.RowActions";
1411
1422
  export {
1412
1423
  DataTable,
1413
- flexRender
1424
+ flexRender,
1425
+ resolveColumnHideBelow
1414
1426
  };
@@ -131,22 +131,17 @@ function Toolbar({
131
131
  role: "group",
132
132
  "aria-label": t("navigation.filterBar.appliedFilters"),
133
133
  className: "ui-filter-bar-chips",
134
- children: chips.map((chip) => /* @__PURE__ */ jsxs("span", { className: "ui-filter-bar-chip", children: [
135
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: chip.label }),
136
- onChipRemove && /* @__PURE__ */ jsx(
137
- Button,
138
- {
139
- variant: "ghost",
140
- size: "icon-sm",
141
- disabled: disabled || chip.disabled,
142
- "aria-label": t("navigation.filterBar.removeFilter", {
143
- label: typeof chip.label === "string" ? chip.label : chip.value
144
- }),
145
- onClick: () => onChipRemove(chip.value),
146
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
147
- }
148
- )
149
- ] }, chip.value))
134
+ children: chips.map((chip) => /* @__PURE__ */ jsx(
135
+ Badge,
136
+ {
137
+ variant: "outline",
138
+ className: "ui-filter-bar-chip",
139
+ onRemove: onChipRemove ? () => onChipRemove(chip.value) : void 0,
140
+ removeDisabled: Boolean(disabled || chip.disabled),
141
+ children: typeof chip.label === "string" ? chip.label : chip.value
142
+ },
143
+ chip.value
144
+ ))
150
145
  }
151
146
  ),
152
147
  showMeta && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-meta", children: error != null ? /* @__PURE__ */ jsx("p", { role: "alert", className: "ui-filter-bar-error", children: error }) : /* @__PURE__ */ jsx("p", { role: "status", className: "ui-filter-bar-count", children: t("navigation.filterBar.resultCount", { count: resultCount }) }) })
@@ -1,6 +1,23 @@
1
1
  import * as React from "react";
2
+ /** Counter pill fields on {@link SegmentedOption} — same vocabulary as `Button` / `Toggle`. */
3
+ type SegmentedCountFields = {
4
+ /**
5
+ * Optional count rendered as a borderless pill after the label — do not nest `Badge` in `label`
6
+ * for this (gh#602): `Badge` `secondary` is `--muted`, which matches the Segmented track.
7
+ */
8
+ count?: number | string;
9
+ /** Cap for numeric `count` — beyond it the pill shows `{overflowCount}+` (e.g. `99+`). */
10
+ overflowCount?: number;
11
+ /** Render the pill when numeric `count` is 0. Default `true`. */
12
+ showZero?: boolean;
13
+ /**
14
+ * Localized description of what the count means, folded into the accessible name
15
+ * (`全件, 54 件` when supplied).
16
+ */
17
+ countLabel?: string;
18
+ };
2
19
  /** One choice in a {@link Segmented}. */
3
- export type SegmentedOption = {
20
+ export type SegmentedOption = SegmentedCountFields & {
4
21
  /** Wire value — what `onValueChange` reports and what a form submits. */
5
22
  value: string;
6
23
  /** Visible label. It is also the item's accessible name, so it is required. */
@@ -69,3 +86,4 @@ export type SegmentedProps = SegmentedProp;
69
86
  * row each of them means.
70
87
  */
71
88
  export declare const Segmented: React.ForwardRefExoticComponent<SegmentedProp & React.RefAttributes<HTMLDivElement>>;
89
+ export {};
@@ -1,9 +1,32 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Radio as AriaRadio, RadioGroup as AriaRadioGroup } from "react-aria-components";
5
5
  import { withOwnHitTarget } from "../data-entry/choice-hit-target.js";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { useTranslation } from "../../i18n/use-translation.js";
8
+ import { numberFormat } from "../../lib/intl-cache.js";
9
+ function SegmentedCountPill({
10
+ count,
11
+ overflowCount = 99,
12
+ showZero = true,
13
+ countLabel
14
+ }) {
15
+ const { locale } = useTranslation();
16
+ const visible = count != null && count !== "" && (typeof count !== "number" || count !== 0 || showZero);
17
+ const formatted = React.useMemo(() => {
18
+ if (count == null || count === "") return "";
19
+ if (typeof count === "string") return count;
20
+ const format = numberFormat(locale);
21
+ return count > overflowCount ? `${format.format(overflowCount)}+` : format.format(count);
22
+ }, [count, locale, overflowCount]);
23
+ if (!visible) return null;
24
+ const spoken = countLabel ? `${formatted} ${countLabel}` : formatted;
25
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
26
+ /* @__PURE__ */ jsx("span", { "data-slot": "segmented-count", className: "ui-segmented-count", "aria-hidden": "true", children: formatted }),
27
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${spoken}` })
28
+ ] });
29
+ }
7
30
  const Segmented = React.forwardRef(function Segmented2({
8
31
  options,
9
32
  value,
@@ -59,7 +82,16 @@ const Segmented = React.forwardRef(function Segmented2({
59
82
  children: option.icon
60
83
  }
61
84
  ),
62
- /* @__PURE__ */ jsx("span", { "data-slot": "segmented-item-label", className: "ui-segmented-item-label", children: option.label })
85
+ /* @__PURE__ */ jsx("span", { "data-slot": "segmented-item-label", className: "ui-segmented-item-label", children: option.label }),
86
+ /* @__PURE__ */ jsx(
87
+ SegmentedCountPill,
88
+ {
89
+ count: option.count,
90
+ overflowCount: option.overflowCount,
91
+ showZero: option.showZero,
92
+ countLabel: option.countLabel
93
+ }
94
+ )
63
95
  ]
64
96
  },
65
97
  option.value
@@ -289,6 +289,11 @@ export type BadgeProp = {
289
289
  tabular?: boolean;
290
290
  className?: ClassNameProp;
291
291
  children?: ChildrenProp;
292
+ /**
293
+ * antd `Tag` `closable` + `onClose` — omit for a plain chip; set to draw a × that calls this once.
294
+ * Port name `onRemove` (not `onClose`) — see {@link Badge} JSDoc and `docs/DESIGN-AUTHORITY.md`.
295
+ */
296
+ onRemove?: () => void;
292
297
  };
293
298
  /** @see CredentialReveal */
294
299
  /**
@@ -627,7 +627,7 @@ export declare const VOCABULARY_REGISTRY: {
627
627
  readonly ColumnDefProp: {
628
628
  readonly file: "vocabulary/data.prop.ts";
629
629
  readonly category: "data";
630
- readonly description: 'DataTable column definition — key/header/render/sortable/align/width/pin/hiddenOnMobile/enableHiding/ariaLabel plus `priority` (TableColumnPriorityProp), read by DataTable preset="action-collection"';
630
+ readonly description: 'DataTable column definition — key/header/render/sortable/align/width/pin/hideBelow/hiddenOnMobile/enableHiding/ariaLabel plus `priority` (TableColumnPriorityProp), read by DataTable preset="action-collection"';
631
631
  };
632
632
  readonly SelectedIdsProp: {
633
633
  readonly file: "vocabulary/data.prop.ts";
@@ -628,7 +628,7 @@ const VOCABULARY_REGISTRY = {
628
628
  ColumnDefProp: {
629
629
  file: "vocabulary/data.prop.ts",
630
630
  category: "data",
631
- description: 'DataTable column definition \u2014 key/header/render/sortable/align/width/pin/hiddenOnMobile/enableHiding/ariaLabel plus `priority` (TableColumnPriorityProp), read by DataTable preset="action-collection"'
631
+ description: 'DataTable column definition \u2014 key/header/render/sortable/align/width/pin/hideBelow/hiddenOnMobile/enableHiding/ariaLabel plus `priority` (TableColumnPriorityProp), read by DataTable preset="action-collection"'
632
632
  },
633
633
  SelectedIdsProp: {
634
634
  file: "vocabulary/data.prop.ts",
@@ -38,6 +38,17 @@ export type ColumnDefProp<T> = {
38
38
  * usual case.
39
39
  */
40
40
  headerAlign?: ColumnAlignProp;
41
+ /**
42
+ * Hide this column below a canonical viewport step — the SAME contract as `Flex hideBelow`
43
+ * (sm 40rem · md 48rem · lg 64rem · xl 80rem). Stamped on both `<th>` and `<td>` as
44
+ * `data-hide-below`; a media query cannot read a `var()`, so the step values are the tokenized
45
+ * scale written out in the stylesheet. Wins over `hiddenOnMobile` when both are set.
46
+ */
47
+ hideBelow?: BreakpointProp;
48
+ /**
49
+ * @deprecated Prefer `hideBelow: 'md'`. When `hideBelow` is omitted, `true` is an alias for
50
+ * `hideBelow: 'md'`.
51
+ */
41
52
  hiddenOnMobile?: boolean;
42
53
  /**
43
54
  * List this column in DataTable.ViewOptions (the column show/hide "set view"
@@ -50,4 +50,29 @@
50
50
  height: var(--badge-icon-size);
51
51
  flex-shrink: 0;
52
52
  }
53
+
54
+ [data-slot="badge"][data-removable] {
55
+ padding-inline-end: var(--badge-space-x-removable);
56
+ }
57
+
58
+ [data-slot="badge-remove"] {
59
+ flex-shrink: 0;
60
+ margin-inline-end: var(--badge-remove-offset-inline-end);
61
+ color: inherit;
62
+ opacity: var(--badge-remove-rest-alpha);
63
+ border: 0;
64
+ padding: 0;
65
+ cursor: pointer;
66
+ background: transparent;
67
+ }
68
+
69
+ [data-slot="badge-remove"]:hover,
70
+ [data-slot="badge-remove"]:focus-visible {
71
+ opacity: var(--badge-remove-hover-alpha);
72
+ }
73
+
74
+ [data-slot="badge-remove"] svg {
75
+ width: var(--badge-remove-icon-size);
76
+ height: var(--badge-remove-icon-size);
77
+ }
53
78
  }
@@ -1136,6 +1136,22 @@
1136
1136
  vertical-align: middle;
1137
1137
  }
1138
1138
 
1139
+ .ui-segmented-count {
1140
+ display: inline-flex;
1141
+ flex: 0 0 auto;
1142
+ align-items: center;
1143
+ justify-content: center;
1144
+ min-inline-size: var(--segmented-count-min-width);
1145
+ margin-inline-start: var(--segmented-count-gap);
1146
+ border-radius: var(--segmented-count-radius);
1147
+ padding-inline: var(--segmented-count-space-inline);
1148
+ background: var(--segmented-count-background, hsl(var(--primary)));
1149
+ color: var(--segmented-count-color, hsl(var(--primary-foreground)));
1150
+ font-size: var(--segmented-count-font-size);
1151
+ line-height: 1;
1152
+ font-variant-numeric: tabular-nums;
1153
+ }
1154
+
1139
1155
  .ui-tag-input {
1140
1156
  display: flex;
1141
1157
  flex-wrap: wrap;
@@ -18,6 +18,7 @@
18
18
  .ui-control-inline-affix-action,
19
19
  .ui-search-input-clear,
20
20
  .ui-tag-input-remove,
21
+ .ui-badge-remove,
21
22
  .ui-color-picker-input,
22
23
  .ui-upload-tile-add,
23
24
  .ui-upload-picture-empty,
@@ -78,6 +79,7 @@
78
79
  .ui-control-inline-affix-action,
79
80
  .ui-search-input-clear,
80
81
  .ui-tag-input-remove,
82
+ .ui-badge-remove,
81
83
  .ui-color-picker-input,
82
84
  .ui-upload-tile-add,
83
85
  .ui-upload-picture-empty,
@@ -1238,7 +1238,6 @@
1238
1238
  .ui-filter-bar-chip {
1239
1239
  display: inline-flex;
1240
1240
  align-items: center;
1241
- gap: var(--space-1);
1242
1241
  min-width: 0;
1243
1242
  }
1244
1243
 
@@ -1417,28 +1416,36 @@
1417
1416
 
1418
1417
  @media (width < 40rem) {
1419
1418
  .ui-flex[data-hide-below="sm"],
1420
- .ui-topbar-item[data-hide-below="sm"] {
1419
+ .ui-topbar-item[data-hide-below="sm"],
1420
+ [data-slot="table-head"][data-hide-below="sm"],
1421
+ [data-slot="table-cell"][data-hide-below="sm"] {
1421
1422
  display: none;
1422
1423
  }
1423
1424
  }
1424
1425
 
1425
1426
  @media (width < 48rem) {
1426
1427
  .ui-flex[data-hide-below="md"],
1427
- .ui-topbar-item[data-hide-below="md"] {
1428
+ .ui-topbar-item[data-hide-below="md"],
1429
+ [data-slot="table-head"][data-hide-below="md"],
1430
+ [data-slot="table-cell"][data-hide-below="md"] {
1428
1431
  display: none;
1429
1432
  }
1430
1433
  }
1431
1434
 
1432
1435
  @media (width < 64rem) {
1433
1436
  .ui-flex[data-hide-below="lg"],
1434
- .ui-topbar-item[data-hide-below="lg"] {
1437
+ .ui-topbar-item[data-hide-below="lg"],
1438
+ [data-slot="table-head"][data-hide-below="lg"],
1439
+ [data-slot="table-cell"][data-hide-below="lg"] {
1435
1440
  display: none;
1436
1441
  }
1437
1442
  }
1438
1443
 
1439
1444
  @media (width < 80rem) {
1440
1445
  .ui-flex[data-hide-below="xl"],
1441
- .ui-topbar-item[data-hide-below="xl"] {
1446
+ .ui-topbar-item[data-hide-below="xl"],
1447
+ [data-slot="table-head"][data-hide-below="xl"],
1448
+ [data-slot="table-cell"][data-hide-below="xl"] {
1442
1449
  display: none;
1443
1450
  }
1444
1451
  }
@@ -12,6 +12,12 @@
12
12
 
13
13
  --badge-icon-size: var(--icon-size-xs);
14
14
 
15
+ --badge-space-x-removable: var(--space-1);
16
+ --badge-remove-offset-inline-end: calc(-1 * var(--space-1));
17
+ --badge-remove-icon-size: var(--badge-icon-size);
18
+ --badge-remove-rest-alpha: 0.7;
19
+ --badge-remove-hover-alpha: 1;
20
+
15
21
  --badge-tint-fill: 18%;
16
22
  --badge-tint-edge: 45%;
17
23
 
@@ -22,4 +22,12 @@
22
22
 
23
23
  --segmented-item-selected-background: var(--background);
24
24
  --segmented-item-selected-shadow: var(--shadow-sm);
25
+
26
+ --segmented-count-min-width: var(--button-count-min-width);
27
+ --segmented-count-space-inline: var(--button-count-space-inline);
28
+ --segmented-count-font-size: var(--button-count-font-size);
29
+ --segmented-count-radius: var(--radius-pill);
30
+ --segmented-count-gap: var(--space-1);
31
+ --segmented-count-background: initial;
32
+ --segmented-count-color: initial;
25
33
  }
@@ -242,6 +242,17 @@ in page CSS.
242
242
  - **A capability this library already has keeps its own name.** antd's `size`
243
243
  (`small | middle | large`) IS `density` (`compact | default | comfortable`); antd's `locale` IS
244
244
  the `t()` layer. Adding the antd spelling as an alias would be duplication, not parity.
245
+ - **`SegmentedOption.count`.** antd's `SegmentedItemType` has no count field — consumers embed counts
246
+ in `label`. Nested `Badge` `secondary` is `--muted`, which matches the Segmented track
247
+ (gh#602, measured 1.00:1). This library adds `count` / `overflowCount` / `showZero` /
248
+ `countLabel` on `SegmentedOption` with a DS-owned pill (`--segmented-count-*` tokens), same
249
+ counter vocabulary as `Button` / `Toggle`.
250
+ - **`Badge.onRemove` instead of antd `Tag`'s `closable` + `onClose`.** Semantics match antd's
251
+ closable tag (a chip that draws its own × and fires one callback). The boolean is folded into
252
+ prop presence (`onRemove` omitted ⇒ no ×), and the callback is named `onRemove` rather than
253
+ `onClose`, because `onClose` already means overlay dismiss across Dialog/Drawer and would read
254
+ as closing a surface, not removing one applied filter chip. Implemented on `Badge` — the DS chip
255
+ primitive — rather than adding a separate `Tag` export beside `Badge`.
245
256
 
246
257
  **A knob that only a fork could reach is not parity either.** antd's `components`,
247
258
  `filterDropdown`, `classNames`/`styles` semantic maps and `prefixCls` all exist to let a consumer
@@ -1,11 +1,4 @@
1
- import {
2
- Badge,
3
- Card,
4
- CardContent,
5
- CardDescription,
6
- CardHeader,
7
- CardTitle,
8
- } from "@godxjp/ui/data-display";
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
9
2
  import { Segmented } from "@godxjp/ui/data-entry";
10
3
  import { Button, Text } from "@godxjp/ui/general";
11
4
  import { Flex, PageContainer } from "@godxjp/ui/layout";
@@ -24,17 +17,7 @@ const OPTIONS = [
24
17
  { value: "ginou", label: "技能実習", count: 82 },
25
18
  { value: "tokutei", label: "特定技能", count: 51 },
26
19
  { value: "ikusei", label: "育成就労", count: 0 },
27
- ].map(({ value, label, count }) => ({
28
- value,
29
- label: (
30
- <>
31
- {label}
32
- <Badge as="span" variant="secondary">
33
- {count}
34
- </Badge>
35
- </>
36
- ),
37
- }));
20
+ ];
38
21
 
39
22
  export default function SegmentedInFilterRow() {
40
23
  return (
@@ -1,13 +1,6 @@
1
1
  import { useState } from "react";
2
2
 
3
- import {
4
- Badge,
5
- Card,
6
- CardContent,
7
- CardDescription,
8
- CardHeader,
9
- CardTitle,
10
- } from "@godxjp/ui/data-display";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
11
4
  import { FormField, Segmented } from "@godxjp/ui/data-entry";
12
5
  import { Text, VisuallyHidden } from "@godxjp/ui/general";
13
6
  import { Flex, PageContainer } from "@godxjp/ui/layout";
@@ -108,17 +101,7 @@ export default function Demo() {
108
101
  { value: "active", label: "実習中", count: 96 },
109
102
  { value: "pending", label: "申請中", count: 12 },
110
103
  { value: "gone", label: "失踪・帰国", count: 0 },
111
- ].map(({ value, label, count }) => ({
112
- value,
113
- label: (
114
- <>
115
- {label}
116
- <Badge as="span" variant="secondary">
117
- {count}
118
- </Badge>
119
- </>
120
- ),
121
- }))}
104
+ ]}
122
105
  />
123
106
  </CardContent>
124
107
  </Card>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "23.4.0",
4
- "godxUiMcp": "23.4.0",
3
+ "version": "23.4.2",
4
+ "godxUiMcp": "23.4.2",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,13 +24,37 @@ import {
24
24
  import { basename, dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
 
27
- /** The godxjp-ui MCP server — pulled on demand via npx (no extra dependency to ship). */
28
27
  /** This package's own root — the source of the version we stamp with. */
29
28
  const SELF_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
30
29
 
31
- export const MCP_SERVER = { command: "npx", args: ["@godxjp/ui-mcp"] };
32
30
  export const MCP_KEY = "godx-ui";
33
31
 
32
+ /** @deprecated Prefer `mcpServerFor(root)` — bare npx resolves latest on the registry (gh#543). */
33
+ export const MCP_SERVER = { command: "npx", args: ["@godxjp/ui-mcp"] };
34
+
35
+ function mcpEntryMatches(a, b) {
36
+ if (a?.command !== b?.command) return false;
37
+ const argsA = a?.args ?? [];
38
+ const argsB = b?.args ?? [];
39
+ if (argsA.length !== argsB.length || argsA.some((x, i) => x !== argsB[i])) return false;
40
+ const envA = a?.env ?? {};
41
+ const envB = b?.env ?? {};
42
+ const keysA = Object.keys(envA).sort();
43
+ const keysB = Object.keys(envB).sort();
44
+ if (keysA.length !== keysB.length || keysA.some((k, i) => k !== keysB[i])) return false;
45
+ return keysA.every((k) => envA[k] === envB[k]);
46
+ }
47
+
48
+ function mcpConfigMismatchMessage(root, existing, expected) {
49
+ const ui = readConsumerUiMetadata(root);
50
+ const pin = expected.args?.[0] ?? "@godxjp/ui-mcp";
51
+ const ver = ui?.version ?? "(unknown)";
52
+ return (
53
+ `present (custom godx-ui MCP entry — not overwritten; expected ${pin} with ` +
54
+ `env.GODX_UI_VERSION=${ver} from node_modules/@godxjp/ui)`
55
+ );
56
+ }
57
+
34
58
  /** Commands wired into the consumer's .claude/settings.json. */
35
59
  export const AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.mjs";
36
60
  export const PRIMER_CMD = "cat .claude/godxjp-ui-workflow.md";
@@ -46,6 +70,31 @@ const SUGGESTED_HOOKS = {
46
70
  /** The per-session workflow mandate the SessionStart hook injects into the agent. */
47
71
  export const KIT_VERSION = readJson(join(SELF_ROOT, "package.json"))?.version ?? "0.0.0";
48
72
 
73
+ /**
74
+ * Read the consumer's installed @godxjp/ui metadata (postinstall runs with UI already in
75
+ * node_modules). Returns null when absent — caller falls back to this package's release train.
76
+ */
77
+ export function readConsumerUiMetadata(root) {
78
+ const uiPkg = readJson(join(root, "node_modules/@godxjp/ui/package.json"));
79
+ if (!uiPkg?.version) return null;
80
+ return {
81
+ version: String(uiPkg.version),
82
+ godxUiMcp: uiPkg.godxUiMcp ? String(uiPkg.godxUiMcp) : String(uiPkg.version),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * MCP launch config for a consumer root: pin @godxjp/ui-mcp to godxUiMcp from the installed UI
88
+ * package and pass the real UI version via env (gh#543 — MCP must not read the repo to learn it).
89
+ */
90
+ export function mcpServerFor(root) {
91
+ const meta = readConsumerUiMetadata(root);
92
+ const mcpPin = meta?.godxUiMcp ?? KIT_VERSION;
93
+ const server = { command: "npx", args: [`@godxjp/ui-mcp@${mcpPin}`] };
94
+ if (meta?.version) server.env = { GODX_UI_VERSION: meta.version };
95
+ return server;
96
+ }
97
+
49
98
  const STAMP = (v) => `<!-- godxjp-ui:version ${v} -->`;
50
99
  const STAMP_RE = /<!-- godxjp-ui:version ([^\s]+) -->/;
51
100
 
@@ -291,13 +340,11 @@ export function refreshBlock(current, next, startMarker, endMarker) {
291
340
 
292
341
  export function ensureMcpJson(root) {
293
342
  const path = join(root, ".mcp.json");
343
+ const expected = mcpServerFor(root);
344
+ const suggested = JSON.stringify({ mcpServers: { [MCP_KEY]: expected } }, null, 2) + "\n";
294
345
  const read = readJsonFile(path);
295
346
  if (read.state !== "ok" && read.state !== "missing") {
296
- return refuseAndSuggest(
297
- path,
298
- JSON.stringify({ mcpServers: { [MCP_KEY]: MCP_SERVER } }, null, 2) + "\n",
299
- READ_FAILURE[read.state],
300
- );
347
+ return refuseAndSuggest(path, suggested, READ_FAILURE[read.state]);
301
348
  }
302
349
  const json = read.state === "ok" ? read.json : {};
303
350
  if (
@@ -306,16 +353,17 @@ export function ensureMcpJson(root) {
306
353
  typeof json.mcpServers !== "object" ||
307
354
  Array.isArray(json.mcpServers))
308
355
  ) {
309
- return refuseAndSuggest(
310
- path,
311
- JSON.stringify({ mcpServers: { [MCP_KEY]: MCP_SERVER } }, null, 2) + "\n",
312
- "`mcpServers` is not an object",
313
- );
356
+ return refuseAndSuggest(path, suggested, "`mcpServers` is not an object");
314
357
  }
315
358
  json.mcpServers = json.mcpServers ?? {};
316
- if (json.mcpServers[MCP_KEY]) return "present";
359
+ if (json.mcpServers[MCP_KEY]) {
360
+ if (!mcpEntryMatches(json.mcpServers[MCP_KEY], expected)) {
361
+ return mcpConfigMismatchMessage(root, json.mcpServers[MCP_KEY], expected);
362
+ }
363
+ return "present";
364
+ }
317
365
  const created = read.state === "missing";
318
- json.mcpServers[MCP_KEY] = MCP_SERVER;
366
+ json.mcpServers[MCP_KEY] = expected;
319
367
  writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
320
368
  return created ? "created" : "added";
321
369
  }
@@ -22,7 +22,7 @@ try {
22
22
  const r = ensureMcpJson(root);
23
23
  // A refusal is a full sentence, not one of the three status words — say it on its own line
24
24
  // rather than folding it into "MCP in .mcp.json (…)", where it would read as a success.
25
- if (r.startsWith("left untouched")) {
25
+ if (r.startsWith("left untouched") || r.startsWith("present (custom godx-ui")) {
26
26
  console.log(`\n @godxjp/ui → .mcp.json ${r}\n`);
27
27
  }
28
28
  // The mandate is plain text the agent reads every turn (CLAUDE.md block + workflow file). It
@@ -797,12 +797,76 @@ function walk(dir, acc = []) {
797
797
  return acc;
798
798
  }
799
799
 
800
+ /** True when `index` sits inside an unclosed backtick template starting at or after `from`. */
801
+ function insideTemplateLiteral(source, from, index) {
802
+ let ticks = 0;
803
+ for (let i = from; i < index; i++) {
804
+ if (source[i] === "\\") {
805
+ i++;
806
+ continue;
807
+ }
808
+ if (source[i] === "`") ticks++;
809
+ }
810
+ return ticks % 2 === 1;
811
+ }
812
+
813
+ /** `<TurnResult>(` is a generic call, not JSX — scanning after its `>` hits `` `/path/${id}` ``. */
814
+ function isGenericTypeBeforeCall(source, openStart, closeIndex) {
815
+ const name = source.slice(openStart + 1, closeIndex);
816
+ if (!/^[A-Z][\w]*$/.test(name)) return false;
817
+ return /^\s*\(/.test(source.slice(closeIndex + 1));
818
+ }
819
+
820
+ /**
821
+ * Hand currency in JSX text after `>`: `>¥{amount}`, `>${price}`, `$1,200` — not `` `/chat/${id}` ``.
822
+ * `$` immediately before `{` inside a template literal is interpolation, never a price sigil.
823
+ */
824
+ function findHandCurrencyInJsxText(source, from) {
825
+ for (let i = from; i < source.length; i++) {
826
+ const char = source[i];
827
+ if (char === "<") break;
828
+ if (char === "{") break;
829
+ if (char === "`") {
830
+ i++;
831
+ while (i < source.length) {
832
+ if (source[i] === "\\") {
833
+ i += 2;
834
+ continue;
835
+ }
836
+ if (source[i] === "`") break;
837
+ i++;
838
+ }
839
+ continue;
840
+ }
841
+ if (char === "$") {
842
+ if (insideTemplateLiteral(source, from, i)) continue;
843
+ const digits = source.slice(i).match(/^\$(?:\d[\d,]*(?:\.\d+)?|\d)/);
844
+ if (digits) return { end: i + digits[0].length };
845
+ const brace = source.slice(i).match(/^\$\s*\{/);
846
+ if (brace && !insideTemplateLiteral(source, from, i)) {
847
+ return { end: i + brace[0].length };
848
+ }
849
+ continue;
850
+ }
851
+ if (/[¥€£₫]/.test(char)) {
852
+ if (insideTemplateLiteral(source, from, i)) continue;
853
+ const brace = source.slice(i).match(/^[¥€£₫]\s*\{/);
854
+ if (brace) return { end: i + brace[0].length };
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+
800
860
  /** Match real JSX text after a balanced opening tag, including props with comparisons. */
801
861
  function* currencyMatches(source) {
802
862
  for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {
803
863
  const end = jsxOpeningEnd(source, opening.index);
804
- const text = source.slice(end + 1).match(/^[^<>{}]*[¥$€£₫]\s*\{/);
805
- if (text) yield { 0: source.slice(opening.index, end + 1) + text[0], index: opening.index };
864
+ if (end >= source.length) continue;
865
+ if (isGenericTypeBeforeCall(source, opening.index, end)) continue;
866
+ const hit = findHandCurrencyInJsxText(source, end + 1);
867
+ if (hit) {
868
+ yield { 0: source.slice(opening.index, hit.end), index: opening.index };
869
+ }
806
870
  }
807
871
  yield* source.matchAll(/\}\s*円\s*</g);
808
872
  }