@godxjp/ui 27.0.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 (68) 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-errors.d.ts +5 -0
  13. package/dist/components/data-entry/form-errors.js +6 -0
  14. package/dist/components/data-entry/form-field.js +9 -8
  15. package/dist/components/data-entry/index.d.ts +1 -0
  16. package/dist/components/data-entry/search-select.js +145 -136
  17. package/dist/components/data-entry/select.js +38 -3
  18. package/dist/components/feedback/dialog.d.ts +4 -3
  19. package/dist/components/feedback/dialog.js +46 -19
  20. package/dist/components/feedback/non-modal-layer.d.ts +16 -0
  21. package/dist/components/feedback/non-modal-layer.js +37 -0
  22. package/dist/components/feedback/sheet.d.ts +7 -4
  23. package/dist/components/feedback/sheet.js +63 -40
  24. package/dist/components/layout/org-switcher.js +1 -1
  25. package/dist/components/layout/responsive-grid.d.ts +8 -2
  26. package/dist/components/layout/responsive-grid.js +2 -0
  27. package/dist/components/query/mutation-feedback.js +2 -4
  28. package/dist/contracts/measurement.json +1 -1
  29. package/dist/form/form-field-control.d.ts +1 -1
  30. package/dist/form/form-field-control.js +32 -23
  31. package/dist/form/form-root.d.ts +1 -1
  32. package/dist/form/form-root.js +25 -6
  33. package/dist/i18n/messages/en.json +2 -1
  34. package/dist/i18n/messages/ja.json +2 -1
  35. package/dist/i18n/messages/vi.json +2 -1
  36. package/dist/props/components/data-display.prop.d.ts +12 -0
  37. package/dist/props/components/data-entry.prop.d.ts +10 -0
  38. package/dist/props/components/feedback.prop.d.ts +7 -1
  39. package/dist/props/components/form.prop.d.ts +52 -3
  40. package/dist/props/components/index.d.ts +1 -1
  41. package/dist/props/components/layout.prop.d.ts +7 -0
  42. package/dist/props/registry.d.ts +26 -3
  43. package/dist/props/registry.js +25 -2
  44. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  45. package/dist/props/vocabulary/index.d.ts +1 -1
  46. package/dist/styles/control.css +44 -1
  47. package/dist/styles/data-display-layout.css +58 -1
  48. package/dist/styles/form-layout.css +21 -0
  49. package/dist/styles/layout.css +7 -0
  50. package/dist/styles/navigation-layout.css +5 -1
  51. package/dist/styles/shell-layout.css +1 -14
  52. package/dist/styles/table-layout.css +80 -8
  53. package/dist/tokens/components/control.css +5 -0
  54. package/dist/tokens/components/data-display.css +6 -0
  55. package/dist/tokens/components/navigation.css +1 -1
  56. package/dist/tokens/components/table.css +2 -0
  57. package/docs/DESIGN-AUTHORITY.md +32 -1
  58. package/docs/FORMS.md +11 -10
  59. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  60. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  61. package/docs/data-display/popover.tsx +43 -1
  62. package/docs/data-display/timeline.tsx +73 -9
  63. package/docs/data-entry/command.tsx +80 -1
  64. package/docs/data-entry/form-root.tsx +12 -2
  65. package/docs/feedback/dialog.tsx +82 -0
  66. package/docs/feedback/sheet.tsx +84 -0
  67. package/docs/layout/responsive-grid.tsx +100 -0
  68. package/package.json +4 -2
@@ -91,6 +91,10 @@ export default function Demo() {
91
91
  new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", timeZone: "UTC" }).format(
92
92
  Date.UTC(2026, 8, day),
93
93
  );
