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
@@ -2,7 +2,6 @@
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
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)
4
4
 
5
- import type {CSSProperties} from 'react';
6
5
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
7
6
  import {Text, Heading} from '@astryxdesign/core/Text';
8
7
  import {Button} from '@astryxdesign/core/Button';
@@ -10,171 +9,16 @@ import {Icon} from '@astryxdesign/core/Icon';
10
9
  import {Grid} from '@astryxdesign/core/Grid';
11
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
12
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';
13
14
 
14
- // NOTE: The only custom styling here is gallery fill + corner radius. It
15
- // exists because Astryx has no image primitive — AspectRatio exposes no
16
- // objectFit or radius props and there's no Image. Tracked in issue #2582;
17
- // replace these with component props once it lands.
18
- // Fills the AspectRatio box. No objectFit prop on AspectRatio (#2582).
19
- const galleryImage: CSSProperties = {
20
- width: '100%',
21
- height: '100%',
22
- display: 'block',
23
- };
24
- // Rounds the gallery corners. No radius prop on AspectRatio (#2582);
25
- // overflow clip masks the SVG scene to the rounded corners.
26
- const galleryImageClip: CSSProperties = {
27
- borderRadius: 'var(--radius-container)',
28
- overflow: 'clip',
29
- };
30
-
31
- function NightCastleScene() {
32
- return (
33
- <svg
34
- style={galleryImage}
35
- viewBox="0 0 400 500"
36
- role="img"
37
- aria-label="Moonlit castle on midnight hills">
38
- <title>Castle under a harvest moon</title>
39
- <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
40
- <g fill="var(--dracula-purple)">
41
- <circle cx="50" cy="60" r="2.5" />
42
- <circle cx="140" cy="130" r="2" />
43
- <circle cx="240" cy="50" r="2.5" />
44
- <circle cx="330" cy="110" r="2" />
45
- <circle cx="300" cy="200" r="2.5" />
46
- </g>
47
- <g fill="var(--dracula-cyan)">
48
- <circle cx="95" cy="90" r="2" />
49
- <circle cx="200" cy="100" r="2.5" />
50
- <circle cx="360" cy="60" r="2" />
51
- </g>
52
- <circle cx="290" cy="120" r="52" fill="var(--dracula-yellow)" />
53
- <circle
54
- cx="272"
55
- cy="106"
56
- r="9"
57
- fill="var(--dracula-orange)"
58
- opacity={0.5}
59
- />
60
- <g
61
- fill="none"
62
- stroke="var(--dracula-purple)"
63
- strokeWidth={4}
64
- strokeLinecap="round">
65
- <path d="M70 170 q12 -12 24 0 q12 -12 24 0" />
66
- <path d="M150 130 q9 -9 18 0 q9 -9 18 0" />
67
- </g>
68
- <path
69
- d="M0 360 Q140 300 260 350 T400 335 V500 H0 Z"
70
- fill="var(--dracula-selection)"
71
- />
72
- <path
73
- d="M0 420 Q160 365 300 410 T400 400 V500 H0 Z"
74
- fill="var(--dracula-bg-light)"
75
- />
76
- <g>
77
- <rect
78
- x="140"
79
- y="270"
80
- width="120"
81
- height="110"
82
- rx={4}
83
- fill="var(--dracula-bg-lighter)"
84
- />
85
- <rect
86
- x="118"
87
- y="235"
88
- width="44"
89
- height="145"
90
- rx={4}
91
- fill="var(--dracula-bg-lighter)"
92
- />
93
- <rect
94
- x="238"
95
- y="235"
96
- width="44"
97
- height="145"
98
- rx={4}
99
- fill="var(--dracula-bg-lighter)"
100
- />
101
- <g fill="var(--dracula-yellow)">
102
- <rect x="160" y="300" width="14" height="20" rx={2} />
103
- <rect x="196" y="300" width="14" height="20" rx={2} />
104
- <rect x="232" y="300" width="14" height="20" rx={2} />
105
- <rect x="131" y="262" width="10" height="14" rx={2} />
106
- <rect x="253" y="262" width="10" height="14" rx={2} />
107
- </g>
108
- <path
109
- d="M185 380 v-28 a15 15 0 0 1 30 0 v28 Z"
110
- fill="var(--dracula-bg-dark)"
111
- />
112
- </g>
113
- </svg>
114
- );
115
- }
116
-
117
- function HarvestMoonScene() {
118
- return (
119
- <svg
120
- style={galleryImage}
121
- viewBox="0 0 400 500"
122
- role="img"
123
- aria-label="Harvest moon over midnight pines">
124
- <title>Harvest moon over the pines</title>
125
- <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
126
- <g fill="var(--dracula-pink)">
127
- <circle cx="60" cy="80" r="2" />
128
- <circle cx="160" cy="50" r="2.5" />
129
- <circle cx="320" cy="70" r="2" />
130
- <circle cx="90" cy="190" r="2.5" />
131
- <circle cx="250" cy="160" r="2" />
132
- </g>
133
- <g fill="var(--dracula-cyan)">
134
- <circle cx="110" cy="120" r="2" />
135
- <circle cx="220" cy="90" r="2" />
136
- <circle cx="350" cy="150" r="2.5" />
137
- </g>
138
- <circle cx="200" cy="190" r="72" fill="var(--dracula-yellow)" />
139
- <circle
140
- cx="176"
141
- cy="170"
142
- r="12"
143
- fill="var(--dracula-orange)"
144
- opacity={0.5}
145
- />
146
- <circle
147
- cx="222"
148
- cy="210"
149
- r="8"
150
- fill="var(--dracula-orange)"
151
- opacity={0.5}
152
- />
153
- <g
154
- fill="none"
155
- stroke="var(--dracula-purple)"
156
- strokeWidth={4}
157
- strokeLinecap="round">
158
- <path d="M90 260 q12 -12 24 0 q12 -12 24 0" />
159
- <path d="M260 250 q9 -9 18 0 q9 -9 18 0" />
160
- </g>
161
- <path
162
- d="M0 380 Q200 320 400 370 V500 H0 Z"
163
- fill="var(--dracula-selection)"
164
- />
165
- <g fill="var(--dracula-bg-light)">
166
- <path d="M70 500 V400 l28 -44 28 44 v100 Z" />
167
- <path d="M170 500 V420 l28 -44 28 44 v80 Z" />
168
- <path d="M270 500 V410 l28 -44 28 44 v90 Z" />
169
- </g>
170
- <g fill="var(--dracula-yellow)">
171
- <rect x="81" y="442" width="8" height="12" rx={2} />
172
- <rect x="181" y="458" width="8" height="12" rx={2} />
173
- <rect x="281" y="450" width="8" height="12" rx={2} />
174
- </g>
175
- </svg>
176
- );
177
- }
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.
178
22
 
