astryx-dracula 0.2.0 → 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 +22 -11
  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 +67 -32
  31. package/templates/ai-chat.tsx +41 -20
  32. package/templates/blank.tsx +5 -2
  33. package/templates/centered-hero.tsx +15 -124
  34. package/templates/classic-gallery.tsx +21 -65
  35. package/templates/contact-form.tsx +62 -40
  36. package/templates/dashboard-portfolio.tsx +146 -227
  37. package/templates/dashboard.tsx +123 -121
  38. package/templates/detail-page.tsx +101 -95
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +85 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +33 -21
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +54 -74
  45. package/templates/editor.tsx +119 -89
  46. package/templates/file-explorer.tsx +133 -84
  47. package/templates/form-two-column.tsx +62 -124
  48. package/templates/gallery-hero.tsx +17 -171
  49. package/templates/ide.tsx +27 -21
  50. package/templates/incident-console.tsx +105 -69
  51. package/templates/kanban-board.tsx +100 -41
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +38 -41
  54. package/templates/login-card.tsx +59 -86
  55. package/templates/login-split.tsx +88 -128
  56. package/templates/login-sso.tsx +107 -68
  57. package/templates/login.tsx +108 -52
  58. package/templates/messaging-shell.tsx +172 -143
  59. package/templates/mixed-gallery.tsx +31 -130
  60. package/templates/payment-form.tsx +369 -289
  61. package/templates/product-detail.tsx +40 -57
  62. package/templates/product-gallery.tsx +31 -40
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +145 -286
  66. package/templates/settings-sidebar.tsx +179 -267
  67. package/templates/settings.tsx +42 -13
  68. package/templates/shell-nav.tsx +32 -23
  69. package/templates/shell-side-nav.tsx +30 -19
  70. package/templates/shell-top-nav.tsx +81 -43
  71. package/templates/side-gallery.tsx +39 -93
  72. package/templates/table-grouped.tsx +63 -33
  73. package/templates/table-page-chart.tsx +24 -297
  74. package/templates/table-page-heatmap-status.tsx +157 -91
  75. package/templates/table-page-shoe-store-heatmap.tsx +19 -288
  76. package/templates/table-page.tsx +24 -6
  77. package/templates/table.tsx +24 -7
  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 +313 -249
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > LC[p=6] > V[g=10] > (V[g=6 a=center] > (V[g=3 a=center] > Hd"Little haunts, everywhere you roam"[level=1 t=display-2] + Tx"Sometimes all it takes"[t=body]) + (H[j=center g=3] > B.primary"Enter the gallery" + B.secondary"Read the lore")) + (G[c={min:200} g=4] > AR*3)
2
4
 
3
- import type {CSSProperties} from 'react';
4
5
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
5
6
  import {Text, Heading} from '@astryxdesign/core/Text';
6
7
  import {Button} from '@astryxdesign/core/Button';
@@ -8,171 +9,16 @@ import {Icon} from '@astryxdesign/core/Icon';
8
9
  import {Grid} from '@astryxdesign/core/Grid';
9
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
10
11
  import {ArrowRight} from 'lucide-react';
12
+ import {galleryImage, galleryImageClip} from 'astryx-dracula/shared/gallery-image';
13
+ import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
11
14
 
