@godxjp/ui 31.5.0 → 31.7.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 (81) hide show
  1. package/agent/START-HERE.md +5 -5
  2. package/agent/components/BranchScopePicker.json +5 -0
  3. package/agent/components/Breadcrumb.json +1 -1
  4. package/agent/components/Cascader.json +1 -1
  5. package/agent/components/DataTable.json +2 -2
  6. package/agent/components/InfiniteQueryState.json +6 -1
  7. package/agent/components/PageContainer.json +1 -1
  8. package/agent/components/Progress.json +12 -1
  9. package/agent/components/RangeTimeline.json +6 -0
  10. package/agent/components/RecordPicker.json +10 -0
  11. package/agent/components/Text.json +1 -1
  12. package/agent/components/Transfer.json +5 -0
  13. package/agent/components/Tree.json +21 -4
  14. package/agent/components/TreeSelect.json +1 -1
  15. package/agent/components.json +72 -13
  16. package/agent/index.json +5 -5
  17. package/agent/llms.txt +6 -6
  18. package/agent/tokens.json +12 -0
  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/progress.d.ts +16 -0
  22. package/dist/components/data-display/progress.js +17 -3
  23. package/dist/components/data-display/range-timeline.d.ts +20 -0
  24. package/dist/components/data-display/range-timeline.js +217 -197
  25. package/dist/components/data-display/tree.js +179 -23
  26. package/dist/components/data-entry/branch-scope-picker.d.ts +1 -0
  27. package/dist/components/data-entry/branch-scope-picker.js +2 -1
  28. package/dist/components/data-entry/cascader.js +4 -5
  29. package/dist/components/data-entry/record-picker.d.ts +2 -0
  30. package/dist/components/data-entry/record-picker.js +22 -3
  31. package/dist/components/data-entry/transfer.d.ts +1 -1
  32. package/dist/components/data-entry/transfer.js +5 -2
  33. package/dist/components/data-entry/tree-select.js +14 -5
  34. package/dist/components/feedback/tooltip.d.ts +21 -0
  35. package/dist/components/feedback/tooltip.js +101 -0
  36. package/dist/components/general/typography.js +30 -11
  37. package/dist/components/layout/breadcrumb-ellipsis.d.ts +16 -0
  38. package/dist/components/layout/breadcrumb-ellipsis.js +28 -0
  39. package/dist/components/layout/breadcrumb.js +16 -7
  40. package/dist/components/layout/page-container.js +29 -20
  41. package/dist/components/query/infinite-query-state.d.ts +1 -1
  42. package/dist/components/query/infinite-query-state.js +2 -1
  43. package/dist/contracts/measurement.json +1 -1
  44. package/dist/i18n/messages/en.json +18 -3
  45. package/dist/i18n/messages/ja.json +9 -3
  46. package/dist/i18n/messages/vi.json +9 -3
  47. package/dist/lib/hooks.d.ts +8 -4
  48. package/dist/lib/hooks.js +5 -2
  49. package/dist/lib/tree.d.ts +23 -0
  50. package/dist/lib/tree.js +29 -0
  51. package/dist/props/components/data-display.prop.d.ts +21 -2
  52. package/dist/props/components/data-entry.prop.d.ts +31 -4
  53. package/dist/props/components/query.prop.d.ts +8 -1
  54. package/dist/props/registry.d.ts +29 -6
  55. package/dist/props/registry.js +46 -4
  56. package/dist/props/vocabulary/data.prop.d.ts +32 -2
  57. package/dist/props/vocabulary/index.d.ts +2 -2
  58. package/dist/props/vocabulary/navigation.prop.d.ts +14 -0
  59. package/dist/styles/data-display-layout.css +52 -0
  60. package/dist/styles/layers.json +3 -2
  61. package/dist/styles/layout.css +22 -0
  62. package/dist/styles/table-layout.css +24 -1
  63. package/dist/tokens/components/navigation.css +2 -0
  64. package/dist/tokens/components/tree.css +2 -0
  65. package/docs/data-display/data-table/examples/server-paged.tsx +23 -0
  66. package/docs/data-display/data-table/index.md +2 -0
  67. package/docs/data-display/progress.tsx +24 -6
  68. package/docs/data-display/timeline.tsx +33 -0
  69. package/docs/data-display/tree.tsx +154 -0
  70. package/docs/data-entry/branch-scope-picker.tsx +6 -1
  71. package/docs/data-entry/record-picker.tsx +1 -0
  72. package/docs/data-entry/transfer.tsx +1 -0
  73. package/docs/general/typography.tsx +84 -1
  74. package/docs/i18n/messages/en.json +48 -1
  75. package/docs/i18n/messages/ja.json +47 -1
  76. package/docs/i18n/messages/vi.json +47 -1
  77. package/docs/navigation/breadcrumb.tsx +24 -0
  78. package/docs/query/infinite-query-state.tsx +5 -1
  79. package/docs/roadmap/tree-components.md +2 -1
  80. package/package.json +2 -2
  81. package/scripts/ui-audit.mjs +15 -5
