@godxjp/ui 27.1.0 → 27.4.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 (74) 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/general/icon.d.ts +31 -0
  17. package/dist/components/general/icon.js +23 -0
  18. package/dist/components/general/index.d.ts +2 -0
  19. package/dist/components/general/index.js +2 -0
  20. package/dist/components/layout/flex.d.ts +2 -2
  21. package/dist/components/layout/flex.js +7 -1
  22. package/dist/components/layout/org-switcher.js +1 -1
  23. package/dist/components/layout/responsive-grid.d.ts +8 -2
  24. package/dist/components/layout/responsive-grid.js +2 -0
  25. package/dist/components/layout/topbar-item.d.ts +1 -0
  26. package/dist/components/layout/topbar-item.js +3 -1
  27. package/dist/contracts/measurement.json +1 -1
  28. package/dist/form/form-field-control.d.ts +1 -1
  29. package/dist/form/form-field-control.js +32 -23
  30. package/dist/i18n/messages/en.json +2 -1
  31. package/dist/i18n/messages/ja.json +2 -1
  32. package/dist/i18n/messages/vi.json +2 -1
  33. package/dist/props/components/data-display.prop.d.ts +12 -0
  34. package/dist/props/components/data-entry.prop.d.ts +10 -0
  35. package/dist/props/components/form.prop.d.ts +34 -2
  36. package/dist/props/components/general.prop.d.ts +42 -1
  37. package/dist/props/components/index.d.ts +1 -1
  38. package/dist/props/components/layout.prop.d.ts +49 -0
  39. package/dist/props/registry.d.ts +57 -3
  40. package/dist/props/registry.js +62 -2
  41. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  42. package/dist/props/vocabulary/index.d.ts +2 -2
  43. package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
  44. package/dist/styles/control.css +44 -1
  45. package/dist/styles/core.css +2 -0
  46. package/dist/styles/data-display-layout.css +58 -1
  47. package/dist/styles/form-layout.css +21 -0
  48. package/dist/styles/icon-layout.css +37 -0
  49. package/dist/styles/index.css +2 -0
  50. package/dist/styles/layout.css +7 -0
  51. package/dist/styles/navigation-layout.css +5 -1
  52. package/dist/styles/shell-layout.css +11 -14
  53. package/dist/styles/table-layout.css +80 -8
  54. package/dist/styles/text-layout.css +16 -8
  55. package/dist/tokens/components/control.css +5 -0
  56. package/dist/tokens/components/data-display.css +6 -0
  57. package/dist/tokens/components/navigation.css +1 -1
  58. package/dist/tokens/components/table.css +2 -0
  59. package/docs/CONSUMER-RULES.md +2 -2
  60. package/docs/DESIGN-AUTHORITY.md +32 -1
  61. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  62. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  63. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  64. package/docs/data-display/popover.tsx +43 -1
  65. package/docs/data-display/timeline.tsx +73 -9
  66. package/docs/data-entry/command.tsx +80 -1
  67. package/docs/data-entry/form-root.tsx +12 -2
  68. package/docs/general/icon.tsx +215 -0
  69. package/docs/layout/flex.tsx +59 -0
  70. package/docs/layout/responsive-grid.tsx +100 -0
  71. package/docs/layout/topbar-item.tsx +29 -1
  72. package/package.json +3 -2
  73. package/scripts/consumer-rule.md +18 -17
  74. package/scripts/ui-audit.mjs +128 -0
@@ -51,37 +51,45 @@
51
51
  }
52
52
 
53
53
  [data-slot="text"][data-tone="default"],