179
23
  function BatFlightScene() {
180
24
  return (
@@ -182,10 +26,9 @@ function BatFlightScene() {
182
26
  style={galleryImage}
183
27
  viewBox="0 0 400 500"
184
28
  role="img"
185
- aria-label="Bats crossing a violet night sky">
186
- <title>Bats over violet hills</title>
29
+ aria-label="Bats crossing a moonless night sky">
187
30
  <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
188
- <circle cx="200" cy="140" r="60" fill="var(--dracula-purple)" />
31
+ <circle cx="200" cy="140" r="60" fill="var(--dracula-comment)" />
189
32
  <circle
190
33
  cx="200"
191
34
  cy="140"
@@ -231,14 +74,15 @@ function BatFlightScene() {
231
74
  }
232
75
 
233
76
  const SCENES = [
234
- {alt: 'Moonlit castle on midnight hills', scene: <NightCastleScene />},
235
- {alt: 'Harvest moon over midnight pines', scene: <HarvestMoonScene />},
236
- {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 />},
237
80
  ];
238
81
 
239
82
  export default function GalleryHero() {
240
83
  return (
241
84
  <Layout
85
+ height="auto"
242
86
  content={
243
87
  <LayoutContent padding={6}>
244
88
  <VStack gap={10}>
package/templates/ide.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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] > 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")))
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")))
4
4
 
5
5
  import {useState, useMemo, type CSSProperties} from 'react';
6
6
 
@@ -8,6 +8,7 @@ import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
8
8
  import {StatusDot} from '@astryxdesign/core/StatusDot';
9
9
  import {ResizeHandle, useResizable} from '@astryxdesign/core/Resizable';
10
10
  import {Text, Heading} from '@astryxdesign/core/Text';
11
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
11
12
  import {Center} from '@astryxdesign/core/Center';
12
13
  import {CodeBlock} from '@astryxdesign/core/CodeBlock';
13
14
  import {EmptyState} from '@astryxdesign/core/EmptyState';
@@ -38,20 +39,21 @@ const styles: Record<string, CSSProperties> = {
38
39
  },
39
40
  tabListPadding: {
40
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)',
41
47
  },
42
48
  metadataCompact: {
43
49
  gap: 'var(--spacing-1) var(--spacing-3)',
44
50
  },
45
- historyTimelineDot: {
46
- width: 8,
47
- height: 8,
48
- borderRadius: '50%',
49
- backgroundColor: 'var(--color-border-emphasized)',
50
- marginTop: 6,
51
- flexShrink: 0,
52
- },
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.
53
56
  editorArea: {
54
- overflow: 'auto',
55
57
  minHeight: 0,
56
58
  },
57
59
  fileExplorer: {
@@ -83,11 +85,6 @@ const containerStyle = {
83
85
  gap: 8,
84
86
  padding: 16,
85
87
  };
86
- const counterStyle = {
87
- fontSize: 48,
88
- fontWeight: 700,
89
- fontVariantNumeric: 'tabular-nums',
90
- };
91
88
 
92
89
  export default function NightCounter() {
93
90
  const [count, setCount] = useState(0);
@@ -101,14 +98,14 @@ export default function NightCounter() {
101
98
  }, []);
102
99
 
103
100
  return (
104
- <div style={containerStyle}>
101
+ <Stack direction="vertical" gap={2} style={containerStyle}>
105
102
  <Text type="label">Night counter</Text>
106
- <span style={counterStyle}>
103
+ <Text type="display-1" hasTabularNumbers>
107
104
  {count}
108
- </span>
105
+ </Text>
109
106
  <Button label="Increment" onClick={increment} />
110
107
  <Button label="Reset" variant="secondary" onClick={reset} />
111
- </div>
108
+ </Stack>
112
109
  );
113
110
  }`;
114
111
 
@@ -243,10 +240,12 @@ export default function IdeWorkspace() {
243
240
  collapsedSize: 50,
244
241
  });
245
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.
246
245
  const bottomPanel = useResizable({
247
246
  defaultSize: 300,
248
247
  minSizePx: 80,
249
- maxSizePx: Infinity,
248
+ maxSizePx: 520,
250
249
  collapsible: true,
251
250
  collapsedSize: 40,
252
251
  });
@@ -258,6 +257,9 @@ export default function IdeWorkspace() {
258
257
  height="fill"
259
258
  content={
260
259
  <LayoutContent padding={0}>
260
+ <VisuallyHidden as="h1">
261
+ Code editor
262
+ </VisuallyHidden>
261
263
  <Layout
262
264
  height="fill"
263
265
  start={
@@ -1,22 +1,9 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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)*6)) + (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))
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))
4
4
 
5
5
  /**
6
- * Incident Console — an on-call incident response tool for the night watch.
7
- *
8
- * Frame-first layout (see `npx astryx docs layout`), distilled from
9
- * product-scale apps built with the design system:
10
- *
11
- * Frame: header | grouped incident rows (fill) | inspector 380 (resizable)
12
- *
13
- * Responsive contract:
14
- * > 1024px header | rows | inspector 380
15
- * <= 1024px inspector hidden; rows keep full width
16
- *
17
- * Container policy (tracker archetype): dense data renders as rows —
18
- * edge-to-edge lists grouped by status, zero cards. Status is carried by
19
- * 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.)
20
7
  */
21
8
 
22
9
  import {useMemo, useState, type CSSProperties} from 'react';
@@ -68,14 +55,23 @@ const styles: Record<string, CSSProperties> = {
68
55
  padding: 'var(--spacing-2) var(--spacing-3)',
69
56
  backgroundColor: 'var(--color-background-muted)',
70
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.
71
62
  rows: {
72
63
  overflowY: 'auto',
73
64
  minHeight: 0,
65
+ padding: 'var(--spacing-1)',
74
66
  },
75
67
  inspector: {
76
68
  padding: 'var(--spacing-4)',
77
69
  height: '100%',
78
- 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',
79
75
  },
80
76
  };
81
77
 
@@ -271,8 +267,9 @@ const STATUS_LABEL: Record<Status, string> = {
271
267
  resolved: 'Resolved',
272
268
  };
273
269
 
274
- const STATUS_TOKEN_COLOR: Record<Status, 'red' | 'yellow' | 'green'> = {
275
- 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',
276
273
  mitigated: 'yellow',
277
274
  resolved: 'green',
278
275
  };
@@ -329,8 +326,8 @@ function IncidentRows({
329
326
  return (
330
327
  <EmptyState
331
328
  title="Quiet in the crypt"
332
- description="No incidents match — adjust the status filter or clear search filters."
333
- 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" />}
334
331
  />
335
332
  );
336
333
  }
@@ -340,10 +337,13 @@ function IncidentRows({
340
337
  {groups.map(group => (
341
338
  <VStack gap={0} key={group.status}>
342
339
  <HStack gap={2} vAlign="center" style={styles.groupHeader}>
343
- <Text type="label" color="secondary">
344
- {STATUS_LABEL[group.status]}
345
- </Text>
346
- <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>
347
347
  {group.items.length}
348
348
  </Text>
349
349
  </HStack>
@@ -354,14 +354,20 @@ function IncidentRows({
354
354
  label={incident.title}
355
355
  description={`${incident.id} · ${incident.service} · ${incident.impact}`}
356
356
  startContent={
357
- <StatusDot
358
- variant={SEVERITY_DOT[incident.severity]}
359
- label={incident.severity.toUpperCase()}
360
- isPulsing={
361
- incident.severity === 'sev1' &&
362
- incident.status === 'investigating'
363
- }
364
- />
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>
365
371
  }
366
372
  endContent={
367
373
  <HStack gap={3} vAlign="center">
@@ -399,11 +405,15 @@ function IncidentInspector({incident}: {incident: Incident}) {
399
405
  return (
400
406
  <VStack gap={4} style={styles.inspector}>
401
407
  <VStack gap={2}>
402
- <HStack gap={2} vAlign="center">
408
+ <HStack gap={1} vAlign="center">
403
409
  <StatusDot
404
410
  variant={SEVERITY_DOT[incident.severity]}
405
411
  label={incident.severity.toUpperCase()}
412
+ isPulsing={incident.status === 'investigating'}
406
413
  />
414
+ <Text type="supporting" color="secondary">
415
+ {incident.severity.toUpperCase()}
416
+ </Text>
407
417
  <Text type="supporting" color="secondary">
408
418
  {incident.id}
409
419
  </Text>
@@ -417,8 +427,8 @@ function IncidentInspector({incident}: {incident: Incident}) {
417
427
  </VStack>
418
428
 
419
429
  <HStack gap={2}>
420
- <Button label={nextAction} variant="primary" size="sm" />
421
- <Button label="Escalate" variant="secondary" size="sm" />
430
+ <Button label={nextAction} variant="primary" />
431
+ <Button label="Escalate" variant="secondary" />
422
432
  </HStack>
423
433
 
424
434
  <Divider />
@@ -491,46 +501,65 @@ export default function IncidentConsole() {
491
501
  incident => incident.status === 'investigating',
492
502
  ).length;
493
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
+
494
533
  return (
495
534
  <Layout
496
535
  height="fill"
497
536
  header={
498
- <LayoutHeader hasDivider>
499
- <HStack gap={3} vAlign="center">
500
- <StackItem size="fill">
501
- <HStack gap={2} vAlign="center">
502
- <Heading level={1}>Night watch</Heading>
503
- <Text
504
- type="supporting"
505
- color="secondary"
506
- hasTabularNumbers>
507
- {openCount} investigating
508
- </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}
509
550
  </HStack>
510
- </StackItem>
511
- <SegmentedControl
512
- label="Filter by status"
513
- value={statusFilter}
514
- onChange={setStatusFilter}
515
- size="sm">
516
- <SegmentedControlItem label="All" value="all" />
517
- <SegmentedControlItem
518
- label="Investigating"
519
- value="investigating"
520
- />
521
- <SegmentedControlItem label="Mitigated" value="mitigated" />
522
- <SegmentedControlItem label="Resolved" value="resolved" />
523
- </SegmentedControl>
524
- <Button
525
- label="Declare incident"
526
- icon={<Icon icon={Plus} size="sm" />}
527
- size="sm"
528
- />
529
- </HStack>
551
+ </VStack>
552
+ ) : (
553
+ <HStack gap={3} vAlign="center">
554
+ <StackItem size="fill">{titleGroup}</StackItem>
555
+ {statusFilterControl}
556
+ {declareButton}
557
+ </HStack>
558
+ )}
530
559
  </LayoutHeader>
531
560
  }
532
561
  content={
533
- <LayoutContent padding={0}>
562
+ <LayoutContent padding={0} isScrollable={false}>
534
563
  <VStack gap={0} style={styles.contentFill}>
535
564
  <HStack style={styles.searchRow}>
536
565
  <StackItem size="fill">
@@ -538,12 +567,17 @@ export default function IncidentConsole() {
538
567
  config={config}
539
568
  filters={filters}
540
569
  onChange={newFilters => setFilters([...newFilters])}
541
- placeholder="Search the night watch..."
570
+ placeholder="Search the night watch…"
542
571
  resultCount={visible.length}
543
572
  />
544
573
  </StackItem>
545
574
  </HStack>
546
- <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">
547
581
  <IncidentRows
548
582
  incidents={visible}
549
583
  selectedId={selected?.id ?? null}
@@ -573,7 +607,7 @@ export default function IncidentConsole() {
573
607
  <EmptyState
574
608
  title="No incident under the lens"
575
609
  description="Select an incident from the rows to inspect its timeline."
576
- icon={<Icon icon={BellRing} size="lg" />}
610
+ icon={<Icon icon={BellRing} size="lg" color="secondary" />}
577
611
  isCompact
578
612
  />
579
613
  )}