@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.
- package/agent/START-HERE.md +5 -5
- package/agent/components/BranchScopePicker.json +5 -0
- package/agent/components/Breadcrumb.json +1 -1
- package/agent/components/Cascader.json +1 -1
- package/agent/components/DataTable.json +2 -2
- package/agent/components/InfiniteQueryState.json +6 -1
- package/agent/components/PageContainer.json +1 -1
- package/agent/components/Progress.json +12 -1
- package/agent/components/RangeTimeline.json +6 -0
- package/agent/components/RecordPicker.json +10 -0
- package/agent/components/Text.json +1 -1
- package/agent/components/Transfer.json +5 -0
- package/agent/components/Tree.json +21 -4
- package/agent/components/TreeSelect.json +1 -1
- package/agent/components.json +72 -13
- package/agent/index.json +5 -5
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +12 -0
- package/dist/components/data-display/data-table.d.ts +3 -0
- package/dist/components/data-display/data-table.js +415 -370
- package/dist/components/data-display/progress.d.ts +16 -0
- package/dist/components/data-display/progress.js +17 -3
- package/dist/components/data-display/range-timeline.d.ts +20 -0
- package/dist/components/data-display/range-timeline.js +217 -197
- package/dist/components/data-display/tree.js +179 -23
- package/dist/components/data-entry/branch-scope-picker.d.ts +1 -0
- package/dist/components/data-entry/branch-scope-picker.js +2 -1
- package/dist/components/data-entry/cascader.js +4 -5
- package/dist/components/data-entry/record-picker.d.ts +2 -0
- package/dist/components/data-entry/record-picker.js +22 -3
- package/dist/components/data-entry/transfer.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +5 -2
- package/dist/components/data-entry/tree-select.js +14 -5
- package/dist/components/feedback/tooltip.d.ts +21 -0
- package/dist/components/feedback/tooltip.js +101 -0
- package/dist/components/general/typography.js +30 -11
- package/dist/components/layout/breadcrumb-ellipsis.d.ts +16 -0
- package/dist/components/layout/breadcrumb-ellipsis.js +28 -0
- package/dist/components/layout/breadcrumb.js +16 -7
- package/dist/components/layout/page-container.js +29 -20
- package/dist/components/query/infinite-query-state.d.ts +1 -1
- package/dist/components/query/infinite-query-state.js +2 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +18 -3
- package/dist/i18n/messages/ja.json +9 -3
- package/dist/i18n/messages/vi.json +9 -3
- package/dist/lib/hooks.d.ts +8 -4
- package/dist/lib/hooks.js +5 -2
- package/dist/lib/tree.d.ts +23 -0
- package/dist/lib/tree.js +29 -0
- package/dist/props/components/data-display.prop.d.ts +21 -2
- package/dist/props/components/data-entry.prop.d.ts +31 -4
- package/dist/props/components/query.prop.d.ts +8 -1
- package/dist/props/registry.d.ts +29 -6
- package/dist/props/registry.js +46 -4
- package/dist/props/vocabulary/data.prop.d.ts +32 -2
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/navigation.prop.d.ts +14 -0
- package/dist/styles/data-display-layout.css +52 -0
- package/dist/styles/layers.json +3 -2
- package/dist/styles/layout.css +22 -0
- package/dist/styles/table-layout.css +24 -1
- package/dist/tokens/components/navigation.css +2 -0
- package/dist/tokens/components/tree.css +2 -0
- package/docs/data-display/data-table/examples/server-paged.tsx +23 -0
- package/docs/data-display/data-table/index.md +2 -0
- package/docs/data-display/progress.tsx +24 -6
- package/docs/data-display/timeline.tsx +33 -0
- package/docs/data-display/tree.tsx +154 -0
- package/docs/data-entry/branch-scope-picker.tsx +6 -1
- package/docs/data-entry/record-picker.tsx +1 -0
- package/docs/data-entry/transfer.tsx +1 -0
- package/docs/general/typography.tsx +84 -1
- package/docs/i18n/messages/en.json +48 -1
- package/docs/i18n/messages/ja.json +47 -1
- package/docs/i18n/messages/vi.json +47 -1
- package/docs/navigation/breadcrumb.tsx +24 -0
- package/docs/query/infinite-query-state.tsx +5 -1
- package/docs/roadmap/tree-components.md +2 -1
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +15 -5
|
@@ -26,6 +26,10 @@ import { Download, FileText, LayoutDashboard, Plus, ReceiptText } from "lucide-r
|
|
|
26
26
|
* showTotal, onChange }}` — the real Pagination, total beside the page numbers, bottom-end. The
|
|
27
27
|
* table is compact, so the pager is `size="sm"` and sits on the same control step as the toolbar
|
|
28
28
|
* buttons above it. `data` is only the current page; the footer offers ceil(total / pageSize).
|
|
29
|
+
*
|
|
30
|
+
* Selection on a server-paged list (gh#1036): the header checkbox ticks THIS PAGE, and says so.
|
|
31
|
+
* Once the page is fully ticked, `rowSelection.matching` offers every matching row; while that is
|
|
32
|
+
* chosen the bulk action sends the query's filter to the server instead of a list of ids.
|
|
29
33
|
*/
|
|
30
34
|
const sections: SidebarSectionProp[] = [
|
|
31
35
|
{
|
|
@@ -86,6 +90,8 @@ export default function Demo() {
|
|
|
86
90
|
const [page, setPage] = useState(1);
|
|
87
91
|
const [pageSize, setPageSize] = useState(10);
|
|
88
92
|
const rows = useMemo(() => fetchPage(page, pageSize), [page, pageSize]);
|
|
93
|
+
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
|
94
|
+
const [allMatching, setAllMatching] = useState(false);
|
|
89
95
|
|
|
90
96
|
return (
|
|
91
97
|
<AppShell
|
|
@@ -126,6 +132,17 @@ export default function Demo() {
|
|
|
126
132
|
data={rows}
|
|
127
133
|
columns={columns}
|
|
128
134
|
getRowId={(row) => row.id}
|
|
135
|
+
rowSelection={{
|
|
136
|
+
selectedRowKeys: selectedKeys,
|
|
137
|
+
onChange: (keys) => {
|
|
138
|
+
setSelectedKeys(keys);
|
|
139
|
+
},
|
|
140
|
+
matching: {
|
|
141
|
+
total: SERVER_TOTAL,
|
|
142
|
+
selected: allMatching,
|
|
143
|
+
onSelectedChange: setAllMatching,
|
|
144
|
+
},
|
|
145
|
+
}}
|
|
129
146
|
pagination={{
|
|
130
147
|
total: SERVER_TOTAL,
|
|
131
148
|
current: page,
|
|
@@ -144,6 +161,12 @@ export default function Demo() {
|
|
|
144
161
|
<Download />
|
|
145
162
|
エクスポート
|
|
146
163
|
</Button>
|
|
164
|
+
{/* While "all matching" is chosen the count is the server's, not the page's. */}
|
|
165
|
+
<DataTable.BulkActions count={allMatching ? SERVER_TOTAL : undefined}>
|
|
166
|
+
<Button size="sm" variant="outline">
|
|
167
|
+
Mark as paid
|
|
168
|
+
</Button>
|
|
169
|
+
</DataTable.BulkActions>
|
|
147
170
|
</Flex>
|
|
148
171
|
<DataTable.ViewOptions />
|
|
149
172
|
</DataTable.Toolbar>
|
|
@@ -5,3 +5,5 @@ The admin list primitive — sticky header, sorting, bulk selection, density tog
|
|
|
5
5
|
See the "Approval queue" example.
|
|
6
6
|
|
|
7
7
|
See the "Selectable inbox" example for a `ListRow` hosted in a selectable DataTable: mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016).
|
|
8
|
+
|
|
9
|
+
See the "Server paged" example for selection on a server-paged list (gh#1036). `rowSelection.selectAllLabel` names the header checkbox; `rowSelection.matching={{ total, selected, onSelectedChange }}` adds the "Select all N matching rows" banner once the page is fully ticked. antd has no equivalent of `matching` — it is the Gmail / Jira / GitHub pattern, and the one deliberate deviation from antd's `rowSelection` here. While `selected` is true every row of any page shows ticked; send the query's filter to the server instead of the ids. Unticking a row or "Clear selection" reports `false`. `selections` also takes antd's `DataTable.SELECTION_ALL` / `SELECTION_INVERT` / `SELECTION_NONE` mixed with custom entries.
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CardTitle,
|
|
7
7
|
Progress,
|
|
8
8
|
} from "@godxjp/ui/data-display";
|
|
9
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
9
10
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -13,6 +14,7 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
|
13
14
|
* Composed only from real @godxjp/ui components.
|
|
14
15
|
*/
|
|
15
16
|
export default function Demo() {
|
|
17
|
+
const { t, locale } = useTranslation();
|
|
16
18
|
return (
|
|
17
19
|
<PageContainer title="Progress" subtitle="0–100 bar · optional label + semantic tone">
|
|
18
20
|
<Flex direction="col" gap="lg">
|
|
@@ -37,11 +39,10 @@ export default function Demo() {
|
|
|
37
39
|
<CardHeader>
|
|
38
40
|
<CardTitle level={2}>shape="ring"</CardTitle>
|
|
39
41
|
<CardDescription>
|
|
40
|
-
同じメーターを弧で描きます。バーと同じ value / tone / size / ARIA
|
|
41
|
-
|
|
42
|
-
件」を見出しの横に出したいとき、バー+キャプションの 2
|
|
43
|
-
|
|
44
|
-
場所が正方形のときに選びます。
|
|
42
|
+
同じメーターを弧で描きます。バーと同じ value / tone / size / ARIA のまま、label
|
|
43
|
+
がリングの内側に入ります。狭いアプリバーで「18 / 42
|
|
44
|
+
件」を見出しの横に出したいとき、バー+キャプションの 2 段ではなく 1
|
|
45
|
+
マスで済むのが理由です。数値が重要なときではなく、 場所が正方形のときに選びます。
|
|
45
46
|
</CardDescription>
|
|
46
47
|
</CardHeader>
|
|
47
48
|
<CardContent>
|
|
@@ -68,7 +69,10 @@ export default function Demo() {
|
|
|
68
69
|
1 つの合計を状態ごとに分割します。割合ではなく実数を渡すと、各スライスの比率は
|
|
69
70
|
コンポーネントが計算します。メーターより背の高いトラックになるのは、0.5rem の pill
|
|
70
71
|
上に 3 色を並べると比率が読めなくなるためです。role="img"
|
|
71
|
-
として、全スライスを 1 つの名前で読み上げます。
|
|
72
|
+
として、全スライスを 1 つの名前で読み上げます。total を渡すと全体は合計ではなく total
|
|
73
|
+
になり、残りはトラックのまま「残り」として読み上げます(antd の percent +
|
|
74
|
+
success.percent と同じく、未塗りのトラックが残り。antd の全体は常に 100
|
|
75
|
+
ですが、ここは実数の単位で total を指定します)。
|
|
72
76
|
</CardDescription>
|
|
73
77
|
</CardHeader>
|
|
74
78
|
<CardContent>
|
|
@@ -89,6 +93,20 @@ export default function Demo() {
|
|
|
89
93
|
]}
|
|
90
94
|
label="みどり農産株式会社(0 件のスライスは幅 0)"
|
|
91
95
|
/>
|
|
96
|
+
{/* total を渡すと、スライスは合計ではなく total に対して描かれ、残りはトラックのまま。
|
|
97
|
+
読み上げには残り(remainderLabel)が Intl 形式の数値で入ります。 */}
|
|
98
|
+
<Progress
|
|
99
|
+
segments={[
|
|
100
|
+
{ value: 3, tone: "success", label: t("progressDocs.testRun.passed") },
|
|
101
|
+
{ value: 1, tone: "destructive", label: t("progressDocs.testRun.failed") },
|
|
102
|
+
{ value: 2, tone: "warning", label: t("progressDocs.testRun.blocked") },
|
|
103
|
+
]}
|
|
104
|
+
total={400}
|
|
105
|
+
remainderLabel={t("progressDocs.testRun.notRun")}
|
|
106
|
+
label={t("progressDocs.testRun.caption", {
|
|
107
|
+
total: new Intl.NumberFormat(locale).format(400),
|
|
108
|
+
})}
|
|
109
|
+
/>
|
|
92
110
|
</Flex>
|
|
93
111
|
</CardContent>
|
|
94
112
|
</Card>
|
|
@@ -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
|
|
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)}
|
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
TableHead,
|
|
22
22
|
TableHeader,
|
|
23
23
|
TableRow,
|
|
24
|
+
DataTable,
|
|
25
|
+
Tree,
|
|
24
26
|
} from "@godxjp/ui/data-display";
|
|
25
|
-
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
27
|
+
import { Breadcrumb, Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout";
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Text + Heading — the typographic primitives. Use these INSTEAD of a hand-rolled
|
|
@@ -46,8 +48,15 @@ const headingTones = [
|
|
|
46
48
|
{ tone: "info", title: "システムメンテナンスのお知らせ" },
|
|
47
49
|
] as const;
|
|
48
50
|
|
|
51
|
+
type SuiteRow = { id: string; title: string };
|
|
52
|
+
|
|
49
53
|
export default function Demo() {
|
|
50
54
|
const { t } = useTranslation();
|
|
55
|
+
const longTitle = t("textExamples.ellipsisLongTitle");
|
|
56
|
+
const suiteRows: SuiteRow[] = [
|
|
57
|
+
{ id: "TS-0412", title: longTitle },
|
|
58
|
+
{ id: "TS-0413", title: t("textExamples.ellipsisShortTitle") },
|
|
59
|
+
];
|
|
51
60
|
return (
|
|
52
61
|
<PageContainer
|
|
53
62
|
title="Typography"
|
|
@@ -388,6 +397,80 @@ export default function Demo() {
|
|
|
388
397
|
</Flex>
|
|
389
398
|
</CardContent>
|
|
390
399
|
</Card>
|
|
400
|
+
<Card>
|
|
401
|
+
<CardHeader>
|
|
402
|
+
<CardTitle level={2}>{t("textExamples.ellipsisTitle")}</CardTitle>
|
|
403
|
+
<CardDescription>{t("textExamples.ellipsisDescription")}</CardDescription>
|
|
404
|
+
</CardHeader>
|
|
405
|
+
<CardContent>
|
|
406
|
+
<Flex direction="col" gap="md">
|
|
407
|
+
<ResponsiveGrid columns={{ sm: 2, lg: 3 }}>
|
|
408
|
+
<Flex direction="col" gap="xs">
|
|
409
|
+
<Text size="xs" tone="muted">
|
|
410
|
+
{t("textExamples.ellipsisPlain")}
|
|
411
|
+
</Text>
|
|
412
|
+
<Text ellipsis={{ tooltip: true }}>{longTitle}</Text>
|
|
413
|
+
</Flex>
|
|
414
|
+
<Flex direction="col" gap="xs">
|
|
415
|
+
<Text size="xs" tone="muted">
|
|
416
|
+
{t("textExamples.ellipsisLink")}
|
|
417
|
+
</Text>
|
|
418
|
+
<Text asChild link ellipsis={{ tooltip: true }}>
|
|
419
|
+
<a href="#typography">{longTitle}</a>
|
|
420
|
+
</Text>
|
|
421
|
+
</Flex>
|
|
422
|
+
<Flex direction="col" gap="xs">
|
|
423
|
+
<Text size="xs" tone="muted">
|
|
424
|
+
{t("textExamples.ellipsisTree")}
|
|
425
|
+
</Text>
|
|
426
|
+
<Tree
|
|
427
|
+
aria-label={t("textExamples.ellipsisTree")}
|
|
428
|
+
treeData={[
|
|
429
|
+
{
|
|
430
|
+
value: "suite",
|
|
431
|
+
label: longTitle,
|
|
432
|
+
children: [
|
|
433
|
+
{ value: "case-1", label: t("textExamples.ellipsisShortTitle") },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
]}
|
|
437
|
+
defaultExpandedValues={["suite"]}
|
|
438
|
+
titleRender={(node) => <Text ellipsis={{ tooltip: true }}>{node.label}</Text>}
|
|
439
|
+
/>
|
|
440
|
+
</Flex>
|
|
441
|
+
</ResponsiveGrid>
|
|
442
|
+
<DataTable<SuiteRow>
|
|
443
|
+
label={t("textExamples.ellipsisTable")}
|
|
444
|
+
data={suiteRows}
|
|
445
|
+
getRowId={(row) => row.id}
|
|
446
|
+
columns={[
|
|
447
|
+
{ key: "id", header: "ID", width: "7rem", render: (row) => row.id },
|
|
448
|
+
{
|
|
449
|
+
key: "title",
|
|
450
|
+
header: (
|
|
451
|
+
<Text ellipsis={{ tooltip: true }}>
|
|
452
|
+
{t("textExamples.ellipsisLongHeader")}
|
|
453
|
+
</Text>
|
|
454
|
+
),
|
|
455
|
+
width: "12rem",
|
|
456
|
+
ellipsis: true,
|
|
457
|
+
sortable: true,
|
|
458
|
+
sorter: (a, b) => a.title.localeCompare(b.title),
|
|
459
|
+
render: (row) => <Text ellipsis={{ tooltip: true }}>{row.title}</Text>,
|
|
460
|
+
},
|
|
461
|
+
]}
|
|
462
|
+
/>
|
|
463
|
+
<Breadcrumb
|
|
464
|
+
ariaLabel={t("textExamples.ellipsisBreadcrumb")}
|
|
465
|
+
items={[
|
|
466
|
+
{ label: t("textExamples.ellipsisCrumbHome"), to: "#typography" },
|
|
467
|
+
{ label: longTitle, to: "#typography", ellipsis: true },
|
|
468
|
+
{ label: longTitle, ellipsis: true },
|
|
469
|
+
]}
|
|
470
|
+
/>
|
|
471
|
+
</Flex>
|
|
472
|
+
</CardContent>
|
|
473
|
+
</Card>
|
|
391
474
|
<Card>
|
|
392
475
|
<CardHeader>
|
|
393
476
|
<CardTitle level={2}>copyable · editable · 装飾</CardTitle>
|
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
"breakDescription": "An identifier with nowhere to break — an email address, a coupon code, a login ID — takes break=\"anywhere\", and the column stays inside the viewport even in a table cell. whitespace=\"pre-wrap\" is for newlines a person typed; it cannot wrap an identifier in a table cell.",
|
|
7
7
|
"breakColumnOwner": "Owner",
|
|
8
8
|
"breakColumnEmail": "Email address",
|
|
9
|
-
"breakSampleName": "Sato"
|
|
9
|
+
"breakSampleName": "Sato",
|
|
10
|
+
"ellipsisTitle": "ellipsis · the full text in a tooltip while it is cut",
|
|
11
|
+
"ellipsisDescription": "ellipsis={{ tooltip: true }} keeps one line and shows the whole text on hover and on keyboard focus of the control it sits in (the link, the tree item, the sort button). A title that fits shows no tooltip, and the Text never becomes a tab stop of its own.",
|
|
12
|
+
"ellipsisPlain": "Plain Text",
|
|
13
|
+
"ellipsisLink": "Text asChild · link",
|
|
14
|
+
"ellipsisTree": "Tree titleRender",
|
|
15
|
+
"ellipsisTable": "Test suites",
|
|
16
|
+
"ellipsisBreadcrumb": "Truncated breadcrumb",
|
|
17
|
+
"ellipsisCrumbHome": "Home",
|
|
18
|
+
"ellipsisLongTitle": "Q3 integration test · payment module · invoice re-issue scenarios and acceptance criteria review",
|
|
19
|
+
"ellipsisShortTitle": "Login smoke test",
|
|
20
|
+
"ellipsisLongHeader": "Latest run name (release candidate 2026-09)"
|
|
10
21
|
},
|
|
11
22
|
"themeShowcase": {
|
|
12
23
|
"page": {
|
|
@@ -1450,5 +1461,41 @@
|
|
|
1450
1461
|
"nameColumn": "Name",
|
|
1451
1462
|
"inviteTitle": "Send invitation",
|
|
1452
1463
|
"send": "Send"
|
|
1464
|
+
},
|
|
1465
|
+
"treeDocs": {
|
|
1466
|
+
"longList": {
|
|
1467
|
+
"title": "Long child lists (height · virtual · filterTreeNode)",
|
|
1468
|
+
"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.",
|
|
1469
|
+
"searchLabel": "Search cases",
|
|
1470
|
+
"searchPlaceholder": "Highlight by case name (e.g. 042)",
|
|
1471
|
+
"treeLabel": "Test suites",
|
|
1472
|
+
"suiteIntegration": "Integration tests",
|
|
1473
|
+
"suiteAcceptance": "Acceptance tests",
|
|
1474
|
+
"section": "Section {number}",
|
|
1475
|
+
"sectionFailing": "Section {number} (first load fails)",
|
|
1476
|
+
"sectionPaged": "Section {number} ({count} at a time)",
|
|
1477
|
+
"case": "Case {number}",
|
|
1478
|
+
"loadMore": {
|
|
1479
|
+
"one": "Load {count} more",
|
|
1480
|
+
"other": "Load {count} more"
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
"progressDocs": {
|
|
1485
|
+
"testRun": {
|
|
1486
|
+
"passed": "Passed",
|
|
1487
|
+
"failed": "Failed",
|
|
1488
|
+
"blocked": "Blocked",
|
|
1489
|
+
"notRun": "Not run",
|
|
1490
|
+
"caption": "Regression suite — {total} cases"
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
"breadcrumbExamples": {
|
|
1494
|
+
"ellipsisTitle": "ellipsis · a long segment on one line",
|
|
1495
|
+
"ellipsisDescription": "ellipsis on a segment keeps the trail on one line and cuts that label with \"…\"; the full label shows in a tooltip on hover and when the crumb link takes keyboard focus. antd Breadcrumb has no per-item ellipsis — the shape is Typography’s. PageContainer breadcrumb takes the same item field.",
|
|
1496
|
+
"ellipsisAria": "Truncated breadcrumb",
|
|
1497
|
+
"home": "Home",
|
|
1498
|
+
"longSuite": "Q3 integration test · payment module · invoice re-issue scenarios",
|
|
1499
|
+
"longSection": "Acceptance criteria review for the release candidate of September 2026"
|
|
1453
1500
|
}
|
|
1454
1501
|
}
|
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
"breakDescription": "メールアドレス・クーポンコード・ログイン ID のような区切りのない識別子は break=\"anywhere\"。表のセルでも列が画面幅に収まります。whitespace=\"pre-wrap\" は人が入力した改行のためのもので、表のセルでは識別子を折り返せません。",
|
|
7
7
|
"breakColumnOwner": "担当者",
|
|
8
8
|
"breakColumnEmail": "メールアドレス",
|
|
9
|
-
"breakSampleName": "佐藤"
|
|
9
|
+
"breakSampleName": "佐藤",
|
|
10
|
+
"ellipsisTitle": "ellipsis · 省略中は全文をツールチップで",
|
|
11
|
+
"ellipsisDescription": "ellipsis={{ tooltip: true }} は 1 行に収め、ホバーと、置かれたコントロール(リンク・ツリー項目・並べ替えボタン)へのキーボードフォーカスで全文を表示します。収まるタイトルにはツールチップを出さず、Text 自体はタブ移動先になりません。",
|
|
12
|
+
"ellipsisPlain": "通常の Text",
|
|
13
|
+
"ellipsisLink": "Text asChild · リンク",
|
|
14
|
+
"ellipsisTree": "Tree の titleRender",
|
|
15
|
+
"ellipsisTable": "テストスイート",
|
|
16
|
+
"ellipsisBreadcrumb": "省略付きパンくずリスト",
|
|
17
|
+
"ellipsisCrumbHome": "ホーム",
|
|
18
|
+
"ellipsisLongTitle": "第3四半期 結合テスト 決済モジュール 請求書再発行シナリオと受入基準のレビュー",
|
|
19
|
+
"ellipsisShortTitle": "ログインのスモークテスト",
|
|
20
|
+
"ellipsisLongHeader": "最新の実行名(2026年9月 リリース候補)"
|
|
10
21
|
},
|
|
11
22
|
"themeShowcase": {
|
|
12
23
|
"page": {
|
|
@@ -1444,5 +1455,40 @@
|
|
|
1444
1455
|
"nameColumn": "氏名",
|
|
1445
1456
|
"inviteTitle": "招待を送信",
|
|
1446
1457
|
"send": "送信"
|
|
1458
|
+
},
|
|
1459
|
+
"treeDocs": {
|
|
1460
|
+
"longList": {
|
|
1461
|
+
"title": "長い子リスト(height · virtual · filterTreeNode)",
|
|
1462
|
+
"description": "2 スイート → 28 セクション → 最大 421 ケース。height を渡すとツリー自身がその高さでスクロールし、見えている行だけを描画します(antd の height + virtual)。検索語に一致するノードは filterTreeNode で強調されるだけで、隠れません。セクション 2 は初回の loadData が失敗し、もう一度開くと再取得します。セクション 3 は API がページングされている場合の組み立て例で、末尾の葉を選ぶと次の 100 件を読み込みます。",
|
|
1463
|
+
"searchLabel": "ケースを検索",
|
|
1464
|
+
"searchPlaceholder": "ケース名で強調(例: 042)",
|
|
1465
|
+
"treeLabel": "テストスイート",
|
|
1466
|
+
"suiteIntegration": "結合テスト",
|
|
1467
|
+
"suiteAcceptance": "受入テスト",
|
|
1468
|
+
"section": "セクション {number}",
|
|
1469
|
+
"sectionFailing": "セクション {number}(初回の読み込みが失敗)",
|
|
1470
|
+
"sectionPaged": "セクション {number}({count} 件ずつ)",
|
|
1471
|
+
"case": "ケース {number}",
|
|
1472
|
+
"loadMore": {
|
|
1473
|
+
"other": "さらに {count} 件を読み込む"
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
"progressDocs": {
|
|
1478
|
+
"testRun": {
|
|
1479
|
+
"passed": "合格",
|
|
1480
|
+
"failed": "失敗",
|
|
1481
|
+
"blocked": "ブロック",
|
|
1482
|
+
"notRun": "未実施",
|
|
1483
|
+
"caption": "回帰テスト — 全 {total} 件"
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
"breadcrumbExamples": {
|
|
1487
|
+
"ellipsisTitle": "ellipsis · 長いセグメントを 1 行に",
|
|
1488
|
+
"ellipsisDescription": "セグメントに ellipsis を付けるとパンくずは 1 行のまま、そのラベルを「…」で省略します。全文はホバーと、パンくずのリンクへのキーボードフォーカスでツールチップに出ます。antd の Breadcrumb に項目単位の省略はなく、形は Typography に合わせています。PageContainer の breadcrumb も同じ項目フィールドを受け付けます。",
|
|
1489
|
+
"ellipsisAria": "省略付きパンくずリスト",
|
|
1490
|
+
"home": "ホーム",
|
|
1491
|
+
"longSuite": "第3四半期 結合テスト 決済モジュール 請求書再発行シナリオ",
|
|
1492
|
+
"longSection": "2026年9月 リリース候補の受入基準レビュー"
|
|
1447
1493
|
}
|
|
1448
1494
|
}
|