@godxjp/ui 30.5.2 → 30.5.3
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/agent/START-HERE.md +1 -1
- package/agent/components/RecordPicker.json +6 -1
- package/agent/components.json +6 -1
- package/agent/index.json +2 -2
- package/agent/llms.txt +3 -3
- package/dist/components/data-entry/record-picker.d.ts +1 -0
- package/dist/components/data-entry/record-picker.js +109 -46
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/data-entry.prop.d.ts +6 -0
- package/dist/styles/data-entry-layout.css +11 -0
- package/package.json +2 -2
package/agent/START-HERE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are about to write code against a design system you did not author. This file is the whole
|
|
4
4
|
contract. Read it before you write JSX.
|
|
5
5
|
|
|
6
|
-
**This catalog describes `@godxjp/ui` 30.5.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 30.5.3.** If the project you are editing has a different
|
|
7
7
|
version in its `package.json`, read the pinned catalog for THAT version instead
|
|
8
8
|
(`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
|
|
9
9
|
not exist yet; older, and it hides props that do. Neither failure announces itself.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
"description": "Server fetcher, debounced. `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented.",
|
|
30
|
+
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost.",
|
|
31
31
|
"name": "loadOptions",
|
|
32
32
|
"type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
|
|
33
33
|
},
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"name": "placeholder",
|
|
67
67
|
"type": "string"
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
"description": "Field name, normally supplied by FormField. Declared explicitly because BOTH branches must keep it: a server's 422 is pinned to `data-field`, and a consumer measured the dropdown branch dropping it while the dialog branch kept it (gh#942).",
|
|
71
|
+
"name": "data-field",
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
69
74
|
{
|
|
70
75
|
"description": "Dialog heading. Defaults to the localized `dataEntry.recordPicker.dialogTitle`.",
|
|
71
76
|
"name": "dialogTitle",
|
package/agent/components.json
CHANGED
|
@@ -6013,7 +6013,7 @@
|
|
|
6013
6013
|
"type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
|
|
6014
6014
|
},
|
|
6015
6015
|
{
|
|
6016
|
-
"description": "Server fetcher, debounced. `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented.",
|
|
6016
|
+
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost.",
|
|
6017
6017
|
"name": "loadOptions",
|
|
6018
6018
|
"type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
|
|
6019
6019
|
},
|
|
@@ -6052,6 +6052,11 @@
|
|
|
6052
6052
|
"name": "placeholder",
|
|
6053
6053
|
"type": "string"
|
|
6054
6054
|
},
|
|
6055
|
+
{
|
|
6056
|
+
"description": "Field name, normally supplied by FormField. Declared explicitly because BOTH branches must keep it: a server's 422 is pinned to `data-field`, and a consumer measured the dropdown branch dropping it while the dialog branch kept it (gh#942).",
|
|
6057
|
+
"name": "data-field",
|
|
6058
|
+
"type": "string"
|
|
6059
|
+
},
|
|
6055
6060
|
{
|
|
6056
6061
|
"description": "Dialog heading. Defaults to the localized `dataEntry.recordPicker.dialogTitle`.",
|
|
6057
6062
|
"name": "dialogTitle",
|
package/agent/index.json
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
|
|
49
49
|
"read": {
|
|
50
50
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
51
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.5.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.5.3/agent/index.json"
|
|
52
52
|
},
|
|
53
53
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
54
54
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
63
63
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
64
64
|
},
|
|
65
|
-
"version": "30.5.
|
|
65
|
+
"version": "30.5.3"
|
|
66
66
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
3
|
> A Japanese-enterprise React design system: 173 components, 2073 design tokens,
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.5.
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.5.3.
|
|
5
5
|
|
|
6
6
|
If your client can run a process, do not read these files — run the MCP server instead
|
|
7
|
-
(`npx @godxjp/ui-mcp@30.5.
|
|
7
|
+
(`npx @godxjp/ui-mcp@30.5.3`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -26,7 +26,7 @@ that can only fetch URLs.
|
|
|
26
26
|
## Pinning
|
|
27
27
|
|
|
28
28
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
29
|
-
the tag: `.../godx-jp/godxjp-ui/v30.5.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v30.5.3/agent/...`. A catalog that does not match the installed
|
|
30
30
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
31
31
|
|
|
32
32
|
Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
|
|
@@ -39,6 +39,7 @@ const RecordPicker = React.forwardRef(
|
|
|
39
39
|
disabled = false,
|
|
40
40
|
size,
|
|
41
41
|
className,
|
|
42
|
+
"data-field": fieldName,
|
|
42
43
|
...props
|
|
43
44
|
}, ref) {
|
|
44
45
|
const { t } = useTranslation();
|
|
@@ -55,6 +56,17 @@ const RecordPicker = React.forwardRef(
|
|
|
55
56
|
},
|
|
56
57
|
[controlled, isMultiple, onValueChange]
|
|
57
58
|
);
|
|
59
|
+
const selectLoadOptions = React.useMemo(
|
|
60
|
+
() => loadOptions ? async ({ query, page }) => {
|
|
61
|
+
const r = await loadOptions({
|
|
62
|
+
query,
|
|
63
|
+
filters: {},
|
|
64
|
+
cursor: page > 1 ? String(page) : void 0
|
|
65
|
+
});
|
|
66
|
+
return { options: r.options, hasMore: r.nextCursor !== void 0 };
|
|
67
|
+
} : void 0,
|
|
68
|
+
[loadOptions]
|
|
69
|
+
);
|
|
58
70
|
const staticOptions = React.useMemo(
|
|
59
71
|
() => options ? normalizeSelectOptions(options) : [],
|
|
60
72
|
[options]
|
|
@@ -66,6 +78,10 @@ const RecordPicker = React.forwardRef(
|
|
|
66
78
|
Select,
|
|
67
79
|
{
|
|
68
80
|
mode: isMultiple ? "multiple" : void 0,
|
|
81
|
+
loadOptions: selectLoadOptions,
|
|
82
|
+
id: props.id,
|
|
83
|
+
"data-field": fieldName,
|
|
84
|
+
"aria-describedby": props["aria-describedby"],
|
|
69
85
|
labelInValue: false,
|
|
70
86
|
showSearch: true,
|
|
71
87
|
options: withEmptyOption(options, emptyOption),
|
|
@@ -82,6 +98,7 @@ const RecordPicker = React.forwardRef(
|
|
|
82
98
|
DialogPicker,
|
|
83
99
|
{
|
|
84
100
|
ref,
|
|
101
|
+
"data-field": fieldName,
|
|
85
102
|
isMultiple,
|
|
86
103
|
selected,
|
|
87
104
|
commit,
|
|
@@ -130,23 +147,30 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
130
147
|
const [filterValues, setFilterValues] = React.useState({});
|
|
131
148
|
const [rows, setRows] = React.useState(staticOptions);
|
|
132
149
|
const [status, setStatus] = React.useState("idle");
|
|
150
|
+
const [cursor, setCursor] = React.useState(void 0);
|
|
133
151
|
const [draft, setDraft] = React.useState(selected);
|
|
134
152
|
React.useEffect(() => {
|
|
135
153
|
if (open) setDraft(selected);
|
|
136
154
|
}, [open, selected]);
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
const labelCache = React.useRef(/* @__PURE__ */ new Map());
|
|
156
|
+
for (const o of [...selectedOptions ?? [], ...staticOptions, ...rows]) {
|
|
157
|
+
labelCache.current.set(o.value, o);
|
|
158
|
+
}
|
|
159
|
+
if (emptyOption) {
|
|
160
|
+
labelCache.current.set(emptyOption.value, {
|
|
161
|
+
value: emptyOption.value,
|
|
162
|
+
label: emptyOption.label
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const labels = labelCache.current;
|
|
143
166
|
const load = React.useCallback(
|
|
144
|
-
async (q, f) => {
|
|
167
|
+
async (q, f, more) => {
|
|
145
168
|
if (!loadOptions) return;
|
|
146
169
|
setStatus("loading");
|
|
147
170
|
try {
|
|
148
|
-
const result = await loadOptions({ query: q, filters: f });
|
|
149
|
-
setRows(result.options);
|
|
171
|
+
const result = await loadOptions({ query: q, filters: f, cursor: more });
|
|
172
|
+
setRows((prev) => more ? [...prev, ...result.options] : result.options);
|
|
173
|
+
setCursor(result.nextCursor);
|
|
150
174
|
setStatus("idle");
|
|
151
175
|
} catch {
|
|
152
176
|
setStatus("error");
|
|
@@ -183,27 +207,39 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
183
207
|
};
|
|
184
208
|
const chips = selected.map((v) => labels.get(v) ?? { value: v, label: v });
|
|
185
209
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
186
|
-
/* @__PURE__ */ jsxs(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "xs", align: "center", className: "ui-record-picker-row", children: [
|
|
211
|
+
/* @__PURE__ */ jsxs(
|
|
212
|
+
Button,
|
|
213
|
+
{
|
|
214
|
+
ref,
|
|
215
|
+
type: "button",
|
|
216
|
+
variant: "outline",
|
|
217
|
+
size,
|
|
218
|
+
disabled,
|
|
219
|
+
"aria-haspopup": "dialog",
|
|
220
|
+
onClick: () => setOpen(true),
|
|
221
|
+
className: cn("ui-record-picker-trigger", className),
|
|
222
|
+
...props,
|
|
223
|
+
children: [
|
|
224
|
+
/* @__PURE__ */ jsx("span", { className: "ui-record-picker-trigger-label", children: chips.length === 0 ? /* @__PURE__ */ jsx(Text, { as: "span", size: "sm", tone: "muted", children: placeholder ?? t("dataEntry.recordPicker.placeholder") }) : /* @__PURE__ */ jsx(Flex, { direction: "row", gap: "xs", wrap: true, align: "center", children: chips.map((c) => /* @__PURE__ */ jsxs(Badge, { variant: "secondary", as: "span", children: [
|
|
225
|
+
c.icon,
|
|
226
|
+
c.label
|
|
227
|
+
] }, c.value)) }) }),
|
|
228
|
+
/* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" })
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
!isMultiple && selected.length > 0 && !disabled ? /* @__PURE__ */ jsx(
|
|
233
|
+
Button,
|
|
234
|
+
{
|
|
235
|
+
variant: "ghost",
|
|
236
|
+
size: "icon-sm",
|
|
237
|
+
"aria-label": t("dataEntry.recordPicker.clear"),
|
|
238
|
+
onClick: () => commit([]),
|
|
239
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
240
|
+
}
|
|
241
|
+
) : null
|
|
242
|
+
] }),
|
|
207
243
|
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "ui-record-picker-dialog", children: [
|
|
208
244
|
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle ?? t("dataEntry.recordPicker.dialogTitle") }) }),
|
|
209
245
|
/* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs(Flex, { direction: "col", gap: "md", children: [
|
|
@@ -218,28 +254,46 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
218
254
|
placeholder: t("dataEntry.recordPicker.searchPlaceholder")
|
|
219
255
|
}
|
|
220
256
|
),
|
|
221
|
-
filters?.length ? /* @__PURE__ */ jsx(
|
|
222
|
-
|
|
257
|
+
filters?.length ? /* @__PURE__ */ jsx(
|
|
258
|
+
Flex,
|
|
223
259
|
{
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
260
|
+
direction: "row",
|
|
261
|
+
gap: "sm",
|
|
262
|
+
wrap: true,
|
|
263
|
+
role: "group",
|
|
264
|
+
"aria-label": t("dataEntry.recordPicker.filters"),
|
|
265
|
+
children: filters.map((f) => /* @__PURE__ */ jsx(
|
|
266
|
+
Select,
|
|
267
|
+
{
|
|
268
|
+
size: "sm",
|
|
269
|
+
"aria-label": f.label,
|
|
270
|
+
placeholder: f.label,
|
|
271
|
+
value: filterValues[f.name] ?? "",
|
|
272
|
+
onValueChange: (next) => setFilterValues((prev) => ({ ...prev, [f.name]: next })),
|
|
273
|
+
options: [
|
|
274
|
+
{ value: "", label: t("dataEntry.recordPicker.allFilter") },
|
|
275
|
+
...f.options
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
f.name
|
|
279
|
+
))
|
|
280
|
+
}
|
|
281
|
+
) : null,
|
|
236
282
|
/* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? /* @__PURE__ */ jsx(
|
|
237
283
|
EmptyState,
|
|
238
284
|
{
|
|
239
285
|
variant: "compact",
|
|
240
286
|
tone: "destructive",
|
|
241
287
|
title: t("dataEntry.recordPicker.error"),
|
|
242
|
-
action: /* @__PURE__ */ jsx(
|
|
288
|
+
action: /* @__PURE__ */ jsx(
|
|
289
|
+
Button,
|
|
290
|
+
{
|
|
291
|
+
variant: "outline",
|
|
292
|
+
size: "sm",
|
|
293
|
+
onClick: () => void load(query, filterValues),
|
|
294
|
+
children: t("dataEntry.recordPicker.more")
|
|
295
|
+
}
|
|
296
|
+
)
|
|
243
297
|
}
|
|
244
298
|
) : status === "loading" ? /* @__PURE__ */ jsx("div", { role: "status", className: "ui-record-picker-status", children: t("dataEntry.recordPicker.loading") }) : visible.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { variant: "compact", title: t("dataEntry.recordPicker.empty") }) : groups.map(([heading, items]) => {
|
|
245
299
|
const rendered = items.map((o) => {
|
|
@@ -267,7 +321,16 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
267
321
|
);
|
|
268
322
|
});
|
|
269
323
|
return heading ? /* @__PURE__ */ jsx(CommandGroup, { heading, children: rendered }, heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rendered }, "__ungrouped");
|
|
270
|
-
}) })
|
|
324
|
+
}) }),
|
|
325
|
+
cursor !== void 0 && status !== "loading" ? /* @__PURE__ */ jsx(
|
|
326
|
+
Button,
|
|
327
|
+
{
|
|
328
|
+
variant: "outline",
|
|
329
|
+
size: "sm",
|
|
330
|
+
onClick: () => void load(query, filterValues, cursor),
|
|
331
|
+
children: t("dataEntry.recordPicker.more")
|
|
332
|
+
}
|
|
333
|
+
) : null
|
|
271
334
|
] }) }),
|
|
272
335
|
isMultiple ? /* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
273
336
|
/* @__PURE__ */ jsx(Text, { as: "span", size: "xs", tone: "muted", className: "me-auto", children: t("dataEntry.recordPicker.selected", { count: draft.length }) }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "30.5.
|
|
3
|
+
"version": "30.5.3",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -1186,6 +1186,12 @@ export type RecordPickerProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement
|
|
|
1186
1186
|
placeholder?: string;
|
|
1187
1187
|
dialogTitle?: string;
|
|
1188
1188
|
size?: "xs" | "sm" | "md" | "lg";
|
|
1189
|
+
/**
|
|
1190
|
+
* Tên trường, do `FormField` truyền xuống. Khai tường minh vì CẢ HAI nhánh phải giữ nó: một
|
|
1191
|
+
* lỗi 422 của server bám theo `data-field`, và consumer đo được nhánh dropdown đánh rơi nó
|
|
1192
|
+
* trong khi nhánh Dialog thì giữ (gh#942).
|
|
1193
|
+
*/
|
|
1194
|
+
"data-field"?: string;
|
|
1189
1195
|
};
|
|
1190
1196
|
export type SearchSelectLoadParamsProp = {
|
|
1191
1197
|
query: string;
|
|
@@ -835,3 +835,14 @@
|
|
|
835
835
|
font-size: var(--font-size-sm);
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
|
+
|
|
839
|
+
@layer components {
|
|
840
|
+
|
|
841
|
+
.ui-record-picker-row {
|
|
842
|
+
inline-size: 100%;
|
|
843
|
+
}
|
|
844
|
+
.ui-record-picker-row > .ui-record-picker-trigger {
|
|
845
|
+
flex: 1 1 auto;
|
|
846
|
+
min-inline-size: 0;
|
|
847
|
+
}
|
|
848
|
+
}
|