@@ -12,6 +12,7 @@ import {
12
12
  import { cn } from "../../lib/utils.js";
13
13
  import { Slot } from "../../lib/slot.js";
14
14
  const DEFAULT_DELAY_DURATION = 200;
15
+ const TooltipAnchorContext = React.createContext(null);
15
16
  function toPlacement(side, align) {
16
17
  if (align === "center") return side;
17
18
  if (side === "top" || side === "bottom") return `${side} ${align}`;
@@ -76,9 +77,11 @@ function TooltipContent({
76
77
  ...props
77
78
  }) {
78
79
  const overlayPortalContainer = useOverlayPortalContainer();
80
+ const anchorRef = React.useContext(TooltipAnchorContext);
79
81
  return /* @__PURE__ */ jsx(
80
82
  AriaTooltip,
81
83
  {
84
+ ...anchorRef ? { triggerRef: anchorRef } : null,
82
85
  UNSTABLE_portalContainer: overlayPortalContainer,
83
86
  placement: toPlacement(side, align),
84
87
  offset: sideOffset,
@@ -119,7 +122,105 @@ function TooltipContent({
119
122
  }
120
123
  );
121
124
  }
125
+ const FOCUS_HOST = 'a[href], button, input, select, textarea, summary, [tabindex]:is([role="treeitem"], [role="gridcell"], [role="row"], [role="option"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="tab"], [role="link"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"])';
126
+ function isClipping(node) {
127
+ const box = node.querySelector(':scope > [data-slot="typography-content"]') ?? node;
128
+ return box.scrollWidth > box.clientWidth || box.scrollHeight > box.clientHeight;
129
+ }
130
+ function isFocusVisible(node) {
131
+ try {
132
+ return node.matches(":focus-visible");
133
+ } catch {
134
+ return true;
135
+ }
136
+ }
137
+ function EllipsisTooltip({
138
+ anchorRef,
139
+ title
140
+ }) {
141
+ const [open, setOpen] = React.useState(false);
142
+ const [side, setSide] = React.useState("top");
143
+ const timer = React.useRef(void 0);
144
+ React.useEffect(() => {
145
+ const node = anchorRef.current;
146
+ if (!node) return void 0;
147
+ const host = node.matches(FOCUS_HOST) ? node : node.parentElement?.closest(FOCUS_HOST);
148
+ const schedule = (next, delay) => {
149
+ clearTimeout(timer.current);
150
+ timer.current = setTimeout(() => {
151
+ setOpen(next);
152
+ }, delay);
153
+ };
154
+ const show = (delay) => {
155
+ if (!isClipping(node)) return;
156
+ setSide(host && host !== node && host.hasAttribute("data-state") ? "bottom" : "top");
157
+ schedule(true, delay);
158
+ };
159
+ const hide = () => {
160
+ schedule(false, 100);
161
+ };
162
+ const onPointerEnter = () => {
163
+ show(DEFAULT_DELAY_DURATION);
164
+ };
165
+ const onFocusIn = () => {
166
+ const active = document.activeElement;
167
+ if (active && (active === host || node.contains(active)) && isFocusVisible(active)) show(0);
168
+ };
169
+ node.addEventListener("pointerenter", onPointerEnter);
170
+ node.addEventListener("pointerleave", hide);
171
+ node.addEventListener("focusin", onFocusIn);
172
+ node.addEventListener("focusout", hide);
173
+ host?.addEventListener("focusin", onFocusIn);
174
+ host?.addEventListener("focusout", hide);
175
+ return () => {
176
+ clearTimeout(timer.current);
177
+ node.removeEventListener("pointerenter", onPointerEnter);
178
+ node.removeEventListener("pointerleave", hide);
179
+ node.removeEventListener("focusin", onFocusIn);
180
+ node.removeEventListener("focusout", hide);
181
+ host?.removeEventListener("focusin", onFocusIn);
182
+ host?.removeEventListener("focusout", hide);
183
+ };
184
+ }, [anchorRef]);
185
+ React.useEffect(() => {
186
+ if (!open) return void 0;
187
+ const onKeyDown = (event) => {
188
+ if (event.key === "Escape") {
189
+ clearTimeout(timer.current);
190
+ setOpen(false);
191
+ }
192
+ };
193
+ document.addEventListener("keydown", onKeyDown);
194
+ return () => {
195
+ document.removeEventListener("keydown", onKeyDown);
196
+ };
197
+ }, [open]);
198
+ return /* @__PURE__ */ jsx(
199
+ Tooltip,
200
+ {
201
+ open,
202
+ onOpenChange: (next) => {
203
+ if (!next) setOpen(false);
204
+ },
205
+ children: /* @__PURE__ */ jsx(TooltipAnchorContext.Provider, { value: anchorRef, children: /* @__PURE__ */ jsx(
206
+ TooltipContent,
207
+ {
208
+ side,
209
+ "data-ellipsis-tooltip": "",
210
+ onPointerEnter: () => {
211
+ clearTimeout(timer.current);
212
+ },
213
+ onPointerLeave: () => {
214
+ setOpen(false);
215
+ },
216
+ children: title
217
+ }
218
+ ) })
219
+ }
220
+ );
221
+ }
122
222
  export {
223
+ EllipsisTooltip,
123
224
  Tooltip,
124
225
  TooltipContent,
125
226
  TooltipProvider,
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { Check, Copy, LoaderCircle, Pencil } from "lucide-react";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { useTranslation } from "../../i18n/use-translation.js";
7
- import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
7
+ import { EllipsisTooltip, Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
8
8
  import { Textarea } from "../data-entry/textarea.js";
9
9
  import { isDevelopment } from "../../lib/dev.js";
10
10
  const COPIED_RESET_MS = 3e3;
@@ -347,7 +347,9 @@ function useTypographyBlock(props, allowRows) {
347
347
  wrapsContent,
348
348
  onTextClick: enableEdit && triggerType.includes("text") ? startEditing : void 0,
349
349
  ellipsisTooltip,
350
- isEllipsis
350
+ isEllipsis,
351
+ anchorRef: elementRef,
352
+ showsEllipsisTooltip: clamping && ellipsisTooltip !== void 0 && ellipsisTooltip !== null && ellipsisTooltip !== false && ellipsisTooltip !== ""
351
353
  };
352
354
  }
353
355
  function usesBlockBehaviour(props) {
@@ -409,7 +411,11 @@ const TextBase = React.forwardRef((props, ref) => {
409
411
  italic: _italic,
410
412
  ...rest
411
413
  } = props;
412
- const block = useTypographyBlock(props, allowRows);
414
+ const asChildElement = asChild && React.isValidElement(children) ? children : null;
415
+ const block = useTypographyBlock(
416
+ asChildElement ? { ...props, children: asChildElement.props.children } : props,
417
+ allowRows
418
+ );
413
419
  const element = as ?? component ?? "span";
414
420
  const clampLines = typeof clamp === "number" && Number.isFinite(clamp) && clamp >= 1 ? Math.floor(clamp) : void 0;
415
421
  const ellipsisRowCount = ellipsis ? ellipsisRows(ellipsis) : 0;
@@ -492,21 +498,29 @@ const TextBase = React.forwardRef((props, ref) => {
492
498
  ) : block.text,
493
499
  block.trailing
494
500
  ] });
501
+ let rendered;
495
502
  if (asChild) {
496
503
  const child = React.Children.only(children);
497
504
  const cloned = {
498
505
  ...typography,
499
506
  ...child.props,
500
- ref,
507
+ // The measuring ref too, or an `asChild` ellipsis never learns it is clipping (gh#1045).
508
+ ref: composeRefs(block.measureRef, ref),
501
509
  className: cn(typography.className, child.props.className)
502
510
  };
503
- return usesBlockBehaviour(props) ? React.cloneElement(child, cloned, body) : React.cloneElement(child, cloned);
511
+ rendered = usesBlockBehaviour(props) ? React.cloneElement(child, cloned, body) : React.cloneElement(child, cloned);
512
+ } else {
513
+ rendered = React.createElement(
514
+ element,
515
+ { ref: composeRefs(block.measureRef, ref), ...typography },
516
+ body
517
+ );
504
518
  }
505
- return React.createElement(
506
- element,
507
- { ref: composeRefs(block.measureRef, ref), ...typography },
508
- body
509
- );
519
+ if (!block.showsEllipsisTooltip) return rendered;
520
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
521
+ rendered,
522
+ /* @__PURE__ */ jsx(EllipsisTooltip, { anchorRef: block.anchorRef, title: block.ellipsisTooltip })
523
+ ] });
510
524
  });
