@godxjp/ui 19.3.1 → 19.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 (128) hide show
  1. package/dist/components/data-display/badge.d.ts +9 -1
  2. package/dist/components/data-display/badge.js +3 -2
  3. package/dist/components/data-display/index.d.ts +2 -0
  4. package/dist/components/data-display/index.js +2 -0
  5. package/dist/components/data-display/popover.d.ts +13 -1
  6. package/dist/components/data-display/popover.js +3 -1
  7. package/dist/components/data-display/table.d.ts +19 -2
  8. package/dist/components/data-display/table.js +4 -1
  9. package/dist/components/data-display/timeline-grid.d.ts +18 -0
  10. package/dist/components/data-display/timeline-grid.js +222 -0
  11. package/dist/components/data-entry/cascader.js +4 -6
  12. package/dist/components/data-entry/checkbox.js +1 -1
  13. package/dist/components/data-entry/search-select.js +5 -7
  14. package/dist/components/data-entry/select.js +32 -28
  15. package/dist/components/data-entry/tree-select.js +4 -6
  16. package/dist/components/general/inert-background.d.ts +5 -0
  17. package/dist/components/general/inert-background.js +48 -0
  18. package/dist/components/layout/auth-shell.d.ts +4 -2
  19. package/dist/components/layout/auth-shell.js +7 -1
  20. package/dist/components/layout/centered-shell.d.ts +2 -2
  21. package/dist/components/layout/flex.d.ts +1 -1
  22. package/dist/components/layout/flex.js +2 -1
  23. package/dist/components/layout/index.d.ts +3 -0
  24. package/dist/components/layout/index.js +2 -0
  25. package/dist/components/layout/mobile-shell.d.ts +21 -0
  26. package/dist/components/layout/mobile-shell.js +43 -0
  27. package/dist/components/layout/sidebar-link.d.ts +1 -1
  28. package/dist/components/navigation/context-menu.js +13 -9
  29. package/dist/components/navigation/dropdown-menu.js +17 -13
  30. package/dist/inertia/index.d.ts +1 -1
  31. package/dist/lib/variants.d.ts +3 -3
  32. package/dist/lib/variants.js +1 -0
  33. package/dist/props/components/data-display.prop.d.ts +65 -0
  34. package/dist/props/components/data-entry.prop.d.ts +1 -1
  35. package/dist/props/components/layout.prop.d.ts +93 -6
  36. package/dist/props/registry.d.ts +109 -2
  37. package/dist/props/registry.js +143 -0
  38. package/dist/props/vocabulary/data.prop.d.ts +6 -0
  39. package/dist/props/vocabulary/index.d.ts +3 -3
  40. package/dist/props/vocabulary/layout.prop.d.ts +17 -2
  41. package/dist/props/vocabulary/shared.prop.d.ts +7 -0
  42. package/dist/styles/control.css +18 -1
  43. package/dist/styles/data-display-layout.css +167 -0
  44. package/dist/styles/layout.css +7 -0
  45. package/dist/styles/shell-layout.css +148 -0
  46. package/dist/styles/table-layout.css +23 -4
  47. package/dist/tokens/components/control.css +1 -0
  48. package/dist/tokens/components/data-display.css +20 -0
  49. package/dist/tokens/components/shell.css +22 -0
  50. package/dist/tokens/components/table.css +2 -0
  51. package/docs/FRAME-A11Y-CI.md +99 -17
  52. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  53. package/docs/SPACING.md +19 -6
  54. package/docs/charts/compact-bar-trend.tsx +5 -6
  55. package/docs/data-display/avatar.tsx +7 -9
  56. package/docs/data-display/badge.tsx +1 -1
  57. package/docs/data-display/card/index.tsx +6 -4
  58. package/docs/data-display/data-table/examples/approval-queue.tsx +1 -1
  59. package/docs/data-display/data-table/index.tsx +1 -1
  60. package/docs/data-display/permission-matrix.tsx +2 -2
  61. package/docs/data-display/popover.tsx +4 -1
  62. package/docs/data-display/scroll-area.tsx +1 -1
  63. package/docs/data-display/table.tsx +1 -1
  64. package/docs/data-display/timeline-grid.tsx +119 -0
  65. package/docs/data-entry/branch-scope-picker.tsx +1 -1
  66. package/docs/data-entry/date-picker.tsx +2 -1
  67. package/docs/data-entry/form-field/index.tsx +2 -2
  68. package/docs/data-entry/input-otp.tsx +1 -1
  69. package/docs/data-entry/select-matrix.tsx +2 -1
  70. package/docs/data-entry/select.tsx +3 -1
  71. package/docs/data-entry/textarea.tsx +1 -1
  72. package/docs/data-entry/toggle-count.tsx +1 -1
  73. package/docs/feedback/alert.tsx +1 -1
  74. package/docs/feedback/banner.tsx +1 -1
  75. package/docs/feedback/dialog.tsx +2 -1
  76. package/docs/feedback/sheet.tsx +2 -1
  77. package/docs/foundation/email-tokens.tsx +1 -1
  78. package/docs/general/activity.tsx +1 -1
  79. package/docs/layout/app-shell.tsx +4 -4
  80. package/docs/layout/auth-account-summary.tsx +1 -1
  81. package/docs/layout/auth-recovery/examples/mobile-390.tsx +1 -1
  82. package/docs/layout/auth-recovery/examples/password-recovery.tsx +0 -2
  83. package/docs/layout/auth-shell-context.tsx +1 -1
  84. package/docs/layout/auth-shell-device.tsx +2 -2
  85. package/docs/layout/auth-shell-registration.tsx +1 -1
  86. package/docs/layout/auth-shell.tsx +1 -1
  87. package/docs/layout/master-detail.tsx +2 -2
  88. package/docs/layout/mobile-shell.tsx +101 -0
  89. package/docs/layout/page-container.tsx +6 -6
  90. package/docs/layout/responsive-grid.tsx +5 -5
  91. package/docs/layout/separator.tsx +2 -2
  92. package/docs/layout/service-role-panel.tsx +1 -1
  93. package/docs/layout/sidebar.tsx +9 -9
  94. package/docs/layout/split-pane.tsx +5 -6
  95. package/docs/layout/topbar.tsx +2 -8
  96. package/docs/navigation/context-menu.tsx +6 -1
  97. package/docs/navigation/dropdown-menu.tsx +2 -1
  98. package/docs/navigation/filter-bar.tsx +2 -2
  99. package/docs/navigation/pagination.tsx +2 -2
  100. package/docs/navigation/steps.tsx +1 -1
  101. package/docs/navigation/tabs.tsx +3 -3
  102. package/docs/showcase/acme-portal.tsx +11 -12
  103. package/docs/showcase/acme-website.tsx +7 -7
  104. package/docs/showcase/case1-warehouse-dashboard.tsx +11 -12
  105. package/docs/showcase/case2-employee-me.tsx +10 -16
  106. package/docs/showcase/case4-login.tsx +144 -156
  107. package/docs/showcase/case5-shift-calendar.tsx +181 -280
  108. package/docs/showcase/case6-agency-handy.tsx +446 -443
  109. package/docs/showcase/futurelastic-web.tsx +9 -8
  110. package/docs/showcase/org-switcher.tsx +18 -11
  111. package/docs/showcase/permission-matrix.tsx +13 -13
  112. package/docs/showcase/public-landing.tsx +1 -1
  113. package/docs/showcase/settings-account-sections.tsx +4 -4
  114. package/docs/showcase/settings-security-mfa.tsx +3 -3
  115. package/docs/showcase/table-approval-queue.tsx +1 -1
  116. package/docs/showcase/table-bulk-actions.tsx +49 -52
  117. package/docs/showcase/table-conditional-format.tsx +1 -1
  118. package/docs/showcase/table-crud-list.tsx +1 -1
  119. package/docs/showcase/table-expandable-rows.tsx +101 -98
  120. package/docs/showcase/table-filter-chips.tsx +78 -66
  121. package/docs/showcase/table-footer-totals.tsx +23 -19
  122. package/docs/showcase/table-grouped-subtotals.tsx +7 -4
  123. package/docs/showcase/table-master-detail.tsx +13 -15
  124. package/docs/showcase/table-states.tsx +34 -29
  125. package/docs/showcase/table-sticky-columns.tsx +3 -2
  126. package/docs/showcase/table-tree-rows.tsx +22 -15
  127. package/docs/showcase/table-view-tabs.tsx +12 -14
  128. package/package.json +7 -6