54
- [data-slot="heading"][data-tone="default"] {
54
+ [data-slot="heading"][data-tone="default"],
55
+ [data-slot="icon"][data-tone="default"] {
55
56
  color: hsl(var(--foreground));
56
57
  }
57
58
  [data-slot="text"][data-tone="muted"],
58
- [data-slot="heading"][data-tone="muted"] {
59
+ [data-slot="heading"][data-tone="muted"],
60
+ [data-slot="icon"][data-tone="muted"] {
59
61
  color: hsl(var(--muted-foreground));
60
62
  }
61
63
 
62
64
  [data-slot="text"][data-tone="inherit"],
63
- [data-slot="heading"][data-tone="inherit"] {
65
+ [data-slot="heading"][data-tone="inherit"],
66
+ [data-slot="icon"][data-tone="inherit"] {
64
67
  color: inherit;
65
68
  }
66
69
  [data-slot="text"][data-tone="primary"],
67
- [data-slot="heading"][data-tone="primary"] {
70
+ [data-slot="heading"][data-tone="primary"],
71
+ [data-slot="icon"][data-tone="primary"] {
68
72
  color: hsl(var(--primary));
69
73
  }
70
74
 
71
75
  [data-slot="text"][data-tone="success"],
72
- [data-slot="heading"][data-tone="success"] {
76
+ [data-slot="heading"][data-tone="success"],
77
+ [data-slot="icon"][data-tone="success"] {
73
78
  color: hsl(var(--text-success));
74
79
  }
75
80
  [data-slot="text"][data-tone="warning"],
76
- [data-slot="heading"][data-tone="warning"] {
81
+ [data-slot="heading"][data-tone="warning"],
82
+ [data-slot="icon"][data-tone="warning"] {
77
83
  color: hsl(var(--text-warning));
78
84
  }
79
85
  [data-slot="text"][data-tone="destructive"],
80
- [data-slot="heading"][data-tone="destructive"] {
86
+ [data-slot="heading"][data-tone="destructive"],
87
+ [data-slot="icon"][data-tone="destructive"] {
81
88
  color: hsl(var(--text-error));
82
89
  }
83
90
  [data-slot="text"][data-tone="info"],
84
- [data-slot="heading"][data-tone="info"] {
91
+ [data-slot="heading"][data-tone="info"],
92
+ [data-slot="icon"][data-tone="info"] {
85
93
  color: hsl(var(--text-info));
86
94
  }
87
95
 
@@ -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
  }
@@ -5,9 +5,9 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
5
5
  1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled, 729 sliced woff2 faces, ~11.7 MB), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all), `@import "@godxjp/ui/styles/core-with-fallbacks"` (`core` + six `local()`-only metric-matched faces, still zero network bytes — for when you supply Noto Sans JP yourself) or `@import "@godxjp/ui/styles/core-with-jis-level1"` (`core-with-fallbacks` + Noto Sans JP merged to JIS X 0208 level 1: **3 requests, ~1.53 MB, once** instead of ~8 font requests on every navigation — for a Japanese app that wants the bundled face). Never cherry-pick `*-layout.css`.
6
6
  2. Every page is `<PageContainer title subtitle extra footer>`; its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
7
7
  3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
8
- 4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`.
8
+ 4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`. A LIST of those rows is `<Flex as="ul" marker="none" direction="col" gap="none">` with `<ListRow as="li">` children — `marker="none"` keeps the `<ul>`, the `<li>` semantics and the gap token while dropping the bullet and the indent. Never a raw `<ul>`/`<ol>` (it carries no gap token), never `<div role="list">` + `<div role="listitem">`, and never a wrapper around each row: the divider is `:not(:last-child)` among siblings, so a row alone in its own wrapper is always the last one and EVERY divider disappears silently. A bulleted prose list is the same `<Flex as="ul">` without `marker`.
9
9
  5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below.
10
- 6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`.
10
+ 6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`. A STANDALONE glyph is `<Icon as={Lock} size="sm" tone="muted" />` from `@godxjp/ui/general`: a bare lucide icon carries its own `width="24" height="24"` and only four places re-size it for you (`Button`, a `DropdownMenuItem`, a `TopbarItem`, a `ListRow` leading slot), so everywhere else — in a `Text`, a table cell, an `<a>` — it draws at 24px beside 14px type. `size-4` and `w-[16px]` are closed to you by rules 3 and 8, and `size={16}` hard-codes a number the theme owns; `Icon` puts the glyph on the `--icon-size-*` scale (`2xs…4xl`) instead, and is `aria-hidden` unless you give it a `label` (then it is `role="img"` with that name). The audit rule is `lucide-icon-needs-size`.
11
11
  7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
12
12
  8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
13
13
  9. Logical directions (`ms-`, `me-`, `start-`, `end-`) when a utility is unavoidable; never `ml-` / `left-`.
@@ -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
@@ -3,8 +3,8 @@
3
3
  > Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
4
4
  > Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
5
5
 
6
- - Public components: **163**
7
- - With ≥1 frame: **142** · zero-frame: **21**
6
+ - Public components: **164**
7
+ - With ≥1 frame: **143** · zero-frame: **21**
8
8
  - Fully covered (every contract axis declared): **0**
9
9
 
10
10
  ## UNTESTED — no `/frame/**` route
@@ -74,6 +74,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
74
74
  | Logo | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
75
75
  | Reveal | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
76
76
  | Activity | ✓ | · | N/A | N/A | · | · | · | · | · | · |
77
+ | Icon | ✓ | · | N/A | N/A | · | · | · | · | · | · |
77
78
  | DataTable | ✓ | ✓ | · | · | · | · | · | · | · | · |
78
79
  | Card | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
79
80
  | CardContent | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
@@ -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>