@godxjp/ui 30.4.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.
Files changed (64) hide show
  1. package/agent/START-HERE.md +6 -6
  2. package/agent/components/Carousel.json +4 -1
  3. package/agent/components/RecordPicker.json +108 -0
  4. package/agent/components/ResponsiveGrid.json +3 -1
  5. package/agent/components/Reveal.json +6 -1
  6. package/agent/components/Table.json +6 -0
  7. package/agent/components/Text.json +8 -0
  8. package/agent/components-index.json +5 -0
  9. package/agent/components.json +135 -3
  10. package/agent/index.json +7 -7
  11. package/agent/llms.txt +7 -7
  12. package/agent/patterns/brand-theme-block.json +33 -0
  13. package/agent/patterns-index.json +32 -0
  14. package/agent/patterns.json +33 -0
  15. package/agent/tokens.json +12 -0
  16. package/dist/components/data-display/table.d.ts +30 -1
  17. package/dist/components/data-display/table.js +2 -1
  18. package/dist/components/data-entry/index.d.ts +2 -0
  19. package/dist/components/data-entry/index.js +2 -0
  20. package/dist/components/data-entry/record-picker.d.ts +63 -0
  21. package/dist/components/data-entry/record-picker.js +359 -0
  22. package/dist/components/general/typography.d.ts +1 -0
  23. package/dist/components/general/typography.js +10 -0
  24. package/dist/contracts/measurement.json +1 -1
  25. package/dist/i18n/messages/en.json +18 -0
  26. package/dist/i18n/messages/ja.json +18 -0
  27. package/dist/i18n/messages/vi.json +18 -0
  28. package/dist/props/components/data-entry.prop.d.ts +64 -0
  29. package/dist/props/components/general.prop.d.ts +15 -1
  30. package/dist/props/registry.d.ts +17 -0
  31. package/dist/props/registry.js +17 -0
  32. package/dist/props/vocabulary/index.d.ts +1 -1
  33. package/dist/props/vocabulary/interaction.prop.d.ts +13 -0
  34. package/dist/styles/data-display-layout.css +6 -3
  35. package/dist/styles/data-entry-layout.css +82 -0
  36. package/dist/styles/dialog-layout.css +8 -0
  37. package/dist/styles/focus-ring.css +2 -0
  38. package/dist/styles/table-layout.css +13 -0
  39. package/dist/styles/text-layout.css +8 -0
  40. package/dist/tokens/components/data-display.css +2 -0
  41. package/dist/tokens/components/data-entry.css +3 -0
  42. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  43. package/docs/assets/tcgm/card-1.svg +1 -0
  44. package/docs/assets/tcgm/card-2.svg +1 -0
  45. package/docs/assets/tcgm/card-3.svg +1 -0
  46. package/docs/assets/tcgm/card-4.svg +1 -0
  47. package/docs/assets/tcgm/card-5.svg +1 -0
  48. package/docs/assets/tcgm/card-6.svg +1 -0
  49. package/docs/assets/tcgm/hero-prism.svg +1 -0
  50. package/docs/assets/tcgm/scene-1.svg +1 -0
  51. package/docs/assets/tcgm/scene-2.svg +1 -0
  52. package/docs/assets/tcgm/scene-3.svg +1 -0
  53. package/docs/assets/tcgm/scene-4.svg +1 -0
  54. package/docs/assets/tcgm/scene-5.svg +1 -0
  55. package/docs/assets/tcgm/tcgm-lockup-color.svg +1 -0
  56. package/docs/assets/tcgm/tcgm-symbol-color.svg +1 -0
  57. package/docs/data-entry/record-picker.tsx +208 -0
  58. package/docs/general/typography.tsx +28 -0
  59. package/docs/i18n/messages/en.json +185 -1
  60. package/docs/i18n/messages/ja.json +185 -1
  61. package/docs/i18n/messages/vi.json +185 -1
  62. package/docs/showcase/tcgm-website.tsx +1047 -0
  63. package/package.json +3 -3
  64. package/scripts/ui-audit.mjs +23 -0