@@ -10,18 +10,17 @@
10
10
  * page chrome ............. AppShell + Sidebar + Topbar
11
11
  * page header + nav ....... PageContainer (extra slot) + Button + ToggleGroup (月/週/日)
12
12
  * month jump-to ........... Calendar (date-picker) inside a Popover
13
- * month event grid ........ Card + a 7-col CSS grid of day cells + shift "pills" ── GAP, see below
14
- * week time-axis + now-line Card + a 06–21 row grid + an absolutely-placed now bar ── GAP, see below
13
+ * month event grid ........ Card + Table bordered (one <tr> per week, one <td> per day)
14
+ * week time-axis + now-line Card + TimelineGrid (hour axis, day columns, `now` marker)
15
15
  * day staff×time .......... Card + Timeline (per-staff lane) + EmptyState (understaffed)
16
16
  * shift legend ............ Card + the wa-iro decorative palette swatches
17
17
  * date detail (mobile) .... Sheet (master-detail collapses to a drawer < lg)
18
18
  *
19
- * GAP (documented in .design/research/screens.md §6 + ui-kit-surfaces.md §9):
20
- * @godxjp/ui's `Calendar` is a single-date PICKER, not a month/week event grid.
21
- * There is no `ShiftCalendar` / scheduling-grid primitive. The month grid and the
22
- * week time-axis are therefore COMPOSED from real primitives (Card + token-driven
23
- * CSS grid + Badge), never a hand-rolled faux component. The real Calendar is still
24
- * used for its genuine job (jumping to a month). See `gapNotes`.
19
+ * The two grids each take the primitive that fits their shape. A MONTH is a day matrix, so it is
20
+ * a real `Table`. A WEEK is a continuous time axis, so it is `TimelineGrid` — which also places
21
+ * the 木曜 早番/遅番 pair side by side instead of stacking one on top of the other, and clips the
22
+ * 夜勤 22:00–06:00 at the 24:00 edge while the block still prints its real range. `Calendar` keeps
23
+ * its genuine job: a single-date PICKER for jumping to a month.
25
24
  *