511
525
  TextBase.displayName = "TextBase";
512
526
  const Text = React.forwardRef((props, ref) => /* @__PURE__ */ jsx(TextBase, { ref, ...props }));
@@ -615,7 +629,7 @@ const Title = React.forwardRef((props, ref) => {
615
629
  ) : block.text,
616
630
  block.trailing
617
631
  ] });
618
- return React.createElement(
632
+ const rendered = React.createElement(
619
633
  element,
620
634
  {
621
635
  ref: composeRefs(block.measureRef, ref),
@@ -635,6 +649,11 @@ const Title = React.forwardRef((props, ref) => {
635
649
  },
636
650
  body
637
651
  );
652
+ if (!block.showsEllipsisTooltip) return rendered;
653
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
654
+ rendered,
655
+ /* @__PURE__ */ jsx(EllipsisTooltip, { anchorRef: block.anchorRef, title: block.ellipsisTooltip })
656
+ ] });
638
657
  });
639
658
  Title.displayName = "Title";
640
659
  const TypographyRoot = React.forwardRef(
@@ -0,0 +1,16 @@
1
+ import { type ReactElement } from "react";
2
+ import type { BreadcrumbItemProp, BreadcrumbProp } from "../../props/vocabulary/navigation.prop.js";
3
+ /** `data-ellipsis` for the `<ol>`: present when any crumb truncates, so the trail stays one line. */
4
+ export declare function trailEllipsis(items: BreadcrumbProp): "" | undefined;
5
+ /**
6
+ * gh#1046 — one crumb's text box, bounded to one line with "…", and the full label in a tooltip on
7
+ * hover and on keyboard focus of the link. The crumb element itself is the clipped box (never a
8
+ * wrapper around it), so the link's own focus ring is not clipped. Shared with PageContainer's
9
+ * trail, which renders its own markup.
10
+ */
11
+ export declare function BreadcrumbCrumbText({ item, children, }: {
12
+ item: BreadcrumbItemProp;
13
+ children: ReactElement<{
14
+ className?: string;
15
+ }>;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { cloneElement, useRef } from "react";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { EllipsisTooltip } from "../feedback/tooltip.js";
6
+ function trailEllipsis(items) {
7
+ return items.some((item) => item.ellipsis) ? "" : void 0;
8
+ }
9
+ function BreadcrumbCrumbText({
10
+ item,
11
+ children
12
+ }) {
13
+ const anchorRef = useRef(null);
14
+ if (!item.ellipsis) return children;
15
+ const tooltip = typeof item.ellipsis === "object" && "tooltip" in item.ellipsis ? item.ellipsis.tooltip === true ? item.label : item.ellipsis.tooltip : item.label;
16
+ const hasTooltip = tooltip !== void 0 && tooltip !== null && tooltip !== false && tooltip !== "";
17
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
18
+ cloneElement(children, {
19
+ ref: anchorRef,
20
+ className: cn(children.props.className, "ui-breadcrumb-ellipsis")
21
+ }),
22
+ hasTooltip ? /* @__PURE__ */ jsx(EllipsisTooltip, { anchorRef, title: tooltip }) : null
23
+ ] });
24
+ }
25
+ export {
26
+ BreadcrumbCrumbText,
27
+ trailEllipsis
28
+ };
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown, ChevronRight } from "lucide-react";
4
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
+ import { BreadcrumbCrumbText, trailEllipsis } from "./breadcrumb-ellipsis.js";
5
6
  import {
6
7
  DropdownMenu,
7
8
  DropdownMenuContent,
@@ -17,12 +18,20 @@ function Breadcrumb({
17
18
  }) {
18
19
  const { t } = useTranslation();
19
20
  const separatorNode = separator === void 0 ? /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) : separator;
20
- return /* @__PURE__ */ jsx("nav", { "aria-label": ariaLabel ?? t("navigation.breadcrumb.ariaLabel"), className: "ui-breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: items.map((item, index) => {
21
+ return /* @__PURE__ */ jsx("nav", { "aria-label": ariaLabel ?? t("navigation.breadcrumb.ariaLabel"), className: "ui-breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", "data-ellipsis": trailEllipsis(items), children: items.map((item, index) => {
21
22
  const isLast = index === items.length - 1;
22
- return /* @__PURE__ */ jsxs("li", { className: "ui-breadcrumb-item", children: [
23
- renderBreadcrumbSegment({ item, index, isLast, items, itemRender, LinkComponent }),
24
- !isLast ? /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-separator", "aria-hidden": "true", children: separatorNode }) : null
25
- ] }, item.to ?? index);
23
+ return /* @__PURE__ */ jsxs(
24
+ "li",
25
+ {
26
+ className: "ui-breadcrumb-item",
27
+ "data-ellipsis": item.ellipsis ? "" : void 0,
28
+ children: [
29
+ renderBreadcrumbSegment({ item, index, isLast, items, itemRender, LinkComponent }),
30
+ !isLast ? /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-separator", "aria-hidden": "true", children: separatorNode }) : null
31
+ ]
32
+ },
33
+ item.to ?? index
34
+ );
26
35
  }) }) });
27
36
  }