94
+ const monthLabel = (month: number) =>
95
+ new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", timeZone: "UTC" }).format(
96
+ Date.UTC(2026, month, 1),
97
+ );
94
98
  return (
95
99
  <PageContainer title="Timeline" subtitle="現在のステップを強調する縦型のイベント一覧">
96
100
  <Flex direction="col" gap="lg">
@@ -162,23 +166,83 @@ export default function Demo() {
162
166
  <Card>
163
167
  <CardHeader>
164
168
  <CardTitle level={2}>RangeTimeline</CardTitle>
169
+ <CardDescription>
170
+ 日単位の軸。行ごとの区切り線と、列ごとの縦線が本体全体に引かれます(bordered は既定で
171
+ true)。土日は columns[].muted
172
+ で本体の高さいっぱいに塗り分け、表示範囲の外にある期間は、その方向の端に矢印付きで示します。
173
+ </CardDescription>
165
174
  </CardHeader>
166
175
  <CardContent>
167
176
  <RangeTimeline
168
177
  label={t("rangeTimeline.schedule")}
169
- bands={[
170
- {
171
- label: new Intl.DateTimeFormat(locale, {
172
- year: "numeric",
173
- month: "long",
174
- timeZone: "UTC",
175
- }).format(Date.UTC(2026, 8, 1)),
176
- units: 7,
177
- },
178
+ today={8}
179
+ bands={[{ label: monthLabel(8), units: 14 }]}
180
+ columns={Array.from({ length: 14 }, (_, index) => {
181
+ const weekday = new Date(Date.UTC(2026, 8, index + 1)).getUTCDay();
182
+ return {
183
+ label: new Intl.NumberFormat(locale).format(index + 1),
184
+ units: 1,
185
+ muted: weekday === 0 || weekday === 6,
186
+ };
187
+ })}
188
+ rows={[
189
+ { id: "design", label: "要件定義", start: 0, end: 3 },
190
+ { id: "build", label: "実装", start: 3, end: 10 },
191
+ { id: "review", label: "レビュー", start: 9, end: 12 },
192
+ { id: "kickoff", label: "キックオフ(前月)", start: -12, end: -9 },
193
+ { id: "release", label: "リリース(来月)", start: 20, end: 22 },
194
+ ].map((row) => ({
195
+ ...row,
196
+ startLabel: dayLabel(row.start + 1),
197
+ endLabel: dayLabel(row.end + 1),
198
+ }))}
199
+ />
200
+ </CardContent>
201
+ </Card>
202
+
203
+ <Card>
204
+ <CardHeader>
205
+ <CardTitle level={2}>月単位の軸(列ごとに units が異なる)</CardTitle>
206
+ <CardDescription>
207
+ units
208
+ は日数です。9月30・10月31・11月30と幅が異なっても、本体の縦線はヘッダーの列の境界に揃います。
209
+ </CardDescription>
210
+ </CardHeader>
211
+ <CardContent>
212
+ <RangeTimeline
213
+ label={t("rangeTimeline.schedule")}
214
+ columns={[
215
+ { label: monthLabel(8), units: 30 },
216
+ { label: monthLabel(9), units: 31 },
217
+ { label: monthLabel(10), units: 30 },
178
218
  ]}
219
+ rows={[
220
+ { id: "phase-1", label: "第1期", start: 0, end: 44 },
221
+ { id: "phase-2", label: "第2期", start: 45, end: 90 },
222
+ ].map((row) => ({
223
+ ...row,
224
+ startLabel: dayLabel(row.start + 1),
225
+ endLabel: dayLabel(row.end + 1),
226
+ }))}
227
+ />
228
+ </CardContent>
229
+ </Card>
230
+
231
+ <Card>
232
+ <CardHeader>
233
+ <CardTitle level={2}>罫線なし(bordered=false)</CardTitle>
234
+ <CardDescription>
235
+ ヘッダーの列線だけを残し、本体の罫線を外します。muted の列の塗りは残ります。
236
+ </CardDescription>
237
+ </CardHeader>
238
+ <CardContent>
239
+ <RangeTimeline
240
+ label={t("rangeTimeline.schedule")}
241
+ bordered={false}
179
242
  columns={Array.from({ length: 7 }, (_, index) => ({
180
243
  label: dayLabel(index + 1),
181
244
  units: 1,
245
+ muted: index === 5,
182
246
  }))}
183
247
  rows={[
184
248
  {
@@ -1,7 +1,17 @@
1
1
  import { useEffect, useState } from "react";
2
2
 
3
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
4
3
  import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "@godxjp/ui/data-display";
13
+ import {
14
+ Checkbox,
5
15
  Command,
6
16
  CommandEmpty,
7
17
  CommandGroup,
@@ -10,7 +20,9 @@ import {
10
20
  CommandList,
11
21
  } from "@godxjp/ui/data-entry";
12
22
  import { Skeleton } from "@godxjp/ui/feedback";
23
+ import { Button } from "@godxjp/ui/general";
13
24
  import { Flex, PageContainer } from "@godxjp/ui/layout";
25
+ import { ChevronDown } from "lucide-react";
14
26
 
15
27
  /**
16
28
  * Command — キーボードナビゲーション対応のコマンドパレット。
@@ -30,6 +42,14 @@ const VENDOR_DIRECTORY: Suggestion[] = [
30
42
  { value: "v-005", label: "田中物産" },
31
43
  ];
32
44
 
45
+ const MEMBERS: Suggestion[] = [
46
+ { value: "m-001", label: "山田 太郎" },
47
+ { value: "m-002", label: "佐藤 花子" },
48
+ { value: "m-003", label: "鈴木 一郎" },
49
+ { value: "m-004", label: "高橋 美咲" },
50
+ { value: "m-005", label: "PHAM THAI DUONG(プロジェクト外)" },
51
+ ];
52
+
33
53
  export default function Demo() {
34
54
  // Card 1: grouped quick-select. Controlled `value` pre-highlights an item so the
35
55
  // [aria-selected=true] accent is visible at rest, without keyboard interaction.
@@ -44,6 +64,13 @@ export default function Demo() {
44
64
  const [loading, setLoading] = useState<boolean>(true);
45
65
  const [results, setResults] = useState<Suggestion[]>([]);
46
66
 
67
+ // Card 4: filter facet — a checklist, so the list is `split` (ruled rows, 0 list padding).
68
+ const [assignees, setAssignees] = useState<string[]>(["m-002"]);
69
+ const toggleAssignee = (member: string) =>
70
+ setAssignees((current) =>
71
+ current.includes(member) ? current.filter((m) => m !== member) : [...current, member],
72
+ );
73
+
47
74
  useEffect(() => {
48
75
  let active = true;
49
76
  setLoading(true);
@@ -131,6 +158,58 @@ export default function Demo() {
131
158
  </CardContent>
132
159
  </Card>
133
160
 
161
+ <Card>
162
+ <CardHeader>
163
+ <CardTitle level={2}>担当者フィルター(split)</CardTitle>
164
+ <CardDescription>
165
+ Popover + Command に split を付けると、行間に区切り線・リスト余白 0・行は popover
166
+ の端まで届く一つの箱として描画されます。チェックボックスで複数選択(選択中:{" "}
167
+ {assignees.length} 名)。
168
+ </CardDescription>
169
+ </CardHeader>
170
+ <CardContent>
171
+ <Popover>
172
+ <PopoverTrigger asChild>
173
+ <Button variant="outline">
174
+ 担当者{assignees.length > 0 ? ` (${assignees.length})` : ""}
175
+ <ChevronDown aria-hidden="true" />
176
+ </Button>
177
+ </PopoverTrigger>
178
+ <PopoverContent flush align="start">
179
+ <Command label="担当者を絞り込む" split>
180
+ <CommandInput placeholder="担当者 を絞り込む" />
181
+ <CommandList>
182
+ <CommandEmpty>該当する担当者がいません。</CommandEmpty>
183
+ <CommandGroup>
184
+ {MEMBERS.map((member) => {
185
+ const checked = assignees.includes(member.value);
186
+ return (
187
+ <CommandItem
188
+ key={member.value}
189
+ value={member.label}
190
+ aria-checked={checked}
191
+ onSelect={() => toggleAssignee(member.value)}
192
+ >
193
+ {/* The ROW is the option and owns the click; the box only mirrors it,
194
+ so it leaves the tab ring (TreeSelect does the same). */}
195
+ <Checkbox
196
+ checked={checked}
197
+ tabIndex={-1}
198
+ onCheckedChange={() => toggleAssignee(member.value)}
199
+ aria-labelledby={`assignee-${member.value}`}
200
+ />
201
+ <span id={`assignee-${member.value}`}>{member.label}</span>
202
+ </CommandItem>
203
+ );
204
+ })}
205
+ </CommandGroup>
206
+ </CommandList>
207
+ </Command>
208
+ </PopoverContent>
209
+ </Popover>
210
+ </CardContent>
211
+ </Card>
212
+
134
213
  <Card>
135
214
  <CardHeader>
136
215
  <CardTitle level={2}>勤怠アクションパレット(Cmd+K 想定)</CardTitle>
@@ -1,7 +1,7 @@
1
1
  import { useState } from "react";
2
2
  import { z } from "zod";
3
3
  import { Alert, AlertDescription } from "@godxjp/ui/feedback";
4
- import { Input } from "@godxjp/ui/data-entry";
4
+ import { Checkbox, Input } from "@godxjp/ui/data-entry";
5
5
  import { Button } from "@godxjp/ui/general";
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
7
7
  import { Flex, PageContainer } from "@godxjp/ui/layout";
@@ -10,12 +10,13 @@ import { FormFieldControl, FormRoot, useZodForm } from "@godxjp/ui/form";
10
10
  const schema = z.object({
11
11
  name: z.string().min(1, "組織名を入力してください"),
12
12
  email: z.string().email("有効なメールアドレスを入力してください"),
13
+ is_shared: z.boolean(),
13
14
  });
14
15
 
15
16
  export default function Demo() {
16
17
  const [status, setStatus] = useState<"idle" | "pending" | "success" | "server-error">("idle");
17
18
  const form = useZodForm(schema, {
18
- defaultValues: { name: "株式会社アクメ", email: "billing@example.jp" },
19
+ defaultValues: { name: "株式会社アクメ", email: "billing@example.jp", is_shared: false },
19
20
  });
20
21
 
21
22
  async function submit() {
@@ -70,6 +71,15 @@ export default function Demo() {
70
71
  />
71
72
  )}
72
73
  </FormFieldControl>
74
+ {/* Boolean field (antd `valuePropName="checked"`): the label is the Checkbox's children,
75
+ on the same line as the box — no label row above it. */}
76
+ <FormFieldControl name="is_shared" valuePropName="checked">
77
+ {(field) => (
78
+ <Checkbox {...field} disabled={status === "pending"}>
79
+ 請求書をプロジェクトメンバーに共有する
80
+ </Checkbox>
81
+ )}
82
+ </FormFieldControl>
73
83
  <Flex gap="sm" wrap>
74
84
  <Button type="submit" loading={status === "pending"}>
75
85
  保存
@@ -34,6 +34,15 @@ const headerTones = [
34
34
  "neutral",
35
35
  ] as const;
36
36
 
37
+ /** Order lines behind the non-modal payment dialog — they stay editable while it is open. */
38
+ const initialLines = [
39
+ { id: "coffee", name: "ブレンドコーヒー", unitPrice: 480, quantity: 2 },
40
+ { id: "sandwich", name: "ミックスサンド", unitPrice: 650, quantity: 1 },
41
+ { id: "cake", name: "チーズケーキ", unitPrice: 520, quantity: 1 },
42
+ ];
43
+
44
+ const yen = new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" });
45
+
37
46
  /**
38
47
  * Dialog · compound controlled modal for form-style flows. Always control via
39
48
  * open + onOpenChange. Include DialogHeader > DialogTitle (required for a11y).
@@ -46,6 +55,16 @@ export default function Demo() {
46
55
  const [headerTone, setHeaderTone] = useState<(typeof headerTones)[number]>("default");
47
56
  const [terminateOpen, setTerminateOpen] = useState(false);
48
57
  const [reason, setReason] = useState("");
58
+ const [paymentOpen, setPaymentOpen] = useState(false);
59
+ const [lines, setLines] = useState(initialLines);
60
+ const total = lines.reduce((sum, line) => sum + line.unitPrice * line.quantity, 0);
61
+ const changeQuantity = (id: string, delta: number) => {
62
+ setLines((current) =>
63
+ current.map((line) =>
64
+ line.id === id ? { ...line, quantity: Math.max(0, line.quantity + delta) } : line,
65
+ ),
66
+ );
67
+ };
49
68
 
50
69
  return (
51
70
  <PageContainer
@@ -249,6 +268,69 @@ export default function Demo() {
249
268
  </Dialog>
250
269
  </CardContent>
251
270
  </Card>
271
+
272
+ <Card>
273
+ <CardHeader>
274
+ <CardTitle level={2}>Non-modal dialog: the list behind stays editable</CardTitle>
275
+ <CardDescription>
276
+ modal=&#123;false&#125; renders a non-modal dialog, a pattern WAI-ARIA APG allows. The
277
+ page behind keeps working: no scrim, no scroll lock, nothing hidden from assistive
278
+ tech, and a press outside does not close the dialog. Open the payment dialog, then
279
+ change a quantity below. The total inside the dialog follows. Focus moves into the
280
+ dialog on open, Tab can leave it, Escape closes it while focus is inside, and focus
281
+ returns to the trigger.
282
+ </CardDescription>
283
+ </CardHeader>
284
+ <CardContent>
285
+ <Flex direction="col" gap="md">
286
+ {lines.map((line) => (
287
+ <Flex key={line.id} direction="row" align="center" justify="between" gap="sm">
288
+ <Text>{line.name}</Text>
289
+ <Flex direction="row" align="center" gap="sm">
290
+ <Button
291
+ variant="outline"
292
+ size="sm"
293
+ aria-label={`${line.name}を減らす`}
294
+ disabled={line.quantity === 0}
295
+ onClick={() => changeQuantity(line.id, -1)}
296
+ >
297
+ −
298
+ </Button>
299
+ <Text aria-live="polite">{line.quantity}</Text>
300
+ <Button
301
+ variant="outline"
302
+ size="sm"
303
+ aria-label={`${line.name}を増やす`}
304
+ onClick={() => changeQuantity(line.id, 1)}
305
+ >
306
+ +
307
+ </Button>
308
+ </Flex>
309
+ </Flex>
310
+ ))}
311
+ <Dialog modal={false} open={paymentOpen} onOpenChange={setPaymentOpen}>
312
+ <DialogTrigger asChild>
313
+ <Button size="sm">お会計へ</Button>
314
+ </DialogTrigger>
315
+ <DialogContent className="max-w-sm">
316
+ <DialogHeader title="お会計" subtitle="注文明細は背面で編集を続けられます。" />
317
+ <DialogBody>
318
+ <Flex direction="row" justify="between">
319
+ <Text tone="muted">合計</Text>
320
+ <Text>{yen.format(total)}</Text>
321
+ </Flex>
322
+ </DialogBody>
323
+ <DialogFooter>
324
+ <Button variant="outline" onClick={() => setPaymentOpen(false)}>
325
+ キャンセル
326
+ </Button>
327
+ <Button onClick={() => setPaymentOpen(false)}>支払いを確定</Button>
328
+ </DialogFooter>
329
+ </DialogContent>
330
+ </Dialog>
331
+ </Flex>
332
+ </CardContent>
333
+ </Card>
252
334
  </Flex>
253
335
  </PageContainer>
254
336
  );
@@ -42,6 +42,15 @@ const headerTones = [
42
42
  "neutral",
43
43
  ] as const;
44
44
 
45
+ /** Order lines behind the non-modal sheet — they stay editable while it is open. */
46
+ const initialLines = [
47
+ { id: "coffee", name: "ブレンドコーヒー", unitPrice: 480, quantity: 2 },
48
+ { id: "sandwich", name: "ミックスサンド", unitPrice: 650, quantity: 1 },
49
+ { id: "cake", name: "チーズケーキ", unitPrice: 520, quantity: 1 },
50
+ ];
51
+
52
+ const yen = new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" });
53
+
45
54
  /**
46
55
  * Sheet · side-panel drawer (Radix Dialog). Slides in from an edge. Compose
47
56
  * Sheet > SheetTrigger (asChild) > SheetContent(side) > SheetHeader >
@@ -84,6 +93,18 @@ export default function Demo() {
84
93
  const [toneOpen, setToneOpen] = useState(false);
85
94
  const [headerTone, setHeaderTone] = useState<(typeof headerTones)[number]>("default");
86
95
 
96
+ // Card 6 · non-modal sheet: the order lines behind it stay editable while it is open.
97
+ const [summaryOpen, setSummaryOpen] = useState(false);
98
+ const [lines, setLines] = useState(initialLines);
99
+ const total = lines.reduce((sum, line) => sum + line.unitPrice * line.quantity, 0);
100
+ const changeQuantity = (id: string, delta: number) => {
101
+ setLines((current) =>
102
+ current.map((line) =>
103
+ line.id === id ? { ...line, quantity: Math.max(0, line.quantity + delta) } : line,
104
+ ),
105
+ );
106
+ };
107
+
87
108
  return (
88
109
  <PageContainer
89
110
  title="Sheet"
@@ -415,6 +436,69 @@ export default function Demo() {
415
436
  </Flex>
416
437
  </CardContent>
417
438
  </Card>
439
+
440
+ <Card>
441
+ <CardHeader>
442
+ <CardTitle level={2}>Non-modal sheet: the list behind stays editable</CardTitle>
443
+ <CardDescription>
444
+ modal=&#123;false&#125; on Sheet renders a non-modal panel, a pattern WAI-ARIA APG
445
+ allows. The page behind keeps working: no scrim, no scroll lock, nothing hidden from
446
+ assistive tech, and a press outside does not close the panel. Open the order summary
447
+ (side=&quot;left&quot;, so the quantity controls stay uncovered), then change a quantity
448
+ below. The total inside the panel follows. Focus moves into the
449
+ panel on open, Tab can leave it, Escape closes it while focus is inside, and focus
450
+ returns to the trigger.
451
+ </CardDescription>
452
+ </CardHeader>
453
+ <CardContent>
454
+ <Flex direction="col" gap="md">
455
+ {lines.map((line) => (
456
+ <Flex key={line.id} direction="row" align="center" justify="between" gap="sm">
457
+ <Text>{line.name}</Text>
458
+ <Flex direction="row" align="center" gap="sm">
459
+ <Button
460
+ variant="outline"
461
+ size="sm"
462
+ aria-label={`${line.name}を減らす`}
463
+ disabled={line.quantity === 0}
464
+ onClick={() => changeQuantity(line.id, -1)}
465
+ >
466
+ −
467
+ </Button>
468
+ <Text aria-live="polite">{line.quantity}</Text>
469
+ <Button
470
+ variant="outline"
471
+ size="sm"
472
+ aria-label={`${line.name}を増やす`}
473
+ onClick={() => changeQuantity(line.id, 1)}
474
+ >
475
+ +
476
+ </Button>
477
+ </Flex>
478
+ </Flex>
479
+ ))}
480
+ <Sheet modal={false} open={summaryOpen} onOpenChange={setSummaryOpen}>
481
+ <SheetTrigger asChild>
482
+ <Button variant="outline" size="sm">
483
+ 注文サマリー
484
+ </Button>
485
+ </SheetTrigger>
486
+ <SheetContent side="left" width={360}>
487
+ <SheetHeader title="注文サマリー" subtitle="明細は背面で編集を続けられます。" />
488
+ <SheetBody>
489
+ <Flex direction="row" justify="between">
490
+ <Text tone="muted">合計</Text>
491
+ <Text>{yen.format(total)}</Text>
492
+ </Flex>
493
+ </SheetBody>
494
+ <SheetFooter>
495
+ <Button onClick={() => setSummaryOpen(false)}>完了</Button>
496
+ </SheetFooter>
497
+ </SheetContent>
498
+ </Sheet>
499
+ </Flex>
500
+ </CardContent>
501
+ </Card>
418
502
  </Flex>
419
503
  </PageContainer>
420
504
  );
@@ -1,11 +1,20 @@
1
+ import { useState } from "react";
1
2
  import {
2
3
  Card,
3
4
  CardContent,
4
5
  CardDescription,
5
6
  CardHeader,
6
7
  CardTitle,
8
+ ScrollArea,
7
9
  StatCard,
8
10
  } from "@godxjp/ui/data-display";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@godxjp/ui/data-entry";
9
18
  import { Text } from "@godxjp/ui/general";
10
19
  import { Flex, PageContainer, ResponsiveGrid, SplitPane } from "@godxjp/ui/layout";
11
20
 
@@ -20,7 +29,25 @@ import { Flex, PageContainer, ResponsiveGrid, SplitPane } from "@godxjp/ui/layou
20
29
  * recognised collection shape instead of hand-rolling the breakpoint map. Composed only from
21
30
  * real @godxjp/ui components.
22
31
  */
32
+ const KANBAN_LANES = [
33
+ { key: "todo", title: "未着手" },
34
+ { key: "doing", title: "進行中" },
35
+ { key: "done", title: "完了" },
36
+ ] as const;
37
+
38
+ type KanbanLane = (typeof KANBAN_LANES)[number]["key"];
39
+
40
+ const KANBAN_TASKS: { id: string; title: string; lane: KanbanLane; priority: string }[] = [
41
+ { id: "T-101", title: "請求書テンプレートの改訂", lane: "todo", priority: "high" },
42
+ { id: "T-102", title: "取引先マスタの重複整理", lane: "todo", priority: "medium" },
43
+ { id: "T-103", title: "月次締めチェックリスト", lane: "doing", priority: "low" },
44
+ ];
45
+
23
46
  export default function Demo() {
47
+ // Kanban: which lane each card is in, moved by native drag and drop.
48
+ const [tasks, setTasks] = useState(KANBAN_TASKS);
49
+ const moveTask = (id: string, lane: KanbanLane) =>
50
+ setTasks((current) => current.map((task) => (task.id === id ? { ...task, lane } : task)));
24
51
  return (
25
52
  <PageContainer
26
53
  title="ResponsiveGrid"
@@ -255,6 +282,79 @@ export default function Demo() {
255
282
  </CardContent>
256
283
  </Card>
257
284
 
285
+ <Card>
286
+ <CardHeader>
287
+ <CardTitle level={2}>
288
+ flow=&quot;columns&quot; align=&quot;stretch&quot; · カンバンのレーン
289
+ </CardTitle>
290
+ <CardDescription>
291
+ align=&quot;stretch&quot;(antd Row
292
+ align)で全レーンが最も高いレーンと同じ高さになり、空の
293
+ 「完了」レーンも全高のドロップ先になる。省略時は columns フローが start、rows フローが
294
+ stretch のまま。カードはドラッグでき、中の Select
295
+ はドラッグ画像をカードの外へ広げない。
296
+ </CardDescription>
297
+ </CardHeader>
298
+ <CardContent>
299
+ <ScrollArea orientation="horizontal">
300
+ <ResponsiveGrid flow="columns" align="stretch">
301
+ {KANBAN_LANES.map((lane) => (
302
+ <Card
303
+ key={lane.key}
304
+ data-lane={lane.key}
305
+ onDragOver={(event) => event.preventDefault()}
306
+ onDrop={(event) => {
307
+ event.preventDefault();
308
+ moveTask(event.dataTransfer.getData("text/plain"), lane.key);
309
+ }}
310
+ >
311
+ <CardHeader>
312
+ <CardTitle>{lane.title}</CardTitle>
313
+ </CardHeader>
314
+ <CardContent>
315
+ <Flex direction="col" gap="sm">
316
+ {tasks
317
+ .filter((task) => task.lane === lane.key)
318
+ .map((task) => (
319
+ <Card
320
+ key={task.id}
321
+ draggable
322
+ data-task={task.id}
323
+ onDragStart={(event) =>
324
+ event.dataTransfer.setData("text/plain", task.id)
325
+ }
326
+ >
327
+ <CardContent>
328
+ <Flex direction="col" gap="xs">
329
+ <Text tone="muted">{task.id}</Text>
330
+ <Text>{task.title}</Text>
331
+ <Select
332
+ aria-label={`${task.id} 優先度`}
333
+ name={`priority-${task.id}`}
334
+ defaultValue={task.priority}
335
+ >
336
+ <SelectTrigger aria-label={`${task.id} 優先度`} size="sm">
337
+ <SelectValue />
338
+ </SelectTrigger>
339
+ <SelectContent>
340
+ <SelectItem value="high">高</SelectItem>
341
+ <SelectItem value="medium">中</SelectItem>
342
+ <SelectItem value="low">低</SelectItem>
343
+ </SelectContent>
344
+ </Select>
345
+ </Flex>
346
+ </CardContent>
347
+ </Card>
348
+ ))}
349
+ </Flex>
350
+ </CardContent>
351
+ </Card>
352
+ ))}
353
+ </ResponsiveGrid>
354
+ </ScrollArea>
355
+ </CardContent>
356
+ </Card>
357
+
258
358
  <Card>
259
359
  <CardHeader>
260
360
  <CardTitle level={2}>一覧と補助情報 2:1</CardTitle>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "27.0.0",
4
- "godxUiMcp": "27.0.0",
3
+ "version": "27.3.1",
4
+ "godxUiMcp": "27.3.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -317,6 +317,7 @@
317
317
  "test:visual:topbar-collision": "node scripts/topbar-collision-visual.mjs",
318
318
  "test:visual:table-collection-cjk": "node scripts/table-collection-cjk-visual.mjs",
319
319
  "test:visual:chart-cjk-axis": "node scripts/chart-cjk-axis-visual.mjs",
320
+ "test:visual:kanban-select": "node scripts/kanban-select-visual.mjs",
320
321
  "test:watch": "vitest",
321
322
  "test:coverage": "vitest run --coverage",
322
323
  "check:example-imports": "node scripts/check-example-imports.mjs",
@@ -460,6 +461,7 @@
460
461
  "input-otp": "^1.5.0",
461
462
  "lucide-react": "^1.37.0",
462
463
  "qrcode.react": "4.2.0",
464
+ "react-aria": "3.52.1",
463
465
  "react-aria-components": "^1.21.1",
464
466
  "react-day-picker": "^10.0.1",
465
467
  "react-resizable-panels": "^4.12.3",