26
25
  * DNA applied: compact density, small headings (20/18/14/13), fixed color signaling
27
26
  * (success 若竹 / warning 山吹 / info 群青 / attention 朱 / danger 茜), tabular-nums,
@@ -52,10 +51,20 @@ import {
52
51
  CardHeader,
53
52
  CardTitle,
54
53
  EmptyState,
54
+ ListRow,
55
55
  Popover,
56
56
  PopoverContent,
57
57
  PopoverTrigger,
58
+ Table,
59
+ TableBody,
60
+ TableCell,
61
+ TableHead,
62
+ TableHeader,
63
+ TableRow,
58
64
  Timeline,
65
+ TimelineGrid,
66
+ type TimelineGridColumnProp,
67
+ type TimelineGridEventProp,
59
68
  type TimelineItem,
60
69
  } from "@godxjp/ui/data-display";
61
70
  import { Calendar, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
@@ -107,17 +116,14 @@ function ShiftPill({
107
116
  }) {
108
117
  const meta = SHIFT_META[kind];
109
118
  return (
110
- <span
119
+ <Badge
111
120
  data-shift={kind}
112
- className={`flex items-center gap-1 truncate rounded-sm border-s-2 px-1.5 py-0.5 leading-tight ${className ?? ""}`}
113
- style={{
114
- borderLeftColor: `var(${meta.cssVar})`,
115
- background: `color-mix(in oklch, var(${meta.cssVar}) 12%, transparent)`,
116
- color: meta.muted ? "var(--muted-foreground)" : "var(--foreground)",
117
- }}
121
+ color={`var(${meta.cssVar})`}
122
+ shape="sharp"
123
+ className={`truncate leading-tight ${className ?? ""}`}
118
124
  title={`${meta.label} ${meta.time}${staff ? ` · ${staff}` : ""}`}
119
125
  >
120
- <Text as="span" size="2xs" weight="medium" style={{ color: "inherit" }}>
126
+ <Text as="span" size="2xs" weight="medium">
121
127
  {meta.label}
122
128
  </Text>
123
129
  {staff ? (
@@ -125,7 +131,7 @@ function ShiftPill({
125
131
  {staff}
126
132
  </Text>
127
133
  ) : null}
128
- </span>
134
+ </Badge>
129
135
  );
130
136
  }
131
137
 
@@ -218,44 +224,61 @@ function buildMonth(): DayCell[] {
218
224
 
219
225
  const MONTH_CELLS = buildMonth();
220
226
  const WEEKDAY_HEAD = ["日", "月", "火", "水", "木", "金", "土"];
227
+ /** 42 day cells as six calendar rows — the month grid is a real <table>, one <tr> per week. */
228
+ const MONTH_WEEKS = Array.from({ length: MONTH_CELLS.length / 7 }, (_, w) =>
229
+ MONTH_CELLS.slice(w * 7, w * 7 + 7),
230
+ );
221
231
 
222
- // ── Week view (2026-05-11 〜 05-17), time axis 06:00–22:00 ─────────────────────
223
- const WEEK_DATES = [
232
+ // ── Week view (2026-05-11 〜 05-17), time axis 06:00–24:00 ─────────────────────
233
+ // The week axis is a real `TimelineGrid`: columns are days, blocks are placed by start time and
234
+ // duration, and two shifts that overlap are laid out side by side by the primitive.
235
+ const WEEK_COLUMNS: TimelineGridColumnProp[] = [
224
236
  { date: 11, weekday: 1 },
225
237
  { date: 12, weekday: 2 },
226
238
  { date: 13, weekday: 3 },
227
- { date: 14, weekday: 4, today: true },
239
+ { date: 14, weekday: 4, current: true },
228
240
  { date: 15, weekday: 5 },
229
241
  { date: 16, weekday: 6 },
230
242
  { date: 17, weekday: 0 },
231
- ];
232
- const HOURS = [6, 8, 10, 12, 14, 16, 18, 20, 22];
233
-
234
- type WeekBlock = {
235
- col: number; // 0..6 day index
236
- kind: ShiftKind;
237
- staff: string;
238
- startHour: number;
239
- endHour: number; // for night/通し may exceed 24 → clamp display
240
- };
241
- const WEEK_BLOCKS: WeekBlock[] = [
242
- { col: 3, kind: "early", staff: "田中", startHour: 9, endHour: 17.5 },
243
- { col: 3, kind: "late", staff: "高橋", startHour: 13, endHour: 22 },
244
- { col: 3, kind: "night", staff: "伊藤", startHour: 22, endHour: 30 },
245
- { col: 0, kind: "tsushi", staff: "佐藤", startHour: 9, endHour: 22 },
246
- { col: 4, kind: "early", staff: "佐藤", startHour: 9, endHour: 17.5 },
247
- { col: 4, kind: "ot", staff: "鈴木", startHour: 18, endHour: 21 },
248
- { col: 1, kind: "leave", staff: "鈴木", startHour: 6, endHour: 22 },
249
- ];
250
-
251
- const AXIS_START = 6;
252
- const AXIS_END = 22; // display window; night spills are clamped + noted
253
- const NOW_HOUR = 14 + 35 / 60; // 14:35 now-line
243
+ ].map(({ date, weekday, current }) => ({
244
+ id: `05-${date}`,
245
+ label: (
246
+ <Text
247
+ as="span"
248
+ size="2xs"
249
+ weight="medium"
250
+ tabular
251
+ style={{
252
+ color:
253
+ weekday === 0
254
+ ? "var(--destructive)"
255
+ : weekday === 6
256
+ ? "var(--info)"
257
+ : "var(--muted-foreground)",
258
+ }}
259
+ >
260
+ {WEEKDAY_HEAD[weekday]} {date}
261
+ </Text>
262
+ ),
263
+ current,
264
+ }));
254
265
 
255
- function hourToPct(h: number): number {
256
- const clamped = Math.max(AXIS_START, Math.min(AXIS_END, h));
257
- return ((clamped - AXIS_START) / (AXIS_END - AXIS_START)) * 100;
258
- }
266
+ // 夜勤 22:00→06:00: an end at or before the start means the shift runs into the next day, so the
267
+ // grid clips it at the 24:00 edge and still prints the real range on the block.
268
+ const WEEK_SHIFTS: TimelineGridEventProp[] = [
269
+ { id: "w1", columnId: "05-14", start: "09:00", end: "17:30", kind: "early", staff: "田中" },
270
+ { id: "w2", columnId: "05-14", start: "13:00", end: "22:00", kind: "late", staff: "高橋" },
271
+ { id: "w3", columnId: "05-14", start: "22:00", end: "06:00", kind: "night", staff: "伊藤" },
272
+ { id: "w4", columnId: "05-11", start: "09:00", end: "22:00", kind: "tsushi", staff: "佐藤" },
273
+ { id: "w5", columnId: "05-15", start: "09:00", end: "17:30", kind: "early", staff: "佐藤" },
274
+ { id: "w6", columnId: "05-15", start: "18:00", end: "21:00", kind: "ot", staff: "鈴木" },
275
+ { id: "w7", columnId: "05-12", start: "06:00", end: "22:00", kind: "leave", staff: "鈴木" },
276
+ ].map(({ kind, staff, ...event }) => ({
277
+ ...event,
278
+ title: SHIFT_META[kind as ShiftKind].label,
279
+ description: staff,
280
+ color: `var(${SHIFT_META[kind as ShiftKind].cssVar})`,
281
+ }));
259
282
 
260
283
  // ── Day view (2026-05-14) — per-staff lanes via Timeline + understaffed gap ───
261
284
  const DAY_LANES: Array<{ staff: string; role: string; items: TimelineItem[] }> = [
@@ -406,22 +429,14 @@ export default function ShiftCalendarShowcase() {
406
429
  <CardContent>
407
430
  <Flex direction="row" gap="sm" wrap>
408
431
  {(Object.keys(SHIFT_META) as ShiftKind[]).map((k) => (
409
- <span
410
- key={k}
411
- className="inline-flex items-center gap-2 rounded-sm border px-2 py-1 text-xs"
412
- >
413
- <span
414
- aria-hidden="true"
415
- className="size-3 shrink-0 rounded-sm"
416
- style={{ background: `var(${SHIFT_META[k].cssVar})` }}
417
- />
432
+ <Badge key={k} color={`var(${SHIFT_META[k].cssVar})`} shape="sharp">
418
433
  <Text size="xs" weight="medium" className="whitespace-nowrap">
419
434
  {SHIFT_META[k].label}
420
435
  </Text>
421
436
  <Text size="xs" tone="muted" tabular className="whitespace-nowrap">
422
437
  {SHIFT_META[k].time}
423
438
  </Text>
424
- </span>
439
+ </Badge>
425
440
  ))}
426
441
  </Flex>
427
442
  </CardContent>
@@ -454,18 +469,16 @@ export default function ShiftCalendarShowcase() {
454
469
  {detailDate && detailDate.shifts.length > 0 ? (
455
470
  <Flex direction="col" gap="sm">
456
471
  {detailDate.shifts.map((s, i) => (
457
- <Flex
472
+ <ListRow
458
473
  key={`${s.kind}-${i}`}
459
- align="center"
460
- justify="between"
461
- gap="sm"
462
- className="rounded-lg border px-3 py-2"
463
- >
464
- <ShiftPill kind={s.kind} staff={s.staff} />
465
- <Text size="xs" tone="muted" tabular className="whitespace-nowrap">
466
- {SHIFT_META[s.kind].time}
467
- </Text>
468
- </Flex>
474
+ density="compact"
475
+ title={<ShiftPill kind={s.kind} staff={s.staff} />}
476
+ trailing={
477
+ <Text size="xs" tone="muted" tabular className="whitespace-nowrap">
478
+ {SHIFT_META[s.kind].time}
479
+ </Text>
480
+ }
481
+ />
469
482
  ))}
470
483
  </Flex>
471
484
  ) : (
@@ -482,7 +495,7 @@ export default function ShiftCalendarShowcase() {
482
495
  );
483
496
  }
484
497
 
485
- // ── Month grid — composed from primitives (GAP: no event-calendar primitive) ──
498
+ // ── Month grid — a real Table: one <tr> per week, one <td> per day ────────────
486
499
  function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
487
500
  return (
488
501
  <Card>
@@ -495,18 +508,14 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
495
508
  </CardAction>
496
509
  </CardHeader>
497
510
  <CardContent flush>
498
- <div className="overflow-x-auto">
499
- <div className="min-w-[720px]">
500
- {/* Weekday head — Sun→danger, Sat→info */}
501
- <ResponsiveGrid columns={{ sm: 7, md: 7, lg: 7 }} className="bg-secondary border-b">
511
+ {/* Weekday head — Sun→danger, Sat→info; 42 day cells as six <tr> weeks */}
512
+ <Table bordered className="min-w-[720px]">
513
+ <TableHeader className="bg-secondary">
514
+ <TableRow>
502
515
  {WEEKDAY_HEAD.map((w, i) => (
503
- <Text
504
- as="div"
516
+ <TableHead
505
517
  key={w}
506
- size="2xs"
507
- weight="medium"
508
- align="center"
509
- className="px-2 py-1.5"
518
+ className="text-center"
510
519
  style={{
511
520
  color:
512
521
  i === 0
@@ -517,84 +526,96 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
517
526
  }}
518
527
  >
519
528
  {w}
520
- </Text>
529
+ </TableHead>
521
530
  ))}
522
- </ResponsiveGrid>
523
- {/* 42 cells */}
524
- <ResponsiveGrid columns={{ sm: 7, md: 7, lg: 7 }}>
525
- {MONTH_CELLS.map((cell, idx) => {
526
- const isSun = cell.weekday === 0;
527
- const isSat = cell.weekday === 6;
528
- const shown = cell.shifts.slice(0, 3);
529
- const overflow = cell.shifts.length - shown.length;
530
- return (
531
- <button
532
- key={idx}
533
- type="button"
534
- onClick={() => onPick(cell)}
535
- className="hover:bg-accent focus-visible:ring-ring flex min-h-[96px] flex-col gap-1 border-e border-b p-1.5 text-start transition-colors focus:outline-none focus-visible:ring-2 [&:nth-child(7n)]:border-e-0"
536
- style={{
537
- background: cell.today
538
- ? "color-mix(in oklch, var(--primary) 5%, transparent)"
539
- : cell.dim
540
- ? "color-mix(in oklch, var(--secondary) 40%, transparent)"
541
- : undefined,
542
- outline: cell.today ? "2px solid var(--primary)" : undefined,
543
- outlineOffset: cell.today ? "-2px" : undefined,
544
- opacity: cell.dim ? 0.5 : 1,
545
- }}
546
- >
547
- <Flex align="center" justify="between">
548
- <Text
549
- as="span"
550
- size="sm"
551
- weight="medium"
552
- tabular
553
- style={{
554
- color: cell.holiday
555
- ? "var(--destructive)"
556
- : isSun
557
- ? "var(--destructive)"
558
- : isSat
559
- ? "var(--info)"
560
- : "var(--foreground)",
561
- }}
531
+ </TableRow>
532
+ </TableHeader>
533
+ <TableBody>
534
+ {MONTH_WEEKS.map((week, r) => (
535
+ <TableRow key={r}>
536
+ {week.map((cell, c) => {
537
+ const isSun = cell.weekday === 0;
538
+ const isSat = cell.weekday === 6;
539
+ const shown = cell.shifts.slice(0, 3);
540
+ const overflow = cell.shifts.length - shown.length;
541
+ return (
542
+ <TableCell
543
+ key={c}
544
+ className="align-top"
545
+ style={{
546
+ background: cell.today
547
+ ? "color-mix(in oklch, var(--primary) 5%, transparent)"
548
+ : cell.dim
549
+ ? "color-mix(in oklch, var(--secondary) 40%, transparent)"
550
+ : undefined,
551
+ outline: cell.today ? "2px solid var(--primary)" : undefined,
552
+ outlineOffset: cell.today ? "-2px" : undefined,
553
+ opacity: cell.dim ? 0.5 : 1,
554
+ }}
555
+ >
556
+ <button
557
+ type="button"
558
+ onClick={() => onPick(cell)}
559
+ className="hover:bg-accent focus-visible:ring-ring min-h-[96px] w-full text-start transition-colors focus:outline-none focus-visible:ring-2"
562
560
  >
563
- {cell.date}
564
- </Text>
565
- {cell.holiday ? (
566
- <Badge
567
- tone="destructive"
568
- variant="outline"
569
- className="px-1 py-0 text-[var(--font-size-2xs)]"
570
- >
571
-
572
- </Badge>
573
- ) : null}
574
- </Flex>
575
- <Flex direction="col" gap="xs">
576
- {cell.holiday ? <ShiftPill kind="holiday" staff={cell.holiday} /> : null}
577
- {shown.map((s, i) => (
578
- <ShiftPill key={`${s.kind}-${i}`} kind={s.kind} staff={s.staff} />
579
- ))}
580
- {overflow > 0 ? (
581
- <Text as="span" size="2xs" tone="muted" tabular>
582
- {overflow}
583
- </Text>
584
- ) : null}
585
- </Flex>
586
- </button>
587
- );
588
- })}
589
- </ResponsiveGrid>
590
- </div>
591
- </div>
561
+ <Flex direction="col" gap="xs">
562
+ <Flex align="center" justify="between">
563
+ <Text
564
+ as="span"
565
+ size="sm"
566
+ weight="medium"
567
+ tabular
568
+ style={{
569
+ color: cell.holiday
570
+ ? "var(--destructive)"
571
+ : isSun
572
+ ? "var(--destructive)"
573
+ : isSat
574
+ ? "var(--info)"
575
+ : "var(--foreground)",
576
+ }}
577
+ >
578
+ {cell.date}
579
+ </Text>
580
+ {cell.holiday ? (
581
+ <Badge
582
+ tone="destructive"
583
+ variant="outline"
584
+ shape="sharp"
585
+ className="text-[var(--font-size-2xs)]"
586
+ >
587
+
588
+ </Badge>
589
+ ) : null}
590
+ </Flex>
591
+ <Flex direction="col" gap="xs">
592
+ {cell.holiday ? (
593
+ <ShiftPill kind="holiday" staff={cell.holiday} />
594
+ ) : null}
595
+ {shown.map((sh, i) => (
596
+ <ShiftPill key={`${sh.kind}-${i}`} kind={sh.kind} staff={sh.staff} />
597
+ ))}
598
+ {overflow > 0 ? (
599
+ <Text as="span" size="2xs" tone="muted" tabular>
600
+ +{overflow} 件
601
+ </Text>
602
+ ) : null}
603
+ </Flex>
604
+ </Flex>
605
+ </button>
606
+ </TableCell>
607
+ );
608
+ })}
609
+ </TableRow>
610
+ ))}
611
+ </TableBody>
612
+ </Table>
592
613
  </CardContent>
593
614
  </Card>
594
615
  );
595
616
  }
596
617
 
597
- // ── Week time-axis with a now-line (GAP: no time-grid primitive) ──────────────
618
+ // ── Week time-axis the TimelineGrid primitive (#354 item 7 closed) ──────────
598
619
  function WeekTimeline() {
599
620
  return (
600
621
  <Card>
@@ -602,140 +623,20 @@ function WeekTimeline() {
602
623
  <CardTitle level={2}>週</CardTitle>
603
624
  <CardAction>
604
625
  <Text size="xs" tone="muted" tabular className="whitespace-nowrap">
605
- 5月11日〜17日 · 06:00–22:00
626
+ 5月11日〜17日 · 06:00–24:00
606
627
  </Text>
607
628
  </CardAction>
608
629
  </CardHeader>
609
630
  <CardContent flush>
610
- <div className="overflow-x-auto">
611
- <div className="min-w-[760px]">
612
- {/* Day header row */}
613
- <ResponsiveGrid
614
- columns={{ sm: 1, md: 1, lg: 1 }}
615
- className="bg-secondary grid-cols-[48px_repeat(7,1fr)] border-b"
616
- >
617
- <div aria-hidden="true" />
618
- {WEEK_DATES.map((d) => (
619
- <Text
620
- as="div"
621
- key={d.date}
622
- size="2xs"
623
- weight="medium"
624
- tabular
625
- align="center"
626
- className="px-2 py-1.5"
627
- style={{
628
- color:
629
- d.weekday === 0
630
- ? "var(--destructive)"
631
- : d.weekday === 6
632
- ? "var(--info)"
633
- : "var(--muted-foreground)",
634
- background:
635
- "today" in d && d.today
636
- ? "color-mix(in oklch, var(--primary) 6%, transparent)"
637
- : undefined,
638
- }}
639
- >
640
- {WEEKDAY_HEAD[d.weekday]} {d.date}
641
- </Text>
642
- ))}
643
- </ResponsiveGrid>
644
- {/* Time grid body */}
645
- <ResponsiveGrid
646
- columns={{ sm: 1, md: 1, lg: 1 }}
647
- className="relative grid-cols-[48px_repeat(7,1fr)]"
648
- >
649
- {/* Hour axis labels */}
650
- <div className="relative" style={{ height: `${(AXIS_END - AXIS_START) * 22}px` }}>
651
- {HOURS.map((h) => (
652
- <Text
653
- as="div"
654
- key={h}
655
- size="2xs"
656
- tone="muted"
657
- tabular
658
- className="absolute end-1.5 -translate-y-1/2"
659
- style={{ top: `${hourToPct(h)}%` }}
660
- >
661
- {String(h).padStart(2, "0")}:00
662
- </Text>
663
- ))}
664
- </div>
665
- {/* 7 day columns */}
666
- {WEEK_DATES.map((d, col) => (
667
- <div
668
- key={d.date}
669
- className="relative border-s"
670
- style={{ height: `${(AXIS_END - AXIS_START) * 22}px` }}
671
- >
672
- {/* hour gridlines */}
673
- {HOURS.map((h) => (
674
- <div
675
- key={h}
676
- aria-hidden="true"
677
- className="border-border/60 absolute inset-x-0 border-t"
678
- style={{ top: `${hourToPct(h)}%` }}
679
- />
680
- ))}
681
- {/* shift blocks for this column */}
682
- {WEEK_BLOCKS.filter((b) => b.col === col).map((b, i) => {
683
- const top = hourToPct(b.startHour);
684
- const bottom = hourToPct(b.endHour);
685
- const meta = SHIFT_META[b.kind];
686
- const spills = b.endHour > AXIS_END;
687
- return (
688
- <div
689
- key={`${b.kind}-${i}`}
690
- className="absolute inset-x-0.5 overflow-hidden rounded-sm border-s-2 px-1 py-0.5 leading-tight"
691
- style={{
692
- top: `${top}%`,
693
- height: `${Math.max(bottom - top, 4)}%`,
694
- borderLeftColor: `var(${meta.cssVar})`,
695
- background: `color-mix(in oklch, var(${meta.cssVar}) 14%, transparent)`,
696
- color: meta.muted ? "var(--muted-foreground)" : "var(--foreground)",
697
- }}
698
- title={`${meta.label} ${meta.time} · ${b.staff}`}
699
- >
700
- <Text
701
- as="div"
702
- size="2xs"
703
- weight="medium"
704
- truncate
705
- style={{ color: "inherit" }}
706
- >
707
- {meta.label}
708
- </Text>
709
- <Text as="div" size="2xs" tone="muted" truncate>
710
- {b.staff}
711
- </Text>
712
- {spills ? (
713
- <Text as="div" size="2xs" tone="muted" tabular>
714
- 翌日へ ↓
715
- </Text>
716
- ) : null}
717
- </div>
718
- );
719
- })}
720
- {/* now-line — only on "today" (col 3), 14:35 */}
721
- {"today" in d && d.today ? (
722
- <div
723
- aria-label="現在時刻 14:35"
724
- className="pointer-events-none absolute inset-x-0 z-10"
725
- style={{ top: `${hourToPct(NOW_HOUR)}%` }}
726
- >
727
- <div className="h-px" style={{ background: "var(--destructive)" }} />
728
- <div
729
- className="absolute -start-1 -top-1 size-2 rounded-full"
730
- style={{ background: "var(--destructive)" }}
731
- />
732
- </div>
733
- ) : null}
734
- </div>
735
- ))}
736
- </ResponsiveGrid>
737
- </div>
738
- </div>
631
+ <TimelineGrid
632
+ label="週シフト 2026年5月11日〜17日"
633
+ columns={WEEK_COLUMNS}
634
+ events={WEEK_SHIFTS}
635
+ start="06:00"
636
+ end="24:00"
637
+ interval={2}
638
+ now="14:35"
639
+ />
739
640
  </CardContent>
740
641
  </Card>
741
642
  );