@@ -0,0 +1,359 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Check, ChevronDown, X } from "lucide-react";
4
+ import * as React from "react";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ import { normalizeSelectOptions } from "../../lib/select-options.js";
8
+ import { Badge } from "../data-display/badge.js";
9
+ import { EmptyState } from "../data-display/empty-state.js";
10
+ import { Button } from "../general/button.js";
11
+ import { Text } from "../general/typography.js";
12
+ import { Flex } from "../layout/flex.js";
13
+ import {
14
+ Dialog,
15
+ DialogBody,
16
+ DialogContent,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle
20
+ } from "../feedback/dialog.js";
21
+ import { Command, CommandGroup } from "./command.js";
22
+ import { Input } from "./input.js";
23
+ import { Select } from "./select.js";
24
+ const RecordPicker = React.forwardRef(
25
+ function RecordPicker2({
26
+ mode = "single",
27
+ value,
28
+ defaultValue,
29
+ onValueChange,
30
+ options,
31
+ loadOptions,
32
+ count,
33
+ threshold = 10,
34
+ filters,
35
+ selectedOptions,
36
+ emptyOption,
37
+ placeholder,
38
+ dialogTitle,
39
+ disabled = false,
40
+ size,
41
+ className,
42
+ "data-field": fieldName,
43
+ ...props
44
+ }, ref) {
45
+ const { t } = useTranslation();
46
+ const isMultiple = mode === "multiple";
47
+ const controlled = value !== void 0;
48
+ const [internal, setInternal] = React.useState(
49
+ () => toArray(defaultValue ?? (isMultiple ? [] : null))
50
+ );
51
+ const selected = controlled ? toArray(value) : internal;
52
+ const commit = React.useCallback(
53
+ (next) => {
54
+ if (!controlled) setInternal(next);
55
+ onValueChange?.(isMultiple ? next : next[0] ?? null);
56
+ },
57
+ [controlled, isMultiple, onValueChange]
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
+ );
70
+ const staticOptions = React.useMemo(
71
+ () => options ? normalizeSelectOptions(options) : [],
72
+ [options]
73
+ );
74
+ const total = count ?? (options ? staticOptions.length : void 0);
75
+ const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
76
+ if (!asDialog) {
77
+ return /* @__PURE__ */ jsx(
78
+ Select,
79
+ {
80
+ mode: isMultiple ? "multiple" : void 0,
81
+ loadOptions: selectLoadOptions,
82
+ id: props.id,
83
+ "data-field": fieldName,
84
+ "aria-describedby": props["aria-describedby"],
85
+ labelInValue: false,
86
+ showSearch: true,
87
+ options: withEmptyOption(options, emptyOption),
88
+ value: isMultiple ? selected : selected[0] ?? void 0,
89
+ onValueChange: ((next) => commit(toArray(next))),
90
+ placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
91
+ disabled,
92
+ size,
93
+ className
94
+ }
95
+ );
96
+ }
97
+ return /* @__PURE__ */ jsx(
98
+ DialogPicker,
99
+ {
100
+ ref,
101
+ "data-field": fieldName,
102
+ isMultiple,
103
+ selected,
104
+ commit,
105
+ staticOptions,
106
+ loadOptions,
107
+ filters,
108
+ selectedOptions,
109
+ emptyOption,
110
+ placeholder,
111
+ dialogTitle,
112
+ disabled,
113
+ size,
114
+ className,
115
+ ...props
116
+ }
117
+ );
118
+ }
119
+ );
120
+ function toArray(v) {
121
+ if (v === null || v === void 0) return [];
122
+ return Array.isArray(v) ? v : [v];
123
+ }
124
+ function withEmptyOption(options, emptyOption) {
125
+ if (!emptyOption) return options;
126
+ return [{ value: emptyOption.value, label: emptyOption.label }, ...options ?? []];
127
+ }
128
+ const DialogPicker = React.forwardRef(function DialogPicker2({
129
+ isMultiple,
130
+ selected,
131
+ commit,
132
+ staticOptions,
133
+ loadOptions,
134
+ filters,
135
+ selectedOptions,
136
+ emptyOption,
137
+ placeholder,
138
+ dialogTitle,
139
+ disabled,
140
+ size,
141
+ className,
142
+ ...props
143
+ }, ref) {
144
+ const { t } = useTranslation();
145
+ const [open, setOpen] = React.useState(false);
146
+ const [query, setQuery] = React.useState("");
147
+ const [filterValues, setFilterValues] = React.useState({});
148
+ const [rows, setRows] = React.useState(staticOptions);
149
+ const [status, setStatus] = React.useState("idle");
150
+ const [cursor, setCursor] = React.useState(void 0);
151
+ const [draft, setDraft] = React.useState(selected);
152
+ React.useEffect(() => {
153
+ if (open) setDraft(selected);
154
+ }, [open, selected]);
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;
166
+ const load = React.useCallback(
167
+ async (q, f, more) => {
168
+ if (!loadOptions) return;
169
+ setStatus("loading");
170
+ try {
171
+ const result = await loadOptions({ query: q, filters: f, cursor: more });
172
+ setRows((prev) => more ? [...prev, ...result.options] : result.options);
173
+ setCursor(result.nextCursor);
174
+ setStatus("idle");
175
+ } catch {
176
+ setStatus("error");
177
+ }
178
+ },
179
+ [loadOptions]
180
+ );
181
+ React.useEffect(() => {
182
+ if (!open || !loadOptions) return;
183
+ const id = setTimeout(() => void load(query, filterValues), 250);
184
+ return () => clearTimeout(id);
185
+ }, [open, query, filterValues, load, loadOptions]);
186
+ const visible = React.useMemo(() => {
187
+ const base = loadOptions ? rows : staticOptions.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase()));
188
+ return emptyOption && !query.trim() ? [{ value: emptyOption.value, label: emptyOption.label }, ...base] : base;
189
+ }, [loadOptions, rows, staticOptions, query, emptyOption]);
190
+ const groups = React.useMemo(() => {
191
+ const out = /* @__PURE__ */ new Map();
192
+ for (const o of visible) {
193
+ const key = o.group ?? "";
194
+ out.set(key, [...out.get(key) ?? [], o]);
195
+ }
196
+ return [...out.entries()];
197
+ }, [visible]);
198
+ const toggle = (optionValue) => {
199
+ if (!isMultiple) {
200
+ commit([optionValue]);
201
+ setOpen(false);
202
+ return;
203
+ }
204
+ setDraft(
205
+ (prev) => prev.includes(optionValue) ? prev.filter((v) => v !== optionValue) : [...prev, optionValue]
206
+ );
207
+ };
208
+ const chips = selected.map((v) => labels.get(v) ?? { value: v, label: v });
209
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
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
+ ] }),
243
+ /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "ui-record-picker-dialog", children: [
244
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle ?? t("dataEntry.recordPicker.dialogTitle") }) }),
245
+ /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs(Flex, { direction: "col", gap: "md", children: [
246
+ /* @__PURE__ */ jsx(
247
+ Input,
248
+ {
249
+ autoFocus: true,
250
+ type: "search",
251
+ value: query,
252
+ onChange: (e) => setQuery(e.target.value),
253
+ "aria-label": t("dataEntry.recordPicker.search"),
254
+ placeholder: t("dataEntry.recordPicker.searchPlaceholder")
255
+ }
256
+ ),
257
+ filters?.length ? /* @__PURE__ */ jsx(
258
+ Flex,
259
+ {
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,
282
+ /* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? /* @__PURE__ */ jsx(
283
+ EmptyState,
284
+ {
285
+ variant: "compact",
286
+ tone: "destructive",
287
+ title: t("dataEntry.recordPicker.error"),
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
+ )
297
+ }
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]) => {
299
+ const rendered = items.map((o) => {
300
+ const picked = (isMultiple ? draft : selected).includes(o.value);
301
+ return /* @__PURE__ */ jsxs(
302
+ "button",
303
+ {
304
+ type: "button",
305
+ role: "option",
306
+ "aria-selected": picked,
307
+ "data-picked": picked ? "" : void 0,
308
+ disabled: o.disabled,
309
+ className: "ui-record-picker-option",
310
+ onClick: () => toggle(o.value),
311
+ children: [
312
+ /* @__PURE__ */ jsx("span", { className: "ui-record-picker-tick", "aria-hidden": "true", children: picked ? /* @__PURE__ */ jsx(Check, {}) : null }),
313
+ o.icon,
314
+ /* @__PURE__ */ jsxs("span", { className: "ui-record-picker-option-label", children: [
315
+ o.label,
316
+ o.sublabel ? /* @__PURE__ */ jsx(Text, { as: "span", size: "2xs", tone: "muted", children: o.sublabel }) : null
317
+ ] })
318
+ ]
319
+ },
320
+ o.value
321
+ );
322
+ });
323
+ return heading ? /* @__PURE__ */ jsx(CommandGroup, { heading, children: rendered }, heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rendered }, "__ungrouped");
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
334
+ ] }) }),
335
+ isMultiple ? /* @__PURE__ */ jsxs(DialogFooter, { children: [
336
+ /* @__PURE__ */ jsx(Text, { as: "span", size: "xs", tone: "muted", className: "me-auto", children: t("dataEntry.recordPicker.selected", { count: draft.length }) }),
337
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setDraft([]), children: [
338
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
339
+ t("dataEntry.recordPicker.clear")
340
+ ] }),
341
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: t("dataEntry.recordPicker.cancel") }),
342
+ /* @__PURE__ */ jsx(
343
+ Button,
344
+ {
345
+ size: "sm",
346
+ onClick: () => {
347
+ commit(draft);
348
+ setOpen(false);
349
+ },
350
+ children: t("dataEntry.recordPicker.confirm")
351
+ }
352
+ )
353
+ ] }) : null
354
+ ] }) })
355
+ ] });
356
+ });
357
+ export {
358
+ RecordPicker
359
+ };
@@ -20,6 +20,7 @@ export declare const Text: React.ForwardRefExoticComponent<Omit<React.HTMLAttrib
20
20
  clamp?: number;
