@godxjp/ui 31.4.0 → 31.6.0

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 (80) hide show
  1. package/agent/START-HERE.md +6 -6
  2. package/agent/components/BranchScopePicker.json +5 -0
  3. package/agent/components/Cascader.json +1 -1
  4. package/agent/components/CodeBlock.json +7 -1
  5. package/agent/components/DataTable.json +2 -2
  6. package/agent/components/InfiniteQueryState.json +6 -1
  7. package/agent/components/OrgChart.json +59 -0
  8. package/agent/components/RangeTimeline.json +6 -0
  9. package/agent/components/RecordPicker.json +10 -0
  10. package/agent/components/Transfer.json +5 -0
  11. package/agent/components/Tree.json +21 -4
  12. package/agent/components/TreeSelect.json +1 -1
  13. package/agent/components-index.json +5 -0
  14. package/agent/components.json +123 -10
  15. package/agent/index.json +7 -7
  16. package/agent/llms.txt +7 -7
  17. package/agent/tokens.json +114 -0
  18. package/dist/components/data-display/code-block.js +90 -4
  19. package/dist/components/data-display/data-table.d.ts +3 -0
  20. package/dist/components/data-display/data-table.js +415 -370
  21. package/dist/components/data-display/index.d.ts +2 -0
  22. package/dist/components/data-display/index.js +2 -0
  23. package/dist/components/data-display/org-chart.d.ts +5 -0
  24. package/dist/components/data-display/org-chart.js +169 -0
  25. package/dist/components/data-display/range-timeline.d.ts +20 -0
  26. package/dist/components/data-display/range-timeline.js +217 -197
  27. package/dist/components/data-display/tree.js +179 -23
  28. package/dist/components/data-entry/branch-scope-picker.d.ts +1 -0
  29. package/dist/components/data-entry/branch-scope-picker.js +2 -1
  30. package/dist/components/data-entry/cascader.js +4 -5
  31. package/dist/components/data-entry/record-picker.d.ts +2 -0
  32. package/dist/components/data-entry/record-picker.js +22 -3
  33. package/dist/components/data-entry/transfer.d.ts +1 -1
  34. package/dist/components/data-entry/transfer.js +5 -2
  35. package/dist/components/data-entry/tree-select.js +14 -5
  36. package/dist/components/general/typography.d.ts +32 -0
  37. package/dist/components/general/typography.js +11 -2
  38. package/dist/components/query/infinite-query-state.d.ts +1 -1
  39. package/dist/components/query/infinite-query-state.js +2 -1
  40. package/dist/contracts/measurement.json +1 -1
  41. package/dist/i18n/messages/en.json +26 -2
  42. package/dist/i18n/messages/ja.json +17 -2
  43. package/dist/i18n/messages/vi.json +17 -2
  44. package/dist/lib/hooks.d.ts +8 -4
  45. package/dist/lib/hooks.js +5 -2
  46. package/dist/lib/tree.d.ts +23 -0
  47. package/dist/lib/tree.js +29 -0
  48. package/dist/props/components/data-display.prop.d.ts +72 -3
  49. package/dist/props/components/data-entry.prop.d.ts +31 -4
  50. package/dist/props/components/query.prop.d.ts +8 -1
  51. package/dist/props/registry.d.ts +63 -6
  52. package/dist/props/registry.js +90 -4
  53. package/dist/props/vocabulary/data.prop.d.ts +32 -2
  54. package/dist/props/vocabulary/index.d.ts +1 -1
  55. package/dist/styles/data-display-layout.css +225 -0
  56. package/dist/styles/layers.json +14 -1
  57. package/dist/styles/table-layout.css +16 -1
  58. package/dist/tokens/base.css +1 -0
  59. package/dist/tokens/components/org-chart.css +27 -0
  60. package/dist/tokens/components/tree.css +2 -0
  61. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  62. package/docs/data-display/code-block.tsx +27 -0
  63. package/docs/data-display/data-table/examples/server-paged.tsx +23 -0
  64. package/docs/data-display/data-table/index.md +2 -0
  65. package/docs/data-display/org-chart.tsx +165 -0
  66. package/docs/data-display/timeline.tsx +33 -0
  67. package/docs/data-display/tree.tsx +154 -0
  68. package/docs/data-entry/branch-scope-picker.tsx +6 -1
  69. package/docs/data-entry/record-picker.tsx +1 -0
  70. package/docs/data-entry/transfer.tsx +1 -0
  71. package/docs/i18n/messages/en.json +19 -0
  72. package/docs/i18n/messages/ja.json +18 -0
  73. package/docs/i18n/messages/vi.json +18 -0
  74. package/docs/navigation/tabs.tsx +17 -15
  75. package/docs/query/data-state.tsx +2 -4
  76. package/docs/query/infinite-query-state.tsx +5 -1
  77. package/docs/roadmap/tree-components.md +2 -1
  78. package/docs/showcase/table-view-tabs.tsx +1 -1
  79. package/package.json +2 -2
  80. package/scripts/ui-audit.mjs +15 -5
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { mergeRefs } from "@react-aria/utils";
4
5
  import { ChevronDown, ChevronRight, File as FileIcon, Folder, FolderOpen } from "lucide-react";
