@godxjp/ui 27.1.0 → 27.3.1

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 (53) hide show
  1. package/dist/components/data-display/badge.d.ts +10 -3
  2. package/dist/components/data-display/badge.js +14 -3
  3. package/dist/components/data-display/data-table.d.ts +20 -12
  4. package/dist/components/data-display/data-table.js +53 -5
  5. package/dist/components/data-display/range-timeline.d.ts +16 -0
  6. package/dist/components/data-display/range-timeline.js +115 -80
  7. package/dist/components/data-display/table.d.ts +16 -0
  8. package/dist/components/data-display/table.js +2 -0
  9. package/dist/components/data-entry/checkbox.js +23 -5
  10. package/dist/components/data-entry/command.d.ts +3 -1
  11. package/dist/components/data-entry/command.js +10 -2
  12. package/dist/components/data-entry/form-field.js +9 -8
  13. package/dist/components/data-entry/index.d.ts +1 -0
  14. package/dist/components/data-entry/search-select.js +145 -136
  15. package/dist/components/data-entry/select.js +38 -3
  16. package/dist/components/layout/org-switcher.js +1 -1
  17. package/dist/components/layout/responsive-grid.d.ts +8 -2
  18. package/dist/components/layout/responsive-grid.js +2 -0
  19. package/dist/contracts/measurement.json +1 -1
  20. package/dist/form/form-field-control.d.ts +1 -1
  21. package/dist/form/form-field-control.js +32 -23
  22. package/dist/i18n/messages/en.json +2 -1
  23. package/dist/i18n/messages/ja.json +2 -1
  24. package/dist/i18n/messages/vi.json +2 -1
  25. package/dist/props/components/data-display.prop.d.ts +12 -0
  26. package/dist/props/components/data-entry.prop.d.ts +10 -0
  27. package/dist/props/components/form.prop.d.ts +34 -2
  28. package/dist/props/components/index.d.ts +1 -1
  29. package/dist/props/components/layout.prop.d.ts +7 -0
  30. package/dist/props/registry.d.ts +26 -3
  31. package/dist/props/registry.js +25 -2
  32. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  33. package/dist/props/vocabulary/index.d.ts +1 -1
  34. package/dist/styles/control.css +44 -1
  35. package/dist/styles/data-display-layout.css +58 -1
  36. package/dist/styles/form-layout.css +21 -0
  37. package/dist/styles/layout.css +7 -0
  38. package/dist/styles/navigation-layout.css +5 -1
  39. package/dist/styles/shell-layout.css +1 -14
  40. package/dist/styles/table-layout.css +80 -8
  41. package/dist/tokens/components/control.css +5 -0
  42. package/dist/tokens/components/data-display.css +6 -0
  43. package/dist/tokens/components/navigation.css +1 -1
  44. package/dist/tokens/components/table.css +2 -0
  45. package/docs/DESIGN-AUTHORITY.md +32 -1
  46. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  47. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  48. package/docs/data-display/popover.tsx +43 -1
  49. package/docs/data-display/timeline.tsx +73 -9
  50. package/docs/data-entry/command.tsx +80 -1
  51. package/docs/data-entry/form-root.tsx +12 -2
  52. package/docs/layout/responsive-grid.tsx +100 -0
  53. package/package.json +3 -2
@@ -338,8 +338,12 @@
338
338
  justify-content: safe flex-end;
339
339
  }
340
340
 
341
+ .ui-pagination[data-align="end"] > .ui-pagination-total {
342
+ margin-inline-end: 0;
343
+ }
344
+
341
345
  .ui-pagination[data-size="sm"] {
342
- --control-height: var(--pagination-control-height-sm);
346
+ --control-height: var(--pagination-control-height-sm, var(--control-height-sm));
343
347
  }
344
348
 
345
349
  .ui-pagination-jumper {
@@ -1165,14 +1165,11 @@
1165
1165
  }
1166
1166
 