12
- // NOTE: The only custom styling here is gallery fill + corner radius. It
13
- // exists because Astryx has no image primitive — AspectRatio exposes no
14
- // objectFit or radius props and there's no Image. Tracked in issue #2582;
15
- // replace these with component props once it lands.
16
- // Fills the AspectRatio box. No objectFit prop on AspectRatio (#2582).
17
- const galleryImage: CSSProperties = {
18
- width: '100%',
19
- height: '100%',
20
- display: 'block',
21
- };
22
- // Rounds the gallery corners. No radius prop on AspectRatio (#2582);
23
- // overflow clip masks the SVG scene to the rounded corners.
24
- const galleryImageClip: CSSProperties = {
25
- borderRadius: 'var(--radius-container)',
26
- overflow: 'clip',
27
- };
28
-
29
- function NightCastleScene() {
30
- return (
31
- <svg
32
- style={galleryImage}
33
- viewBox="0 0 400 500"
34
- role="img"
35
- aria-label="Moonlit castle on midnight hills">
36
- <title>Castle under a harvest moon</title>
37
- <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
38
- <g fill="var(--dracula-purple)">
39
- <circle cx="50" cy="60" r="2.5" />
40
- <circle cx="140" cy="130" r="2" />
41
- <circle cx="240" cy="50" r="2.5" />
42
- <circle cx="330" cy="110" r="2" />
43
- <circle cx="300" cy="200" r="2.5" />
44
- </g>
45
- <g fill="var(--dracula-cyan)">
46
- <circle cx="95" cy="90" r="2" />
47
- <circle cx="200" cy="100" r="2.5" />
48
- <circle cx="360" cy="60" r="2" />
49
- </g>
50
- <circle cx="290" cy="120" r="52" fill="var(--dracula-yellow)" />
51
- <circle
52
- cx="272"
53
- cy="106"
54
- r="9"
55
- fill="var(--dracula-orange)"
56
- opacity={0.5}
57
- />
58
- <g
59
- fill="none"
60
- stroke="var(--dracula-purple)"
61
- strokeWidth={4}
62
- strokeLinecap="round">
63
- <path d="M70 170 q12 -12 24 0 q12 -12 24 0" />
64
- <path d="M150 130 q9 -9 18 0 q9 -9 18 0" />
65
- </g>
66
- <path
67
- d="M0 360 Q140 300 260 350 T400 335 V500 H0 Z"
68
- fill="var(--dracula-selection)"
69
- />
70
- <path
71
- d="M0 420 Q160 365 300 410 T400 400 V500 H0 Z"
72
- fill="var(--dracula-bg-light)"
73
- />
74
- <g>
75
- <rect
76
- x="140"
77
- y="270"
78
- width="120"
79
- height="110"
80
- rx={4}
81
- fill="var(--dracula-bg-lighter)"
82
- />
83
- <rect
84
- x="118"
85
- y="235"
86
- width="44"
87
- height="145"
88
- rx={4}
89
- fill="var(--dracula-bg-lighter)"
90
- />
91
- <rect
92
- x="238"
93
- y="235"
94
- width="44"
95
- height="145"
96
- rx={4}
97
- fill="var(--dracula-bg-lighter)"
98
- />
99
- <g fill="var(--dracula-yellow)">
100
- <rect x="160" y="300" width="14" height="20" rx={2} />
101
- <rect x="196" y="300" width="14" height="20" rx={2} />
102
- <rect x="232" y="300" width="14" height="20" rx={2} />
103
- <rect x="131" y="262" width="10" height="14" rx={2} />
104
- <rect x="253" y="262" width="10" height="14" rx={2} />
105
- </g>
106
- <path
107
- d="M185 380 v-28 a15 15 0 0 1 30 0 v28 Z"
108
- fill="var(--dracula-bg-dark)"
109
- />
110
- </g>
111
- </svg>
112
- );
113
- }
114
-
115
- function HarvestMoonScene() {
116
- return (
117
- <svg
118
- style={galleryImage}
119
- viewBox="0 0 400 500"
120
- role="img"
121
- aria-label="Harvest moon over midnight pines">
122
- <title>Harvest moon over the pines</title>
123
- <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
124
- <g fill="var(--dracula-pink)">
125
- <circle cx="60" cy="80" r="2" />
126
- <circle cx="160" cy="50" r="2.5" />
127
- <circle cx="320" cy="70" r="2" />
128
- <circle cx="90" cy="190" r="2.5" />
129
- <circle cx="250" cy="160" r="2" />
130
- </g>
131
- <g fill="var(--dracula-cyan)">
132
- <circle cx="110" cy="120" r="2" />
133
- <circle cx="220" cy="90" r="2" />
134
- <circle cx="350" cy="150" r="2.5" />
135
- </g>
136
- <circle cx="200" cy="190" r="72" fill="var(--dracula-yellow)" />
137
- <circle
138
- cx="176"
139
- cy="170"
140
- r="12"
141
- fill="var(--dracula-orange)"
142
- opacity={0.5}
143
- />
144
- <circle
145
- cx="222"
146
- cy="210"
147
- r="8"
148
- fill="var(--dracula-orange)"
149
- opacity={0.5}
150
- />
151
- <g
152
- fill="none"
153
- stroke="var(--dracula-purple)"
154
- strokeWidth={4}
155
- strokeLinecap="round">
156
- <path d="M90 260 q12 -12 24 0 q12 -12 24 0" />
157
- <path d="M260 250 q9 -9 18 0 q9 -9 18 0" />
158
- </g>
159
- <path
160
- d="M0 380 Q200 320 400 370 V500 H0 Z"
161
- fill="var(--dracula-selection)"
162
- />
163
- <g fill="var(--dracula-bg-light)">
164
- <path d="M70 500 V400 l28 -44 28 44 v100 Z" />
165
- <path d="M170 500 V420 l28 -44 28 44 v80 Z" />
166
- <path d="M270 500 V410 l28 -44 28 44 v90 Z" />
167
- </g>
168
- <g fill="var(--dracula-yellow)">
169
- <rect x="81" y="442" width="8" height="12" rx={2} />
170
- <rect x="181" y="458" width="8" height="12" rx={2} />
171
- <rect x="281" y="450" width="8" height="12" rx={2} />
172
- </g>
173
- </svg>
174
- );
175
- }
15
+ // Castle + pines banners live in shared/scene-castle (`card`/`tall` variants:
16
+ // harvest-yellow moon, fg-stroke bats, per-scene hill constants). Fill +
17
+ // radius-clip live in shared/gallery-image (no Image primitive, #2582).
18
+ // The bats tile stays local: it is deliberately moonless-magic (the violet
19
+ // disc is ambient glow (comment grey), never tappable purple — the alt names the moonless sky), so the
20
+ // harvest-yellow rule does not apply; yellowing it would duplicate the pines
21
+ // tile instead.
176
22
 
