@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
@@ -0,0 +1,165 @@
1
+ import {
2
+ Avatar,
3
+ AvatarFallback,
4
+ Badge,
5
+ Card,
6
+ CardContent,
7
+ OrgChart,
8
+ type OrgChartNodeProp,
9
+ } from "@godxjp/ui/data-display";
10
+ import { Text } from "@godxjp/ui/general";
11
+ import {
12
+ AppShell,
13
+ Flex,
14
+ PageContainer,
15
+ Sidebar,
16
+ type SidebarSectionProp,
17
+ Topbar,
18
+ } from "@godxjp/ui/layout";
19
+ import { Bot, Building2, Settings, Users } from "lucide-react";
20
+
21
+ /**
22
+ * OrgChart — who reports to whom, people and AI agents on one chart (gh#1034).
23
+ *
24
+ * Three levels: the CEO, three department heads, and their reports, two of which are AI agents.
25
+ * An agent's box is dashed and its accessible name ends in "AI agent", so the difference is never
26
+ * the stroke alone. The chart is an APG tree view: Tab lands on one box, the arrow keys walk it.
27
+ *
28
+ * Narrow the preview: under 40rem of CONTAINER width the same data turns into an indented Tree.
29
+ * The second card is deliberately narrow to show that form beside the wide one.
30
+ *
31
+ * Composed only from real @godxjp/ui components.
32
+ */
33
+ const sections: SidebarSectionProp[] = [
34
+ {
35
+ label: "Organization",
36
+ items: [
37
+ { id: "chart", label: "Org chart", icon: Building2 },
38
+ { id: "members", label: "Members", icon: Users },
39
+ { id: "agents", label: "Agents", icon: Bot },
40
+ ],
41
+ },
42
+ { label: "Admin", items: [{ id: "settings", label: "Settings", icon: Settings }] },
43
+ ];
44
+
45
+ const mark = (initials: string) => (
46
+ <Avatar size="sm">
47
+ <AvatarFallback>{initials}</AvatarFallback>
48
+ </Avatar>
49
+ );
50
+
51
+ const agentMark = (
52
+ <Avatar size="sm" appearance="tinted">
53
+ <AvatarFallback>
54
+ <Bot aria-hidden="true" />
55
+ </AvatarFallback>
56
+ </Avatar>
57
+ );
58
+
59
+ const org: OrgChartNodeProp[] = [
60
+ {
61
+ key: "ceo",
62
+ name: "Haruka Tanaka",
63
+ title: "Chief Executive Officer",
64
+ avatar: mark("HT"),
65
+ children: [
66
+ {
67
+ key: "cto",
68
+ name: "Kenji Watanabe",
69
+ title: "Chief Technology Officer",
70
+ avatar: mark("KW"),
71
+ children: [
72
+ {
73
+ key: "review-agent",
74
+ name: "Review Agent",
75
+ title: "Code review · pull requests",
76
+ variant: "agent",
77
+ avatar: agentMark,
78
+ extra: <Badge tone="success">Running</Badge>,
79
+ },
80
+ {
81
+ key: "platform",
82
+ name: "Mai Suzuki",
83
+ title: "Platform Lead",
84
+ avatar: mark("MS"),
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ key: "coo",
90
+ name: "Daniel Park",
91
+ title: "Chief Operating Officer",
92
+ avatar: mark("DP"),
93
+ children: [
94
+ {
95
+ key: "support-agent",
96
+ name: "Support Agent",
97
+ title: "Tier-1 customer support",
98
+ variant: "agent",
99
+ avatar: agentMark,
100
+ extra: <Badge tone="warning">Paused</Badge>,
101
+ },
102
+ {
103
+ key: "ops",
104
+ name: "Linh Nguyen",
105
+ title: "Operations Manager",
106
+ avatar: mark("LN"),
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ key: "cfo",
112
+ name: "Sofia Rossi",
113
+ title: "Chief Financial Officer",
114
+ avatar: mark("SR"),
115
+ children: [
116
+ {
117
+ key: "controller",
118
+ name: "Takumi Ito",
119
+ title: "Financial Controller",
120
+ avatar: mark("TI"),
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ },
126
+ ];
127
+
128
+ export default function OrgChartDoc() {
129
+ return (
130
+ <AppShell
131
+ sidebar={
132
+ <Sidebar
133
+ activeId="chart"
134
+ sections={sections}
135
+ onSelect={() => {}}
136
+ product={{ name: "CoreDesk", role: "Admin", color: "hsl(var(--primary))" }}
137
+ />
138
+ }
139
+ topbar={<Topbar />}
140
+ >
141
+ <PageContainer
142
+ title="Org chart"
143
+ subtitle="OrgChart · people and AI agents, top-down. Dashed boxes are agents."
144
+ >
145
+ <Card>
146
+ <CardContent>
147
+ <OrgChart data={org} label="Company org chart" />
148
+ </CardContent>
149
+ </Card>
150
+
151
+ {/* THE NARROW FORM: the same data under 40rem of container width. */}
152
+ <Flex direction="col" gap="sm">
153
+ <Text size="sm" weight="medium">
154
+ narrow container · the same data as a Tree
155
+ </Text>
156
+ <Card className="max-w-sm">
157
+ <CardContent>
158
+ <OrgChart data={org} label="Company org chart (list)" />
159
+ </CardContent>
160
+ </Card>
161
+ </Flex>
162
+ </PageContainer>
163
+ </AppShell>
164
+ );
165
+ }
@@ -230,6 +230,39 @@ export default function Demo() {
230
230
  </CardContent>
231
231
  </Card>
232
232
 
233
+ {/* sticky sits OUTSIDE a Card on purpose: Card clips (`overflow: hidden`), and a clipping
234
+ ancestor becomes the box the header sticks to — the same limit as antd Table sticky. */}
235
+ <Card>
236
+ <CardHeader>
237
+ <CardTitle level={2}>sticky · the axis stays on screen (gh#1044)</CardTitle>
238
+ <CardDescription>
239
+ A long schedule scrolled by the page, placed as a page section rather than inside a
240
+ Card (a Card clips, which would pin the header to the card instead of the viewport).
241
+ With sticky the band and tick rows stay at the top of the viewport — offsetHeader
242
+ clears a fixed app topbar — and still scroll sideways with the body.
243
+ </CardDescription>
244
+ </CardHeader>
245
+ </Card>
246
+ <RangeTimeline
247
+ data-testid="range-timeline-sticky"
248
+ label={t("rangeTimeline.schedule")}
249
+ sticky={{ offsetHeader: 0 }}
250
+ today={8}
251
+ bands={[{ label: monthLabel(8), units: 30 }]}
252
+ columns={Array.from({ length: 30 }, (_, index) => ({
253
+ label: new Intl.NumberFormat(locale).format(index + 1),
254
+ units: 1,
255
+ }))}
256
+ rows={Array.from({ length: 40 }, (_, index) => ({
257
+ id: `task-${index + 1}`,
258
+ label: `Task ${index + 1}`,
259
+ start: index % 20,
260
+ end: (index % 20) + 5,
261
+ startLabel: dayLabel((index % 20) + 1),
262
+ endLabel: dayLabel((index % 20) + 6),
263
+ }))}
264
+ />
265
+
233
266
  <Card>
234
267
  <CardHeader>
235
268
  <CardTitle level={2}>月をまたぐ日単位の軸(bands)</CardTitle>
@@ -12,7 +12,9 @@ import {
12
12
  Tree,
13
13
  type TreeNodeProp,
14
14
  } from "@godxjp/ui/data-display";
15
+ import { SearchInput } from "@godxjp/ui/data-entry";
15
16
  import { Text } from "@godxjp/ui/general";
17
+ import { useTranslation } from "@godxjp/ui/i18n";
16
18
  import { Flex, PageContainer, Separator } from "@godxjp/ui/layout";
17
19
  import { Building2, FolderTree, ShieldCheck } from "lucide-react";
18
20
 
@@ -167,7 +169,85 @@ function attachChildren(nodes: Department[], id: string, children: Department[])
167
169
  );
168
170
  }
169
171
 
172
+ // ── card 4 · a long lazy level (height + virtual · filterTreeNode · retry) ────────────────────
173
+ // The godx-task shape (gh#1041-1043): 2 suites → 28 sections → up to 421 cases per section,
174
+ // fetched when a section opens. antd's Tree has no "load more" node — `height` keeps a 421-row
175
+ // level to a few dozen DOM rows — so the one paged section below composes its own: a leaf whose
176
+ // selection asks for the next page. It is a real treeitem, so the keyboard reaches it.
177
+ const CASE_PAGE = 100;
178
+ // The data carries VALUES only; every visible label is derived from the value through `t()` at
179
+ // render (`labelSuiteTree`), so the demo follows the active locale like the library's chrome does.
180
+ const suites: TreeNodeProp[] = [1, 2].map((suiteNumber) => ({
181
+ value: `suite-${suiteNumber}`,
182
+ label: "",
183
+ children: Array.from({ length: 14 }, (_, sectionIndex) => ({
184
+ value: `suite-${suiteNumber}/section-${sectionIndex + 1}`,
185
+ label: "",
186
+ isLeaf: false,
187
+ })),
188
+ }));
189
+
190
+ function caseNodes(section: string, from: number, to: number): TreeNodeProp[] {
191
+ return Array.from({ length: to - from }, (_, index) => ({
192
+ value: `${section}/case-${from + index + 1}`,
193
+ label: "",
194
+ isLeaf: true,
195
+ }));
196
+ }
197
+
198
+ function withChildren(
199
+ nodes: TreeNodeProp[],
200
+ value: string,
201
+ children: TreeNodeProp[],
202
+ ): TreeNodeProp[] {
203
+ return nodes.map((node) =>
204
+ node.value === value
205
+ ? { ...node, children }
206
+ : node.children
207
+ ? { ...node, children: withChildren(node.children, value, children) }
208
+ : node,
209
+ );
210
+ }
211
+
212
+ function labelSuiteTree(
213
+ nodes: TreeNodeProp[],
214
+ t: ReturnType<typeof useTranslation>["t"],
215
+ ): TreeNodeProp[] {
216
+ return nodes.map((node) => {
217
+ const value = node.value;
218
+ let label: string;
219
+ if (value.endsWith("::more")) {
220
+ label = t("treeDocs.longList.loadMore", { count: CASE_PAGE });
221
+ } else if (/\/case-\d+$/.test(value)) {
222
+ label = t("treeDocs.longList.case", {
223
+ number: String(Number(value.split("case-").pop())).padStart(3, "0"),
224
+ });
225
+ } else if (/\/section-\d+$/.test(value)) {
226
+ const number = Number(value.split("section-").pop());
227
+ const key =
228
+ value === "suite-1/section-2"
229
+ ? "treeDocs.longList.sectionFailing"
230
+ : value === "suite-1/section-3"
231
+ ? "treeDocs.longList.sectionPaged"
232
+ : "treeDocs.longList.section";
233
+ label = t(key, { number, count: CASE_PAGE });
234
+ } else {
235
+ label = t(
236
+ value === "suite-1"
237
+ ? "treeDocs.longList.suiteIntegration"
238
+ : "treeDocs.longList.suiteAcceptance",
239
+ );
240
+ }
241
+ return {
242
+ ...node,
243
+ label,
244
+ children: node.children ? labelSuiteTree(node.children, t) : undefined,
245
+ };
246
+ });
247
+ }
248
+
170
249
  export default function Demo() {
250
+ const { t } = useTranslation();
171
251
  // Card 1 — checks are controlled so the readout beside the tree can never disagree with it.
172
252
  const [granted, setGranted] = React.useState<string[]>(["billing.invoice.read"]);
173
253
  // Card 2 — selection is controlled and drives the detail pane.
@@ -179,6 +259,45 @@ export default function Demo() {
179
259
  // Card 5 — independent (strict) checks, and a multi-select tree.
180
260
  const [strictChecks, setStrictChecks] = React.useState<string[]>(["people", "people.read"]);
181
261
  const [picked, setPicked] = React.useState<string[]>(["billing", "audit"]);
262
+ // Card 4 — the long lazy level.
263
+ const [suiteTree, setSuiteTree] = React.useState<TreeNodeProp[]>(suites);
264
+ const [caseQuery, setCaseQuery] = React.useState("");
265
+ // "" (not undefined) keeps the tree CONTROLLED while nothing is open.
266
+ const [openCase, setOpenCase] = React.useState("");
267
+ const failedOnce = React.useRef(false);
268
+
269
+ const loadSection = async (node: TreeNodeProp) => {
270
+ await new Promise((resolve) => setTimeout(resolve, 600));
271
+ if (node.value === "suite-1/section-2" && !failedOnce.current) {
272
+ failedOnce.current = true;
273
+ // Rejected: the branch folds back shut, and opening it again asks again (gh#1041).
274
+ throw new Error("network");
275
+ }
276
+ const paged = node.value === "suite-1/section-3";
277
+ const children = caseNodes(node.value, 0, paged ? CASE_PAGE : 421);
278
+ if (paged) {
279
+ children.push({
280
+ value: `${node.value}::more`,
281
+ label: "",
282
+ isLeaf: true,
283
+ });
284
+ }
285
+ setSuiteTree((current) => withChildren(current, node.value, children));
286
+ };
287
+
288
+ const loadMoreCases = (moreValue: string) => {
289
+ const section = moreValue.replace(/::more$/, "");
290
+ setSuiteTree((current) => {
291
+ const sectionNode = current
292
+ .flatMap((suite) => suite.children ?? [])
293
+ .find((node) => node.value === section);
294
+ const loaded = (sectionNode?.children ?? []).filter((node) => !node.value.endsWith("::more"));
295
+ const next = caseNodes(section, loaded.length, Math.min(421, loaded.length + CASE_PAGE));
296
+ const more =
297
+ loaded.length + next.length < 421 ? [{ value: moreValue, label: "", isLeaf: true }] : [];
298
+ return withChildren(current, section, [...loaded, ...next, ...more]);
299
+ });
300
+ };
182
301
 
183
302
  const loadDepartment = async (node: TreeNodeProp) => {
184
303
  // A real delay, so the Skeleton row and aria-busy are OBSERVABLE rather than theoretical.
@@ -365,6 +484,41 @@ export default function Demo() {
365
484
  </CardContent>
366
485
  </Card>
367
486
 
487
+ <Card>
488
+ <CardHeader>
489
+ <CardTitle level={2}>{t("treeDocs.longList.title")}</CardTitle>
490
+ <CardDescription>{t("treeDocs.longList.description")}</CardDescription>
491
+ </CardHeader>
492
+ <CardContent>
493
+ <Flex direction="col" gap="sm">
494
+ <SearchInput
495
+ ariaLabel={t("treeDocs.longList.searchLabel")}
496
+ placeholder={t("treeDocs.longList.searchPlaceholder")}
497
+ value={caseQuery}
498
+ onValueChange={setCaseQuery}
499
+ onSearch={() => {}}
500
+ />
501
+ <Tree
502
+ aria-label={t("treeDocs.longList.treeLabel")}
503
+ treeData={labelSuiteTree(suiteTree, t)}
504
+ height={360}
505
+ defaultExpandedValues={["suite-1"]}
506
+ loadData={loadSection}
507
+ filterTreeNode={(node) =>
508
+ caseQuery.trim() !== "" && String(node.label).includes(caseQuery.trim())
509
+ }
510
+ value={openCase}
511
+ onValueChange={(next) => {
512
+ // The "load more" leaf is an action, not a selection — it never stays selected,
513
+ // so choosing it again always asks for the next page.
514
+ if (typeof next === "string" && next.endsWith("::more")) loadMoreCases(next);
515
+ else setOpenCase((next as string | undefined) ?? "");
516
+ }}
517
+ />
518
+ </Flex>
519
+ </CardContent>
520
+ </Card>
521
+
368
522
  <Card>
369
523
  <CardHeader>
370
524
  <CardTitle level={2}>checkStrictly と multiple(チェックと選択は別の軸)</CardTitle>
@@ -54,7 +54,12 @@ export default function Demo() {
54
54
  </CardHeader>
55
55
  <CardContent>
56
56
  <FormField label="適用範囲" required>
57
- <BranchScopePicker branches={BRANCHES} value={scope} onValueChange={setScope} />
57
+ <BranchScopePicker
58
+ branches={BRANCHES}
59
+ value={scope}
60
+ onValueChange={setScope}
61
+ notFoundContent="No branch matches that search"
62
+ />
58
63
  </FormField>
59
64
  </CardContent>
60
65
  </Card>
@@ -109,6 +109,7 @@ export default function Demo() {
109
109
  value={owner}
110
110
  onValueChange={(v) => setOwner(v as string)}
111
111
  dialogTitle="Choose an owner"
112
+ notFoundContent="No one matches that name"
112
113
  />
113
114
  <Text as="p" size="2xs" tone="muted" tabular>
114
115
  value: {JSON.stringify(owner)}
@@ -104,6 +104,7 @@ export default function Demo() {
104
104
  defaultValue={[]}
105
105
  showSearch
106
106
  pagination={{ pageSize: 3 }}
107
+ locale={{ notFoundContent: ["No matching accounts", "Nothing selected yet"] }}
107
108
  name="accounts[]"
108
109
  />
109
110
  </CardContent>
@@ -1450,5 +1450,24 @@
1450
1450
  "nameColumn": "Name",
1451
1451
  "inviteTitle": "Send invitation",
1452
1452
  "send": "Send"
1453
+ },
1454
+ "treeDocs": {
1455
+ "longList": {
1456
+ "title": "Long child lists (height · virtual · filterTreeNode)",
1457
+ "description": "2 suites → 28 sections → up to 421 cases. With height the tree scrolls inside that height and renders only the rows in view (antd height + virtual). Nodes matching the search are highlighted by filterTreeNode, never hidden. Section 2 fails its first loadData and fetches again when reopened. Section 3 shows how to compose a paged API: selecting the last leaf loads the next 100.",
1458
+ "searchLabel": "Search cases",
1459
+ "searchPlaceholder": "Highlight by case name (e.g. 042)",
1460
+ "treeLabel": "Test suites",
1461
+ "suiteIntegration": "Integration tests",
1462
+ "suiteAcceptance": "Acceptance tests",
1463
+ "section": "Section {number}",
1464
+ "sectionFailing": "Section {number} (first load fails)",
1465
+ "sectionPaged": "Section {number} ({count} at a time)",
1466
+ "case": "Case {number}",
1467
+ "loadMore": {
1468
+ "one": "Load {count} more",
1469
+ "other": "Load {count} more"
1470
+ }
1471
+ }
1453
1472
  }
1454
1473
  }
@@ -1444,5 +1444,23 @@
1444
1444
  "nameColumn": "氏名",
1445
1445
  "inviteTitle": "招待を送信",
1446
1446
  "send": "送信"
1447
+ },
1448
+ "treeDocs": {
1449
+ "longList": {
1450
+ "title": "長い子リスト(height · virtual · filterTreeNode)",
1451
+ "description": "2 スイート → 28 セクション → 最大 421 ケース。height を渡すとツリー自身がその高さでスクロールし、見えている行だけを描画します(antd の height + virtual)。検索語に一致するノードは filterTreeNode で強調されるだけで、隠れません。セクション 2 は初回の loadData が失敗し、もう一度開くと再取得します。セクション 3 は API がページングされている場合の組み立て例で、末尾の葉を選ぶと次の 100 件を読み込みます。",
1452
+ "searchLabel": "ケースを検索",
1453
+ "searchPlaceholder": "ケース名で強調(例: 042)",
1454
+ "treeLabel": "テストスイート",
1455
+ "suiteIntegration": "結合テスト",
1456
+ "suiteAcceptance": "受入テスト",
1457
+ "section": "セクション {number}",
1458
+ "sectionFailing": "セクション {number}(初回の読み込みが失敗)",
1459
+ "sectionPaged": "セクション {number}({count} 件ずつ)",
1460
+ "case": "ケース {number}",
1461
+ "loadMore": {
1462
+ "other": "さらに {count} 件を読み込む"
1463
+ }
1464
+ }
1447
1465
  }
1448
1466
  }
@@ -1444,5 +1444,23 @@
1444
1444
  "nameColumn": "Họ tên",
1445
1445
  "inviteTitle": "Gửi lời mời",
1446
1446
  "send": "Gửi"
1447
+ },
1448
+ "treeDocs": {
1449
+ "longList": {
1450
+ "title": "Danh sách con dài (height · virtual · filterTreeNode)",
1451
+ "description": "2 bộ → 28 mục → tối đa 421 ca. Với height, cây cuộn trong chiều cao đó và chỉ vẽ các hàng đang thấy (antd height + virtual). Nút khớp từ tìm kiếm được filterTreeNode làm nổi bật, không bị ẩn. Mục 2 thất bại ở lần loadData đầu và tải lại khi mở lần nữa. Mục 3 là ví dụ ghép cho API phân trang: chọn lá cuối để tải thêm 100 ca.",
1452
+ "searchLabel": "Tìm ca kiểm thử",
1453
+ "searchPlaceholder": "Làm nổi bật theo tên ca (vd: 042)",
1454
+ "treeLabel": "Bộ kiểm thử",
1455
+ "suiteIntegration": "Kiểm thử tích hợp",
1456
+ "suiteAcceptance": "Kiểm thử chấp nhận",
1457
+ "section": "Mục {number}",
1458
+ "sectionFailing": "Mục {number} (lần tải đầu thất bại)",
1459
+ "sectionPaged": "Mục {number} (mỗi lần {count} ca)",
1460
+ "case": "Ca {number}",
1461
+ "loadMore": {
1462
+ "other": "Tải thêm {count} ca"
1463
+ }
1464
+ }
1447
1465
  }
1448
1466
  }
@@ -651,37 +651,39 @@ export default function Demo() {
651
651
  rendered and the view below is never remounted by a tab switch. */}
652
652
  <Card>
653
653
  <CardHeader>
654
- <CardTitle level={2}>controls · タブ列だけ(外部の領域を切り替える)</CardTitle>
654
+ <CardTitle level={2}>
655
+ controls · strip only (switches a region outside the tabs)
656
+ </CardTitle>
655
657
  <CardDescription>
656
- controls に外部領域の id を渡すと tabpanel を描かず、選択中のタブの aria-controls
657
- がその領域を指す。領域は Tabs の外にあるのでタブを切り替えても再マウントされない。
658
- compound の TabsTrigger も items と同じ count を描く。
658
+ Pass the id of an outside region to controls: no tabpanel is rendered and the selected
659
+ tab's aria-controls points at that region. The region lives outside Tabs, so switching
660
+ tabs never remounts it. A compound TabsTrigger draws the same count as items.
659
661
  </CardDescription>
660
662
  </CardHeader>
661
663
  <CardContent>
662
664
  <Flex direction="col" gap="md">
663
665
  <Tabs
664
- aria-label="対応状況"
666
+ aria-label="Status"
665
667
  controls="tabs-inbox-view"
666
668
  variant="line"
667
669
  value={inboxStatus}
668
670
  onValueChange={setInboxStatus}
669
671
  items={[
670
- { value: "open", label: "未対応", count: 12, countLabel: "件" },
671
- { value: "doing", label: "対応中", count: 3, countLabel: "件" },
672
- { value: "done", label: "完了", count: 128, countLabel: "件" },
672
+ { value: "open", label: "Open", count: 12, countLabel: "threads" },
673
+ { value: "doing", label: "In progress", count: 3, countLabel: "threads" },
674
+ { value: "done", label: "Done", count: 128, countLabel: "threads" },
673
675
  ]}
674
676
  />
675
- <section id="tabs-inbox-view" aria-label="スレッド一覧">
676
- <Text as="p">表示中: {inboxStatus}</Text>
677
+ <section id="tabs-inbox-view" aria-label="Threads">
678
+ <Text as="p">Showing: {inboxStatus}</Text>
677
679
  </section>
678
- <Tabs aria-label="保存ビュー" controls="tabs-inbox-view" defaultValue="mine">
680
+ <Tabs aria-label="Saved views" controls="tabs-inbox-view" defaultValue="mine">
679
681
  <TabsList variant="line">
680
- <TabsTrigger value="mine" count={4} countLabel="件">
681
- 自分の担当
682
+ <TabsTrigger value="mine" count={4} countLabel="threads">
683
+ Mine
682
684
  </TabsTrigger>
683
- <TabsTrigger value="all" count={143} countLabel="件">
684
- すべて
685
+ <TabsTrigger value="all" count={143} countLabel="threads">
686
+ All
685
687
  </TabsTrigger>
686
688
  </TabsList>
687
689
  </Tabs>
@@ -66,9 +66,7 @@ function LoadingBlock() {
66
66
  query={query}
67
67
  skeleton={<SkeletonTable rows={4} columns={3} />}
68
68
  isEmpty={(d) => d.length === 0}
69
- empty={
70
- <EmptyState title="データがありません" description="条件に合う請求書はありません。" />
71
- }
69
+ empty={<EmptyState title="No invoices" description="No invoice matches the filter." />}
72
70
  >
73
71
  {(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
74
72
  </DataState>
@@ -78,7 +76,7 @@ function LoadingBlock() {
78
76
  size="sm"
79
77
  onClick={() => void client.resetQueries({ queryKey: ["ds-loading"] })}
80
78
  >
81
- もう一度読み込む
79
+ Load again
82
80
  </Button>
83
81
  </Flex>
84
82
  );
@@ -54,6 +54,7 @@ function Block() {
54
54
  flatten={flattenItemPages}
55
55
  isEmpty={(items) => items.length === 0}
56
56
  empty={<EmptyState title="アクティビティがありません" />}
57
+ loadMoreLabel="Show older activity"
57
58
  >
58
59
  {(items) => (
59
60
  <Timeline
@@ -80,7 +81,10 @@ export default function Demo() {
80
81
  <Card>
81
82
  <CardHeader>
82
83
  <CardTitle level={2}>アクティビティ</CardTitle>
83
- <CardDescription>Click 「もっと見る」 to append the next page.</CardDescription>
84
+ <CardDescription>
85
+ Click “Show older activity” to append the next page. loadMoreLabel renames the
86
+ built-in button; its pending state stays the component&apos;s.
87
+ </CardDescription>
84
88
  </CardHeader>
85
89
  <CardContent>
86
90
  <Block />
@@ -79,7 +79,8 @@ that every Ant-shaped app reaches for was never built.
79
79
  | Density | — | `size: xs \| sm \| lg` (`md` is the default, never `"default"`) |
80
80
  | Directory variant | `<DirectoryTree>` | `variant="directory"` |
81
81
  | Drag & drop | `draggable` | **out of scope for v1** — file a follow-up issue |
82
- | Virtual scroll | `virtual` / `height` | **out of scope for v1** — cap with `ScrollArea`; file a follow-up issue |
82
+ | Virtual scroll | `virtual` / `height` | `height` + `virtual` (gh#1042) — windowed rows are a flat run of treeitems; antd has no "load more" node, so none is shipped |
83
+ | Match highlight | `filterTreeNode` | `filterTreeNode` (gh#1043) — `data-filter-node="true"` for antd's `filter-node` class, plus an sr-only "matches" |
83
84
 
84
85
  Plus the house rules: forward `ref`, spread `...props`, accept `className` + `id`, export
85
86
  `TreeProp` and `TreeProp as TreeProps` from `src/props/components/data-display.prop.ts`, and
@@ -430,7 +430,7 @@ export default function Demo() {
430
430
  {/* Saved-view ribbon — controlled, strip-only Tabs (gh#1021): `controls` points the
431
431
  selected tab at the grid below, which lives outside the Tabs and is never remounted. */}
432
432
  <Tabs
433
- aria-label="保存ビュー"
433
+ aria-label="Saved views"
434
434
  controls="attendance-grid"
435
435
  value={view}
436
436
  onValueChange={(v) => setView(v as ViewId)}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "31.4.0",
4
- "godxUiMcp": "31.4.0",
3
+ "version": "31.6.0",
4
+ "godxUiMcp": "31.6.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",