28
37
  function renderBreadcrumbSegment({
@@ -54,9 +63,9 @@ function renderBreadcrumbSegment({
54
63
  ] });
55
64
  }
56
65
  if (item.to && !isLast) {
57
- return /* @__PURE__ */ jsx(LinkComponent, { href: item.to, to: item.to, className: "ui-breadcrumb-link", children: item.label });
66
+ return /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(LinkComponent, { href: item.to, to: item.to, className: "ui-breadcrumb-link", children: item.label }) });
58
67
  }
59
- return /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-current", "aria-current": isLast ? "page" : void 0, children: item.label });
68
+ return /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx("span", { className: "ui-breadcrumb-current", "aria-current": isLast ? "page" : void 0, children: item.label }) });
60
69
  }
61
70
  export {
62
71
  Breadcrumb
@@ -5,6 +5,7 @@ import { ChevronRight } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { scrollParent, useInView } from "../../lib/hooks.js";
7
7
  import { cn } from "../../lib/utils.js";
8
+ import { BreadcrumbCrumbText, trailEllipsis } from "./breadcrumb-ellipsis.js";
8
9
  import { densityClass, pageContainerVariantClass, padStyle } from "../../lib/variants.js";
9
10
  function useFooterReveal(enabled) {
10
11
  const headerRef = useRef(null);
@@ -92,7 +93,7 @@ function PageContainerRoot({
92
93
  {
93
94
  "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
94
95
  className: "ui-breadcrumb",
95
- children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
96
+ children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", "data-ellipsis": trailEllipsis(breadcrumb), children: breadcrumb.map((item, i) => {
96
97
  const isLast = i === breadcrumb.length - 1;
97
98
  return (
98
99
  // `--space-inline-xs` gap the previous `.ui-inline-xs` gave, and it brings
@@ -100,25 +101,33 @@ function PageContainerRoot({
100
101
  // longer carries a `size-3` literal. It also drops `.ui-inline-xs`'s
101
102
  // `flex-wrap: wrap` — a crumb must not break between its label and its
102
103
  // chevron; the LIST wraps instead, exactly as <Breadcrumb> already behaves.
103
- /* @__PURE__ */ jsxs("li", { className: "ui-breadcrumb-item", children: [
104
- item.to && !isLast ? /* @__PURE__ */ jsx(
105
- LinkComponent,
106
- {
107
- href: item.to,
108
- to: item.to,
109
- className: "hover:text-foreground hover:underline",
110
- children: item.label
111
- }
112
- ) : /* @__PURE__ */ jsx(
113
- "span",
114
- {
115
- className: isLast ? "text-foreground" : "",
116
- "aria-current": isLast ? "page" : void 0,
117
- children: item.label
118
- }
119
- ),
120
- !isLast && /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
121
- ] }, i)
104
+ /* @__PURE__ */ jsxs(
105
+ "li",
106
+ {
107
+ className: "ui-breadcrumb-item",
108
+ "data-ellipsis": item.ellipsis ? "" : void 0,
109
+ children: [
110
+ item.to && !isLast ? /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(
111
+ LinkComponent,
112
+ {
113
+ href: item.to,
114
+ to: item.to,
115
+ className: "hover:text-foreground hover:underline",
116
+ children: item.label
117
+ }
118
+ ) }) : /* @__PURE__ */ jsx(BreadcrumbCrumbText, { item, children: /* @__PURE__ */ jsx(
119
+ "span",
120
+ {
121
+ className: isLast ? "text-foreground" : "",
122
+ "aria-current": isLast ? "page" : void 0,
123
+ children: item.label
124
+ }
125
+ ) }),
126
+ !isLast && /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
127
+ ]
128
+ },
129
+ i
130
+ )
122
131
  );
123
132
  }) })
124
133
  }
