astryx-dracula 0.1.3 → 0.2.0

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