1167
1167
  .ui-org-switcher-command {
1168
+ --command-list-split-inset: var(--org-switcher-list-inset);
1168
1169
  width: 100%;
1169
1170
  padding-inline: calc(var(--org-switcher-list-inset) - var(--org-switcher-list-offset));
1170
1171
  }
1171
1172
 
1172
- .ui-org-switcher-command .ui-command-group {
1173
- padding: 0;
1174
- }
1175
-
1176
1173
  .ui-org-switcher-command .ui-command-input-wrapper {
1177
1174
 
1178
1175
  border: 1px solid hsl(var(--input));
@@ -1183,16 +1180,6 @@
1183
1180
  background: hsl(var(--background));
1184
1181
  }
1185
1182
 
1186
- .ui-org-switcher-command .ui-command-item {
1187
- border-radius: 0;
1188
- margin-inline: calc(-1 * var(--org-switcher-list-inset));
1189
- padding-inline: calc(var(--org-switcher-list-inset) + var(--command-input-padding-x));
1190
- }
1191
-
1192
- .ui-org-switcher-command .ui-command-item + .ui-command-item {
1193
- border-block-start: 1px solid hsl(var(--border));
1194
- }
1195
-
1196
1183
  [data-slot="dialog-footer"]:has(> .ui-org-switcher-legend),
1197
1184
  [data-slot="sheet-footer"]:has(> .ui-org-switcher-legend) {
1198
1185
  justify-content: flex-start;
@@ -40,7 +40,47 @@
40
40
  }
41
41
 
42
42
  .ui-table-row[data-tone] {
43
- background: hsl(var(--table-row-tone-color) / var(--table-row-tone-wash-alpha));
43
+ background-image: linear-gradient(
44
+ hsl(var(--table-row-tone-color) / var(--table-row-tone-wash-alpha)),
45
+ hsl(var(--table-row-tone-color) / var(--table-row-tone-wash-alpha))
46
+ );
47
+ }
48
+
49
+ :where([data-slot="table"] > tbody > tr:nth-child(even of :not([data-expanded-row]))) {
50
+ background-color: color-mix(
51
+ in srgb,
52
+ var(--table-row-striped-background, hsl(var(--muted) / 0.8)) var(--table-row-striped-alpha),
53
+ transparent
54
+ );
55
+ }
56
+
57
+ :where(
58
+ [data-slot="table"]
59
+ > tbody
60
+ > tr:nth-child(even of :not([data-expanded-row]))
61
+ + tr[data-expanded-row]
62
+ > td
63
+ ) {
64
+ background-image: linear-gradient(
65
+ color-mix(
66
+ in srgb,
67
+ var(--table-row-striped-background, hsl(var(--muted) / 0.8)) var(--table-row-striped-alpha),
68
+ transparent
69
+ ),
70
+ color-mix(
71
+ in srgb,
72
+ var(--table-row-striped-background, hsl(var(--muted) / 0.8)) var(--table-row-striped-alpha),
73
+ transparent
74
+ )
75
+ );
76
+ }
77
+
78
+ [data-slot="table"][data-striped=""] {
79
+ --table-row-striped-alpha: 100%;
80
+ }
81
+
82
+ [data-slot="table"][data-striped="false"] {
83
+ --table-row-striped-alpha: 0%;
44
84
  }
45
85
 
46
86
  .ui-table-row[data-tone] > [data-slot="table-cell"]:first-child {
@@ -172,6 +212,14 @@
172
212
  min-width: 0;
173
213
  }
174
214
 
215
+ .ui-data-table-pagination--footer > .ui-pagination {
216
+ flex: 1 1 auto;
217
+ }
218
+
219
+ .ui-data-table-pagination--footer:empty {
220
+ display: none;
221
+ }
222
+
175
223
  .ui-data-table-page-size {
176
224
  flex: 0 0 auto;
177
225
  flex-wrap: nowrap;
@@ -213,10 +261,6 @@
213
261
  min-inline-size: var(--table-surface-min-inline-size);
214
262
  }
215
263
 