5
6
  import { useTranslation } from "../../i18n/use-translation.js";
6
7
  import { cn } from "../../lib/utils.js";
@@ -8,6 +9,7 @@ import { CheckboxVisual } from "../data-entry/checkbox.js";
8
9
  import { Skeleton } from "../feedback/skeleton.js";
9
10
  import {
10
11
  collectAllExpandableKeys,
12
+ createLazyLoadLedger,
11
13
  flattenVisibleTree,
12
14
  getDescendantValues,
13
15
  normalizeTreeOptions,
@@ -44,6 +46,7 @@ function indexTree(nodes, into = /* @__PURE__ */ new Map()) {
44
46
  function isRtl(element) {
45
47
  return element.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
46
48
  }
49
+ const VIRTUAL_OVERSCAN = 6;
47
50
  function TreeRoot({
48
51
  treeData,
49
52
  fieldNames,
@@ -62,6 +65,9 @@ function TreeRoot({
62
65
  defaultExpandAll = false,
63
66
  loadData,
64
67
  titleRender,
68
+ filterTreeNode,
69
+ height,
70
+ virtual = true,
65
71
  showLine = false,
66
72
  showIcon = false,
67
73
  divided = false,
@@ -114,21 +120,29 @@ function TreeRoot({
114
120
  if (!isCheckedControlled) setInternalChecked(settled);
115
121
  onCheckedValuesChange?.(settled);
116
122
  };
117
- const requestedLoads = React.useRef(/* @__PURE__ */ new Set());
123
+ const [lazyLoads] = React.useState(createLazyLoadLedger);
124
+ const loadedFor = React.useRef(/* @__PURE__ */ new Set());
118
125
  const [loadingValues, setLoadingValues] = React.useState(() => /* @__PURE__ */ new Set());
119
126
  const requestLoad = (node) => {
120
127
  if (!loadData) return;
121
128
  if ((node.children?.length ?? 0) > 0 || node.isLeaf === true) return;
122
- if (requestedLoads.current.has(node.value)) return;
123
- requestedLoads.current.add(node.value);
124
- setLoadingValues((prev) => new Set(prev).add(node.value));
125
- void Promise.resolve(loadData(node)).finally(() => {
126
- setLoadingValues((prev) => {
127
- const next = new Set(prev);
128
- next.delete(node.value);
129
- return next;
130
- });
131
- });
129
+ const started = lazyLoads.run(
130
+ node.value,
131
+ () => loadData(node),
132
+ (ok) => {
133
+ setLoadingValues((prev) => {
134
+ const next = new Set(prev);
135
+ next.delete(node.value);
136
+ return next;
137
+ });
138
+ if (ok) return;
139
+ loadedFor.current.delete(node.value);
140
+ if (!isExpandedControlled) {
141
+ setInternalExpanded((prev) => prev.filter((entry) => entry !== node.value));
142
+ }
143
+ }
144
+ );
145
+ if (started) setLoadingValues((prev) => new Set(prev).add(node.value));
132
146
  };
133
147
  const requestLoadRef = React.useRef(requestLoad);
134
148
  requestLoadRef.current = requestLoad;
@@ -143,7 +157,6 @@ function TreeRoot({
143
157
  walk(options);
144
158
  return map;
145
159
  }, [options]);
146
- const loadedFor = React.useRef(/* @__PURE__ */ new Set());
147
160
  React.useEffect(() => {
148
161
  if (!loadData) return;
149
162
  for (const value2 of expandedSet) {
@@ -157,6 +170,17 @@ function TreeRoot({
157
170
  const visibleIndex = new Map(visible.map((entry, index) => [entry.node.value, index]));
158
171
  const [activeValue, setActiveValue] = React.useState(null);
159
172
  const rovingValue = (activeValue && visibleIndex.has(activeValue) ? activeValue : null) ?? visible.find((entry) => selected.includes(entry.node.value))?.node.value ?? visible[0]?.node.value ?? null;
173
+ const isWindowed = height !== void 0 && virtual;
174
+ const treeRef = React.useRef(null);
175
+ const [scrollTop, setScrollTop] = React.useState(0);
176
+ const [rowHeight, setRowHeight] = React.useState(0);
177
+ const [hasFocusWithin, setHasFocusWithin] = React.useState(false);
178
+ React.useLayoutEffect(() => {
179
+ if (!isWindowed) return;
180
+ const firstRow = treeRef.current?.querySelector('[role="treeitem"]');
181
+ const measured = firstRow?.offsetHeight ?? 0;
182
+ if (measured > 0 && measured !== rowHeight) setRowHeight(measured);
183
+ }, [isWindowed, rowHeight, size, visible.length]);
160
184
  const nodeRefs = React.useRef(/* @__PURE__ */ new Map());
161
185
  const pendingFocus = React.useRef(null);
162
186
  React.useEffect(() => {
@@ -165,9 +189,23 @@ function TreeRoot({
165
189
  pendingFocus.current = null;
166
190
  nodeRefs.current.get(target)?.focus();
167
191
  });
192
+ const revealRow = (value2) => {
193
+ const element = treeRef.current;
194
+ const rowIndex = rowIndexOf.get(value2);
195
+ if (!isWindowed || !element || rowHeight <= 0 || rowIndex === void 0) return;
196
+ const viewport = element.clientHeight || height || 0;
197
+ const top = rowIndex * rowHeight;
198
+ let next = element.scrollTop;
199
+ if (top < next) next = top;
200
+ else if (top + rowHeight > next + viewport) next = top + rowHeight - viewport;
201
+ if (next === element.scrollTop) return;
202
+ element.scrollTop = next;
203
+ setScrollTop(next);
204
+ };
168
205
  const moveTo = (index) => {
169
206
  const entry = visible[index];
170
207
  if (!entry) return;
208
+ revealRow(entry.node.value);
171
209
  setActiveValue(entry.node.value);
172
210
  pendingFocus.current = entry.node.value;
173
211
  };
@@ -318,19 +356,18 @@ function TreeRoot({
318
356
  jumpByLabel(event.key, index);
319
357
  }
320
358
  };
321
- const renderNodes = (nodes, depth) => nodes.flatMap((node, position) => {
359
+ const renderItem = (node, depth, setSize, posInSet, groupId) => {
322
360
  const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
323
361
  const expandable = expandableOf(node);
324
362
  const isOpen = expandable && expandedSet.has(node.value);
325
363
  const isLoading = loadingValues.has(node.value);
326
364
  const isSelected = selected.includes(node.value);
365
+ const isFilterMatch = filterTreeNode ? filterTreeNode(node) : false;
327
366
  const checkState = checkableProp ? checkStateOf(node) : "unchecked";
328
367
  const nodeDisabled = disabled || Boolean(node.disabled);
329
368
  const labelId = `${treeId}-${node.value}-label`;
330
- const groupId = `${treeId}-${node.value}-group`;
331
- const showGroup = isOpen && (hasChildren || isLoading);
332
369
  const glyph = node.icon ?? (variant === "directory" ? hasChildren ? isOpen ? /* @__PURE__ */ jsx(FolderOpen, {}) : /* @__PURE__ */ jsx(Folder, {}) : /* @__PURE__ */ jsx(FileIcon, {}) : null);
333
- const item = /* @__PURE__ */ jsxs(
370
+ return /* @__PURE__ */ jsxs(
334
371
  "div",
335
372
  {
336
373
  ref: (element) => {
@@ -339,11 +376,11 @@ function TreeRoot({
339
376
  role: "treeitem",
340
377
  "aria-labelledby": labelId,
341
378
  "aria-level": depth + 1,
342
- "aria-setsize": nodes.length,
343
- "aria-posinset": position + 1,
379
+ "aria-setsize": setSize,
380
+ "aria-posinset": posInSet,
344
381
  "aria-selected": isSelected,
345
382
  "aria-expanded": expandable ? isOpen : void 0,
346
- "aria-owns": showGroup ? groupId : void 0,
383
+ "aria-owns": groupId,
347
384
  "aria-checked": checkableProp ? checkState === "indeterminate" ? "mixed" : checkState === "checked" : void 0,
348
385
  "aria-disabled": nodeDisabled || void 0,
349
386
  "aria-busy": isLoading || void 0,
@@ -357,9 +394,19 @@ function TreeRoot({
357
394
  "data-value": node.value,
358
395
  "data-selected": isSelected ? "true" : void 0,
359
396
  "data-disabled": nodeDisabled ? "" : void 0,
397
+ "data-filter-node": isFilterMatch ? "true" : void 0,
360
398
  className: "ui-tree-node ui-focus-ring",
361
399
  style: { "--tree-node-level": depth },
362
400
  children: [
401
+ isWindowed && showLine ? Array.from({ length: depth }, (_, level) => /* @__PURE__ */ jsx(
402
+ "span",
403
+ {
404
+ "aria-hidden": "true",
405
+ className: "ui-tree-rail",
406
+ style: { "--tree-rail-level": level + 1 }
407
+ },
408
+ `rail-${level}`
409
+ )) : null,
363
410
  /* @__PURE__ */ jsx(
364
411
  "span",
365
412
  {
@@ -395,11 +442,26 @@ function TreeRoot({
395
442
  ) : null,
396
443
  showIcon && glyph ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ui-tree-icon", children: glyph }) : null,
397
444
  /* @__PURE__ */ jsx("span", { id: labelId, className: "ui-tree-label", children: titleRender ? titleRender(node) : node.label }),
398
- isSelected ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.tree.selected") }) : null
445
+ isSelected ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.tree.selected") }) : null,
446
+ isFilterMatch ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataDisplay.tree.filterMatch") }) : null
399
447
  ]
400
448
  },
401
449
  node.value
402
450
  );
451
+ };
452
+ const renderNodes = (nodes, depth) => nodes.flatMap((node, position) => {
453
+ const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
454
+ const isOpen = expandableOf(node) && expandedSet.has(node.value);
455
+ const isLoading = loadingValues.has(node.value);
456
+ const groupId = `${treeId}-${node.value}-group`;
457
+ const showGroup = isOpen && (hasChildren || isLoading);
458
+ const item = renderItem(
459
+ node,
460
+ depth,
461
+ nodes.length,
462
+ position + 1,
463
+ showGroup ? groupId : void 0
464
+ );
403
465
  if (!showGroup) return [item];
404
466
  return [
405
467
  item,
@@ -426,13 +488,68 @@ function TreeRoot({
426
488
  )
427
489
  ];
428
490
  });
491
+ const flatRows = [];
492
+ if (isWindowed) {
493
+ const walk = (nodes, depth) => {
494
+ nodes.forEach((node, position) => {
495
+ flatRows.push({
496
+ kind: "node",
497
+ node,
498
+ depth,
499
+ setSize: nodes.length,
500
+ posInSet: position + 1
501
+ });
502
+ if (!expandableOf(node) || !expandedSet.has(node.value)) return;
503
+ if ((node.children?.length ?? 0) > 0 && node.isLeaf !== true)
504
+ walk(node.children, depth + 1);
505
+ else if (loadingValues.has(node.value)) {
506
+ flatRows.push({ kind: "loading", key: `${node.value}::loading`, depth: depth + 1 });
507
+ }
508
+ });
509
+ };
510
+ walk(options, 0);
511
+ }
512
+ const rowIndexOf = /* @__PURE__ */ new Map();
513
+ flatRows.forEach((row, index) => {
514
+ if (row.kind === "node") rowIndexOf.set(row.node.value, index);
515
+ });
516
+ let windowStart = 0;
517
+ let windowEnd = flatRows.length;
518
+ if (isWindowed && rowHeight > 0 && height !== void 0) {
519
+ const first = Math.floor(scrollTop / rowHeight);
520
+ windowStart = Math.max(0, first - VIRTUAL_OVERSCAN);
521
+ windowEnd = Math.min(flatRows.length, first + Math.ceil(height / rowHeight) + VIRTUAL_OVERSCAN);
522
+ const focusedRow = rovingValue === null ? void 0 : rowIndexOf.get(rovingValue);
523
+ if (hasFocusWithin && focusedRow !== void 0) {
524
+ windowStart = Math.min(windowStart, focusedRow);
525
+ windowEnd = Math.max(windowEnd, focusedRow + 1);
526
+ }
527
+ }
528
+ const rovingRow = rovingValue === null ? void 0 : rowIndexOf.get(rovingValue);
529
+ const rovingOffscreen = isWindowed && rovingRow !== void 0 && (rovingRow < windowStart || rovingRow >= windowEnd);
429
530
  const isEmpty = visible.length === 0;
531
+ const body = isEmpty ? null : isWindowed ? flatRows.slice(windowStart, windowEnd).map(
532
+ (row) => row.kind === "node" ? renderItem(row.node, row.depth, row.setSize, row.posInSet, void 0) : (
533
+ // A windowed tree has no `group` to hold the placeholder, and `role="tree"` may own
534
+ // only treeitems — so the skeleton is decoration and `aria-busy` on the row speaks.
535
+ /* @__PURE__ */ jsx(
536
+ "div",
537
+ {
538
+ "aria-hidden": "true",
539
+ className: "ui-tree-loading",
540
+ style: { "--tree-node-level": row.depth },
541
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "ui-tree-loading-bar" })
542
+ },
543
+ row.key
544
+ )
545
+ )
546
+ ) : renderNodes(options, 0);
430
547
  return /* @__PURE__ */ jsxs(Fragment, { children: [
431
- /* @__PURE__ */ jsx(
548
+ /* @__PURE__ */ jsxs(
432
549
  "div",
433
550
  {
434
551
  ...ariaProps,
435
- ref: forwardedRef,
552
+ ref: mergeRefs(forwardedRef, treeRef),
436
553
  id: treeId,
437
554
  role: "tree",
438
555
  "aria-multiselectable": multiple || checkableProp,
@@ -442,8 +559,47 @@ function TreeRoot({
442
559
  "data-show-line": showLine ? "true" : void 0,
443
560
  "data-divided": divided ? "true" : void 0,
444
561
  "data-empty": isEmpty ? "true" : void 0,
562
+ "data-virtual": isWindowed ? "true" : void 0,
445
563
  className: cn("ui-tree", className),
446
- children: isEmpty ? null : renderNodes(options, 0)
564
+ tabIndex: rovingOffscreen ? 0 : void 0,
565
+ style: height === void 0 ? void 0 : {
566
+ maxBlockSize: height,
567
+ overflowY: "auto"
568
+ },
569
+ onScroll: isWindowed ? (event) => setScrollTop(event.currentTarget.scrollTop) : void 0,
570
+ onFocus: (event) => {
571
+ if (!isWindowed) return;
572
+ setHasFocusWithin(true);
573
+ if (event.target === event.currentTarget && rovingValue !== null) {
574
+ revealRow(rovingValue);
575
+ pendingFocus.current = rovingValue;
576
+ }
577
+ },
578
+ onBlur: (event) => {
579
+ if (!isWindowed) return;
580
+ if (!event.currentTarget.contains(event.relatedTarget)) {
581
+ setHasFocusWithin(false);
582
+ }
583
+ },
584
+ children: [
585
+ isWindowed ? /* @__PURE__ */ jsx(
586
+ "div",
587
+ {
588
+ "aria-hidden": "true",
589
+ className: "ui-tree-spacer",
590
+ style: { blockSize: windowStart * rowHeight }
591
+ }
592
+ ) : null,
593
+ body,
594
+ isWindowed ? /* @__PURE__ */ jsx(
595
+ "div",
596
+ {
597
+ "aria-hidden": "true",
598
+ className: "ui-tree-spacer",
599
+ style: { blockSize: (flatRows.length - windowEnd) * rowHeight }
600
+ }
601
+ ) : null
602
+ ]
447
603
  }
448
604
  ),
449
605
  isEmpty ? /* @__PURE__ */ jsx("p", { role: "status", className: "ui-tree-empty", children: t("dataDisplay.tree.empty") }) : null
@@ -16,6 +16,7 @@ export declare const BranchScopePicker: React.ForwardRefExoticComponent<import("
16
16
  error?: import("../../props/index.js").ErrorProp;
17
17
  loading?: boolean;
18
18
  empty?: React.ReactNode;
19
+ notFoundContent?: import("../../props/index.js").NotFoundContentProp;
19
20
  listError?: React.ReactNode;
20
21
  denied?: React.ReactNode;
21
22
  allLabel?: React.ReactNode;
@@ -25,6 +25,7 @@ const BranchScopePicker = React.forwardRef(
25
25
  error,
26
26
  loading = false,
27
27
  empty,
28
+ notFoundContent,
28
29
  listError,
29
30
  denied,
30
31
  allLabel,
@@ -156,7 +157,7 @@ const BranchScopePicker = React.forwardRef(
156
157
  ariaLabel: t("dataEntry.branchScope.searchPlaceholder")
157
158
  }
158
159
  ),
159
- filtered.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", "aria-live": "polite", children: t("dataEntry.branchScope.noMatches") }) : (
160
+ filtered.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", "aria-live": "polite", children: notFoundContent ?? t("dataEntry.branchScope.noMatches") }) : (
160
161
  // A bounded list is a scroll container, so it MUST be reachable and scrollable
161
162
  // with the keyboard alone even when every row is unfocusable (all checkboxes
162
163
  // `disabled`) — WCAG 2.1.1 / axe `scrollable-region-focusable`; same contract as
@@ -16,6 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover
16
16
  import { ScrollArea } from "../data-display/scroll-area.js";
17
17
  import { Command, CommandInput } from "./command.js";
18
18
  import {
19
+ createLazyLoadLedger,
19
20
  filterTreeOptions,
20
21
  formatPathLabels,
21
22
  getNodeByPath,
@@ -175,18 +176,16 @@ function Cascader({
175
176
  },
176
177
  [isSearchControlled, onSearchChange]
177
178
  );
178
- const requestedLoads = React.useRef(/* @__PURE__ */ new Set());
179
+ const [lazyLoads] = React.useState(createLazyLoadLedger);
179
180
  const requestLoad = React.useCallback(
180
181
  (path, chain) => {
181
182
  if (!loadData) return;
182
183
  const node = chain.at(-1);
183
184
  if (!node || node.isLeaf === true || (node.children?.length ?? 0) > 0) return;
184
185
  const key = pathKey(path);
185
- if (requestedLoads.current.has(key)) return;
186
- requestedLoads.current.add(key);
187
- void loadData(chain);
186
+ lazyLoads.run(key, () => loadData(chain));
188
187
  },
189
- [loadData]
188
+ [loadData, lazyLoads]
190
189
  );
191
190
  const isControlledSingle = !multiple && value !== void 0;
192
191
  const isControlledMulti = multiple && value !== void 0;
@@ -85,6 +85,8 @@ export declare const RecordPicker: React.ForwardRefExoticComponent<Omit<React.Bu
85
85
  };
86
86
  placeholder?: string;
87
87
  dialogTitle?: string;
88
+ notFoundContent?: import("../../props/index.js").NotFoundContentProp;
89
+ loadMoreLabel?: import("../../props/index.js").LabelProp;
88
90
  size?: "xs" | "sm" | "md" | "lg";
89
91
  "data-field"?: string;
90
92
  } & React.RefAttributes<HTMLButtonElement | HTMLInputElement>>;
@@ -50,6 +50,8 @@ const RecordPicker = React.forwardRef(
50
50
  emptyOption,
51
51
  placeholder,
52
52
  dialogTitle,
53
+ notFoundContent,
54
+ loadMoreLabel,
53
55
  disabled = false,
54
56
  size,
55
57
  className,
@@ -90,6 +92,8 @@ const RecordPicker = React.forwardRef(
90
92
  emptyOption,
91
93
  placeholder,
92
94
  dialogTitle,
95
+ notFoundContent,
96
+ loadMoreLabel,
93
97
  disabled,
94
98
  size,
95
99
  className,
@@ -115,6 +119,7 @@ const RecordPicker = React.forwardRef(
115
119
  value: isMultiple ? selected : selected[0] ?? void 0,
116
120
  onValueChange: ((next) => commit(toArray(next))),
117
121
  placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
122
+ notFoundContent,
118
123
  disabled,
119
124
  size,
120
125
  className
@@ -136,6 +141,8 @@ const RecordPicker = React.forwardRef(
136
141
  emptyOption,
137
142
  placeholder,
138
143
  dialogTitle,
144
+ notFoundContent,
145
+ loadMoreLabel,
139
146
  disabled,
140
147
  size,
141
148
  className,
@@ -164,6 +171,8 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
164
171
  emptyOption,
165
172
  placeholder,
166
173
  dialogTitle,
174
+ notFoundContent,
175
+ loadMoreLabel,
167
176
  disabled,
168
177
  size,
169
178
  className,
@@ -295,6 +304,8 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
295
304
  selectedOptions,
296
305
  emptyOption,
297
306
  dialogTitle,
307
+ notFoundContent,
308
+ loadMoreLabel,
298
309
  initialQuery: dialogQuery,
299
310
  autoOpen: true,
300
311
  onDismiss: () => setDialogQuery(null)
@@ -313,6 +324,8 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
313
324
  emptyOption,
314
325
  placeholder,
315
326
  dialogTitle,
327
+ notFoundContent,
328
+ loadMoreLabel,
316
329
  disabled,
317
330
  size,
318
331
  className,
@@ -479,11 +492,17 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
479
492
  variant: "outline",
480
493
  size: "sm",
481
494
  onClick: () => void load(query, filterValues),
482
- children: t("dataEntry.recordPicker.more")
495
+ children: t("common.retry")
483
496
  }
484
497
  )
485
498
  }
486
- ) : 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]) => {
499
+ ) : status === "loading" ? /* @__PURE__ */ jsx("div", { role: "status", className: "ui-record-picker-status", children: t("dataEntry.recordPicker.loading") }) : visible.length === 0 ? /* @__PURE__ */ jsx(
500
+ EmptyState,
501
+ {
502
+ variant: "compact",
503
+ title: notFoundContent ?? t("dataEntry.recordPicker.empty")
504
+ }
505
+ ) : groups.map(([heading, items]) => {
487
506
  const rendered = items.map((o) => {
488
507
  const picked = (isMultiple ? draft : selected).includes(o.value);
489
508
  return /* @__PURE__ */ jsxs(
@@ -516,7 +535,7 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
516
535
  variant: "outline",
517
536
  size: "sm",
518
537
  onClick: () => void load(query, filterValues, cursor),
519
- children: t("dataEntry.recordPicker.more")
538
+ children: loadMoreLabel ?? t("dataEntry.recordPicker.more")
520
539
  }
521
540
  ) : null
522
541
  ] }) }),
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { TransferProp } from "../../props/components/data-entry.prop.js";
3
3
  export type { TransferProp, TransferProp as TransferProps, TransferItemProp, } from "../../props/components/data-entry.prop.js";
4
- export declare function Transfer({ dataSource, targetKeys: legacyTargetKeys, value, defaultValue, name, readOnly, pagination, defaultTargetKeys, onValueChange, titles, showSearch, showSelectAll, filterOption, render, oneWay, disabled, id, className, selectedKeys: selectedKeysProp, onSelectChange, ...ariaProps }: TransferProp): React.JSX.Element;
4
+ export declare function Transfer({ dataSource, targetKeys: legacyTargetKeys, value, defaultValue, name, readOnly, pagination, defaultTargetKeys, onValueChange, titles, showSearch, showSelectAll, filterOption, render, oneWay, disabled, id, className, selectedKeys: selectedKeysProp, onSelectChange, locale, ...ariaProps }: TransferProp): React.JSX.Element;
@@ -158,6 +158,7 @@ function Transfer({
158
158
  className,
159
159
  selectedKeys: selectedKeysProp,
160
160
  onSelectChange,
161
+ locale,
161
162
  ...ariaProps
162
163
  }) {
163
164
  const { t } = useTranslation();
@@ -192,6 +193,8 @@ function Transfer({
192
193
  if (!selectedKeysProp) setInternalSelected(cleared);
193
194
  onSelectChange?.(cleared[0], cleared[1]);
194
195
  };
196
+ const notFound = locale?.notFoundContent;
197
+ const [leftNotFound, rightNotFound] = Array.isArray(notFound) ? notFound : [notFound, notFound];
195
198
  const leftTitle = titles?.[0] ?? t("dataEntry.transfer.source");
196
199
  const rightTitle = titles?.[1] ?? t("dataEntry.transfer.target");
197
200
  return /* @__PURE__ */ jsxs(
@@ -219,7 +222,7 @@ function Transfer({
219
222
  disabled: disabled || readOnly,
220
223
  pagination,
221
224
  searchPlaceholder: t("dataEntry.transfer.searchPlaceholder"),
222
- emptyText: t("dataEntry.transfer.empty")
225
+ emptyText: leftNotFound ?? t("dataEntry.transfer.empty")
223
226
  }
224
227
  ),
225
228
  /* @__PURE__ */ jsxs("div", { className: "ui-transfer-actions", children: [
@@ -263,7 +266,7 @@ function Transfer({
263
266
  disabled: disabled || readOnly,
264
267
  pagination,
265
268
  searchPlaceholder: t("dataEntry.transfer.searchPlaceholder"),
266
- emptyText: t("dataEntry.transfer.empty")
269
+ emptyText: rightNotFound ?? t("dataEntry.transfer.empty")
267
270
  }
268
271
  )
269
272
  ]
@@ -19,6 +19,7 @@ import { SearchInput } from "./search-input.js";
19
19
  import { Separator } from "../layout/separator.js";
20
20
  import {
21
21
  collectAllExpandableKeys,
22
+ createLazyLoadLedger,
22
23
  filterVisibleTree,
23
24
  findNodeByValue,
24
25
  flattenVisibleTree,
@@ -134,15 +135,23 @@ function TreeSelectRoot({
134
135
  if (!isControlled) setInternal(next);
135
136
  onValueChange?.(checkable || multiple ? next : next[0] ?? void 0);
136
137
  };
137
- const requestedLoads = React.useRef(/* @__PURE__ */ new Set());
138
+ const [lazyLoads] = React.useState(createLazyLoadLedger);
138
139
  const toggleExpand = (node) => {
139
140
  const key = node.value;
140
141
  const willExpand = !expandedKeys.has(key);
141
142
  if (willExpand && loadData && !(node.children?.length ?? 0) && node.isLeaf !== true) {
142
- if (!requestedLoads.current.has(key)) {
143
- requestedLoads.current.add(key);
144
- void loadData(node);
145
- }
143
+ lazyLoads.run(
144
+ key,
145
+ () => loadData(node),
146
+ (ok) => {
147
+ if (ok) return;
148
+ setExpandedKeys((prev) => {
149
+ const next = new Set(prev);
150
+ next.delete(key);
151
+ return next;
152
+ });
153
+ }
154
+ );
146
155
  }
147
156
  setExpandedKeys((prev) => {
148
157
  const next = new Set(prev);
@@ -2,6 +2,38 @@ import * as React from "react";
2
2
  import type { TextProp, TypographyBlockProp } from "../../props/components/general.prop.js";
3
3
  import type { TypographyEllipsisConfigProp } from "../../props/vocabulary/index.js";
4
4
  export type { TextProp, TextProp as TextProps, HeadingProp, HeadingProp as HeadingProps, TypographyProp, TypographyProp as TypographyProps, TypographyBlockProp, TypographyTitleProp, TypographyTitleProp as TitleProps, ParagraphProp, ParagraphProp as ParagraphProps, LinkProp, LinkProp as LinkProps, } from "../../props/components/general.prop.js";
5
+ /** How long the copy button shows its confirmed state. antd's value (`useCopyClick`). */
6
+ export declare const COPIED_RESET_MS = 3000;
7
+ /** antd `getNode` — `true`/`undefined` take the default; `false` suppresses unless a DOM node is required. */
8
+ export declare function getNode(node: React.ReactNode | boolean | undefined, fallback: React.ReactNode, needDom?: boolean): React.ReactNode;
9
+ /** antd `toCopyConfigList` — `false` suppresses BOTH states, otherwise index 0 = idle, 1 = copied. */
10
+ export declare function toCopyConfigList(value: React.ReactNode | false | undefined): (string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined)[] | readonly [false, false];
11
+ /**
12
+ * antd `useMergedConfig` — `true` means "on, all defaults"; an object means "on, these overrides".
13
+ * The boolean is the SUPPORT flag, so `copyable={false}` and `copyable={undefined}` are both off.
14
+ */
15
+ export declare function useMergedConfig<T extends object>(propConfig: boolean | T | undefined, template?: Partial<T>): [boolean, T];
16
+ /**
17
+ * The clipboard write, antd's `_util/copy` reduced to the one path a modern browser takes.
18
+ *
19
+ * antd keeps a `document.execCommand("copy")` fallback for browsers without the async Clipboard
20
+ * API. It is NOT ported: `execCommand` is deprecated, every browser in this package's support
21
+ * matrix ships `navigator.clipboard`, and the fallback's failure mode is worse than none — it
22
+ * reports success from a synchronous flag that a permission prompt can leave stale, so the UI
23
+ * claims a copy that never happened. Here a refusal throws, `onCopy` never fires, and the button
24
+ * never shows its confirmed state.
25
+ */
26
+ export declare function writeClipboard(text: string, format: "text/plain" | "text/html" | undefined): Promise<void>;
27
+ /**
28
+ * A tooltip that disappears entirely when its title is suppressed.
29
+ *
30
+ * antd wraps every action in `<Tooltip>`; this library's Tooltip is the house primitive and is
31
+ * composed (`Tooltip > TooltipTrigger > TooltipContent`), so the wrapper lives here.
32
+ */
33
+ export declare function ActionTooltip({ title, children, }: {
34
+ title: React.ReactNode;
35
+ children: React.ReactNode;
36
+ }): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | null | undefined;
5
37
  /**
6
38
  * Text — the typographic primitive, and antd's `Typography.Text`.
7
39
  *
@@ -62,7 +62,10 @@ async function writeClipboard(text, format) {
62
62
  function childrenToText(children) {
63
63
  return React.Children.toArray(children).map((node) => typeof node === "string" || typeof node === "number" ? String(node) : "").join("");
64
64
  }
65
- function ActionTooltip({ title, children }) {
65
+ function ActionTooltip({
66
+ title,
67
+ children
68
+ }) {
66
69
  if (title === null || title === void 0 || title === false || title === "") return children;
67
70
  return /* @__PURE__ */ jsxs(Tooltip, { children: [
68
71
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children }),
@@ -654,10 +657,16 @@ const Typography = Object.assign(TypographyRoot, {
654
657
  Link
655
658
  });
656
659
  export {
660
+ ActionTooltip,
661
+ COPIED_RESET_MS,
657
662
  Heading,
658
663
  Link,
659
664
  Paragraph,
660
665
  Text,
661
666
  Title,
662
- Typography
667
+ Typography,
668
+ getNode,
669
+ toCopyConfigList,
670
+ useMergedConfig,
671
+ writeClipboard
663
672
  };
@@ -27,4 +27,4 @@ export declare function flattenItemPages<TItem>(data: {
27
27
  */
28
28
  export declare function InfiniteQueryState<TPage, TFlat = TPage extends {
29
29
  items: (infer TItem)[];
30
- } ? TItem[] : unknown>({ query, skeleton, empty, flatten, isEmpty, errorRenderer, showRetry, onRetry, onAuthError, loadingMore, loadMore, showLoadMore, children, }: InfiniteQueryStateProp<TPage, TFlat>): React.JSX.Element;
30
+ } ? TItem[] : unknown>({ query, skeleton, empty, flatten, isEmpty, errorRenderer, showRetry, onRetry, onAuthError, loadingMore, loadMore, loadMoreLabel, showLoadMore, children, }: InfiniteQueryStateProp<TPage, TFlat>): React.JSX.Element;
@@ -26,6 +26,7 @@ function InfiniteQueryState({
26
26
  onAuthError,
27
27
  loadingMore,
28
28
  loadMore,
29
+ loadMoreLabel,
29
30
  showLoadMore = true,
30
31
  children
31
32
  }) {
@@ -73,7 +74,7 @@ function InfiniteQueryState({
73
74
  size: "sm",
74
75
  disabled: query.isFetchingNextPage,
75
76
  onClick: () => void query.fetchNextPage(),
76
- children: query.isFetchingNextPage ? t("common.working") : t("query.loadMore")
77
+ children: query.isFetchingNextPage ? t("common.working") : loadMoreLabel ?? t("query.loadMore")
77
78
  }
78
79
  ) }) : null;
79
80
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -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": "31.4.0",
3
+ "version": "31.6.0",
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,