@@ -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.5.0",
3
+ "version": "31.7.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,
@@ -350,7 +350,8 @@
350
350
  },
351
351
  "progress": {
352
352
  "ariaLabel": "Progress",
353
- "breakdownSeparator": ", "
353
+ "breakdownSeparator": ", ",
354
+ "remainder": "Remaining"
354
355
  },
355
356
  "scrollArea": {
356
357
  "region": "Scrollable region"
@@ -360,7 +361,8 @@
360
361
  "collapse": "Collapse",
361
362
  "loading": "Loading child nodes…",
362
363
  "empty": "Nothing to show",
363
- "selected": "Selected"
364
+ "selected": "Selected",
365
+ "filterMatch": "Matches the filter"
364
366
  },
365
367
  "thoughtChain": {
366
368
  "label": "Reasoning steps",
@@ -585,7 +587,20 @@
585
587
  "sortDescending": "Sort descending",
586
588
  "sortCancel": "Cancel sort",
587
589
  "selectColumn": "Select",
588
- "scrollRegion": "Scrollable table"
590
+ "scrollRegion": "Scrollable table",
591
+ "selectPage": "Select all rows on this page",
592
+ "matchingPage": {
593
+ "one": "{count} row on this page is selected.",
594
+ "other": "All {count} rows on this page are selected."
595
+ },
596
+ "matchingSelectAll": {
597
+ "one": "Select {count} matching row",
598
+ "other": "Select all {count} matching rows"
599
+ },
600
+ "matchingSelected": {
601
+ "one": "{count} matching row is selected.",
602
+ "other": "All {count} matching rows are selected."
603
+ }
589
604
  },