216
- .ui-data-table-surface[data-striped] tbody tr:nth-child(even):not([data-state="selected"]) {
217
- background-color: var(--table-row-striped-background, hsl(var(--muted) / 0.4));
218
- }
219
-
220
264
  .ui-data-table-empty {
221
265
  padding: var(--empty-state-space-y) var(--empty-state-space-x);
222
266
  }
@@ -332,6 +376,13 @@
332
376
  box-shadow: var(--table-fixed-shadow-start);
333
377
  }
334
378
 
379
+ [data-slot="card"]
380
+ .ui-data-table-surface
381
+ tbody
382
+ :is(.ui-data-table-pin-end, .ui-data-table-pin-start) {
383
+ background-color: hsl(var(--card-background, var(--card)));
384
+ }
385
+
335
386
  th.ui-data-table-pin-end,
336
387
  th.ui-data-table-pin-start {
337
388
  background-color: hsl(var(--secondary));
@@ -345,11 +396,27 @@
345
396
  box-shadow: 6px 0 6px -5px hsl(var(--foreground) / 0.12);
346
397
  }
347
398
 
399
+ :where([data-slot="table"] > tbody > tr:nth-child(even of :not([data-expanded-row])))
400
+ > :is(.ui-data-table-pin-end, .ui-data-table-pin-start) {
401
+ background-image: linear-gradient(
402
+ color-mix(
403
+ in srgb,
404
+ var(--table-row-striped-background, hsl(var(--muted) / 0.8)) var(--table-row-striped-alpha),
405
+ transparent
406
+ ),
407
+ color-mix(
408
+ in srgb,
409
+ var(--table-row-striped-background, hsl(var(--muted) / 0.8)) var(--table-row-striped-alpha),
410
+ transparent
411
+ )
412
+ );
413
+ }
414
+
348
415
  .ui-data-table-surface tbody tr:hover > .ui-data-table-pin-end,
349
416
  .ui-data-table-surface tbody tr:hover > .ui-data-table-pin-start {
350
417
  background-image: linear-gradient(
351
- var(--table-row-hover-background, hsl(var(--muted) / 0.5)),
352
- var(--table-row-hover-background, hsl(var(--muted) / 0.5))
418
+ var(--table-row-hover-background, hsl(var(--accent) / 0.7)),
419
+ var(--table-row-hover-background, hsl(var(--accent) / 0.7))
353
420
  );
354
421
  }
355
422
 
@@ -385,7 +452,12 @@
385
452
  }
386
453
 
387
454
  .ui-data-table-expanded-row > [data-slot="table-cell"] {
388
- background-color: var(--table-row-expanded-background, hsl(var(--muted) / 0.3));
455
+ background-color: color-mix(
456
+ in srgb,
457
+ var(--table-row-expanded-background, hsl(var(--muted) / 0.3))
458
+ calc(100% - var(--table-row-striped-alpha)),
459
+ transparent
460
+ );
389
461
  }
390
462
 
391
463
  .ui-data-table-summary {
@@ -140,6 +140,11 @@
140
140
  --command-item-padding-x: var(--space-2);
141
141
 
142
142
  --command-item-gap: var(--control-gap);
143
+
144
+ --command-list-split-padding: 0;
145
+ --command-list-split-inset: 0px;
146
+ --command-item-divider-width: var(--stroke-hairline);
147
+ --command-item-divider-color: initial;
143
148
  --command-palette-width: 35rem;
144
149
  --command-palette-inset-top: calc(var(--space-6) * 4);
145
150
  --command-palette-viewport-inset: var(--space-3);
@@ -151,4 +151,10 @@
151
151
  --range-timeline-row-height: var(--control-height-lg);
152
152
  --range-timeline-bar-height: var(--control-height-sm);
153
153
  --range-timeline-border-color: var(--border);
154
+
155
+ --range-timeline-grid-color: initial;
156
+
157
+ --range-timeline-grid-width: var(--stroke-hairline);
158
+
159
+ --range-timeline-muted-column-background: initial;
154
160
  }