177
23
  function BatFlightScene() {
178
24
  return (
@@ -180,10 +26,9 @@ function BatFlightScene() {
180
26
  style={galleryImage}
181
27
  viewBox="0 0 400 500"
182
28
  role="img"
183
- aria-label="Bats crossing a violet night sky">
184
- <title>Bats over violet hills</title>
29
+ aria-label="Bats crossing a moonless night sky">
185
30
  <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
186
- <circle cx="200" cy="140" r="60" fill="var(--dracula-purple)" />
31
+ <circle cx="200" cy="140" r="60" fill="var(--dracula-comment)" />
187
32
  <circle
188
33
  cx="200"
189
34
  cy="140"
@@ -229,14 +74,15 @@ function BatFlightScene() {
229
74
  }
230
75
 
231
76
  const SCENES = [
232
- {alt: 'Moonlit castle on midnight hills', scene: <NightCastleScene />},
233
- {alt: 'Harvest moon over midnight pines', scene: <HarvestMoonScene />},
234
- {alt: 'Bats crossing a violet night sky', scene: <BatFlightScene />},
77
+ {alt: 'Moonlit castle on midnight hills', scene: <SceneCastle variant="card" />},
78
+ {alt: 'Harvest moon over midnight pines', scene: <SceneCastle variant="tall" />},
79
+ {alt: 'Bats crossing a moonless night sky', scene: <BatFlightScene />},
235
80
  ];
236
81
 
237
82
  export default function GalleryHero() {
238
83
  return (
239
84
  <Layout
85
+ height="auto"
240
86
  content={
241
87
  <LayoutContent padding={6}>
242
88
  <VStack gap={10}>
package/templates/ide.tsx CHANGED
@@ -1,10 +1,14 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > (LP[p=4] > V[g=2] > TI"Search files" + TreeList) + (LC[p=0] > (Hd"Code editor"[level=1] + V > (SI[fill] > Cd) + (V > (TL > Tab"Terminal"! + Tab"Problems" + Tab"Output" + Tab"Debug") + Cd))) + (LP[p=4] > V[g=3] > (SG > SGI"Properties"! + SGI"History") + (V[g=3] > (V[g=1] > Hd"NightCounter.tsx"[level=3] + Tx"src/components/NightCounter.tsx"[t=supporting]) + ML + (V[g=2] > B.secondary"Format Document" + B.secondary"Go to Definition" + B.secondary"Find References")))
2
4
 
3
5
  import {useState, useMemo, type CSSProperties} from 'react';
4
6
 
5
7
  import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
8
+ import {StatusDot} from '@astryxdesign/core/StatusDot';
6
9
  import {ResizeHandle, useResizable} from '@astryxdesign/core/Resizable';
7
10
  import {Text, Heading} from '@astryxdesign/core/Text';
11
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
8
12
  import {Center} from '@astryxdesign/core/Center';
9
13
  import {CodeBlock} from '@astryxdesign/core/CodeBlock';
10
14
  import {EmptyState} from '@astryxdesign/core/EmptyState';
@@ -35,20 +39,21 @@ const styles: Record<string, CSSProperties> = {
35
39
  },
36
40
  tabListPadding: {
37
41
  paddingTop: 'var(--spacing-2)',
42
+ // 4px inline = offset 2 + ring thickness 2, so the focus ring lands INSIDE
43
+ // this box's clip instead of on its edge being cut in half. Deliberately
44
+ // NOT applied to `terminalPanel`: that would inset the CodeBlock itself,
45
+ // moving the code rather than giving the ring somewhere to live.
46
+ paddingInline: 'var(--spacing-1)',
38
47
  },
39
48
  metadataCompact: {
40
49
  gap: 'var(--spacing-1) var(--spacing-3)',
41
50
  },
42
- historyTimelineDot: {
43
- width: 8,
44
- height: 8,
45
- borderRadius: '50%',
46
- backgroundColor: 'var(--color-border-emphasized)',
47
- marginTop: 6,
48
- flexShrink: 0,
49
- },
51
+ // No `overflow` here, deliberately. The only child is a `CodeBlock` at
52
+ // `height: 100%` whose own root is `overflow: hidden`, so this box could
53
+ // never overflow and the scroller never fired — it only clipped. An
54
+ // `overflow: auto` on a parent whose sole child is a fixed-height
55
+ // `overflow: hidden` box is inert, which is what the deleted line was.
50
56
  editorArea: {
51
- overflow: 'auto',
52
57
  minHeight: 0,
53
58
  },
54
59
  fileExplorer: {
@@ -80,11 +85,6 @@ const containerStyle = {
80
85
  gap: 8,
81
86
  padding: 16,
82
87
  };
83
- const counterStyle = {
84
- fontSize: 48,
85
- fontWeight: 700,
86
- fontVariantNumeric: 'tabular-nums',
87
- };
88
88
 
89
89
  export default function NightCounter() {
90
90
  const [count, setCount] = useState(0);
@@ -98,14 +98,14 @@ export default function NightCounter() {
98
98
  }, []);
99
99
 
100
100
  return (
101
- <div style={containerStyle}>
101
+ <Stack direction="vertical" gap={2} style={containerStyle}>
102
102
  <Text type="label">Night counter</Text>
103
- <span style={counterStyle}>
103
+ <Text type="display-1" hasTabularNumbers>
104
104
  {count}
105
- </span>
105
+ </Text>
106
106
  <Button label="Increment" onClick={increment} />
107
107
  <Button label="Reset" variant="secondary" onClick={reset} />
108
- </div>
108
+ </Stack>
109
109
  );
110
110
  }`;
111
111
 
@@ -240,10 +240,12 @@ export default function IdeWorkspace() {
240
240
  collapsedSize: 50,
241
241
  });
242
242
 
243
+ // Capped so the terminal cannot swallow the canvas: the editor stack above
244
+ // has no minimum of its own, and an unbounded drag leaves nothing to grab.
243
245
  const bottomPanel = useResizable({
244
246
  defaultSize: 300,
245
247
  minSizePx: 80,
246
- maxSizePx: Infinity,
248
+ maxSizePx: 520,
247
249
  collapsible: true,
248
250
  collapsedSize: 40,
249
251
  });
@@ -255,6 +257,9 @@ export default function IdeWorkspace() {
255
257
  height="fill"
256
258
  content={
257
259
  <LayoutContent padding={0}>
260
+ <VisuallyHidden as="h1">
261
+ Code editor
262
+ </VisuallyHidden>
258
263
  <Layout
259
264
  height="fill"
260
265
  start={
@@ -507,8 +512,9 @@ export default function IdeWorkspace() {
507
512
  </Text>
508
513
  }
509
514
  startContent={
510
- <span
511
- style={styles.historyTimelineDot}
515
+ <StatusDot
516
+ variant="neutral"
517
+ label="Timeline marker"
512
518
  />
513
519
  }
514
520
  />
@@ -1,20 +1,9 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > (LH > H[g3 a=center] > (H[g2 a=center] > Hd"Night watch"[level=1] + Tx"3 investigating"[t=supporting]) + SegmentedControl"Filter by status" + B"Declare incident") + (LC[p0] > V[g0] > (H > PowerSearch"Search the night watch…") + (List > (ListItem)*7)) + (LayoutPanel[w380 p0] > V[g4] > (V[g2] > (H[g2 a=center] > StatusDot + Tx"INC-2417"[t=supporting] + Token"Investigating") + Hd"Checkout API elevated 5xx rate"[level=2]) + (H[g2] > B.primary"Mark mitigated" + B.secondary"Escalate") + Divider + MetadataList + Divider + (V[g2] > Hd"Timeline"[level=3] + List))
2
4
 
3
5
  /**
4
- * Incident Console — an on-call incident response tool for the night watch.
5
- *
6
- * Frame-first layout (see `npx astryx docs layout`), distilled from
7
- * product-scale apps built with the design system:
8
- *
9
- * Frame: header | grouped incident rows (fill) | inspector 380 (resizable)
10
- *
11
- * Responsive contract:
12
- * > 1024px header | rows | inspector 380
13
- * <= 1024px inspector hidden; rows keep full width
14
- *
15
- * Container policy (tracker archetype): dense data renders as rows —
16
- * edge-to-edge lists grouped by status, zero cards. Status is carried by
17
- * StatusDot (severity) and Token (state), not decorative badges.
6
+ * Incident Console — an on-call incident response tool for the night watch. Frame-first layout (see `bunx astryx docs layout`), distilled fro (Frame/responsive/container: see XLE header above.)
18
7
  */
19
8
 
20
9
  import {useMemo, useState, type CSSProperties} from 'react';
@@ -66,14 +55,23 @@ const styles: Record<string, CSSProperties> = {
66
55
  padding: 'var(--spacing-2) var(--spacing-3)',
67
56
  backgroundColor: 'var(--color-background-muted)',
68
57
  },
58
+ // All FOUR edges, not block-only. With `overflowY: auto` and no `overflowX`,
59
+ // the inline axis computes to `auto` too, so this box clips horizontally as
60
+ // well as vertically. A block-only padding lands green and still leaves the
61
+ // inline half of the ring clipped.
69
62
  rows: {
70
63
  overflowY: 'auto',
71
64
  minHeight: 0,
65
+ padding: 'var(--spacing-1)',
72
66
  },
73
67
  inspector: {
74
68
  padding: 'var(--spacing-4)',
75
69
  height: '100%',
76
- overflowY: 'auto',
70
+ },
71
+ controlsRow: {
72
+ // The four-option filter plus the action never fit a phone header; the
73
+ // row scrolls instead of clipping options the pointer cannot reach.
74
+ overflowX: 'auto',
77
75
  },
78
76
  };
79
77
 
@@ -269,8 +267,9 @@ const STATUS_LABEL: Record<Status, string> = {
269
267
  resolved: 'Resolved',
270
268
  };
271
269
 
272
- const STATUS_TOKEN_COLOR: Record<Status, 'red' | 'yellow' | 'green'> = {
273
- investigating: 'red',
270
+ const STATUS_TOKEN_COLOR: Record<Status, 'cyan' | 'yellow' | 'green'> = {
271
+ // Info vocab: an open investigation pulses cyan, never red.
272
+ investigating: 'cyan',
274
273
  mitigated: 'yellow',
275
274
  resolved: 'green',
276
275
  };
@@ -327,8 +326,8 @@ function IncidentRows({
327
326
  return (
328
327
  <EmptyState
329
328
  title="Quiet in the crypt"
330
- description="No incidents match — adjust the status filter or clear search filters."
331
- icon={<Icon icon={BellRing} size="lg" />}
329
+ description="No incidents match. Adjust the status filter or clear search filters."
330
+ icon={<Icon icon={BellRing} size="lg" color="secondary" />}
332
331
  />
333
332
  );
334
333
  }
@@ -338,10 +337,13 @@ function IncidentRows({
338
337
  {groups.map(group => (
339
338
  <VStack gap={0} key={group.status}>
340
339
  <HStack gap={2} vAlign="center" style={styles.groupHeader}>
341
- <Text type="label" color="secondary">
342
- {STATUS_LABEL[group.status]}
343
- </Text>
344
- <Text type="supporting" color="secondary" hasTabularNumbers>
340
+ {/* Primary, not secondary: --color-text-secondary #9AA1BC on
341
+ --color-background-muted #44475A is 3.57:1, under the 4.5:1
342
+ floor. The group heading is the string this sidebar is scanned
343
+ for. Hierarchy comes from the two type roles (principle 6),
344
+ not from dimming the count. */}
345
+ <Text type="label">{STATUS_LABEL[group.status]}</Text>
346
+ <Text type="supporting" hasTabularNumbers>
345
347
  {group.items.length}
346
348
  </Text>
347
349
  </HStack>
@@ -352,14 +354,20 @@ function IncidentRows({
352
354
  label={incident.title}
353
355
  description={`${incident.id} · ${incident.service} · ${incident.impact}`}
354
356
  startContent={
355
- <StatusDot
356
- variant={SEVERITY_DOT[incident.severity]}
357
- label={incident.severity.toUpperCase()}
358
- isPulsing={
359
- incident.severity === 'sev1' &&
360
- incident.status === 'investigating'
361
- }
362
- />
357
+ // StatusDot's label is aria-label on a role="img" span
358
+ // (core StatusDot.js:107-120), so it reaches assistive tech
359
+ // and paints nothing. A bare dot leaves SEV1/2/3 as hue
360
+ // alone. Dot and word travel together, as at :751-757.
361
+ <HStack gap={1} vAlign="center">
362
+ <StatusDot
363
+ variant={SEVERITY_DOT[incident.severity]}
364
+ label={incident.severity.toUpperCase()}
365
+ isPulsing={incident.status === 'investigating'}
366
+ />
367
+ <Text type="supporting" color="secondary">
368
+ {incident.severity.toUpperCase()}
369
+ </Text>
370
+ </HStack>
363
371
  }
364
372
  endContent={
365
373
  <HStack gap={3} vAlign="center">
@@ -397,11 +405,15 @@ function IncidentInspector({incident}: {incident: Incident}) {
397
405
  return (
398
406
  <VStack gap={4} style={styles.inspector}>
399
407
  <VStack gap={2}>
400
- <HStack gap={2} vAlign="center">
408
+ <HStack gap={1} vAlign="center">
401
409
  <StatusDot
402
410
  variant={SEVERITY_DOT[incident.severity]}
403
411
  label={incident.severity.toUpperCase()}
412
+ isPulsing={incident.status === 'investigating'}
404
413
  />
414
+ <Text type="supporting" color="secondary">
415
+ {incident.severity.toUpperCase()}
416
+ </Text>
405
417
  <Text type="supporting" color="secondary">
406
418
  {incident.id}
407
419
  </Text>
@@ -415,8 +427,8 @@ function IncidentInspector({incident}: {incident: Incident}) {
415
427
  </VStack>
416
428
 
417
429
  <HStack gap={2}>
418
- <Button label={nextAction} size="sm" />
419
- <Button label="Escalate" variant="secondary" size="sm" />
430
+ <Button label={nextAction} variant="primary" />
431
+ <Button label="Escalate" variant="secondary" />
420
432
  </HStack>
421
433
 
422
434
  <Divider />
@@ -489,46 +501,65 @@ export default function IncidentConsole() {
489
501
  incident => incident.status === 'investigating',
490
502
  ).length;
491
503
 
504
+ // Extracted so a narrow header can drop them onto their own scrollable row
505
+ // instead of squeezing the title, the filter and the action into one line.
506
+ const statusFilterControl = (
507
+ <SegmentedControl
508
+ label="Filter by status"
509
+ value={statusFilter}
510
+ onChange={setStatusFilter}
511
+ size="sm">
512
+ <SegmentedControlItem label="All" value="all" />
513
+ <SegmentedControlItem label="Investigating" value="investigating" />
514
+ <SegmentedControlItem label="Mitigated" value="mitigated" />
515
+ <SegmentedControlItem label="Resolved" value="resolved" />
516
+ </SegmentedControl>
517
+ );
518
+ const declareButton = (
519
+ <Button
520
+ label="Declare incident"
521
+ icon={<Icon icon={Plus} size="sm" />}
522
+ />
523
+ );
524
+ const titleGroup = (
525
+ <HStack gap={2} vAlign="center">
526
+ <Heading level={1} type="display-2">Night watch</Heading>
527
+ <Text type="supporting" color="secondary" hasTabularNumbers>
528
+ {openCount} investigating
529
+ </Text>
530
+ </HStack>
531
+ );
532
+
492
533
  return (
493
534
  <Layout
494
535
  height="fill"
495
536
  header={
496
- <LayoutHeader hasDivider>
497
- <HStack gap={3} vAlign="center">
498
- <StackItem size="fill">
499
- <HStack gap={2} vAlign="center">
500
- <Heading level={1}>Night watch</Heading>
501
- <Text
502
- type="supporting"
503
- color="secondary"
504
- hasTabularNumbers>
505
- {openCount} investigating
506
- </Text>
537
+ <LayoutHeader hasDivider padding={6}>
538
+ {isNarrow ? (
539
+ <VStack gap={2}>
540
+ {titleGroup}
541
+ <HStack
542
+ gap={2}
543
+ vAlign="center"
544
+ style={styles.controlsRow}
545
+ tabIndex={0}
546
+ role="region"
547
+ aria-label="Incident filter actions">
548
+ {statusFilterControl}
549
+ {declareButton}
507
550
  </HStack>
508
- </StackItem>
509
- <SegmentedControl
510
- label="Filter by status"
511
- value={statusFilter}
512
- onChange={setStatusFilter}
513
- size="sm">
514
- <SegmentedControlItem label="All" value="all" />
515
- <SegmentedControlItem
516
- label="Investigating"
517
- value="investigating"
518
- />
519
- <SegmentedControlItem label="Mitigated" value="mitigated" />
520
- <SegmentedControlItem label="Resolved" value="resolved" />
521
- </SegmentedControl>
522
- <Button
523
- label="Declare incident"
524
- icon={<Icon icon={Plus} size="sm" />}
525
- size="sm"
526
- />
527
- </HStack>
551
+ </VStack>
552
+ ) : (
553
+ <HStack gap={3} vAlign="center">
554
+ <StackItem size="fill">{titleGroup}</StackItem>
555
+ {statusFilterControl}
556
+ {declareButton}
557
+ </HStack>
558
+ )}
528
559
  </LayoutHeader>
529
560
  }
530
561
  content={
531
- <LayoutContent padding={0}>
562
+ <LayoutContent padding={0} isScrollable={false}>
532
563
  <VStack gap={0} style={styles.contentFill}>
533
564
  <HStack style={styles.searchRow}>
534
565
  <StackItem size="fill">
@@ -536,12 +567,17 @@ export default function IncidentConsole() {
536
567
  config={config}
537
568
  filters={filters}
538
569
  onChange={newFilters => setFilters([...newFilters])}
539
- placeholder="Search the night watch..."
570
+ placeholder="Search the night watch…"
540
571
  resultCount={visible.length}
541
572
  />
542
573
  </StackItem>
543
574
  </HStack>
544
- <StackItem size="fill" style={styles.rows}>
575
+ <StackItem
576
+ size="fill"
577
+ style={styles.rows}
578
+ tabIndex={0}
579
+ role="region"
580
+ aria-label="Incident rows">
545
581
  <IncidentRows
546
582
  incidents={visible}
547
583
  selectedId={selected?.id ?? null}
@@ -571,7 +607,7 @@ export default function IncidentConsole() {
571
607
  <EmptyState
572
608
  title="No incident under the lens"
573
609
  description="Select an incident from the rows to inspect its timeline."
574
- icon={<Icon icon={BellRing} size="lg" />}
610
+ icon={<Icon icon={BellRing} size="lg" color="secondary" />}
575
611
  isCompact
576
612
  />
577
613
  )}