astryx-dracula 0.5.0 → 0.6.2

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 (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,557 +1,550 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > (LH[divider] > V[g=4] > (H[j=between a=center] > Hd"Night-shift issues"[level=1] + B.primary"Raise issue"[opens=#raise-issue]) + (H[g=2 a=center] > PS + Po > B.secondary"View options")) + (LC[p=0] > (Tx"Scroll the table sideways to see every column"[t=supporting] + D) + T[hover] > (TR > THC*7) + (TR > TC*7)*4) + (LP > V[g=4] > (H > Tx[t=supporting] + IB"Close panel") + (V[g=1] > Hd[level=2] + Tx[t=body]) + ML + (D + (V[g=2] > Tx"Labels"[t=label] + (H[g=2] > Tk*2)))) ;; Dlg#raise-issue > (DH"Raise issue" + (LC[p=4] > V[g=4] > TI*4) + (LF > H[j=end g=2] > B.secondary"Cancel" + B.primary"Raise issue"))
2
+ // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
+ // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
+ // human drift check — update it with the file, never alone):
5
+ // L[h=fill] > (LH[divider p=6] > V[g=4] > (H[g=3 a=center wrap] > (SI[fill] > Hd"Night-shift issues"[level=1 t=display-2]) + B.primary"Raise issue"[opens=#raise-issue]) + (H[g=2 a=center] > (SI[fill] > PS"Filter issues…") + (Po > (B.secondary"View options" + (V[g=4] > RL"Group by" > RLI*5))))) + (LC[p=0] > T[hover density=balanced dv=rows] > (TR > THC*7) + ((TR > TC[colSpan=7] > (H[g=2 a=center] > Ic + Tx"In Progress"[t=body] + Bd.neutral"3")) + (TR > TC*7))*4)) + (LP[divider p=4] > V[g=4] > (H[g=2 a=center] > (SI[fill] > Tx[t=supporting]) + B.ghost"Close panel") + (V[g=1] > Hd[level=2] + Tx[t=body]) + (ML > MLI*6) + (D + (V[g=2] > Tx"Labels"[t=label] + (H[g=2] > Tk*2)))) ;; Dlg#raise-issue > L[h=auto] > (DH"Raise issue" + (LC[p=4] > V[g=4] > TI"Title" + SE{Selector}"Status" + SE{Selector}"Priority" + TI"Project") + (LF[divider] > H[g=2 j=end] > B.secondary"Cancel" + B.primary"Raise issue"))
4
6
 
5
7
  /**
6
8
  * Table Grouped — the night-shift issue tracker: a grouped, collapsible issue table with a PowerSearch bar and a resizable detail inspector. (Frame/responsive/container: see XLE header above.)
7
9
  */
8
10
 
9
- import React, {useState, useMemo} from 'react';
10
- import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
11
- import type {ResizableProps} from '@astryxdesign/core/Resizable';
11
+ import { Avatar } from "@astryxdesign/core/Avatar";
12
+ import { Badge } from "@astryxdesign/core/Badge";
13
+ import { Button } from "@astryxdesign/core/Button";
14
+ import { Dialog, DialogHeader } from "@astryxdesign/core/Dialog";
15
+ import { Divider } from "@astryxdesign/core/Divider";
16
+ import { DropdownMenu } from "@astryxdesign/core/DropdownMenu";
17
+ import { useMediaQuery } from "@astryxdesign/core/hooks";
18
+ import { Icon } from "@astryxdesign/core/Icon";
12
19
  import {
13
- Layout,
14
- LayoutContent,
15
- LayoutFooter,
16
- LayoutHeader,
17
- LayoutPanel,
18
- VStack,
19
- HStack,
20
- StackItem,
21
- } from '@astryxdesign/core/Layout';
22
- import {Text, Heading} from '@astryxdesign/core/Text';
23
- import {TextInput} from '@astryxdesign/core/TextInput';
24
- import {Button} from '@astryxdesign/core/Button';
25
- import {Badge} from '@astryxdesign/core/Badge';
26
- import {Avatar} from '@astryxdesign/core/Avatar';
27
- import {Selector} from '@astryxdesign/core/Selector';
28
- import {PowerSearch, usePowerSearchConfig} from '@astryxdesign/core/PowerSearch';
29
- import type {PowerSearchFilter} from '@astryxdesign/core/PowerSearch';
30
- import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
31
- import {Popover} from '@astryxdesign/core/Popover';
32
- import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
33
- import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
34
- import {Icon} from '@astryxdesign/core/Icon';
35
- import {StatusDot} from '@astryxdesign/core/StatusDot';
36
- import {Divider} from '@astryxdesign/core/Divider';
37
- import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
38
- import {Token} from '@astryxdesign/core/Token';
39
- import {useMediaQuery} from '@astryxdesign/core/hooks';
20
+ HStack,
21
+ Layout,
22
+ LayoutContent,
23
+ LayoutFooter,
24
+ LayoutHeader,
25
+ LayoutPanel,
26
+ StackItem,
27
+ VStack,
28
+ } from "@astryxdesign/core/Layout";
29
+ import { MetadataList, MetadataListItem } from "@astryxdesign/core/MetadataList";
30
+ import { Popover } from "@astryxdesign/core/Popover";
31
+ import type { PowerSearchFilter } from "@astryxdesign/core/PowerSearch";
32
+ import { PowerSearch, usePowerSearchConfig } from "@astryxdesign/core/PowerSearch";
33
+ import { RadioList, RadioListItem } from "@astryxdesign/core/RadioList";
34
+ import type { ResizableProps } from "@astryxdesign/core/Resizable";
35
+ import { ResizeHandle, useResizable } from "@astryxdesign/core/Resizable";
36
+ import { Selector } from "@astryxdesign/core/Selector";
37
+ import { StatusDot } from "@astryxdesign/core/StatusDot";
38
+ import type { TableColumn } from "@astryxdesign/core/Table";
40
39
  import {
41
- Table,
42
- TableRow,
43
- TableCell,
44
- TableHeaderCell,
45
- proportional,
46
- pixel,
47
- resolveColumnWidths,
48
- } from '@astryxdesign/core/Table';
49
- import type {TableColumn} from '@astryxdesign/core/Table';
40
+ pixel,
41
+ proportional,
42
+ resolveColumnWidths,
43
+ Table,
44
+ TableCell,
45
+ TableHeaderCell,
46
+ TableRow,
47
+ } from "@astryxdesign/core/Table";
48
+ import { Heading, Text } from "@astryxdesign/core/Text";
49
+ import { TextInput } from "@astryxdesign/core/TextInput";
50
+ import { Token } from "@astryxdesign/core/Token";
50
51
  import {
51
- ChevronRight,
52
- ChevronDown,
53
- Pencil,
54
- Copy,
55
- ArrowRight,
56
- Tag,
57
- User,
58
- Trash,
59
- Ellipsis,
60
- X,
61
- ChartBar,
62
- } from 'lucide-react';
52
+ ArrowRight,
53
+ ChartBar,
54
+ ChevronDown,
55
+ ChevronRight,
56
+ Copy,
57
+ Ellipsis,
58
+ Pencil,
59
+ Tag,
60
+ Trash,
61
+ User,
62
+ X,
63
+ } from "lucide-react";
64
+ import React, { useMemo, useState } from "react";
63
65
 
64
66
  // Plain inline styles using Astryx design-token CSS variables (declared at
65
67
  // :root by `@astryxdesign/core/astryx.css`). No StyleX compiler required.
66
68
  // The group-header background + cursor live on the colSpan TableCell (which
67
69
  // reliably forwards `style`) so they fill the full row width.
68
70
  const groupHeaderCell: React.CSSProperties = {
69
- cursor: 'pointer',
70
- backgroundColor: 'var(--color-background-muted)',
71
- padding: 'var(--spacing-3) var(--spacing-4)',
71
+ cursor: "pointer",
72
+ backgroundColor: "var(--color-background-muted)",
73
+ padding: "var(--spacing-3) var(--spacing-4)",
72
74
  };
73
75
 
74
76
  // Types
75
- type TaskStatus = 'in_progress' | 'todo' | 'backlog' | 'done';
76
- type TaskPriority = 'urgent' | 'high' | 'medium' | 'low' | 'none';
77
+ type TaskStatus = "in_progress" | "todo" | "backlog" | "done";
78
+ type TaskPriority = "urgent" | "high" | "medium" | "low" | "none";
77
79
 
78
80
  interface TaskRow extends Record<string, unknown> {
79
- id: string;
80
- taskId: string;
81
- title: string;
82
- subtitle: string;
83
- status: TaskStatus;
84
- priority: TaskPriority;
85
- project: string;
86
- tags: string[];
87
- created: string;
88
- updated: string;
89
- assignee: string;
81
+ id: string;
82
+ taskId: string;
83
+ title: string;
84
+ subtitle: string;
85
+ status: TaskStatus;
86
+ priority: TaskPriority;
87
+ project: string;
88
+ tags: string[];
89
+ created: string;
90
+ updated: string;
91
+ assignee: string;
90
92
  }
91
93
 
92
- const STATUS_DOT_VARIANT: Record<
93
- TaskStatus,
94
- 'success' | 'neutral' | 'warning' | 'info'
95
- > = {
96
- in_progress: 'info',
97
- todo: 'warning',
98
- backlog: 'neutral',
99
- done: 'success',
94
+ const STATUS_DOT_VARIANT: Record<TaskStatus, "success" | "neutral" | "warning" | "info"> = {
95
+ in_progress: "info",
96
+ todo: "warning",
97
+ backlog: "neutral",
98
+ done: "success",
100
99
  };
101
100
 
102
- const PRIORITY_COLOR: Record<
103
- TaskPriority,
104
- 'error' | 'warning' | 'secondary' | 'disabled'
105
- > = {
106
- urgent: 'error',
107
- high: 'warning',
108
- medium: 'secondary',
109
- low: 'disabled',
110
- none: 'disabled',
101
+ const PRIORITY_COLOR: Record<TaskPriority, "error" | "warning" | "secondary" | "disabled"> = {
102
+ urgent: "error",
103
+ high: "warning",
104
+ medium: "secondary",
105
+ low: "disabled",
106
+ none: "disabled",
111
107
  };
112
108
 
113
109
  // Mock data matching a task tracker
114
110
  const allTasks: TaskRow[] = [
115
- {
116
- id: '1',
117
- taskId: 'T235040469',
118
- title: 'Update user interface',
119
- subtitle: 'Update castle gate integration',
120
- status: 'in_progress',
121
- priority: 'medium',
122
- project: 'Castle gate integration 2.0',
123
- tags: [],
124
- created: 'Jul 3',
125
- updated: 'Jul 3',
126
- assignee: 'Olivia Martin',
127
- },
128
- {
129
- id: '2',
130
- taskId: 'T235040470',
131
- title: 'Chart the crypt to organize haunts for rituals or releases',
132
- subtitle: '',
133
- status: 'in_progress',
134
- priority: 'medium',
135
- project: '',
136
- tags: [],
137
- created: 'Jul 1',
138
- updated: 'Jul 3',
139
- assignee: 'Jackson Lee',
140
- },
141
- {
142
- id: '3',
143
- taskId: 'T235040471',
144
- title: 'Follow moon cycles to focus haunts over n-weeks',
145
- subtitle: '',
146
- status: 'in_progress',
147
- priority: 'medium',
148
- project: '',
149
- tags: [],
150
- created: 'Jul 1',
151
- updated: 'Jul 3',
152
- assignee: 'Isabella Nguyen',
153
- },
154
- {
155
- id: '4',
156
- taskId: 'T235040472',
157
- title: 'Test the gate wards before moonrise',
158
- subtitle: 'Update castle gate integration',
159
- status: 'todo',
160
- priority: 'medium',
161
- project: 'Castle gate integration 2.0',
162
- tags: [],
163
- created: 'Jul 3',
164
- updated: 'Jul 3',
165
- assignee: 'William Kim',
166
- },
167
- {
168
- id: '5',
169
- taskId: 'T235040473',
170
- title: 'Update backend code',
171
- subtitle: 'Update castle gate integration',
172
- status: 'todo',
173
- priority: 'medium',
174
- project: 'Castle gate integration 2.0',
175
- tags: [],
176
- created: 'Jul 3',
177
- updated: 'Jul 3',
178
- assignee: 'Sofia Davis',
179
- },
180
- {
181
- id: '6',
182
- taskId: 'T235040474',
183
- title: 'Update front end code',
184
- subtitle: 'Update castle gate integration',
185
- status: 'todo',
186
- priority: 'medium',
187
- project: 'Castle gate integration 2.0',
188
- tags: [],
189
- created: 'Jul 3',
190
- updated: 'Jul 3',
191
- assignee: 'Mia Wilson',
192
- },
193
- {
194
- id: '7',
195
- taskId: 'T235040475',
196
- title: 'Update castle gate integration',
197
- subtitle: '',
198
- status: 'todo',
199
- priority: 'high',
200
- project: 'Castle gate integration 2.0',
201
- tags: ['Improvement', '3rd Party'],
202
- created: 'Jul 3',
203
- updated: 'Jul 3',
204
- assignee: 'Lucas Brown',
205
- },
206
- {
207
- id: '8',
208
- taskId: 'T235040476',
209
- title: 'Update payment gateway backend code',
210
- subtitle: '',
211
- status: 'todo',
212
- priority: 'medium',
213
- project: '',
214
- tags: [],
215
- created: 'Jul 3',
216
- updated: 'Jul 3',
217
- assignee: 'Ethan Jones',
218
- },
219
- {
220
- id: '9',
221
- taskId: 'T235040477',
222
- title: 'Invite your fellow familiars to the crypt',
223
- subtitle: '',
224
- status: 'todo',
225
- priority: 'low',
226
- project: '',
227
- tags: [],
228
- created: 'Jul 1',
229
- updated: 'Jul 1',
230
- assignee: 'Ava Taylor',
231
- },
232
- {
233
- id: '10',
234
- taskId: 'T235040478',
235
- title: 'Next rites after moonrise',
236
- subtitle: '',
237
- status: 'todo',
238
- priority: 'none',
239
- project: '',
240
- tags: [],
241
- created: 'Jul 1',
242
- updated: 'Jul 3',
243
- assignee: 'Noah Garcia',
244
- },
245
- {
246
- id: '11',
247
- taskId: 'T235040479',
248
- title: 'Welcome to the crypt',
249
- subtitle: '',
250
- status: 'backlog',
251
- priority: 'none',
252
- project: '',
253
- tags: [],
254
- created: 'Jul 1',
255
- updated: 'Jul 3',
256
- assignee: 'Olivia Martin',
257
- },
258
- {
259
- id: '12',
260
- taskId: 'T235040480',
261
- title: 'Connect GitHub or GitLab',
262
- subtitle: '',
263
- status: 'backlog',
264
- priority: 'none',
265
- project: '',
266
- tags: [],
267
- created: 'Jul 1',
268
- updated: 'Jul 3',
269
- assignee: 'Jackson Lee',
270
- },
271
- {
272
- id: '13',
273
- taskId: 'T235040481',
274
- title: 'Customize settings',
275
- subtitle: '',
276
- status: 'backlog',
277
- priority: 'none',
278
- project: '',
279
- tags: [],
280
- created: 'Jul 1',
281
- updated: 'Jul 3',
282
- assignee: 'Isabella Nguyen',
283
- },
284
- {
285
- id: '14',
286
- taskId: 'T235040482',
287
- title: 'Try 3 ways to navigate: Command menu, keyboard or mouse',
288
- subtitle: '',
289
- status: 'done',
290
- priority: 'none',
291
- project: '',
292
- tags: [],
293
- created: 'Jul 1',
294
- updated: 'Jul 3',
295
- assignee: 'William Kim',
296
- },
297
- {
298
- id: '15',
299
- taskId: 'T235040483',
300
- title: 'Connect to Slack',
301
- subtitle: '',
302
- status: 'done',
303
- priority: 'none',
304
- project: '',
305
- tags: [],
306
- created: 'Jul 1',
307
- updated: 'Jul 3',
308
- assignee: 'Sofia Davis',
309
- },
310
- {
311
- id: '16',
312
- taskId: 'T235040484',
313
- title: 'Migrate database schema to v2',
314
- subtitle: 'Castle gate integration',
315
- status: 'in_progress',
316
- priority: 'high',
317
- project: 'Castle gate integration 2.0',
318
- tags: [],
319
- created: 'Jul 4',
320
- updated: 'Jul 5',
321
- assignee: 'Lucas Brown',
322
- },
323
- {
324
- id: '17',
325
- taskId: 'T235040485',
326
- title: 'Write integration tests for checkout flow',
327
- subtitle: '',
328
- status: 'in_progress',
329
- priority: 'medium',
330
- project: 'Castle gate integration 2.0',
331
- tags: [],
332
- created: 'Jul 4',
333
- updated: 'Jul 5',
334
- assignee: 'Ethan Jones',
335
- },
336
- {
337
- id: '18',
338
- taskId: 'T235040486',
339
- title: 'Set up CI/CD pipeline for staging',
340
- subtitle: '',
341
- status: 'in_progress',
342
- priority: 'high',
343
- project: '',
344
- tags: [],
345
- created: 'Jul 2',
346
- updated: 'Jul 5',
347
- assignee: 'Ava Taylor',
348
- },
349
- {
350
- id: '19',
351
- taskId: 'T235040487',
352
- title: 'Add rate limiting to public API endpoints',
353
- subtitle: '',
354
- status: 'todo',
355
- priority: 'urgent',
356
- project: '',
357
- tags: [],
358
- created: 'Jul 5',
359
- updated: 'Jul 5',
360
- assignee: 'Noah Garcia',
361
- },
362
- {
363
- id: '20',
364
- taskId: 'T235040488',
365
- title: 'Refactor auth middleware to support OAuth2',
366
- subtitle: '',
367
- status: 'todo',
368
- priority: 'high',
369
- project: '',
370
- tags: [],
371
- created: 'Jul 4',
372
- updated: 'Jul 5',
373
- assignee: 'Olivia Martin',
374
- },
375
- {
376
- id: '21',
377
- taskId: 'T235040489',
378
- title: 'Design error pages for 404 and 500',
379
- subtitle: '',
380
- status: 'todo',
381
- priority: 'low',
382
- project: '',
383
- tags: [],
384
- created: 'Jul 3',
385
- updated: 'Jul 4',
386
- assignee: 'Mia Wilson',
387
- },
388
- {
389
- id: '22',
390
- taskId: 'T235040490',
391
- title: 'Audit third-party dependencies for vulnerabilities',
392
- subtitle: '',
393
- status: 'todo',
394
- priority: 'medium',
395
- project: '',
396
- tags: [],
397
- created: 'Jul 5',
398
- updated: 'Jul 5',
399
- assignee: 'Jackson Lee',
400
- },
401
- {
402
- id: '23',
403
- taskId: 'T235040491',
404
- title: 'Implement webhook retry logic with exponential backoff',
405
- subtitle: 'Castle gate integration',
406
- status: 'todo',
407
- priority: 'medium',
408
- project: 'Castle gate integration 2.0',
409
- tags: [],
410
- created: 'Jul 4',
411
- updated: 'Jul 5',
412
- assignee: 'William Kim',
413
- },
414
- {
415
- id: '24',
416
- taskId: 'T235040492',
417
- title: 'Audit dashboard contrast against the Dracula ramp',
418
- subtitle: '',
419
- status: 'backlog',
420
- priority: 'low',
421
- project: '',
422
- tags: [],
423
- created: 'Jul 2',
424
- updated: 'Jul 3',
425
- assignee: 'Sofia Davis',
426
- },
427
- {
428
- id: '25',
429
- taskId: 'T235040493',
430
- title: 'Create onboarding flow for new team members',
431
- subtitle: '',
432
- status: 'backlog',
433
- priority: 'none',
434
- project: '',
435
- tags: [],
436
- created: 'Jul 1',
437
- updated: 'Jul 2',
438
- assignee: 'Isabella Nguyen',
439
- },
440
- {
441
- id: '26',
442
- taskId: 'T235040494',
443
- title: 'Set up error tracking with Sentry',
444
- subtitle: '',
445
- status: 'backlog',
446
- priority: 'medium',
447
- project: '',
448
- tags: [],
449
- created: 'Jul 3',
450
- updated: 'Jul 4',
451
- assignee: 'Ethan Jones',
452
- },
453
- {
454
- id: '27',
455
- taskId: 'T235040495',
456
- title: 'Improve search performance with indexing',
457
- subtitle: '',
458
- status: 'backlog',
459
- priority: 'low',
460
- project: '',
461
- tags: [],
462
- created: 'Jul 2',
463
- updated: 'Jul 3',
464
- assignee: 'Ava Taylor',
465
- },
466
- {
467
- id: '28',
468
- taskId: 'T235040496',
469
- title: 'Write API documentation for v2 endpoints',
470
- subtitle: '',
471
- status: 'done',
472
- priority: 'medium',
473
- project: '',
474
- tags: [],
475
- created: 'Jun 28',
476
- updated: 'Jul 3',
477
- assignee: 'Noah Garcia',
478
- },
479
- {
480
- id: '29',
481
- taskId: 'T235040497',
482
- title: 'Set up staging environment',
483
- subtitle: '',
484
- status: 'done',
485
- priority: 'high',
486
- project: '',
487
- tags: [],
488
- created: 'Jun 25',
489
- updated: 'Jul 1',
490
- assignee: 'Lucas Brown',
491
- },
492
- {
493
- id: '30',
494
- taskId: 'T235040498',
495
- title: 'Fix flaky end-to-end tests in CI',
496
- subtitle: '',
497
- status: 'done',
498
- priority: 'medium',
499
- project: '',
500
- tags: [],
501
- created: 'Jun 30',
502
- updated: 'Jul 2',
503
- assignee: 'Mia Wilson',
504
- },
111
+ {
112
+ id: "1",
113
+ taskId: "T235040469",
114
+ title: "Update user interface",
115
+ subtitle: "Update castle gate integration",
116
+ status: "in_progress",
117
+ priority: "medium",
118
+ project: "Castle gate integration 2.0",
119
+ tags: [],
120
+ created: "Jul 3",
121
+ updated: "Jul 3",
122
+ assignee: "Olivia Martin",
123
+ },
124
+ {
125
+ id: "2",
126
+ taskId: "T235040470",
127
+ title: "Chart the crypt to organize haunts for rituals or releases",
128
+ subtitle: "",
129
+ status: "in_progress",
130
+ priority: "medium",
131
+ project: "",
132
+ tags: [],
133
+ created: "Jul 1",
134
+ updated: "Jul 3",
135
+ assignee: "Jackson Lee",
136
+ },
137
+ {
138
+ id: "3",
139
+ taskId: "T235040471",
140
+ title: "Follow moon cycles to focus haunts over n-weeks",
141
+ subtitle: "",
142
+ status: "in_progress",
143
+ priority: "medium",
144
+ project: "",
145
+ tags: [],
146
+ created: "Jul 1",
147
+ updated: "Jul 3",
148
+ assignee: "Isabella Nguyen",
149
+ },
150
+ {
151
+ id: "4",
152
+ taskId: "T235040472",
153
+ title: "Test the gate wards before moonrise",
154
+ subtitle: "Update castle gate integration",
155
+ status: "todo",
156
+ priority: "medium",
157
+ project: "Castle gate integration 2.0",
158
+ tags: [],
159
+ created: "Jul 3",
160
+ updated: "Jul 3",
161
+ assignee: "William Kim",
162
+ },
163
+ {
164
+ id: "5",
165
+ taskId: "T235040473",
166
+ title: "Update backend code",
167
+ subtitle: "Update castle gate integration",
168
+ status: "todo",
169
+ priority: "medium",
170
+ project: "Castle gate integration 2.0",
171
+ tags: [],
172
+ created: "Jul 3",
173
+ updated: "Jul 3",
174
+ assignee: "Sofia Davis",
175
+ },
176
+ {
177
+ id: "6",
178
+ taskId: "T235040474",
179
+ title: "Update front end code",
180
+ subtitle: "Update castle gate integration",
181
+ status: "todo",
182
+ priority: "medium",
183
+ project: "Castle gate integration 2.0",
184
+ tags: [],
185
+ created: "Jul 3",
186
+ updated: "Jul 3",
187
+ assignee: "Mia Wilson",
188
+ },
189
+ {
190
+ id: "7",
191
+ taskId: "T235040475",
192
+ title: "Update castle gate integration",
193
+ subtitle: "",
194
+ status: "todo",
195
+ priority: "high",
196
+ project: "Castle gate integration 2.0",
197
+ tags: ["Improvement", "3rd Party"],
198
+ created: "Jul 3",
199
+ updated: "Jul 3",
200
+ assignee: "Lucas Brown",
201
+ },
202
+ {
203
+ id: "8",
204
+ taskId: "T235040476",
205
+ title: "Update payment gateway backend code",
206
+ subtitle: "",
207
+ status: "todo",
208
+ priority: "medium",
209
+ project: "",
210
+ tags: [],
211
+ created: "Jul 3",
212
+ updated: "Jul 3",
213
+ assignee: "Ethan Jones",
214
+ },
215
+ {
216
+ id: "9",
217
+ taskId: "T235040477",
218
+ title: "Invite your fellow familiars to the crypt",
219
+ subtitle: "",
220
+ status: "todo",
221
+ priority: "low",
222
+ project: "",
223
+ tags: [],
224
+ created: "Jul 1",
225
+ updated: "Jul 1",
226
+ assignee: "Ava Taylor",
227
+ },
228
+ {
229
+ id: "10",
230
+ taskId: "T235040478",
231
+ title: "Next rites after moonrise",
232
+ subtitle: "",
233
+ status: "todo",
234
+ priority: "none",
235
+ project: "",
236
+ tags: [],
237
+ created: "Jul 1",
238
+ updated: "Jul 3",
239
+ assignee: "Noah Garcia",
240
+ },
241
+ {
242
+ id: "11",
243
+ taskId: "T235040479",
244
+ title: "Welcome to the crypt",
245
+ subtitle: "",
246
+ status: "backlog",
247
+ priority: "none",
248
+ project: "",
249
+ tags: [],
250
+ created: "Jul 1",
251
+ updated: "Jul 3",
252
+ assignee: "Olivia Martin",
253
+ },
254
+ {
255
+ id: "12",
256
+ taskId: "T235040480",
257
+ title: "Connect GitHub or GitLab",
258
+ subtitle: "",
259
+ status: "backlog",
260
+ priority: "none",
261
+ project: "",
262
+ tags: [],
263
+ created: "Jul 1",
264
+ updated: "Jul 3",
265
+ assignee: "Jackson Lee",
266
+ },
267
+ {
268
+ id: "13",
269
+ taskId: "T235040481",
270
+ title: "Customize settings",
271
+ subtitle: "",
272
+ status: "backlog",
273
+ priority: "none",
274
+ project: "",
275
+ tags: [],
276
+ created: "Jul 1",
277
+ updated: "Jul 3",
278
+ assignee: "Isabella Nguyen",
279
+ },
280
+ {
281
+ id: "14",
282
+ taskId: "T235040482",
283
+ title: "Try 3 ways to navigate: Command menu, keyboard or mouse",
284
+ subtitle: "",
285
+ status: "done",
286
+ priority: "none",
287
+ project: "",
288
+ tags: [],
289
+ created: "Jul 1",
290
+ updated: "Jul 3",
291
+ assignee: "William Kim",
292
+ },
293
+ {
294
+ id: "15",
295
+ taskId: "T235040483",
296
+ title: "Connect to Slack",
297
+ subtitle: "",
298
+ status: "done",
299
+ priority: "none",
300
+ project: "",
301
+ tags: [],
302
+ created: "Jul 1",
303
+ updated: "Jul 3",
304
+ assignee: "Sofia Davis",
305
+ },
306
+ {
307
+ id: "16",
308
+ taskId: "T235040484",
309
+ title: "Migrate database schema to v2",
310
+ subtitle: "Castle gate integration",
311
+ status: "in_progress",
312
+ priority: "high",
313
+ project: "Castle gate integration 2.0",
314
+ tags: [],
315
+ created: "Jul 4",
316
+ updated: "Jul 5",
317
+ assignee: "Lucas Brown",
318
+ },
319
+ {
320
+ id: "17",
321
+ taskId: "T235040485",
322
+ title: "Write integration tests for checkout flow",
323
+ subtitle: "",
324
+ status: "in_progress",
325
+ priority: "medium",
326
+ project: "Castle gate integration 2.0",
327
+ tags: [],
328
+ created: "Jul 4",
329
+ updated: "Jul 5",
330
+ assignee: "Ethan Jones",
331
+ },
332
+ {
333
+ id: "18",
334
+ taskId: "T235040486",
335
+ title: "Set up CI/CD pipeline for staging",
336
+ subtitle: "",
337
+ status: "in_progress",
338
+ priority: "high",
339
+ project: "",
340
+ tags: [],
341
+ created: "Jul 2",
342
+ updated: "Jul 5",
343
+ assignee: "Ava Taylor",
344
+ },
345
+ {
346
+ id: "19",
347
+ taskId: "T235040487",
348
+ title: "Add rate limiting to public API endpoints",
349
+ subtitle: "",
350
+ status: "todo",
351
+ priority: "urgent",
352
+ project: "",
353
+ tags: [],
354
+ created: "Jul 5",
355
+ updated: "Jul 5",
356
+ assignee: "Noah Garcia",
357
+ },
358
+ {
359
+ id: "20",
360
+ taskId: "T235040488",
361
+ title: "Refactor auth middleware to support OAuth2",
362
+ subtitle: "",
363
+ status: "todo",
364
+ priority: "high",
365
+ project: "",
366
+ tags: [],
367
+ created: "Jul 4",
368
+ updated: "Jul 5",
369
+ assignee: "Olivia Martin",
370
+ },
371
+ {
372
+ id: "21",
373
+ taskId: "T235040489",
374
+ title: "Design error pages for 404 and 500",
375
+ subtitle: "",
376
+ status: "todo",
377
+ priority: "low",
378
+ project: "",
379
+ tags: [],
380
+ created: "Jul 3",
381
+ updated: "Jul 4",
382
+ assignee: "Mia Wilson",
383
+ },
384
+ {
385
+ id: "22",
386
+ taskId: "T235040490",
387
+ title: "Audit third-party dependencies for vulnerabilities",
388
+ subtitle: "",
389
+ status: "todo",
390
+ priority: "medium",
391
+ project: "",
392
+ tags: [],
393
+ created: "Jul 5",
394
+ updated: "Jul 5",
395
+ assignee: "Jackson Lee",
396
+ },
397
+ {
398
+ id: "23",
399
+ taskId: "T235040491",
400
+ title: "Implement webhook retry logic with exponential backoff",
401
+ subtitle: "Castle gate integration",
402
+ status: "todo",
403
+ priority: "medium",
404
+ project: "Castle gate integration 2.0",
405
+ tags: [],
406
+ created: "Jul 4",
407
+ updated: "Jul 5",
408
+ assignee: "William Kim",
409
+ },
410
+ {
411
+ id: "24",
412
+ taskId: "T235040492",
413
+ title: "Audit dashboard contrast against the Dracula ramp",
414
+ subtitle: "",
415
+ status: "backlog",
416
+ priority: "low",
417
+ project: "",
418
+ tags: [],
419
+ created: "Jul 2",
420
+ updated: "Jul 3",
421
+ assignee: "Sofia Davis",
422
+ },
423
+ {
424
+ id: "25",
425
+ taskId: "T235040493",
426
+ title: "Create onboarding flow for new team members",
427
+ subtitle: "",
428
+ status: "backlog",
429
+ priority: "none",
430
+ project: "",
431
+ tags: [],
432
+ created: "Jul 1",
433
+ updated: "Jul 2",
434
+ assignee: "Isabella Nguyen",
435
+ },
436
+ {
437
+ id: "26",
438
+ taskId: "T235040494",
439
+ title: "Set up error tracking with Sentry",
440
+ subtitle: "",
441
+ status: "backlog",
442
+ priority: "medium",
443
+ project: "",
444
+ tags: [],
445
+ created: "Jul 3",
446
+ updated: "Jul 4",
447
+ assignee: "Ethan Jones",
448
+ },
449
+ {
450
+ id: "27",
451
+ taskId: "T235040495",
452
+ title: "Improve search performance with indexing",
453
+ subtitle: "",
454
+ status: "backlog",
455
+ priority: "low",
456
+ project: "",
457
+ tags: [],
458
+ created: "Jul 2",
459
+ updated: "Jul 3",
460
+ assignee: "Ava Taylor",
461
+ },
462
+ {
463
+ id: "28",
464
+ taskId: "T235040496",
465
+ title: "Write API documentation for v2 endpoints",
466
+ subtitle: "",
467
+ status: "done",
468
+ priority: "medium",
469
+ project: "",
470
+ tags: [],
471
+ created: "Jun 28",
472
+ updated: "Jul 3",
473
+ assignee: "Noah Garcia",
474
+ },
475
+ {
476
+ id: "29",
477
+ taskId: "T235040497",
478
+ title: "Set up staging environment",
479
+ subtitle: "",
480
+ status: "done",
481
+ priority: "high",
482
+ project: "",
483
+ tags: [],
484
+ created: "Jun 25",
485
+ updated: "Jul 1",
486
+ assignee: "Lucas Brown",
487
+ },
488
+ {
489
+ id: "30",
490
+ taskId: "T235040498",
491
+ title: "Fix flaky end-to-end tests in CI",
492
+ subtitle: "",
493
+ status: "done",
494
+ priority: "medium",
495
+ project: "",
496
+ tags: [],
497
+ created: "Jun 30",
498
+ updated: "Jul 2",
499
+ assignee: "Mia Wilson",
500
+ },
505
501
  ];
506
502
 
507
503
  const STATUS_LABEL: Record<TaskStatus, string> = {
508
- in_progress: 'In Progress',
509
- todo: 'Todo',
510
- backlog: 'Backlog',
511
- done: 'Done',
504
+ in_progress: "In Progress",
505
+ todo: "Todo",
506
+ backlog: "Backlog",
507
+ done: "Done",
512
508
  };
513
509
 
514
- type GroupByField = 'status' | 'priority' | 'project' | 'assignee' | 'none';
510
+ type GroupByField = "status" | "priority" | "project" | "assignee" | "none";
515
511
 
516
- const GROUP_BY_OPTIONS: {value: GroupByField; label: string}[] = [
517
- {value: 'none', label: 'None'},
518
- {value: 'status', label: 'Status'},
519
- {value: 'priority', label: 'Priority'},
520
- {value: 'project', label: 'Project'},
521
- {value: 'assignee', label: 'Assignee'},
512
+ const GROUP_BY_OPTIONS: { value: GroupByField; label: string }[] = [
513
+ { value: "none", label: "None" },
514
+ { value: "status", label: "Status" },
515
+ { value: "priority", label: "Priority" },
516
+ { value: "project", label: "Project" },
517
+ { value: "assignee", label: "Assignee" },
522
518
  ];
523
519
 
524
- function groupTasks(
525
- tasks: TaskRow[],
526
- groupBy: GroupByField,
527
- ): Map<string, TaskRow[]> {
528
- if (groupBy === 'none') {
529
- return new Map([['All', tasks]]);
530
- }
531
- const map = new Map<string, TaskRow[]>();
532
- for (const task of tasks) {
533
- const key = String(task[groupBy]) || 'None';
534
- let group = map.get(key);
535
- if (!group) {
536
- group = [];
537
- map.set(key, group);
538
- }
539
- group.push(task);
540
- }
541
- return map;
520
+ function groupTasks(tasks: TaskRow[], groupBy: GroupByField): Map<string, TaskRow[]> {
521
+ if (groupBy === "none") {
522
+ return new Map([["All", tasks]]);
523
+ }
524
+ const map = new Map<string, TaskRow[]>();
525
+ for (const task of tasks) {
526
+ const key = String(task[groupBy]) || "None";
527
+ let group = map.get(key);
528
+ if (!group) {
529
+ group = [];
530
+ map.set(key, group);
531
+ }
532
+ group.push(task);
533
+ }
534
+ return map;
542
535
  }
543
536
 
544
537
  function getGroupLabel(groupBy: GroupByField, key: string): string {
545
- if (groupBy === 'status') {
546
- return STATUS_LABEL[key as TaskStatus] ?? key;
547
- }
548
- if (groupBy === 'priority') {
549
- // Group headers say "No priority" for the unprioritized bucket while the
550
- // inspector keeps the "None" filter vocabulary — same key, different
551
- // surface, so the shared label map is overridden only here.
552
- return {...PRIORITY_LABEL, none: 'No priority'}[key as TaskPriority] ?? key;
553
- }
554
- return key;
538
+ if (groupBy === "status") {
539
+ return STATUS_LABEL[key as TaskStatus] ?? key;
540
+ }
541
+ if (groupBy === "priority") {
542
+ // Group headers say "No priority" for the unprioritized bucket while the
543
+ // inspector keeps the "None" filter vocabulary — same key, different
544
+ // surface, so the shared label map is overridden only here.
545
+ return { ...PRIORITY_LABEL, none: "No priority" }[key as TaskPriority] ?? key;
546
+ }
547
+ return key;
555
548
  }
556
549
 
557
550
  // Real widths, because `resolveColumnWidths` derives the table's aggregate
@@ -568,587 +561,541 @@ function getGroupLabel(groupBy: GroupByField, key: string): string {
568
561
  // Issue column both land on screen, and the remaining columns are one swipe
569
562
  // away.
570
563
  const columns: TableColumn<TaskRow>[] = [
571
- {
572
- key: 'status',
573
- header: '',
574
- // StatusDot + the status word, which WCAG 1.4.1 requires as a channel
575
- // beside the hue.
576
- width: pixel(116),
577
- },
578
- {
579
- key: 'title',
580
- header: 'Issue',
581
- // 2fr of the flexible pair: the issue id and title are what the reader
582
- // scans the list for.
583
- width: proportional(2, {minWidth: 280}),
584
- },
585
- {
586
- key: 'project',
587
- header: 'Project',
588
- width: proportional(1, {minWidth: 150}),
589
- },
590
- {
591
- key: 'created',
592
- header: 'Created',
593
- // 112, not 80: the header label measures 83px at label-semibold, and a
594
- // header cell always truncates, so the floor has to clear the label and
595
- // not just the "Jul 3" value underneath it.
596
- width: pixel(112),
597
- },
598
- {
599
- key: 'updated',
600
- header: 'Updated',
601
- width: pixel(112),
602
- },
603
- {
604
- key: 'assignee',
605
- header: 'Assignee',
606
- // Avatar AND name. At 52px the column showed the avatar alone, which
607
- // left the name reachable only in the inspector — and the inspector is
608
- // gone below 1024. 168 clears the widest name in the data
609
- // ("Isabella Nguyen", 108px of text) beside the 24px avatar, so it never
610
- // breaks across two lines.
611
- width: pixel(168),
612
- },
613
- {
614
- key: 'actions',
615
- header: '',
616
- width: pixel(56),
617
- },
564
+ {
565
+ key: "status",
566
+ header: "",
567
+ // StatusDot + the status word, which WCAG 1.4.1 requires as a channel
568
+ // beside the hue.
569
+ width: pixel(116),
570
+ },
571
+ {
572
+ key: "title",
573
+ header: "Issue",
574
+ // 2fr of the flexible pair: the issue id and title are what the reader
575
+ // scans the list for.
576
+ width: proportional(2, { minWidth: 280 }),
577
+ },
578
+ {
579
+ key: "project",
580
+ header: "Project",
581
+ width: proportional(1, { minWidth: 150 }),
582
+ },
583
+ {
584
+ key: "created",
585
+ header: "Created",
586
+ // 112, not 80: the header label measures 83px at label-semibold, and a
587
+ // header cell always truncates, so the floor has to clear the label and
588
+ // not just the "Jul 3" value underneath it.
589
+ width: pixel(112),
590
+ },
591
+ {
592
+ key: "updated",
593
+ header: "Updated",
594
+ width: pixel(112),
595
+ },
596
+ {
597
+ key: "assignee",
598
+ header: "Assignee",
599
+ // Avatar AND name. At 52px the column showed the avatar alone, which
600
+ // left the name reachable only in the inspector — and the inspector is
601
+ // gone below 1024. 168 clears the widest name in the data
602
+ // ("Isabella Nguyen", 108px of text) beside the 24px avatar, so it never
603
+ // breaks across two lines.
604
+ width: pixel(168),
605
+ },
606
+ {
607
+ key: "actions",
608
+ header: "",
609
+ width: pixel(56),
610
+ },
618
611
  ];
619
612
 
620
613
  const fieldDefs = [
621
- {
622
- key: 'status',
623
- type: 'enum',
624
- label: 'Status',
625
- enumValues: [
626
- {value: 'in_progress', label: 'In Progress'},
627
- {value: 'todo', label: 'Todo'},
628
- {value: 'backlog', label: 'Backlog'},
629
- {value: 'done', label: 'Done'},
630
- ],
631
- },
632
- {
633
- key: 'priority',
634
- type: 'enum',
635
- label: 'Priority',
636
- enumValues: [
637
- {value: 'urgent', label: 'Urgent'},
638
- {value: 'high', label: 'High'},
639
- {value: 'medium', label: 'Medium'},
640
- {value: 'low', label: 'Low'},
641
- {value: 'none', label: 'None'},
642
- ],
643
- },
644
- {key: 'title', type: 'string', label: 'Title'},
645
- {key: 'assignee', type: 'string', label: 'Assignee'},
646
- {key: 'project', type: 'string', label: 'Project'},
614
+ {
615
+ key: "status",
616
+ type: "enum",
617
+ label: "Status",
618
+ enumValues: [
619
+ { value: "in_progress", label: "In Progress" },
620
+ { value: "todo", label: "Todo" },
621
+ { value: "backlog", label: "Backlog" },
622
+ { value: "done", label: "Done" },
623
+ ],
624
+ },
625
+ {
626
+ key: "priority",
627
+ type: "enum",
628
+ label: "Priority",
629
+ enumValues: [
630
+ { value: "urgent", label: "Urgent" },
631
+ { value: "high", label: "High" },
632
+ { value: "medium", label: "Medium" },
633
+ { value: "low", label: "Low" },
634
+ { value: "none", label: "None" },
635
+ ],
636
+ },
637
+ { key: "title", type: "string", label: "Title" },
638
+ { key: "assignee", type: "string", label: "Assignee" },
639
+ { key: "project", type: "string", label: "Project" },
647
640
  ] as const;
648
641
 
649
642
  const PRIORITY_LABEL: Record<TaskPriority, string> = {
650
- urgent: 'Urgent',
651
- high: 'High',
652
- medium: 'Medium',
653
- low: 'Low',
654
- none: 'None',
643
+ urgent: "Urgent",
644
+ high: "High",
645
+ medium: "Medium",
646
+ low: "Low",
647
+ none: "None",
655
648
  };
656
649
 
657
650
  function TaskDetailPanel({
658
- task,
659
- onClose,
660
- resizable,
651
+ task,
652
+ onClose,
653
+ resizable,
661
654
  }: {
662
- task: TaskRow | null;
663
- onClose: () => void;
664
- resizable: ResizableProps;
655
+ task: TaskRow | null;
656
+ onClose: () => void;
657
+ resizable: ResizableProps;
665
658
  }) {
666
- if (!task) {
667
- return null;
668
- }
669
- return (
670
- // Panel owns the separator (its full-height left border). The adjacent
671
- // ResizeHandle is kept divider-less + isAlwaysVisible={false} so its
672
- // always-on pill doesn't float above the panel as a stray stub.
673
- <LayoutPanel
674
- hasDivider
675
- resizable={resizable}
676
- padding={4}
677
- role="complementary"
678
- label="Task details">
679
- <VStack gap={4}>
680
- <HStack gap={2} vAlign="center">
681
- <StackItem size="fill">
682
- <Text type="supporting" color="secondary">
683
- {task.taskId}
684
- </Text>
685
- </StackItem>
686
- <Button
687
- label="Close panel"
688
- variant="ghost"
689
- size="sm"
690
- icon={<Icon icon={X} size="sm" />}
691
- isIconOnly
692
- onClick={onClose}
693
- />
694
- </HStack>
659
+ if (!task) {
660
+ return null;
661
+ }
662
+ return (
663
+ // Panel owns the separator (its full-height left border). The adjacent
664
+ // ResizeHandle is kept divider-less + isAlwaysVisible={false} so its
665
+ // always-on pill doesn't float above the panel as a stray stub.
666
+ <LayoutPanel
667
+ hasDivider
668
+ resizable={resizable}
669
+ padding={4}
670
+ role="complementary"
671
+ label="Task details"
672
+ >
673
+ <VStack gap={4}>
674
+ <HStack gap={2} vAlign="center">
675
+ <StackItem size="fill">
676
+ <Text type="supporting" color="secondary">
677
+ {task.taskId}
678
+ </Text>
679
+ </StackItem>
680
+ <Button
681
+ label="Close panel"
682
+ variant="ghost"
683
+ size="sm"
684
+ icon={<Icon icon={X} size="sm" />}
685
+ isIconOnly
686
+ onClick={onClose}
687
+ />
688
+ </HStack>
695
689
 
696
- <VStack gap={1}>
697
- <Heading level={2}>{task.title}</Heading>
698
- {task.subtitle && (
699
- <Text type="body" color="secondary">
700
- {task.subtitle}
701
- </Text>
702
- )}
703
- </VStack>
690
+ <VStack gap={1}>
691
+ <Heading level={2}>{task.title}</Heading>
692
+ {task.subtitle && (
693
+ <Text type="body" color="secondary">
694
+ {task.subtitle}
695
+ </Text>
696
+ )}
697
+ </VStack>
704
698
 
705
- <MetadataList label={{position: 'start'}}>
706
- <MetadataListItem label="Status">
707
- <HStack gap={1} vAlign="center">
708
- <StatusDot
709
- variant={STATUS_DOT_VARIANT[task.status]}
710
- label={STATUS_LABEL[task.status]}
711
- isPulsing={task.status === 'in_progress'}
712
- />
713
- <Text type="body">{STATUS_LABEL[task.status]}</Text>
714
- </HStack>
715
- </MetadataListItem>
716
- <MetadataListItem label="Priority">
717
- <HStack gap={2} vAlign="center">
718
- <Icon
719
- icon={ChartBar}
720
- size="sm"
721
- color={PRIORITY_COLOR[task.priority]}
722
- />
723
- <Text type="body">{PRIORITY_LABEL[task.priority]}</Text>
724
- </HStack>
725
- </MetadataListItem>
726
- <MetadataListItem label="Assignee">
727
- <HStack gap={2} vAlign="center">
728
- <Avatar name={task.assignee} size="sm" />
729
- <Text type="body">{task.assignee}</Text>
730
- </HStack>
731
- </MetadataListItem>
732
- <MetadataListItem label="Project">
733
- {task.project ? (
734
- <Text type="body">{task.project}</Text>
735
- ) : (
736
- <Text type="supporting" color="secondary">
737
- None
738
- </Text>
739
- )}
740
- </MetadataListItem>
741
- <MetadataListItem label="Created">
742
- <Text type="body">{task.created}</Text>
743
- </MetadataListItem>
744
- <MetadataListItem label="Updated">
745
- <Text type="body">{task.updated}</Text>
746
- </MetadataListItem>
747
- </MetadataList>
699
+ <MetadataList label={{ position: "start" }}>
700
+ <MetadataListItem label="Status">
701
+ <HStack gap={1} vAlign="center">
702
+ <StatusDot
703
+ variant={STATUS_DOT_VARIANT[task.status]}
704
+ label={STATUS_LABEL[task.status]}
705
+ isPulsing={task.status === "in_progress"}
706
+ />
707
+ <Text type="body">{STATUS_LABEL[task.status]}</Text>
708
+ </HStack>
709
+ </MetadataListItem>
710
+ <MetadataListItem label="Priority">
711
+ <HStack gap={2} vAlign="center">
712
+ <Icon icon={ChartBar} size="sm" color={PRIORITY_COLOR[task.priority]} />
713
+ <Text type="body">{PRIORITY_LABEL[task.priority]}</Text>
714
+ </HStack>
715
+ </MetadataListItem>
716
+ <MetadataListItem label="Assignee">
717
+ <HStack gap={2} vAlign="center">
718
+ <Avatar name={task.assignee} size="sm" />
719
+ <Text type="body">{task.assignee}</Text>
720
+ </HStack>
721
+ </MetadataListItem>
722
+ <MetadataListItem label="Project">
723
+ {task.project ? (
724
+ <Text type="body">{task.project}</Text>
725
+ ) : (
726
+ <Text type="supporting" color="secondary">
727
+ None
728
+ </Text>
729
+ )}
730
+ </MetadataListItem>
731
+ <MetadataListItem label="Created">
732
+ <Text type="body">{task.created}</Text>
733
+ </MetadataListItem>
734
+ <MetadataListItem label="Updated">
735
+ <Text type="body">{task.updated}</Text>
736
+ </MetadataListItem>
737
+ </MetadataList>
748
738
 
749
- {task.tags.length > 0 && (
750
- <>
751
- <Divider />
752
- <VStack gap={2}>
753
- <Text type="label">Labels</Text>
754
- <HStack gap={2}>
755
- {task.tags.map(tag => (
756
- <Token key={tag} color="yellow" label={tag} />
757
- ))}
758
- </HStack>
759
- </VStack>
760
- </>
761
- )}
762
- </VStack>
763
- </LayoutPanel>
764
- );
739
+ {task.tags.length > 0 && (
740
+ <>
741
+ <Divider />
742
+ <VStack gap={2}>
743
+ <Text type="label">Labels</Text>
744
+ <HStack gap={2}>
745
+ {task.tags.map((tag) => (
746
+ <Token key={tag} color="yellow" label={tag} />
747
+ ))}
748
+ </HStack>
749
+ </VStack>
750
+ </>
751
+ )}
752
+ </VStack>
753
+ </LayoutPanel>
754
+ );
765
755
  }
766
756
 
767
757
  export default function TableGrouped() {
768
- const [dialogOpen, setDialogOpen] = useState(false);
769
- const [selectedTask, setSelectedTask] = useState<TaskRow | null>(null);
770
- const [powerSearchFilters, setPowerSearchFilters] = useState<
771
- ReadonlyArray<PowerSearchFilter>
772
- >([]);
773
- const {config: powerSearchConfig, applyFilters} = usePowerSearchConfig(
774
- fieldDefs,
775
- 'IssueSearch',
776
- );
777
- const [groupBy, setGroupBy] = useState<GroupByField>('status');
778
- const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
779
- () => new Set(['in_progress', 'todo', 'backlog', 'done']),
780
- );
758
+ const [dialogOpen, setDialogOpen] = useState(false);
759
+ const [selectedTask, setSelectedTask] = useState<TaskRow | null>(null);
760
+ const [powerSearchFilters, setPowerSearchFilters] = useState<ReadonlyArray<PowerSearchFilter>>(
761
+ [],
762
+ );
763
+ const { config: powerSearchConfig, applyFilters } = usePowerSearchConfig(
764
+ fieldDefs,
765
+ "IssueSearch",
766
+ );
767
+ const [groupBy, setGroupBy] = useState<GroupByField>("status");
768
+ const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
769
+ () => new Set(["in_progress", "todo", "backlog", "done"]),
770
+ );
781
771
 
782
- // Responsive contract (see file header): below 1024px the inspector would
783
- // squeeze the grouped table, so it steps aside entirely.
784
- const isNarrow = useMediaQuery('(max-width: 1024px)');
772
+ // Responsive contract (see file header): below 1024px the inspector would
773
+ // squeeze the grouped table, so it steps aside entirely.
774
+ const isNarrow = useMediaQuery("(max-width: 1024px)");
785
775
 
786
- const filtered = useMemo(() => {
787
- return applyFilters(powerSearchFilters, allTasks);
788
- }, [powerSearchFilters, applyFilters]);
776
+ const filtered = useMemo(() => {
777
+ return applyFilters(powerSearchFilters, allTasks);
778
+ }, [powerSearchFilters, applyFilters]);
789
779
 
790
- const grouped = useMemo(
791
- () => groupTasks(filtered, groupBy),
792
- [filtered, groupBy],
793
- );
780
+ const grouped = useMemo(() => groupTasks(filtered, groupBy), [filtered, groupBy]);
794
781
 
795
- const groupKeys = Array.from(grouped.keys());
782
+ const groupKeys = Array.from(grouped.keys());
796
783
 
797
- // Expand every group whenever the grouping itself changes. Keyed on the
798
- // memoized Map, not on groupKeys, which is a fresh array each render.
799
- React.useEffect(() => {
800
- setExpandedGroups(new Set(grouped.keys()));
801
- }, [grouped]);
784
+ // Expand every group whenever the grouping itself changes. Keyed on the
785
+ // memoized Map, not on groupKeys, which is a fresh array each render.
786
+ React.useEffect(() => {
787
+ setExpandedGroups(new Set(grouped.keys()));
788
+ }, [grouped]);
802
789
 
803
- const toggleGroup = (key: string) => {
804
- setExpandedGroups(prev => {
805
- const next = new Set(prev);
806
- if (next.has(key)) {
807
- next.delete(key);
808
- } else {
809
- next.add(key);
810
- }
811
- return next;
812
- });
813
- };
790
+ const toggleGroup = (key: string) => {
791
+ setExpandedGroups((prev) => {
792
+ const next = new Set(prev);
793
+ if (next.has(key)) {
794
+ next.delete(key);
795
+ } else {
796
+ next.add(key);
797
+ }
798
+ return next;
799
+ });
800
+ };
814
801
 
815
- const detailPanel = useResizable({
816
- defaultSize: 360,
817
- minSize: 280,
818
- maxSize: 500,
819
- });
802
+ const detailPanel = useResizable({
803
+ defaultSize: 360,
804
+ minSize: 280,
805
+ maxSize: 500,
806
+ });
820
807
 
821
- const resolvedWidths = resolveColumnWidths(columns);
808
+ const resolvedWidths = resolveColumnWidths(columns);
822
809
 
823
- return (
824
- <>
825
- <Layout
826
- height="fill"
827
- header={
828
- <LayoutHeader hasDivider padding={6}>
829
- <VStack gap={4}>
830
- {/* wrap: below ~460px the Raise issue button drops to its own
810
+ return (
811
+ <>
812
+ <Layout
813
+ height="fill"
814
+ header={
815
+ <LayoutHeader hasDivider padding={6}>
816
+ <VStack gap={4}>
817
+ {/* wrap: below ~460px the Raise issue button drops to its own
831
818
  line instead of starving the heading, so the display-2 title
832
819
  keeps its full measure. The earlier maxLines={1} clipped it
833
820
  to "Night-shift i…" at 390, and its reveal path was a
834
821
  Tooltip — a hover affordance a phone never fires. Wrapping
835
822
  breaks on the space between the two words, so no value is
836
823
  lost and none is sliced mid-word (SC 1.4.10). */}
837
- <HStack gap={3} vAlign="center" wrap="wrap">
838
- <StackItem size="fill">
839
- <Heading level={1} type="display-2">Night-shift issues</Heading>
840
- </StackItem>
841
- <Button
842
- label="Raise issue"
843
- variant="primary"
844
- onClick={() => setDialogOpen(true)}
845
- />
846
- </HStack>
847
- <HStack gap={2} vAlign="center">
848
- <StackItem size="fill">
849
- <PowerSearch
850
- config={powerSearchConfig}
851
- filters={powerSearchFilters}
852
- onChange={newFilters => setPowerSearchFilters(newFilters)}
853
- placeholder="Filter issues…"
854
- resultCount={`${filtered.length} issue${filtered.length !== 1 ? 's' : ''}`}
855
- />
856
- </StackItem>
857
- <Popover
858
- placement="below"
859
- alignment="end"
860
- width="min(20rem, calc(100vw - 2 * var(--space-viewport)))"
861
- label="Grouping options"
862
- content={
863
- <VStack gap={4}>
864
- <RadioList
865
- label="Group by"
866
- value={groupBy}
867
- onChange={v => setGroupBy(v as GroupByField)}>
868
- {GROUP_BY_OPTIONS.map(opt => (
869
- <RadioListItem
870
- key={opt.value}
871
- value={opt.value}
872
- label={opt.label}
873
- />
874
- ))}
875
- </RadioList>
876
- </VStack>
877
- }>
878
- <Button label="View options" variant="secondary" size="md" />
879
- </Popover>
880
- </HStack>
881
- </VStack>
882
- </LayoutHeader>
883
- }
884
- content={
885
- <LayoutContent role="main" padding={0}>
886
- <Table
887
- columns={columns}
888
- density="balanced"
889
- dividers="rows"
890
- textOverflow="wrap"
891
- hasHover>
892
- <colgroup>
893
- {columns.map(col => (
894
- <col
895
- key={col.key}
896
- style={resolvedWidths.columns.get(col.key)?.style}
897
- />
898
- ))}
899
- </colgroup>
900
- {/* A scroller needs its columns named: once the reader has
824
+ <HStack gap={3} vAlign="center" wrap="wrap">
825
+ <StackItem size="fill">
826
+ <Heading level={1} type="display-2">
827
+ Night-shift issues
828
+ </Heading>
829
+ </StackItem>
830
+ <Button label="Raise issue" variant="primary" onClick={() => setDialogOpen(true)} />
831
+ </HStack>
832
+ <HStack gap={2} vAlign="center">
833
+ <StackItem size="fill">
834
+ <PowerSearch
835
+ config={powerSearchConfig}
836
+ filters={powerSearchFilters}
837
+ onChange={(newFilters) => setPowerSearchFilters(newFilters)}
838
+ placeholder="Filter issues…"
839
+ resultCount={`${filtered.length} issue${filtered.length !== 1 ? "s" : ""}`}
840
+ />
841
+ </StackItem>
842
+ <Popover
843
+ placement="below"
844
+ alignment="end"
845
+ width="min(20rem, calc(100vw - 2 * var(--space-viewport)))"
846
+ label="Grouping options"
847
+ content={
848
+ <VStack gap={4}>
849
+ <RadioList
850
+ label="Group by"
851
+ value={groupBy}
852
+ onChange={(v) => setGroupBy(v as GroupByField)}
853
+ >
854
+ {GROUP_BY_OPTIONS.map((opt) => (
855
+ <RadioListItem key={opt.value} value={opt.value} label={opt.label} />
856
+ ))}
857
+ </RadioList>
858
+ </VStack>
859
+ }
860
+ >
861
+ <Button label="View options" variant="secondary" size="md" />
862
+ </Popover>
863
+ </HStack>
864
+ </VStack>
865
+ </LayoutHeader>
866
+ }
867
+ content={
868
+ <LayoutContent role="main" padding={0}>
869
+ <Table
870
+ columns={columns}
871
+ density="balanced"
872
+ dividers="rows"
873
+ textOverflow="wrap"
874
+ hasHover
875
+ >
876
+ <colgroup>
877
+ {columns.map((col) => (
878
+ <col key={col.key} style={resolvedWidths.columns.get(col.key)?.style} />
879
+ ))}
880
+ </colgroup>
881
+ {/* A scroller needs its columns named: once the reader has
901
882
  scrolled sideways at 390, this row is the only thing that
902
883
  says which value they are looking at. Rendered from
903
884
  `columns` so the labels and the widths cannot drift, and as
904
885
  real <th scope="col"> cells so the association reaches
905
886
  assistive tech. */}
906
- <TableRow>
907
- {columns.map(col => (
908
- <TableHeaderCell key={col.key} scope="col">
909
- {col.header}
910
- </TableHeaderCell>
911
- ))}
912
- </TableRow>
913
- {groupKeys.map(key => {
914
- const tasks = grouped.get(key);
915
- if (!tasks || tasks.length === 0) {
916
- return null;
917
- }
918
- const isExpanded = expandedGroups.has(key);
887
+ <TableRow>
888
+ {columns.map((col) => (
889
+ <TableHeaderCell key={col.key} scope="col">
890
+ {col.header}
891
+ </TableHeaderCell>
892
+ ))}
893
+ </TableRow>
894
+ {groupKeys.map((key) => {
895
+ const tasks = grouped.get(key);
896
+ if (!tasks || tasks.length === 0) {
897
+ return null;
898
+ }
899
+ const isExpanded = expandedGroups.has(key);
919
900
 
920
- return (
921
- <React.Fragment key={key}>
922
- {groupBy !== 'none' && (
923
- <TableRow
924
- role="button"
925
- tabIndex={0}
926
- onClick={() => toggleGroup(key)}
927
- onKeyDown={e => {
928
- if (e.key === 'Enter' || e.key === ' ') {
929
- e.preventDefault();
930
- toggleGroup(key);
931
- }
932
- }}>
933
- <TableCell colSpan={columns.length} style={groupHeaderCell}>
934
- <HStack gap={2} vAlign="center">
935
- <Icon
936
- icon={
937
- isExpanded ? ChevronDown : ChevronRight
938
- }
939
- size="sm"
940
- color="secondary"
941
- />
942
- <Text type="body" weight="semibold">
943
- {getGroupLabel(groupBy, key)}
944
- </Text>
945
- <Badge
946
- variant="neutral"
947
- label={String(tasks.length)}
948
- />
949
- </HStack>
950
- </TableCell>
951
- </TableRow>
952
- )}
953
- {(groupBy === 'none' || isExpanded) &&
954
- tasks.map(task => (
955
- <TableRow
956
- key={task.id}
957
- onClick={() => setSelectedTask(task)}>
958
- <TableCell>
959
- {/* StatusDot paints nothing but an 8px dot — its
901
+ return (
902
+ <React.Fragment key={key}>
903
+ {groupBy !== "none" && (
904
+ <TableRow
905
+ role="button"
906
+ tabIndex={0}
907
+ onClick={() => toggleGroup(key)}
908
+ onKeyDown={(e) => {
909
+ if (e.key === "Enter" || e.key === " ") {
910
+ e.preventDefault();
911
+ toggleGroup(key);
912
+ }
913
+ }}
914
+ >
915
+ <TableCell colSpan={columns.length} style={groupHeaderCell}>
916
+ <HStack gap={2} vAlign="center">
917
+ <Icon
918
+ icon={isExpanded ? ChevronDown : ChevronRight}
919
+ size="sm"
920
+ color="secondary"
921
+ />
922
+ <Text type="body" weight="semibold">
923
+ {getGroupLabel(groupBy, key)}
924
+ </Text>
925
+ <Badge variant="neutral" label={String(tasks.length)} />
926
+ </HStack>
927
+ </TableCell>
928
+ </TableRow>
929
+ )}
930
+ {(groupBy === "none" || isExpanded) &&
931
+ tasks.map((task) => (
932
+ <TableRow key={task.id} onClick={() => setSelectedTask(task)}>
933
+ <TableCell>
934
+ {/* StatusDot paints nothing but an 8px dot — its
960
935
  label is aria-label on a role="img" span — so
961
936
  four states here read as hue alone. Same
962
937
  dot-plus-word pairing as the Status entry in
963
938
  TaskDetailPanel's MetadataList. */}
964
- <HStack gap={1} vAlign="center">
965
- <StatusDot
966
- variant={STATUS_DOT_VARIANT[task.status]}
967
- label={STATUS_LABEL[task.status]}
968
- isPulsing={task.status === 'in_progress'}
969
- />
970
- <Text type="supporting" color="secondary">
971
- {STATUS_LABEL[task.status]}
972
- </Text>
973
- </HStack>
974
- </TableCell>
975
- <TableCell>
976
- <HStack gap={3} vAlign="center" wrap="wrap">
977
- <Icon
978
- icon={ChartBar}
979
- size="sm"
980
- color={PRIORITY_COLOR[task.priority]}
981
- />
982
- <Text type="supporting" color="secondary">
983
- {task.taskId}
984
- </Text>
985
- <StackItem size="fill">
986
- <Text type="body">
987
- {task.title}
988
- {task.subtitle && (
989
- <Text type="inherit" color="secondary">
990
- {' › '}
991
- {task.subtitle}
992
- </Text>
993
- )}
994
- </Text>
995
- </StackItem>
996
- </HStack>
997
- </TableCell>
998
- <TableCell>
999
- {task.project ? (
1000
- <Text type="body">
1001
- {task.project}
1002
- </Text>
1003
- ) : (
1004
- <Text type="supporting" color="secondary">
1005
- None
1006
- </Text>
1007
- )}
1008
- </TableCell>
1009
- <TableCell>
1010
- <Text
1011
- type="supporting"
1012
- color="secondary">
1013
- {task.created}
1014
- </Text>
1015
- </TableCell>
1016
- <TableCell>
1017
- <Text
1018
- type="supporting"
1019
- color="secondary">
1020
- {task.updated}
1021
- </Text>
1022
- </TableCell>
1023
- <TableCell>
1024
- {/* Name beside the avatar: below 1024 the
939
+ <HStack gap={1} vAlign="center">
940
+ <StatusDot
941
+ variant={STATUS_DOT_VARIANT[task.status]}
942
+ label={STATUS_LABEL[task.status]}
943
+ isPulsing={task.status === "in_progress"}
944
+ />
945
+ <Text type="supporting" color="secondary">
946
+ {STATUS_LABEL[task.status]}
947
+ </Text>
948
+ </HStack>
949
+ </TableCell>
950
+ <TableCell>
951
+ <HStack gap={3} vAlign="center" wrap="wrap">
952
+ <Icon
953
+ icon={ChartBar}
954
+ size="sm"
955
+ color={PRIORITY_COLOR[task.priority]}
956
+ />
957
+ <Text type="supporting" color="secondary">
958
+ {task.taskId}
959
+ </Text>
960
+ <StackItem size="fill">
961
+ <Text type="body">
962
+ {task.title}
963
+ {task.subtitle && (
964
+ <Text type="inherit" color="secondary">
965
+ {" › "}
966
+ {task.subtitle}
967
+ </Text>
968
+ )}
969
+ </Text>
970
+ </StackItem>
971
+ </HStack>
972
+ </TableCell>
973
+ <TableCell>
974
+ {task.project ? (
975
+ <Text type="body">{task.project}</Text>
976
+ ) : (
977
+ <Text type="supporting" color="secondary">
978
+ None
979
+ </Text>
980
+ )}
981
+ </TableCell>
982
+ <TableCell>
983
+ <Text type="supporting" color="secondary">
984
+ {task.created}
985
+ </Text>
986
+ </TableCell>
987
+ <TableCell>
988
+ <Text type="supporting" color="secondary">
989
+ {task.updated}
990
+ </Text>
991
+ </TableCell>
992
+ <TableCell>
993
+ {/* Name beside the avatar: below 1024 the
1025
994
  inspector is gone, so the avatar alone would
1026
995
  leave the assignee readable by screen reader
1027
996
  but not by eye. */}
1028
- <HStack gap={2} vAlign="center">
1029
- <Avatar name={task.assignee} size="sm" />
1030
- <Text type="supporting" color="secondary">
1031
- {task.assignee}
1032
- </Text>
1033
- </HStack>
1034
- </TableCell>
1035
- <TableCell>
1036
- <DropdownMenu
1037
- button={{
1038
- label: 'Actions',
1039
- variant: 'ghost',
1040
- size: 'sm',
1041
- icon: (
1042
- <Icon
1043
- icon={Ellipsis}
1044
- size="sm"
1045
- />
1046
- ),
1047
- isIconOnly: true,
1048
- }}
1049
- hasChevron={false}
1050
- items={[
1051
- {label: 'Edit issue', icon: Pencil},
1052
- {label: 'Assign to...', icon: User},
1053
- {label: 'Add label', icon: Tag},
1054
- {label: 'Duplicate', icon: Copy},
1055
- {label: 'Move to project', icon: ArrowRight},
1056
- {type: 'divider' as const},
1057
- {label: 'Delete issue', icon: Trash},
1058
- ]}
1059
- />
1060
- </TableCell>
1061
- </TableRow>
1062
- ))}
1063
- </React.Fragment>
1064
- );
1065
- })}
1066
- </Table>
1067
- </LayoutContent>
1068
- }
1069
- end={
1070
- !isNarrow &&
1071
- selectedTask && (
1072
- <>
1073
- <ResizeHandle
1074
- resizable={detailPanel.props}
1075
- isReversed
1076
- isAlwaysVisible={false}
1077
- />
1078
- <TaskDetailPanel
1079
- task={selectedTask}
1080
- onClose={() => setSelectedTask(null)}
1081
- resizable={detailPanel.props}
1082
- />
1083
- </>
1084
- )
1085
- }
1086
- />
1087
- <Dialog isOpen={dialogOpen} onOpenChange={open => setDialogOpen(open)}>
1088
- <Layout
1089
- height="auto"
1090
- header={
1091
- <DialogHeader
1092
- title="Raise issue"
1093
- onOpenChange={open => setDialogOpen(open)}
1094
- />
1095
- }
1096
- content={
1097
- <LayoutContent padding={4}>
1098
- <VStack gap={4}>
1099
- <TextInput
1100
- label="Title"
1101
- placeholder="Issue title"
1102
- value=""
1103
- />
1104
- <Selector
1105
- label="Status"
1106
- value="todo"
1107
- options={[
1108
- {value: 'in_progress', label: 'In Progress'},
1109
- {value: 'todo', label: 'Todo'},
1110
- {value: 'backlog', label: 'Backlog'},
1111
- ]}
1112
- />
1113
- <Selector
1114
- label="Priority"
1115
- value="none"
1116
- options={[
1117
- {value: 'urgent', label: 'Urgent'},
1118
- {value: 'high', label: 'High'},
1119
- {value: 'medium', label: 'Medium'},
1120
- {value: 'low', label: 'Low'},
1121
- {value: 'none', label: 'No priority'},
1122
- ]}
1123
- />
1124
- <TextInput
1125
- label="Project"
1126
- placeholder="Project name"
1127
- value=""
1128
- />
1129
- </VStack>
1130
- </LayoutContent>
1131
- }
1132
- footer={
1133
- <LayoutFooter hasDivider>
1134
- <HStack gap={2} hAlign="end">
1135
- <Button
1136
- label="Cancel"
1137
- variant="secondary"
1138
- size="md"
1139
- onClick={() => setDialogOpen(false)}
1140
- />
1141
- <Button
1142
- label="Raise issue"
1143
- variant="primary"
1144
- size="md"
1145
- onClick={() => setDialogOpen(false)}
1146
- />
1147
- </HStack>
1148
- </LayoutFooter>
1149
- }
1150
- />
1151
- </Dialog>
1152
- </>
1153
- );
997
+ <HStack gap={2} vAlign="center">
998
+ <Avatar name={task.assignee} size="sm" />
999
+ <Text type="supporting" color="secondary">
1000
+ {task.assignee}
1001
+ </Text>
1002
+ </HStack>
1003
+ </TableCell>
1004
+ <TableCell>
1005
+ <DropdownMenu
1006
+ button={{
1007
+ label: "Actions",
1008
+ variant: "ghost",
1009
+ size: "sm",
1010
+ icon: <Icon icon={Ellipsis} size="sm" />,
1011
+ isIconOnly: true,
1012
+ }}
1013
+ hasChevron={false}
1014
+ items={[
1015
+ { label: "Edit issue", icon: Pencil },
1016
+ { label: "Assign to...", icon: User },
1017
+ { label: "Add label", icon: Tag },
1018
+ { label: "Duplicate", icon: Copy },
1019
+ { label: "Move to project", icon: ArrowRight },
1020
+ { type: "divider" as const },
1021
+ { label: "Delete issue", icon: Trash },
1022
+ ]}
1023
+ />
1024
+ </TableCell>
1025
+ </TableRow>
1026
+ ))}
1027
+ </React.Fragment>
1028
+ );
1029
+ })}
1030
+ </Table>
1031
+ </LayoutContent>
1032
+ }
1033
+ end={
1034
+ !isNarrow &&
1035
+ selectedTask && (
1036
+ <>
1037
+ <ResizeHandle resizable={detailPanel.props} isReversed isAlwaysVisible={false} />
1038
+ <TaskDetailPanel
1039
+ task={selectedTask}
1040
+ onClose={() => setSelectedTask(null)}
1041
+ resizable={detailPanel.props}
1042
+ />
1043
+ </>
1044
+ )
1045
+ }
1046
+ />
1047
+ <Dialog isOpen={dialogOpen} onOpenChange={(open) => setDialogOpen(open)}>
1048
+ <Layout
1049
+ height="auto"
1050
+ header={<DialogHeader title="Raise issue" onOpenChange={(open) => setDialogOpen(open)} />}
1051
+ content={
1052
+ <LayoutContent padding={4}>
1053
+ <VStack gap={4}>
1054
+ <TextInput label="Title" placeholder="Issue title" value="" onChange={() => {}} />
1055
+ <Selector
1056
+ label="Status"
1057
+ value="todo"
1058
+ options={[
1059
+ { value: "in_progress", label: "In Progress" },
1060
+ { value: "todo", label: "Todo" },
1061
+ { value: "backlog", label: "Backlog" },
1062
+ ]}
1063
+ />
1064
+ <Selector
1065
+ label="Priority"
1066
+ value="none"
1067
+ options={[
1068
+ { value: "urgent", label: "Urgent" },
1069
+ { value: "high", label: "High" },
1070
+ { value: "medium", label: "Medium" },
1071
+ { value: "low", label: "Low" },
1072
+ { value: "none", label: "No priority" },
1073
+ ]}
1074
+ />
1075
+ <TextInput label="Project" placeholder="Project name" value="" />
1076
+ </VStack>
1077
+ </LayoutContent>
1078
+ }
1079
+ footer={
1080
+ <LayoutFooter hasDivider>
1081
+ <HStack gap={2} hAlign="end">
1082
+ <Button
1083
+ label="Cancel"
1084
+ variant="secondary"
1085
+ size="md"
1086
+ onClick={() => setDialogOpen(false)}
1087
+ />
1088
+ <Button
1089
+ label="Raise issue"
1090
+ variant="primary"
1091
+ size="md"
1092
+ onClick={() => setDialogOpen(false)}
1093
+ />
1094
+ </HStack>
1095
+ </LayoutFooter>
1096
+ }
1097
+ />
1098
+ </Dialog>
1099
+ </>
1100
+ );
1154
1101
  }