21
21
  ellipsis?: boolean | Omit<TypographyEllipsisConfigProp, "rows" | "expandable" | "onExpand">;
22
22
  whitespace?: import("../../props/index.js").TextWhitespaceProp;
23
+ break?: import("../../props/index.js").TextBreakProp;
23
24
  tabular?: boolean;
24
25
  decoration?: "none" | "underline" | "line-through";
25
26
  chip?: boolean;
@@ -380,6 +380,8 @@ const TextBase = React.forwardRef((props, ref) => {
380
380
  truncate,
381
381
  clamp,
382
382
  whitespace,
383
+ // `break` is a reserved word, so it cannot be a binding name.
384
+ break: breakMode,
383
385
  tabular,
384
386
  decoration,
385
387
  chip,
@@ -424,6 +426,11 @@ const TextBase = React.forwardRef((props, ref) => {
424
426
  'Text: `truncate` and `whitespace="pre-wrap"` are mutually exclusive \u2014 `truncate` takes precedence (one line, one ellipsis); use `clamp` to keep preserved line breaks and still bound the height.'
425
427
  );
426
428
  }
429
+ if (breakMode === "anywhere" && truncating) {
430
+ console.warn(
431
+ 'Text: `truncate` and `break="anywhere"` are mutually exclusive \u2014 `truncate` takes precedence (one line has nowhere to break to); use `clamp` to split the token and still bound the height.'
432
+ );
433
+ }
427
434
  if (ellipsis && (truncate || clamp !== void 0)) {
428
435
  console.warn(
429
436
  "Text: `ellipsis` (antd) and `truncate`/`clamp` are the same axis \u2014 `ellipsis` takes precedence; drop the other."
@@ -452,6 +459,9 @@ const TextBase = React.forwardRef((props, ref) => {
452
459
  // Inert default: `normal` is CSS's own behaviour, so it emits no attribute and there is no
453
460
  // `[data-whitespace="normal"]` rule to lose a specificity argument with anything.
454
461
  "data-whitespace": whitespace === "pre-wrap" && !truncating ? "pre-wrap" : void 0,
462
+ // Same shape and the same precedence as `whitespace`: `normal` is inert, and one line has
463
+ // nowhere to break to, so `truncate` simply does not emit it (gh#927).
464
+ "data-break": breakMode === "anywhere" && !truncating ? "anywhere" : void 0,
455
465
  style: clampStyle,
456
466
  "data-tabular": tabular ? "" : void 0,
457
467
  "data-decoration": decoration,
@@ -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.4.2",
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,
@@ -189,6 +189,24 @@
189
189
  "remove": "Remove {name}",
190
190
  "uploading": "Uploading attachment",
191
191
  "error": "Upload failed"
192
+ },
193
+ "recordPicker": {
194
+ "trigger": "Select",
195
+ "placeholder": "Select…",
196
+ "search": "Search",
197
+ "searchPlaceholder": "Search by keyword",
198
+ "dialogTitle": "Select",
199
+ "filters": "Filters",
200
+ "allFilter": "All",
201
+ "selected": "{count} selected",
202
+ "loading": "Loading",
203
+ "empty": "No matching items",
204
+ "error": "Could not load",
205
+ "confirm": "Confirm",
206
+ "cancel": "Cancel",
207
+ "clear": "Clear selection",
208
+ "more": "Load more",
209
+ "openDialog": "Choose from list"
192
210
  }
193
211
  },
194
212
  "feedback": {
@@ -185,6 +185,24 @@
185
185
  "remove": "{name} を削除",
186
186
  "uploading": "添付をアップロード中",
187
187
  "error": "アップロードに失敗しました"
188
+ },
189
+ "recordPicker": {
190
+ "trigger": "選択",
191
+ "placeholder": "選択してください",
192
+ "search": "検索",
193
+ "searchPlaceholder": "キーワードで検索",
194
+ "dialogTitle": "選択",
195
+ "filters": "絞り込み",
196
+ "allFilter": "すべて",
197
+ "selected": "選択中 {count} 件",
198
+ "loading": "読み込み中",
199
+ "empty": "該当する項目がありません",
200
+ "error": "読み込めませんでした",
201
+ "confirm": "決定",
202
+ "cancel": "キャンセル",
203
+ "clear": "選択を解除",
204
+ "more": "さらに読み込む",
205
+ "openDialog": "一覧から選択"
188
206
  }
189
207
  },
190
208
  "feedback": {
@@ -186,6 +186,24 @@
186
186
  "remove": "Xóa {name}",
187
187
  "uploading": "Đang tải tệp đính kèm",
188
188
  "error": "Tải lên thất bại"
189
+ },
190
+ "recordPicker": {
191
+ "trigger": "Chọn",
192
+ "placeholder": "Chọn…",
193
+ "search": "Tìm kiếm",
194
+ "searchPlaceholder": "Tìm theo từ khoá",
195
+ "dialogTitle": "Chọn",
196
+ "filters": "Bộ lọc",
197
+ "allFilter": "Tất cả",
198
+ "selected": "Đã chọn {count}",
199
+ "loading": "Đang tải",
200
+ "empty": "Không có mục nào khớp",
201
+ "error": "Không tải được",
202
+ "confirm": "Xác nhận",
203
+ "cancel": "Huỷ",
204
+ "clear": "Bỏ chọn",
205
+ "more": "Tải thêm",
206
+ "openDialog": "Chọn từ danh sách"
189
207
  }
190
208
  },
191
209
  "feedback": {
@@ -1129,6 +1129,70 @@ export type SearchSelectOptionProp = {
1129
1129
  group?: string;
1130
1130
  disabled?: boolean;
1131
1131
  };
1132
+ /**
1133
+ * One consumer-declared filter on a RecordPicker dialog (gh#932). The `name` is the key the
1134
+ * picker puts into `loadOptions({ filters })`, so the server reads its own vocabulary back.
1135
+ */
1136
+ export type RecordPickerFilterProp = {
1137
+ name: string;
1138
+ label: string;
1139
+ options: {
1140
+ value: string;
1141
+ label: string;
1142
+ }[];
1143
+ };
1144
+ /**
1145
+ * RecordPicker — a picker whose SHAPE follows the size of the set behind it. Under `threshold`
1146
+ * it IS a `Select`; over it, a dialog with search and consumer-declared filters.
1147
+ */
1148
+ export type RecordPickerProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "value" | "defaultValue" | "onChange"> & {
1149
+ /** `single` (default) or `multiple`. Decides the value shape AND whether the dialog confirms. */
1150
+ mode?: "single" | "multiple";
1151
+ value?: string | string[] | null;
1152
+ defaultValue?: string | string[] | null;
1153
+ /** Receives the shape you passed in: an array for `multiple`, a single value (or null) else. */
1154
+ onValueChange?: (value: string | string[] | null) => void;
1155
+ /** Static rows, client-filtered. Provide this OR `loadOptions`. */
1156
+ options?: (SearchSelectOptionProp | SelectOptionGroupProp)[];
1157
+ /** Server fetcher. `filters` carries the consumer's own filter vocabulary. */
1158
+ loadOptions?: (params: {
1159
+ query: string;
1160
+ filters: Record<string, string>;
1161
+ cursor?: string;
1162
+ }) => Promise<{
1163
+ options: SearchSelectOptionProp[];
1164
+ count?: number;
1165
+ nextCursor?: string;
1166
+ }>;
1167
+ /**
1168
+ * Size of the WHOLE set, which only a server knows — a page of results does not. Omitted with
1169
+ * `options`, it is their length; omitted with `loadOptions`, the dialog shape is assumed.
1170
+ */
1171
+ count?: number;
1172
+ /** The dropdown/dialog switch. Default 10. */
1173
+ threshold?: number;
1174
+ /** Filters rendered above the dialog list; their values reach `loadOptions({ filters })`. */
1175
+ filters?: RecordPickerFilterProp[];
1176
+ /**
1177
+ * Labels for values the picker holds but has not loaded — on first render there IS no page. Kept
1178
+ * ahead of anything that loads later, so a chip never renders a raw id and never disappears.
1179
+ */
1180
+ selectedOptions?: SearchSelectOptionProp[];
1181
+ /** A real row meaning "none" (an unassigned owner), pinned first — not a cleared field. */
1182
+ emptyOption?: {
1183
+ value: string;
1184
+ label: string;
1185
+ };
1186
+ placeholder?: string;
1187
+ dialogTitle?: string;
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;
1195
+ };
1132
1196
  export type SearchSelectLoadParamsProp = {
1133
1197
  query: string;
1134
1198
  /** 1-based page for infinite scroll. */
@@ -1,6 +1,6 @@
1
1
  /** Foundation component prop types — @see docs/COMPONENTS.md#foundation */
2
2
  import type * as React from "react";
3
- import type { ActivityAnnounceProp, ActivityVariantProp, AsChildProp, ButtonSizeProp, ButtonVariantProp, ChildrenProp, ClassNameProp, DescriptionProp, DisabledProp, FontWeightProp, HeadingLevelProp, IconSizeProp, IdProp, InViewAmountProp, LabelProp, OnClickProp, OnOpenChangeProp, OpenProp, PendingProp, RevealDelayProp, RevealTriggerProp, ShapeProp, SizeProp, TextAlignProp, TextSizeProp, TextToneProp, TextWhitespaceProp, TitleLevelProp, TypographyActionsConfigProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyTypeProp } from "../vocabulary/index.js";
3
+ import type { ActivityAnnounceProp, ActivityVariantProp, AsChildProp, ButtonSizeProp, ButtonVariantProp, ChildrenProp, ClassNameProp, DescriptionProp, DisabledProp, FontWeightProp, HeadingLevelProp, IconSizeProp, IdProp, InViewAmountProp, LabelProp, OnClickProp, OnOpenChangeProp, OpenProp, PendingProp, RevealDelayProp, RevealTriggerProp, ShapeProp, SizeProp, TextAlignProp, TextSizeProp, TextToneProp, TextWhitespaceProp, TextBreakProp, TitleLevelProp, TypographyActionsConfigProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyTypeProp } from "../vocabulary/index.js";
4
4
  /**
5
5
  * @see Text — typographic primitive; replaces hand-rolled `<span className="text-[13px] …">`.
6
6
  *
@@ -66,6 +66,20 @@ export type TextProp = Omit<React.HTMLAttributes<HTMLElement>, "color"> & Omit<T
66
66
  * `clamp` composes with it — a clamped pre-wrap block shows its first N real lines.
67
67
  */
68
68
  whitespace?: TextWhitespaceProp;
69
+ /**
70
+ * Where an over-long unbroken token may break. Default `normal` (CSS's own: it stays whole).
71
+ *
72
+ * `anywhere` is for a MACHINE identifier — an email address, a coupon code, a login id, a
73
+ * template key — in a narrow `DataTable` cell or `Descriptions` value: it may be split at any
74
+ * character to fit, so the column shrinks to the viewport instead of scrolling the page. It
75
+ * also releases a `nowrap` it inherited (a table cell's default) back to `normal` — newlines
76
+ * still collapse, because an identifier's whitespace is not content.
77
+ *
78
+ * Independent of `whitespace`: `whitespace="pre-wrap" break="anywhere"` keeps typed line breaks
79
+ * AND splits a long token. `truncate` WINS (one line has nowhere to break to), so the attribute
80
+ * is not emitted under it; `clamp` composes.
81
+ */
82
+ break?: TextBreakProp;
69
83
  /** Tabular figures for aligned numbers. */
70
84
  tabular?: boolean;
71
85
  decoration?: "none" | "underline" | "line-through";
@@ -464,6 +464,11 @@ export declare const VOCABULARY_REGISTRY: {
464
464
  readonly category: "interaction";
465
465
  readonly description: "Whitespace handling normal | pre-wrap: `pre-wrap` keeps the newlines and indentation a person typed (a plain-text note, a pasted log) and still wraps at the container edge; `truncate` outranks it";
466
466
  };
467
+ readonly TextBreakProp: {
468
+ readonly file: "vocabulary/interaction.prop.ts";
469
+ readonly category: "interaction";
470
+ readonly description: "Where an over-long unbroken token may break normal | anywhere: `anywhere` (overflow-wrap: anywhere) lets a machine identifier (email, code, login id) split to fit a narrow table cell; independent of TextWhitespaceProp, and `truncate` outranks it";
471
+ };
467
472
  readonly TypographyTypeProp: {
468
473
  readonly file: "vocabulary/interaction.prop.ts";
469
474
  readonly category: "interaction";
@@ -1802,6 +1807,18 @@ export declare const COMPONENT_PROP_REGISTRY: {
1802
1807
  readonly vocabulary: readonly ["ValueProp", "OnValueChangeProp", "PlaceholderProp", "EmptyMessageProp", "DisabledProp", "NameProp", "IdProp", "ClassNameProp", "OpenProp", "OnOpenChangeProp", "SizeProp", "DefaultOpenProp", "ControlStatusProp", "ControlVariantProp", "AllowClearProp", "NotFoundContentProp", "PopupMatchWidthProp", "PendingProp"];
1803
1808
  readonly note: "Ant-style data-driven form of Select (options|loadOptions + showSearch). One Select for all single-selects.";
1804
1809
  };
1810
+ readonly RecordPickerProp: {
1811
+ readonly group: "data-entry";
1812
+ readonly file: "components/data-entry.prop.ts";
1813
+ readonly vocabulary: readonly ["ValueProp", "OnValueChangeProp", "DisabledProp", "SizeProp", "PlaceholderProp"];
1814
+ readonly note: "One picker whose SHAPE follows the size of the set: Select at or under `threshold`, a searchable Dialog with consumer-declared filters over it (gh#932).";
1815
+ };
1816
+ readonly RecordPickerFilterProp: {
1817
+ readonly group: "data-entry";
1818
+ readonly file: "components/data-entry.prop.ts";
1819
+ readonly vocabulary: readonly ["ValueProp", "LabelProp"];
1820
+ readonly note: "One filter on a RecordPicker dialog. `name` is the key that reaches loadOptions({ filters }), so the server reads its own vocabulary back.";
1821
+ };
1805
1822
  readonly SearchSelectOptionProp: {
1806
1823
  readonly group: "data-entry";
1807
1824
  readonly file: "components/data-entry.prop.ts";
@@ -463,6 +463,11 @@ const VOCABULARY_REGISTRY = {
463
463
  category: "interaction",
464
464
  description: "Whitespace handling normal | pre-wrap: `pre-wrap` keeps the newlines and indentation a person typed (a plain-text note, a pasted log) and still wraps at the container edge; `truncate` outranks it"
465
465
  },
466
+ TextBreakProp: {
467
+ file: "vocabulary/interaction.prop.ts",
468
+ category: "interaction",
469
+ description: "Where an over-long unbroken token may break normal | anywhere: `anywhere` (overflow-wrap: anywhere) lets a machine identifier (email, code, login id) split to fit a narrow table cell; independent of TextWhitespaceProp, and `truncate` outranks it"
470
+ },
466
471
  TypographyTypeProp: {
467
472
  file: "vocabulary/interaction.prop.ts",
468
473
  category: "interaction",
@@ -2138,6 +2143,18 @@ const COMPONENT_PROP_REGISTRY = {
2138
2143
  ],
2139
2144
  note: "Ant-style data-driven form of Select (options|loadOptions + showSearch). One Select for all single-selects."
2140
2145
  },
2146
+ RecordPickerProp: {
2147
+ group: "data-entry",
2148
+ file: "components/data-entry.prop.ts",
2149
+ vocabulary: ["ValueProp", "OnValueChangeProp", "DisabledProp", "SizeProp", "PlaceholderProp"],
2150
+ note: "One picker whose SHAPE follows the size of the set: Select at or under `threshold`, a searchable Dialog with consumer-declared filters over it (gh#932)."
2151
+ },
2152
+ RecordPickerFilterProp: {
2153
+ group: "data-entry",
2154
+ file: "components/data-entry.prop.ts",
2155
+ vocabulary: ["ValueProp", "LabelProp"],
2156
+ note: "One filter on a RecordPicker dialog. `name` is the key that reaches loadOptions({ filters }), so the server reads its own vocabulary back."
2157
+ },
2141
2158
  SearchSelectOptionProp: {
2142
2159
  group: "data-entry",
2143
2160
  file: "components/data-entry.prop.ts",
@@ -2,6 +2,6 @@
2
2
  export type { ClassNameProp, ChildrenProp, IdProp, OpenProp, DefaultOpenProp, OnOpenChangeProp, HandlerProp, PendingProp, RequiredProp, DisabledProp, LabelProp, HelperProp, ErrorProp, ErrorBagProp, PlaceholderProp, NameProp, ValueProp, DefaultValueProp, OnValueChangeProp, OnChangeProp, OnClickProp, AsChildProp, FlushProp, WidthProp, ControlWidthProp, AllowClearProp, MaxTagCountProp, MaxTagPlaceholderProp, NotFoundContentProp, PopupMatchWidthProp, } from "./shared.prop.js";
3
3
  export type { TitleProp, SubtitleProp, StatusProp, DescriptionProp, ExtraProp, FooterProp, ActionProp, IconProp, ConfirmLabelProp, CancelLabelProp, ActionsProp, EmptyMessageProp, } from "./content.prop.js";
4
4
  export type { PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, CenteredShellAlignProp, CenteredShellPresetProp, ErrorSurfaceModeProp, ErrorSurfaceStatusProp, AuthShellPresetProp, MobileShellHeightProp, MobileShellWidthProp, OrientationProp, TableDensityProp, DensityProp, GapNameProp, PadProp, PadRawProp, PadSides, GapProp, GapStepProp, } from "./layout.prop.js";
5
- export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, AppSettingToggleAppearanceProp, ShapeProp, AvatarShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, TextWhitespaceProp, TypographyTypeProp, TitleLevelProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyActionsConfigProp, SizeProp, ControlStatusProp, ControlVariantProp, FormLayoutProp, DescriptionsLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, CalloutKindProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, RevealTriggerProp, InViewAmountProp, ActivityVariantProp, ActivityAnnounceProp, IconSizeProp, } from "./interaction.prop.js";
5
+ export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, AppSettingToggleAppearanceProp, ShapeProp, AvatarShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, TextWhitespaceProp, TextBreakProp, TypographyTypeProp, TitleLevelProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyActionsConfigProp, SizeProp, ControlStatusProp, ControlVariantProp, FormLayoutProp, DescriptionsLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, CalloutKindProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, RevealTriggerProp, InViewAmountProp, ActivityVariantProp, ActivityAnnounceProp, IconSizeProp, } from "./interaction.prop.js";
6
6
  export type { BreadcrumbItemProp, BreadcrumbProp, BreadcrumbItemMenuProp, BreadcrumbItemMenuEntryProp, BreadcrumbSeparatorProp, BreadcrumbItemRenderProp, } from "./navigation.prop.js";
7
7
  export type { GetRowIdProp, GetRowLabelProp, OnRowClickProp, ColumnDefProp, TablePresetProp, TableColumnPriorityProp, TableCellIndentProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, TableRowToneProp, RowToneProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, StickyProp, ColumnFixedProp, ColumnFilterValueProp, ColumnFilterItemProp, ColumnFilterStateProp, OnColumnFilterChangeProp, ColumnCompareProp, ColumnSorterProp, TableSelectionItemProp, TableRowSelectionProp, TableExpandableProp, TableSummaryProp, TableScrollProp, TableStickyProp, OnRowProp, TablePaginationProp, TablePaginationPositionProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, } from "./data.prop.js";