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.
- package/BRAND.md +30 -18
- package/README.md +22 -11
- package/USAGE.md +29 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +174 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +94 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +67 -32
- package/templates/ai-chat.tsx +41 -20
- package/templates/blank.tsx +5 -2
- package/templates/centered-hero.tsx +15 -124
- package/templates/classic-gallery.tsx +21 -65
- package/templates/contact-form.tsx +62 -40
- package/templates/dashboard-portfolio.tsx +146 -227
- package/templates/dashboard.tsx +123 -121
- package/templates/detail-page.tsx +101 -95
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +85 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +33 -21
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +54 -74
- package/templates/editor.tsx +119 -89
- package/templates/file-explorer.tsx +133 -84
- package/templates/form-two-column.tsx +62 -124
- package/templates/gallery-hero.tsx +17 -171
- package/templates/ide.tsx +27 -21
- package/templates/incident-console.tsx +105 -69
- package/templates/kanban-board.tsx +100 -41
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +38 -41
- package/templates/login-card.tsx +59 -86
- package/templates/login-split.tsx +88 -128
- package/templates/login-sso.tsx +107 -68
- package/templates/login.tsx +108 -52
- package/templates/messaging-shell.tsx +172 -143
- package/templates/mixed-gallery.tsx +31 -130
- package/templates/payment-form.tsx +369 -289
- package/templates/product-detail.tsx +40 -57
- package/templates/product-gallery.tsx +31 -40
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +145 -286
- package/templates/settings-sidebar.tsx +179 -267
- package/templates/settings.tsx +42 -13
- package/templates/shell-nav.tsx +32 -23
- package/templates/shell-side-nav.tsx +30 -19
- package/templates/shell-top-nav.tsx +81 -43
- package/templates/side-gallery.tsx +39 -93
- package/templates/table-grouped.tsx +63 -33
- package/templates/table-page-chart.tsx +24 -297
- package/templates/table-page-heatmap-status.tsx +157 -91
- package/templates/table-page-shoe-store-heatmap.tsx +19 -288
- package/templates/table-page.tsx +24 -6
- package/templates/table.tsx +24 -7
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +313 -249
- package/theme.css +76 -14
- 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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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-
|
|
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: <
|
|
233
|
-
{alt: 'Harvest moon over midnight pines', scene: <
|
|
234
|
-
{alt: 'Bats crossing a
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
<
|
|
101
|
+
<Stack direction="vertical" gap={2} style={containerStyle}>
|
|
102
102
|
<Text type="label">Night counter</Text>
|
|
103
|
-
<
|
|
103
|
+
<Text type="display-1" hasTabularNumbers>
|
|
104
104
|
{count}
|
|
105
|
-
</
|
|
105
|
+
</Text>
|
|
106
106
|
<Button label="Increment" onClick={increment} />
|
|
107
107
|
<Button label="Reset" variant="secondary" onClick={reset} />
|
|
108
|
-
</
|
|
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:
|
|
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
|
-
<
|
|
511
|
-
|
|
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
|
-
|
|
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, '
|
|
273
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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={
|
|
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}
|
|
419
|
-
<Button label="Escalate" variant="secondary"
|
|
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
|
-
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
</
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
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
|
)}
|