@@ -11,7 +11,7 @@
11
11
 
12
12
  --pagination-icon-size: var(--control-icon-size);
13
13
 
14
- --pagination-control-height-sm: var(--band-height-sm);
14
+ --pagination-control-height-sm: initial;
15
15
 
16
16
  --pagination-jumper-gap: var(--space-inline-xs);
17
17
  --pagination-jumper-width: 4rem;
@@ -37,6 +37,8 @@
37
37
  --table-selection-menu-gap: var(--space-1);
38
38
 
39
39
  --table-row-striped-background: initial;
40
+
41
+ --table-row-striped-alpha: 0%;
40
42
  --table-row-hover-background: initial;
41
43
  --table-row-selected-background: initial;
42
44
  }
@@ -234,7 +234,12 @@ in page CSS.
234
234
  **Three things override antd's spelling, each for a stated reason:**
235
235
 
236
236
  - **Logical over physical.** antd's `fixed: 'left' | 'right'` cannot mirror for an RTL locale, so
237
- only `start` / `end` are published. Same rule that makes `check:rtl` a gate.
237
+ only `start` / `end` are published. Same rule that makes `check:rtl` a gate. Table
238
+ `pagination.position` follows it (gh#705): antd `topLeft | topCenter | topRight | bottomLeft |
239
+ bottomCenter | bottomRight | none` is `topStart | topCenter | topEnd | bottomStart | bottomCenter |
240
+ bottomEnd | none` (`TablePaginationPositionProp`, default `['bottomEnd']`), the camelCase shape
241
+ `DropdownMenuPlacementProp` already uses. antd's `size="small"` Table → small pager is
242
+ `density="compact"` → `Pagination size="sm"`.
238
243
  - **This library's controlled vocabulary wins on values.** antd's `SortOrder` is
239
244
  `'ascend' | 'descend'`; here it stays `SortDirectionProp` (`asc` / `desc`), because that type
240
245
  already exists and a second spelling of the same axis is exactly what `check:prop-vocabulary`
@@ -266,6 +271,32 @@ in page CSS.
266
271
  `dataEntry.chatComposer.hintModEnter` with `{modifier}` from `isApplePlatform()`
267
272
  (`@godxjp/ui/lib/utils`). `allowEmptySubmit` (default `false`, antd's behaviour) sends `""`,
268
273
  still never while `loading` / `disabled` / `readOnly`.
274
+ - **`Table.striped` / `DataTable.striped` (gh#700).** antd names no zebra capability — its docs
275
+ stripe through `rowClassName` — so there is no antd spelling to port. Element Plus calls it
276
+ `stripe`, Bootstrap `.table-striped`, MUI documents `:nth-of-type(odd)`. This library takes
277
+ `striped`: a positive boolean adjective, the shape `bordered` and `hoverable` already have on the
278
+ same component, and the spelling `DataTable` had shipped since 17f4a067. It is TRI-STATE on
279
+ purpose: omitted inherits the service default `--table-row-striped-alpha` (`0%` at `:root`, so
280
+ one theme line — `:root { --table-row-striped-alpha: 100%; }` — stripes every list table),
281
+ `true` / `false` override it per table. `rowClassName` is not the answer here: it counts DOM
282
+ rows, so an expanded detail row shifts every stripe after it, and `ui-audit` blocks the
283
+ utilities it would need.
284
+
285
+ - **`Command.split` is antd `List.split`, with this library's default.** Same name, same meaning:
286
+ a divider between rows and none after the last (gh#699). antd's `List` defaults it to `true`;
287
+ `Command` defaults it to `false`, because its resting home is a command palette, where inset
288
+ pill rows are right. The option lists the library renders itself as a checklist
289
+ (`Select mode="multiple" | "tags"`) set it to `true`, and a consumer composing a filter facet
290
+ (`Popover` + `Command`) opts in with `split`. It also zeroes the list padding and bleeds rows to
291
+ the panel edge, which antd's `split` does not need to say because a `List` has no inset rows.
292
+
293
+ - **`ResponsiveGrid.align` is antd `Row align`, with Flex's values (gh#708).** antd spells the
294
+ cross-axis `top | middle | bottom | stretch`; this library already names that axis
295
+ `FlexAlignProp` (`start | center | end | stretch | baseline`), so `ResponsiveGridAlignProp` is
296
+ `Extract<FlexAlignProp, "start" | "stretch">` — antd `top` → `start`, `stretch` unchanged.
297
+ `middle` / `bottom` are not offered until a screen needs them. The default differs by flow on
298
+ purpose: omitted, `flow="columns"` keeps `start` and `flow="rows"` keeps the grid's stretch, so
299
+ no existing grid moves; antd's `Row` default is `top` in every case.
269
300
 
270
301
  **A knob that only a fork could reach is not parity either.** antd's `components`,
271
302
  `filterDropdown`, `classNames`/`styles` semantic maps and `prefixCls` all exist to let a consumer
@@ -0,0 +1,156 @@
1
+ import { useMemo, useState } from "react";
2
+
3
+ import {
4
+ Avatar,
5
+ AvatarFallback,
6
+ Badge,
7
+ Card,
8
+ CardContent,
9
+ DataTable,
10
+ type ColumnDef,
11
+ } from "@godxjp/ui/data-display";
12
+ import { Button } from "@godxjp/ui/general";
13
+ import {
14
+ AppShell,
15
+ Flex,
16
+ PageContainer,
17
+ Sidebar,
18
+ type SidebarSectionProp,
19
+ Topbar,
20
+ } from "@godxjp/ui/layout";
21
+ import { Download, FileText, LayoutDashboard, Plus, ReceiptText } from "lucide-react";
22
+
23
+ /**
24
+ * Server-paged list (gh#705) — antd's `Table pagination`: the server returns ONE page plus the
25
+ * total, and the table renders its own footer from `pagination={{ total, current, pageSize,
26
+ * showTotal, onChange }}` — the real Pagination, total beside the page numbers, bottom-end. The
27
+ * table is compact, so the pager is `size="sm"` and sits on the same control step as the toolbar
28
+ * buttons above it. `data` is only the current page; the footer offers ceil(total / pageSize).
29
+ */
30
+ const sections: SidebarSectionProp[] = [
31
+ {
32
+ label: "会計",
33
+ items: [
34
+ { id: "dashboard", label: "ダッシュボード", icon: LayoutDashboard },
35
+ { id: "journal", label: "仕訳", icon: FileText },
36
+ { id: "invoices", label: "請求書", icon: ReceiptText },
37
+ ],
38
+ },
39
+ ];
40
+
41
+ type Invoice = {
42
+ id: string;
43
+ partner: string;
44
+ amount: number;
45
+ status: "active" | "pending" | "draft" | "failed";
46
+ };
47
+
48
+ const partners = ["株式会社ベトヤ", "ハノイ物流", "GMO決済", "東京ロジ", "大阪商事"];
49
+ const statuses: Invoice["status"][] = ["active", "pending", "draft", "failed"];
50
+
51
+ // Stands in for the server: 137 rows exist, but only one page ever reaches the table.
52
+ const SERVER_TOTAL = 137;
53
+ function fetchPage(page: number, pageSize: number): Invoice[] {
54
+ const first = (page - 1) * pageSize;
55
+ const count = Math.max(0, Math.min(pageSize, SERVER_TOTAL - first));
56
+ return Array.from({ length: count }, (_, i) => {
57
+ const n = first + i + 1;
58
+ return {
59
+ id: `INV-2024-${String(n).padStart(4, "0")}`,
60
+ partner: partners[n % partners.length],
61
+ amount: 12000 + ((n * 7919) % 480000),
62
+ status: statuses[n % statuses.length],
63
+ };
64
+ });
65
+ }
66
+
67
+ const yen = new Intl.NumberFormat("ja-JP", {
68
+ style: "currency",
69
+ currency: "JPY",
70
+ maximumFractionDigits: 0,
71
+ });
72
+
73
+ const columns: ColumnDef<Invoice>[] = [
74
+ { key: "id", header: "請求書番号", width: "w-40" },
75
+ { key: "partner", header: "取引先" },
76
+ {
77
+ key: "amount",
78
+ header: "金額",
79
+ align: "right",
80
+ render: (row) => <span className="tabular-nums">{yen.format(row.amount)}</span>,
81
+ },
82
+ { key: "status", header: "状態", render: (row) => <Badge status={row.status} /> },
83
+ ];
84
+
85
+ export default function Demo() {
86
+ const [page, setPage] = useState(1);
87
+ const [pageSize, setPageSize] = useState(10);
88
+ const rows = useMemo(() => fetchPage(page, pageSize), [page, pageSize]);
89
+
90
+ return (
91
+ <AppShell
92
+ sidebar={
93
+ <Sidebar
94
+ activeId="invoices"
95
+ sections={sections}
96
+ onSelect={() => {}}
97
+ product={{ name: "CoreBooks", role: "管理コンソール", color: "hsl(var(--primary))" }}
98
+ />
99
+ }
100
+ topbar={
101
+ <Topbar
102
+ start={
103
+ <Avatar className="rounded-md">
104
+ <AvatarFallback className="bg-primary text-primary-foreground font-bold">
105
+ C
106
+ </AvatarFallback>
107
+ </Avatar>
108
+ }
109
+ />
110
+ }
111
+ >
112
+ <PageContainer
113
+ title="請求書 一覧"
114
+ subtitle="サーバー側でページングされた一覧"
115
+ breadcrumb={[{ label: "請求書" }]}
116
+ extra={
117
+ <Button size="sm">
118
+ <Plus />
119
+ 新規請求書
120
+ </Button>
121
+ }
122
+ >
123
+ <Card>
124
+ <CardContent flush>
125
+ <DataTable
126
+ data={rows}
127
+ columns={columns}
128
+ getRowId={(row) => row.id}
129
+ pagination={{
130
+ total: SERVER_TOTAL,
131
+ current: page,
132
+ pageSize,
133
+ pageSizeOptions: [10, 20, 50],
134
+ showTotal: (total, [from, to]) => `${from}〜${to} 件 / 全 ${total} 件`,
135
+ onChange: (nextPage, nextPageSize) => {
136
+ setPage(nextPage);
137
+ setPageSize(nextPageSize);
138
+ },
139
+ }}
140
+ >
141
+ <DataTable.Toolbar>
142
+ <Flex direction="row" align="center" gap="sm">
143
+ <Button size="sm" variant="outline">
144
+ <Download />
145
+ エクスポート
146
+ </Button>
147
+ </Flex>
148
+ <DataTable.ViewOptions />
149
+ </DataTable.Toolbar>
150
+ </DataTable>
151
+ </CardContent>
152
+ </Card>
153
+ </PageContainer>
154
+ </AppShell>
155
+ );
156
+ }
@@ -0,0 +1,173 @@
1
+ import { useState } from "react";
2
+
3
+ import {
4
+ Badge,
5
+ Card,
6
+ CardContent,
7
+ CardHeader,
8
+ CardTitle,
9
+ DataTable,
10
+ Descriptions,
11
+ type ColumnDef,
12
+ } from "@godxjp/ui/data-display";
13
+ import { Text } from "@godxjp/ui/general";
14
+ import { Flex, PageContainer } from "@godxjp/ui/layout";
15
+
16
+ /**
17
+ * DataTable `striped` (gh#700) — a wide issue list striped by LOGICAL record. On one screen: an
18
+ * expanded detail row (it wears its own record's stripe and does not shift the rows after it), a
19
+ * frozen inline-end column (its opaque cells wear the stripe too), a pre-selected row (selection
20
+ * replaces the stripe) and a toned row (the rail and wash stay visible on the stripe).
21
+ *
22
+ * To stripe EVERY list table in a service, the theme sets `--table-row-striped-alpha: 100%` on
23
+ * :root once; `striped` here is the per-table form of the same switch.
24
+ */
25
+ type Issue = {
26
+ id: string;
27
+ key: string;
28
+ subject: string;
29
+ type: "bug" | "task" | "story";
30
+ assignee: string;
31
+ status: "active" | "pending" | "draft" | "failed";
32
+ priority: "高" | "中" | "低";
33
+ due: string;
34
+ milestone: string;
35
+ };
36
+
37
+ const TYPE_LABEL = { bug: "バグ", task: "タスク", story: "ストーリー" } as const;
38
+
39
+ const ROWS: Issue[] = [
40
+ {
41
+ id: "1",
42
+ key: "OPS-128",
43
+ subject: "請求書 PDF の和暦表記が一部崩れる",
44
+ type: "bug",
45
+ assignee: "田中",
46
+ status: "active",
47
+ priority: "高",
48
+ due: "2024-04-12",
49
+ milestone: "4月リリース",
50
+ },
51
+ {
52
+ id: "2",
53
+ key: "OPS-127",
54
+ subject: "取引先マスタの一括取込に重複チェックを追加",
55
+ type: "story",
56
+ assignee: "佐藤",
57
+ status: "pending",
58
+ priority: "中",
59
+ due: "2024-04-15",
60
+ milestone: "4月リリース",
61
+ },
62
+ {
63
+ id: "3",
64
+ key: "OPS-126",
65
+ subject: "承認フローの差し戻し理由を必須にする",
66
+ type: "task",
67
+ assignee: "鈴木",
68
+ status: "draft",
69
+ priority: "中",
70
+ due: "2024-04-18",
71
+ milestone: "5月リリース",
72
+ },
73
+ {
74
+ id: "4",
75
+ key: "OPS-125",
76
+ subject: "夜間バッチが月末に二重起動する",
77
+ type: "bug",
78
+ assignee: "高橋",
79
+ status: "failed",
80
+ priority: "高",
81
+ due: "2024-04-10",
82
+ milestone: "4月リリース",
83
+ },
84
+ {
85
+ id: "5",
86
+ key: "OPS-124",
87
+ subject: "権限マトリクスに監査ログ閲覧を追加",
88
+ type: "story",
89
+ assignee: "伊藤",
90
+ status: "active",
91
+ priority: "低",
92
+ due: "2024-04-22",
93
+ milestone: "5月リリース",
94
+ },
95
+ {
96
+ id: "6",
97
+ key: "OPS-123",
98
+ subject: "CSV 出力の文字コードを選べるようにする",
99
+ type: "task",
100
+ assignee: "渡辺",
101
+ status: "pending",
102
+ priority: "低",
103
+ due: "2024-04-25",
104
+ milestone: "5月リリース",
105
+ },
106
+ ];
107
+
108
+ const columns: ColumnDef<Issue>[] = [
109
+ { key: "key", header: "キー", width: "110px" },
110
+ { key: "type", header: "種別", width: "110px", render: (row) => TYPE_LABEL[row.type] },
111
+ { key: "subject", header: "件名", width: "320px" },
112
+ { key: "assignee", header: "担当者", width: "110px" },
113
+ { key: "status", header: "状態", width: "110px", render: (row) => <Badge status={row.status} /> },
114
+ { key: "priority", header: "優先度", width: "90px" },
115
+ { key: "milestone", header: "マイルストーン", width: "150px" },
116
+ // Frozen to the inline end: its cells are opaque, so they must wear the stripe themselves.
117
+ { key: "due", header: "期日", width: "130px", fixed: "end" },
118
+ ];
119
+
120
+ export default function StripedListDemo() {
121
+ const [selected, setSelected] = useState<string[]>(["3"]);
122
+ const [expanded, setExpanded] = useState<string[]>(["2"]);
123
+
124
+ return (
125
+ <PageContainer title="課題一覧" subtitle="DataTable striped · 論理行ごとの縞模様">
126
+ <Card>
127
+ <CardHeader>
128
+ <CardTitle>未完了の課題</CardTitle>
129
+ </CardHeader>
130
+ <CardContent flush>
131
+ <DataTable
132
+ data={ROWS}
133
+ columns={columns}
134
+ getRowId={(row) => row.id}
135
+ striped
136
+ hoverable
137
+ scroll={{ x: 1240 }}
138
+ rowTone={(row) => (row.status === "failed" ? "destructive" : undefined)}
139
+ rowSelection={{
140
+ selectedRowKeys: selected,
141
+ onChange: (keys) => {
142
+ setSelected(keys);
143
+ },
144
+ }}
145
+ expandable={{
146
+ expandedRowKeys: expanded,
147
+ onExpandedRowsChange: (keys) => {
148
+ setExpanded(keys);
149
+ },
150
+ expandedRowRender: (row) => (
151
+ <Descriptions bordered columns={{ sm: 2, lg: 3 }} layout="horizontal">
152
+ <Descriptions.Item label="キー" mono>
153
+ {row.key}
154
+ </Descriptions.Item>
155
+ <Descriptions.Item label="担当者">{row.assignee}</Descriptions.Item>
156
+ <Descriptions.Item label="マイルストーン">{row.milestone}</Descriptions.Item>
157
+ </Descriptions>
158
+ ),
159
+ }}
160
+ >
161
+ <DataTable.Content />
162
+ </DataTable>
163
+ </CardContent>
164
+ </Card>
165
+
166
+ <Flex direction="col" gap="xs">
167
+ <Text size="sm" tone="muted">
168
+ 選択中: {selected.length} 件 · 展開中: {expanded.length} 件
169
+ </Text>
170
+ </Flex>
171
+ </PageContainer>
172
+ );
173
+ }
@@ -14,7 +14,17 @@ import {
14
14
  PopoverTitle,
15
15
  PopoverTrigger,
16
16
  } from "@godxjp/ui/data-display";
17
- import { Checkbox, FormField, Input, Label } from "@godxjp/ui/data-entry";
17
+ import {
18
+ Checkbox,
19
+ FormField,
20
+ Input,
21
+ Label,
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from "@godxjp/ui/data-entry";
18
28
  import { Button, Text } from "@godxjp/ui/general";
19
29
  import { Flex, PageContainer } from "@godxjp/ui/layout";
20
30
  import { Info, SlidersHorizontal } from "lucide-react";
@@ -50,6 +60,38 @@ export default function Demo() {
50
60
  </CardContent>
51
61
  </Card>
52
62
 
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle level={2}>Picker in a popover · Select defaultOpen</CardTitle>
66
+ <CardDescription>
67
+ Opening the popover opens the workspace list with it. The Select waits for the popover
68
+ to finish sliding in, so the list sits below its trigger exactly as a click-open does
69
+ and the first option is never covered.
70
+ </CardDescription>
71
+ </CardHeader>
72
+ <CardContent>
73
+ <Popover>
74
+ <PopoverTrigger asChild>
75
+ <Button variant="outline" data-picker-popover="">
76
+ ワークスペースを切り替え
77
+ </Button>
78
+ </PopoverTrigger>
79
+ <PopoverContent aria-label="ワークスペース">
80
+ <Select aria-label="ワークスペース" defaultOpen defaultValue="tokyo">
81
+ <SelectTrigger aria-label="ワークスペース" data-picker-trigger="">
82
+ <SelectValue />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ <SelectItem value="tokyo">東京本社</SelectItem>
86
+ <SelectItem value="osaka">大阪支社</SelectItem>
87
+ <SelectItem value="fukuoka">福岡拠点</SelectItem>
88
+ </SelectContent>
89
+ </Select>
90
+ </PopoverContent>
91
+ </Popover>
92
+ </CardContent>
93
+ </Card>
94
+
53
95
  <Card>
54
96
  <CardHeader>
55
97
  <CardTitle level={2}>Controlled trigger + content</CardTitle>