astryx-dracula 0.2.1 → 0.3.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 (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +17 -12
  3. package/USAGE.md +29 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +174 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +299 -237
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > G[c{min:360} g8 a=center] > (V[g6] > (V[g3] > Tx"AFTER DARK"[t=supporting] + Hd"Make every night"[level=1] + Tx"The smallest rituals"[t=body]) + B.primary"Explore the night" + (V[g4] > D + (H[g6] > (V > Tx"12k+"[t=large] + Tx"Night shots"[t=supporting])*3))) + (G[c3 g3] > AR*9)
3
+ // AR*10 is the live count: GALLERY_SCENES has 10 entries and a bare .map renders
4
+ // all 10, with no slice. The header said 9 while 10 rendered.
5
+ // L > LC[p=6] > G[c={min:280} g8 a=center] > (V[g6] > (V[g3] > Tx"AFTER DARK"[t=supporting] + Hd"Make every night"[level=1] + Tx"The smallest rituals"[t=body]) + B.primary"Explore the night" + (V[g4] > D + (H[g6] > (V > Tx"12k+"[t=display-3] + Tx"Night shots"[t=supporting])*3))) + (G[c3 g3] > AR*10)
4
6
 
5
7
  import type {CSSProperties} from 'react';
6
8
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -9,103 +11,33 @@ import {Button} from '@astryxdesign/core/Button';
9
11
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
10
12
  import {Grid} from '@astryxdesign/core/Grid';
11
13
  import {Divider} from '@astryxdesign/core/Divider';
12
-
13
- // NOTE: The only custom styling here is gallery fill + corner radius. It
14
- // exists because Astryx has no image primitive — AspectRatio exposes no
15
- // objectFit or radius props and there's no Image. Tracked in issue #2582;
16
- // replace these with component props once it lands.
17
- // Fills the AspectRatio box. No objectFit prop on AspectRatio (#2582).
18
- const galleryImage: CSSProperties = {
19
- width: '100%',
20
- height: '100%',
21
- display: 'block',
22
- };
23
- // Rounds the gallery corners. No radius prop on AspectRatio (#2582);
24
- // overflow clip masks the SVG scene to the rounded corners.
25
- const galleryImageClip: CSSProperties = {
26
- borderRadius: 'var(--radius-container)',
27
- overflow: 'clip',
28
- };
14
+ import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
15
+ import {SceneTile} from 'astryx-dracula/shared/scene-tile';
16
+ import {SCENE_TILE_ALTS} from 'astryx-dracula/shared/scene-hues';
17
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
29
18
 
30
19
  // ─── Gallery Data ─────────────────────────────────────────────────────────────
31
20
 
32
- // One Dracula accent per tile from the fixed categorical vocabulary.
33
- const GALLERY_SCENES = [
34
- {alt: 'Moonlit ridge trail under a harvest moon', hue: 'var(--dracula-cyan)'},
35
- {alt: 'Late portrait in violet lamplight', hue: 'var(--dracula-pink)'},
36
- {
37
- alt: 'Lamplit reading nook after midnight',
38
- hue: 'var(--dracula-yellow)',
39
- },
40
- {alt: 'Fog rolling over the night pines', hue: 'var(--dracula-purple)'},
41
- {alt: 'Dancer caught mid-step in stage pink', hue: 'var(--dracula-pink)'},
42
- {
43
- alt: 'Kitchen table set for a midnight feast',
44
- hue: 'var(--dracula-orange)',
45
- },
46
- {alt: 'Castle silhouette over sleeping hills', hue: 'var(--dracula-cyan)'},
47
- {alt: 'Attic window glowing amber at 2am', hue: 'var(--dracula-yellow)'},
48
- {alt: 'Cellar shelves lined with bottled dusk', hue: 'var(--dracula-green)'},
21
+ // One Dracula accent per tile from the fixed categorical vocabulary; alts are
22
+ // shared with classic-gallery via SCENE_TILE_ALTS.
23
+ const GALLERY_SCENES: Array<{alt: string; hue: SceneHue}> = [
24
+ {alt: SCENE_TILE_ALTS[0], hue: 'var(--dracula-cyan)'},
25
+ {alt: SCENE_TILE_ALTS[1], hue: 'var(--dracula-pink)'},
26
+ {alt: SCENE_TILE_ALTS[2], hue: 'var(--dracula-yellow)'},
27
+ {alt: SCENE_TILE_ALTS[3], hue: 'var(--dracula-green)'},
28
+ {alt: SCENE_TILE_ALTS[4], hue: 'var(--dracula-pink)'},
29
+ {alt: SCENE_TILE_ALTS[5], hue: 'var(--dracula-orange)'},
30
+ {alt: SCENE_TILE_ALTS[6], hue: 'var(--dracula-cyan)'},
31
+ {alt: SCENE_TILE_ALTS[7], hue: 'var(--dracula-yellow)'},
32
+ {alt: SCENE_TILE_ALTS[8], hue: 'var(--dracula-green)'},
49
33
  ];
50
34
 
51
- // ─── Gallery Art ────────────────────────────────────────────────────────────
52
-
53
- function NightScene({alt, hue, index}: {alt: string; hue: string; index: number}) {
54
- const moonX = 90 + ((index * 53) % 220);
55
- const moonY = 62 + ((index * 29) % 60);
56
- const hillA = 190 + ((index * 13) % 40);
57
- const hillB = 215 + ((index * 17) % 40);
58
- return (
59
- <svg
60
- viewBox="0 0 400 300"
61
- preserveAspectRatio="xMidYMid slice"
62
- style={galleryImage}
63
- role="img"
64
- aria-label={alt}>
65
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
66
- <g fill="var(--dracula-purple)" opacity={0.55}>
67
- <circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
68
- <circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
69
- <circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
70
- <circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
71
- </g>
72
- <circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
73
- <circle
74
- cx={moonX - 12}
75
- cy={moonY - 8}
76
- r={28}
77
- fill="var(--dracula-bg-light)"
78
- opacity={0.55}
79
- />
80
- <path
81
- d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
82
- fill="var(--dracula-current-line)"
83
- />
84
- <path
85
- d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
86
- fill="var(--dracula-bg)"
87
- />
88
- <g
89
- transform={`translate(${60 + ((index * 41) % 280)} ${hillB - 34})`}
90
- fill="none"
91
- stroke={hue}
92
- strokeWidth="4"
93
- strokeLinecap="round"
94
- strokeLinejoin="round">
95
- <rect x="-22" y="-22" width="44" height="44" rx={4} />
96
- <circle cx="9" cy="-9" r="2" fill={hue} stroke="none" />
97
- <path d="M-17 15 L-4 0 L5 9 L10 4 L17 12" />
98
- </g>
99
- </svg>
100
- );
101
- }
102
-
103
35
  // ─── Stat Block ─────────────────────────────────────────────────────────────
104
36
 
105
37
  function StatBlock({value, label}: {value: string; label: string}) {
106
38
  return (
107
39
  <VStack gap={0}>
108
- <Text type="large" weight="semibold" hasTabularNumbers>
40
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
109
41
  {value}
110
42
  </Text>
111
43
  <Text type="supporting" color="secondary">
@@ -122,7 +54,12 @@ function ImageGrid() {
122
54
  <Grid columns={3} gap={3}>
123
55
  {GALLERY_SCENES.map((scene, index) => (
124
56
  <AspectRatio key={scene.alt} ratio={1} style={galleryImageClip}>
125
- <NightScene alt={scene.alt} hue={scene.hue} index={index} />
57
+ <SceneTile
58
+ label={scene.alt}
59
+ hue={scene.hue}
60
+ size="sm"
61
+ index={index}
62
+ />
126
63
  </AspectRatio>
127
64
  ))}
128
65
  </Grid>
@@ -131,18 +68,27 @@ function ImageGrid() {
131
68
 
132
69
  // ─── Main Page ──────────────────────────────────────────────────────────────
133
70
 
71
+ // A definite ancestor for `Layout height="fill"`. 100dvh, not minHeight: '100%':
72
+ // a percentage min-height against an indefinite containing block computes to 0
73
+ // (CSS 2.1 10.5), so the soft failure would survive and the document would keep
74
+ // the scroll. LayoutContent is overflow:auto, so the definite height lands as a
75
+ // content-pane scroller rather than a clip. Same shape as dashboard.tsx and
76
+ // editor.tsx:302.
77
+ const pageStyle: CSSProperties = {height: '100dvh'};
78
+
134
79
  export default function SideGallery() {
135
80
  return (
136
81
  <Layout
82
+ style={pageStyle}
137
83
  height="fill"
138
84
  contentWidth={1400}
139
85
  content={
140
86
  <LayoutContent padding={6}>
141
- <Grid columns={{minWidth: 360, repeat: 'fit'}} gap={8} align="center">
87
+ <Grid columns={{minWidth: 280, repeat: 'fit'}} gap={8} align="center">
142
88
  {/* Left side: Text + CTA */}
143
89
  <VStack gap={6} vAlign="center">
144
90
  <VStack gap={3}>
145
- <Text type="supporting" color="secondary" weight="semibold">
91
+ <Text type="supporting" color="secondary">
146
92
  AFTER DARK
147
93
  </Text>
148
94
  <Heading level={1}>
@@ -157,9 +103,7 @@ export default function SideGallery() {
157
103
  </Text>
158
104
  </VStack>
159
105
 
160
- <HStack gap={3} vAlign="center">
161
- <Button label="Explore the night" variant="primary" />
162
- </HStack>
106
+ <Button label="Explore the night" variant="primary" />
163
107
 
164
108
  <VStack gap={4}>
165
109
  <Divider />
@@ -1,6 +1,10 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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=2] + B.primary"Raise issue") + (H[g=2 a=center] > PS + B.secondary"View Options")) + (LC[p=0] > T[hover] > (TR > THC*7) + (TR > TC*7)*4)
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] > 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"))
4
+
5
+ /**
6
+ * 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
+ */
4
8
 
5
9
  import React, {useState, useMemo} from 'react';
6
10
  import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
@@ -27,11 +31,12 @@ import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
27
31
  import {Popover} from '@astryxdesign/core/Popover';
28
32
  import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
29
33
  import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
30
- import {Center} from '@astryxdesign/core/Center';
31
34
  import {Icon} from '@astryxdesign/core/Icon';
32
35
  import {StatusDot} from '@astryxdesign/core/StatusDot';
33
36
  import {Divider} from '@astryxdesign/core/Divider';
34
37
  import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
38
+ import {Token} from '@astryxdesign/core/Token';
39
+ import {useMediaQuery} from '@astryxdesign/core/hooks';
35
40
  import {
36
41
  Table,
37
42
  TableRow,
@@ -87,9 +92,9 @@ interface TaskRow extends Record<string, unknown> {
87
92
 
88
93
  const STATUS_DOT_VARIANT: Record<
89
94
  TaskStatus,
90
- 'success' | 'accent' | 'neutral' | 'warning'
95
+ 'success' | 'neutral' | 'warning' | 'info'
91
96
  > = {
92
- in_progress: 'accent',
97
+ in_progress: 'info',
93
98
  todo: 'warning',
94
99
  backlog: 'neutral',
95
100
  done: 'success',
@@ -97,10 +102,10 @@ const STATUS_DOT_VARIANT: Record<
97
102
 
98
103
  const PRIORITY_COLOR: Record<
99
104
  TaskPriority,
100
- 'primary' | 'secondary' | 'disabled'
105
+ 'error' | 'warning' | 'secondary' | 'disabled'
101
106
  > = {
102
- urgent: 'primary',
103
- high: 'primary',
107
+ urgent: 'error',
108
+ high: 'warning',
104
109
  medium: 'secondary',
105
110
  low: 'disabled',
106
111
  none: 'disabled',
@@ -126,7 +131,7 @@ const allTasks: TaskRow[] = [
126
131
  {
127
132
  id: '2',
128
133
  taskId: 'T235040470',
129
- title: 'Use Projects to organize work for features or releases',
134
+ title: 'Chart the crypt to organize haunts for rituals or releases',
130
135
  subtitle: '',
131
136
  status: 'in_progress',
132
137
  priority: 'medium',
@@ -141,7 +146,7 @@ const allTasks: TaskRow[] = [
141
146
  {
142
147
  id: '3',
143
148
  taskId: 'T235040471',
144
- title: 'Use Cycles to focus work over n-weeks',
149
+ title: 'Follow moon cycles to focus haunts over n-weeks',
145
150
  subtitle: '',
146
151
  status: 'in_progress',
147
152
  priority: 'medium',
@@ -156,7 +161,7 @@ const allTasks: TaskRow[] = [
156
161
  {
157
162
  id: '4',
158
163
  taskId: 'T235040472',
159
- title: 'Testing code',
164
+ title: 'Test the gate wards before moonrise',
160
165
  subtitle: 'Update castle gate integration',
161
166
  status: 'todo',
162
167
  priority: 'medium',
@@ -231,7 +236,7 @@ const allTasks: TaskRow[] = [
231
236
  {
232
237
  id: '9',
233
238
  taskId: 'T235040477',
234
- title: 'Invite your teammates',
239
+ title: 'Invite your fellow familiars to the crypt',
235
240
  subtitle: '',
236
241
  status: 'todo',
237
242
  priority: 'low',
@@ -246,7 +251,7 @@ const allTasks: TaskRow[] = [
246
251
  {
247
252
  id: '10',
248
253
  taskId: 'T235040478',
249
- title: 'Next steps',
254
+ title: 'Next rites after moonrise',
250
255
  subtitle: '',
251
256
  status: 'todo',
252
257
  priority: 'none',
@@ -456,7 +461,7 @@ const allTasks: TaskRow[] = [
456
461
  {
457
462
  id: '24',
458
463
  taskId: 'T235040492',
459
- title: 'Add dark mode support to dashboard',
464
+ title: 'Audit dashboard contrast against the Dracula ramp',
460
465
  subtitle: '',
461
466
  status: 'backlog',
462
467
  priority: 'low',
@@ -588,7 +593,7 @@ function groupTasks(
588
593
  }
589
594
  const map = new Map<string, TaskRow[]>();
590
595
  for (const task of tasks) {
591
- const key = String(task[groupBy]) || '—';
596
+ const key = String(task[groupBy]) || 'None';
592
597
  let group = map.get(key);
593
598
  if (!group) {
594
599
  group = [];
@@ -630,7 +635,7 @@ const columns: TableColumn<TaskRow>[] = [
630
635
  {
631
636
  key: 'project',
632
637
  header: 'Project',
633
- width: pixel(144),
638
+ width: proportional(1),
634
639
  },
635
640
  {
636
641
  key: 'created',
@@ -731,7 +736,7 @@ function TaskDetailPanel({
731
736
  </HStack>
732
737
 
733
738
  <VStack gap={1}>
734
- <Heading level={3}>{task.title}</Heading>
739
+ <Heading level={2}>{task.title}</Heading>
735
740
  {task.subtitle && (
736
741
  <Text type="body" color="secondary">
737
742
  {task.subtitle}
@@ -741,10 +746,11 @@ function TaskDetailPanel({
741
746
 
742
747
  <MetadataList label={{position: 'start'}}>
743
748
  <MetadataListItem label="Status">
744
- <HStack gap={2} vAlign="center">
749
+ <HStack gap={1} vAlign="center">
745
750
  <StatusDot
746
751
  variant={STATUS_DOT_VARIANT[task.status]}
747
752
  label={STATUS_LABEL[task.status]}
753
+ isPulsing={task.status === 'in_progress'}
748
754
  />
749
755
  <Text type="body">{STATUS_LABEL[task.status]}</Text>
750
756
  </HStack>
@@ -770,7 +776,7 @@ function TaskDetailPanel({
770
776
  <Text type="body">{task.project}</Text>
771
777
  ) : (
772
778
  <Text type="supporting" color="secondary">
773
- —
779
+ None
774
780
  </Text>
775
781
  )}
776
782
  </MetadataListItem>
@@ -789,7 +795,7 @@ function TaskDetailPanel({
789
795
  <Text type="label">Labels</Text>
790
796
  <HStack gap={2}>
791
797
  {task.tags.map(tag => (
792
- <Badge key={tag} variant="yellow" label={tag} />
798
+ <Token key={tag} color="yellow" label={tag} />
793
799
  ))}
794
800
  </HStack>
795
801
  </VStack>
@@ -815,6 +821,10 @@ export default function TableGrouped() {
815
821
  () => new Set(GROUP_ORDER as string[]),
816
822
  );
817
823
 
824
+ // Responsive contract (see file header): below 1024px the inspector would
825
+ // squeeze the grouped table, so it steps aside entirely.
826
+ const isNarrow = useMediaQuery('(max-width: 1024px)');
827
+
818
828
  const filtered = useMemo(() => {
819
829
  return applyFilters(powerSearchFilters, allTasks);
820
830
  }, [powerSearchFilters, applyFilters]);
@@ -824,11 +834,13 @@ export default function TableGrouped() {
824
834
  [filtered, groupBy],
825
835
  );
826
836
 
827
- const groupKeys = useMemo(() => Array.from(grouped.keys()), [grouped]);
837
+ const groupKeys = Array.from(grouped.keys());
828
838
 
839
+ // Expand every group whenever the grouping itself changes. Keyed on the
840
+ // memoized Map, not on groupKeys, which is a fresh array each render.
829
841
  React.useEffect(() => {
830
- setExpandedGroups(new Set(groupKeys));
831
- }, [groupKeys]);
842
+ setExpandedGroups(new Set(grouped.keys()));
843
+ }, [grouped]);
832
844
 
833
845
  const toggleGroup = (key: string) => {
834
846
  setExpandedGroups(prev => {
@@ -856,16 +868,21 @@ export default function TableGrouped() {
856
868
  <Layout
857
869
  height="fill"
858
870
  header={
859
- <LayoutHeader hasDivider padding={4}>
871
+ <LayoutHeader hasDivider padding={6}>
860
872
  <VStack gap={4}>
861
873
  <HStack gap={3} vAlign="center">
862
874
  <StackItem size="fill">
863
- <Heading level={1}>Night-shift issues</Heading>
875
+ {/* maxLines={1}: this is the worst case in the set — 18
876
+ characters at 35px is 378px of a 342px header column, so
877
+ it wraps to THREE lines and the header measures 180px
878
+ (21.3% of a 390x844 viewport). Core wires maxLines to
879
+ useTruncation, so the full title stays reachable in a
880
+ Tooltip (SC 1.4.10). */}
881
+ <Heading level={1} type="display-2" maxLines={1}>Night-shift issues</Heading>
864
882
  </StackItem>
865
883
  <Button
866
884
  label="Raise issue"
867
885
  variant="primary"
868
- size="lg"
869
886
  onClick={() => setDialogOpen(true)}
870
887
  />
871
888
  </HStack>
@@ -875,14 +892,14 @@ export default function TableGrouped() {
875
892
  config={powerSearchConfig}
876
893
  filters={powerSearchFilters}
877
894
  onChange={newFilters => setPowerSearchFilters(newFilters)}
878
- placeholder="Filter issues..."
895
+ placeholder="Filter issues…"
879
896
  resultCount={`${filtered.length} issue${filtered.length !== 1 ? 's' : ''}`}
880
897
  />
881
898
  </StackItem>
882
899
  <Popover
883
900
  placement="below"
884
901
  alignment="end"
885
- width={320}
902
+ width="min(20rem, calc(100vw - 2 * var(--space-viewport)))"
886
903
  label="Grouping options"
887
904
  content={
888
905
  <VStack gap={4}>
@@ -900,7 +917,7 @@ export default function TableGrouped() {
900
917
  </RadioList>
901
918
  </VStack>
902
919
  }>
903
- <Button label="View Options" variant="secondary" size="md" />
920
+ <Button label="View options" variant="secondary" size="md" />
904
921
  </Popover>
905
922
  </HStack>
906
923
  </VStack>
@@ -951,7 +968,7 @@ export default function TableGrouped() {
951
968
  size="sm"
952
969
  color="secondary"
953
970
  />
954
- <Text type="body" weight="bold">
971
+ <Text type="body" weight="semibold">
955
972
  {getGroupLabel(groupBy, key)}
956
973
  </Text>
957
974
  <Badge
@@ -968,12 +985,21 @@ export default function TableGrouped() {
968
985
  key={task.id}
969
986
  onClick={() => setSelectedTask(task)}>
970
987
  <TableCell>
971
- <Center axis="horizontal">
988
+ {/* StatusDot paints nothing but an 8px dot — its
989
+ label is aria-label on a role="img" span — so
990
+ four states here read as hue alone. Same
991
+ dot-plus-word pairing as the summary row at
992
+ :751-757. */}
993
+ <HStack gap={1} vAlign="center">
972
994
  <StatusDot
973
995
  variant={STATUS_DOT_VARIANT[task.status]}
974
996
  label={STATUS_LABEL[task.status]}
997
+ isPulsing={task.status === 'in_progress'}
975
998
  />
976
- </Center>
999
+ <Text type="supporting" color="secondary">
1000
+ {STATUS_LABEL[task.status]}
1001
+ </Text>
1002
+ </HStack>
977
1003
  </TableCell>
978
1004
  <TableCell>
979
1005
  <HStack gap={3} vAlign="center" wrap="wrap">
@@ -1005,7 +1031,7 @@ export default function TableGrouped() {
1005
1031
  </Text>
1006
1032
  ) : (
1007
1033
  <Text type="supporting" color="secondary">
1008
- —
1034
+ None
1009
1035
  </Text>
1010
1036
  )}
1011
1037
  </TableCell>
@@ -1087,6 +1113,7 @@ export default function TableGrouped() {
1087
1113
  </LayoutContent>
1088
1114
  }
1089
1115
  end={
1116
+ !isNarrow &&
1090
1117
  selectedTask && (
1091
1118
  <>
1092
1119
  <ResizeHandle
@@ -1105,9 +1132,10 @@ export default function TableGrouped() {
1105
1132
  />
1106
1133
  <Dialog isOpen={dialogOpen} onOpenChange={open => setDialogOpen(open)}>
1107
1134
  <Layout
1135
+ height="auto"
1108
1136
  header={
1109
1137
  <DialogHeader
1110
- title="Raise an issue"
1138
+ title="Raise issue"
1111
1139
  onOpenChange={open => setDialogOpen(open)}
1112
1140
  />
1113
1141
  }
@@ -1161,7 +1189,7 @@ export default function TableGrouped() {
1161
1189
  onClick={() => setDialogOpen(false)}
1162
1190
  />
1163
1191
  <Button
1164
- label="Raise"
1192
+ label="Raise issue"
1165
1193
  variant="primary"
1166
1194
  size="md"
1167
1195
  onClick={() => setDialogOpen(false)}