590
605
  "dataGrid": {
591
606
  "search": "Search",
@@ -343,7 +343,8 @@
343
343
  },
344
344
  "progress": {
345
345
  "ariaLabel": "進捗",
346
- "breakdownSeparator": "、"
346
+ "breakdownSeparator": "、",
347
+ "remainder": "残り"
347
348
  },
348
349
  "scrollArea": {
349
350
  "region": "スクロール可能な領域"
@@ -353,7 +354,8 @@
353
354
  "collapse": "折りたたむ",
354
355
  "loading": "子ノードを読み込み中…",
355
356
  "empty": "表示する項目がありません",
356
- "selected": "選択中"
357
+ "selected": "選択中",
358
+ "filterMatch": "フィルターに一致"
357
359
  },
358
360
  "thoughtChain": {
359
361
  "label": "思考の手順",
@@ -572,7 +574,11 @@
572
574
  "sortDescending": "降順に並べ替え",
573
575
  "sortCancel": "並べ替えを解除",
574
576
  "selectColumn": "選択",
575
- "scrollRegion": "スクロール可能な表"
577
+ "scrollRegion": "スクロール可能な表",
578
+ "selectPage": "このページの行をすべて選択",
579
+ "matchingPage": "このページの{count}件を選択中",
580
+ "matchingSelectAll": "{count}件すべてを選択",
581
+ "matchingSelected": "{count}件すべてを選択中"
576
582
  },
