@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.
- package/dist/components/data-display/badge.d.ts +14 -1
- package/dist/components/data-display/badge.js +21 -1
- package/dist/components/data-display/data-table.d.ts +4 -2
- package/dist/components/data-display/data-table.js +14 -2
- package/dist/components/navigation/filter-bar.js +11 -16
- package/dist/components/ui/segmented.d.ts +19 -1
- package/dist/components/ui/segmented.js +34 -2
- package/dist/props/components/data-display.prop.d.ts +5 -0
- package/dist/props/registry.d.ts +1 -1
- package/dist/props/registry.js +1 -1
- package/dist/props/vocabulary/data.prop.d.ts +11 -0
- package/dist/styles/badge-layout.css +25 -0
- package/dist/styles/control.css +16 -0
- package/dist/styles/focus-ring.css +2 -0
- package/dist/styles/layout.css +12 -5
- package/dist/tokens/components/badge.css +6 -0
- package/dist/tokens/components/segmented.css +8 -0
- package/docs/DESIGN-AUTHORITY.md +11 -0
- package/docs/data-entry/segmented-in-filter-row.tsx +2 -19
- package/docs/data-entry/segmented.tsx +2 -19
- package/package.json +2 -2
- package/scripts/_agent-setup.mjs +62 -14
- package/scripts/postinstall.mjs +1 -1
- package/scripts/ui-audit.mjs +66 -2
|
@@ -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` / `
|
|
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__ */
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
/**
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -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";
|
package/dist/props/registry.js
CHANGED
|
@@ -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
|
}
|
package/dist/styles/control.css
CHANGED
|
@@ -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,
|
package/dist/styles/layout.css
CHANGED
|
@@ -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
|
}
|
package/docs/DESIGN-AUTHORITY.md
CHANGED
|
@@ -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
|
-
]
|
|
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
|
-
]
|
|
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
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -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])
|
|
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] =
|
|
366
|
+
json.mcpServers[MCP_KEY] = expected;
|
|
319
367
|
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
320
368
|
return created ? "created" : "added";
|
|
321
369
|
}
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -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
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -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
|
-
|
|
805
|
-
if (
|
|
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
|
}
|