577
583
  "dataGrid": {
578
584
  "search": "検索",
@@ -344,7 +344,8 @@
344
344
  },
345
345
  "progress": {
346
346
  "ariaLabel": "Tiến độ",
347
- "breakdownSeparator": ", "
347
+ "breakdownSeparator": ", ",
348
+ "remainder": "Còn lại"
348
349
  },
349
350
  "scrollArea": {
350
351
  "region": "Vùng có thể cuộn"
@@ -354,7 +355,8 @@
354
355
  "collapse": "Thu gọn",
355
356
  "loading": "Đang tải nút con…",
356
357
  "empty": "Không có gì để hiển thị",
357
- "selected": "Đã chọn"
358
+ "selected": "Đã chọn",
359
+ "filterMatch": "Khớp bộ lọc"
358
360
  },
359
361
  "thoughtChain": {
360
362
  "label": "Các bước suy luận",
@@ -573,7 +575,11 @@
573
575
  "sortDescending": "Sắp xếp giảm dần",
574
576
  "sortCancel": "Bỏ sắp xếp",
575
577
  "selectColumn": "Chọn",
576
- "scrollRegion": "Bảng có thể cuộn"
578
+ "scrollRegion": "Bảng có thể cuộn",
579
+ "selectPage": "Chọn tất cả các dòng trên trang này",
580
+ "matchingPage": "Đã chọn {count} dòng trên trang này.",
581
+ "matchingSelectAll": "Chọn tất cả {count} dòng khớp",
582
+ "matchingSelected": "Đã chọn tất cả {count} dòng khớp."
577
583
  },
578
584
  "dataGrid": {
579
585
  "search": "Tìm kiếm",
@@ -53,12 +53,16 @@ export declare function useScrollableRegionTabIndex(element: HTMLElement | null)
53
53
  * overflows. Reading it as "no overflow" would strand the overflow from every keyboard user — a
54
54
  * worse failure than an extra tab stop — so an unmeasured box counts as scrolling.
55
55
  *
56
- * `axis` is the box's OWN `overflow`, not a preference: an axis it does not scroll on is `hidden`
57
- * there, so overflow on that axis is CLIPPED rather than reachable, and measuring it would keep a
58
- * tab stop that scrolls nothing. `ScrollArea`'s `orientation` is exactly this union.
56
+ * `axis` names the axes the caller KNOWS scroll; an axis whose computed `overflow` is `auto` or
57
+ * `scroll` is measured as well, because the box scrolls there whatever the caller asked. An axis
58
+ * that is `hidden` CLIPS rather than scrolls, so it is never measured — that would keep a tab stop
59
+ * that scrolls nothing. `ScrollArea`'s `orientation` is exactly this union.
59
60
  */
60
61
  export declare function useScrollsOnAxis(ref: RefObject<HTMLElement | null>, enabled: boolean, axis: "horizontal" | "vertical" | "both"): boolean;
61
- /** A table's wrapper scrolls on one axis only, so it asks the one question it has (gh#817). */
62
+ /**
63
+ * The horizontal question a table asks (gh#817). The block axis is measured TOO whenever the box's
64
+ * computed `overflow-y` scrolls — `overflow-x: auto` computes it to `auto` — see `useScrollsOnAxis`.
65
+ */
62
66
  export declare function useScrollsHorizontally(ref: RefObject<HTMLElement | null>, enabled: boolean): boolean;
63
67
  /**
64
68
  * Nearest scrollable ancestor — the box an element actually scrolls inside — else `null`, which is
package/dist/lib/hooks.js CHANGED
@@ -111,9 +111,12 @@ function useScrollsOnAxis(ref, enabled, axis) {
111
111
  useEffect(() => {
112
112
  const el = ref.current;
113
113
  if (!enabled || !el) return void 0;
114
- const overflows = (client, scroll) => client === 0 || scroll - client > 1;
114
+ const overflows = (client, scroll) => scroll - client > 1;
115
+ const scrollsOn = (value) => value === "auto" || value === "scroll";
115
116
  const update = (sync) => {
116
- const next = axis !== "vertical" && overflows(el.clientWidth, el.scrollWidth) || axis !== "horizontal" && overflows(el.clientHeight, el.scrollHeight);
117
+ const unmeasured = el.clientWidth === 0 && el.clientHeight === 0;
118
+ const style = getComputedStyle(el);
119
+ const next = unmeasured || (axis !== "vertical" || scrollsOn(style.overflowX)) && overflows(el.clientWidth, el.scrollWidth) || (axis !== "horizontal" || scrollsOn(style.overflowY)) && overflows(el.clientHeight, el.scrollHeight);
117
120
  if (next && sync) flushSync(() => setScrolls(true));
118
121
  else setScrolls(next);
119
122
  };
@@ -50,4 +50,27 @@ export declare function filterVisibleTree(options: NormalizedTreeOption[], query
50
50
  }[];
51
51
  export declare function collectAllExpandableKeys(options: NormalizedTreeOption[]): string[];
52
52
  export declare function findNodeByValue(options: NormalizedTreeOption[], value: string): NormalizedTreeOption | undefined;
53
+ /**
54
+ * rc-tree gives up on a node after this many rejected `loadData` calls and treats it as loaded
55
+ * (rc-tree `MAX_RETRY_TIMES`), so a permanently broken endpoint cannot be hammered forever.
56
+ */
57
+ export declare const MAX_LAZY_LOAD_RETRIES = 10;
58
+ /**
59
+ * The lazy-children ledger shared by `Tree`, `TreeSelect` and `Cascader` — rc-tree's
60
+ * `loadedKeys` / `loadingKeys` / `loadingRetryTimes`, in one place (gh#1041).
61
+ *
62
+ * A key is asked for at most once while in flight and never again once it RESOLVED. A REJECTED
63
+ * load is not "loaded": the key leaves the in-flight set and the next expand asks again, until
64
+ * `MAX_LAZY_LOAD_RETRIES` failures. The old per-component ledgers recorded the key before the
65
+ * promise settled and never cleared it, so one network blip left a branch unloadable for the life
66
+ * of the component.
67
+ */
68
+ export type LazyLoadLedger = {
69
+ /**
70
+ * Call `load` for `key` unless it is in flight or already loaded. A synchronous throw counts
71
+ * as a rejection. `onSettle(ok)` runs once the load settles. Returns whether `load` was called.
72
+ */
73
+ run: (key: string, load: () => void | Promise<void>, onSettle?: (ok: boolean) => void) => boolean;
74
+ };
75
+ export declare function createLazyLoadLedger(): LazyLoadLedger;
53
76
  export {};
package/dist/lib/tree.js CHANGED
@@ -136,10 +136,39 @@ function findNodeByValue(options, value) {
136
136
  }
137
137
  return void 0;
138
138
  }
139
+ const MAX_LAZY_LOAD_RETRIES = 10;
140
+ function createLazyLoadLedger() {
141
+ const loaded = /* @__PURE__ */ new Set();
142
+ const inFlight = /* @__PURE__ */ new Set();
143
+ const failures = /* @__PURE__ */ new Map();
144
+ return {
145
+ run(key, load, onSettle) {
146
+ if (loaded.has(key) || inFlight.has(key)) return false;
147
+ inFlight.add(key);
148
+ new Promise((resolve) => resolve(load())).then(
149
+ () => {
150
+ inFlight.delete(key);
151
+ loaded.add(key);
152
+ onSettle?.(true);
153
+ },
154
+ () => {
155
+ inFlight.delete(key);
156
+ const count = (failures.get(key) ?? 0) + 1;
157
+ failures.set(key, count);
158
+ if (count >= MAX_LAZY_LOAD_RETRIES) loaded.add(key);
159
+ onSettle?.(false);
160
+ }
161
+ );
162
+ return true;
163
+ }
164
+ };
165
+ }
139
166
  export {
167
+ MAX_LAZY_LOAD_RETRIES,
140
168
  collectAllExpandableKeys,
141
169
  collectAllPaths,
142
170
  collectLeafPaths,
171
+ createLazyLoadLedger,
143
172
  filterTreeOptions,
144
173
  filterVisibleTree,
145
174
  findNodeByValue,