astryx-dracula 0.5.0 → 0.6.2
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 +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
5
|
+
// L[h=fill] > (LH[divider p=6] > H[g=2 a=center] > (SI[size=fill] > Hd"Status"[level=1 t=display-2]) + IB"Filter"[variant=ghost] + IB"Export"[variant=ghost] + B.secondary"Refresh") + (LC[p=3] > V[g=4] > (V[g=3] > C[p=4] + (V[g=1] > (H[g=2 a=center] > (H[g=1 a=center] > Sw + Tx"0"[t=supporting])*4) + Tx"Incidents by day and hour · Jan 12–18"[t=supporting])) + (T[hover density=balanced dv=rows] > (TR > THC"Incident" + THC"Product" + THC"Description" + THC"Started" + THC"Duration" + THC"On-call" + THC"Status" + THC"Date") + (TR > TC"INC-4008" + TC"Business Suite" + TC"Notification delivery delays" + TC"09:15" + TC"ongoing" + TC"Carlos Mendez" + (TC > Tk.red"Ongoing") + TC"2025-01-15")*14))
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Status — the incident heatmap for the week plus the log behind it.
|
|
@@ -19,388 +21,359 @@
|
|
|
19
21
|
* viewports scroll the table horizontally instead of widening the page.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
|
-
import
|
|
24
|
+
import { Avatar } from "@astryxdesign/core/Avatar";
|
|
25
|
+
import { Button } from "@astryxdesign/core/Button";
|
|
26
|
+
import { Card } from "@astryxdesign/core/Card";
|
|
27
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
28
|
+
import { IconButton } from "@astryxdesign/core/IconButton";
|
|
23
29
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} from
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
41
|
-
import {
|
|
42
|
-
import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
|
|
43
|
-
import {ChartLegend, type ChartLegendEntry} from 'astryx-dracula/shared/chart-legend';
|
|
44
|
-
import {CHART_PANEL_STYLE} from 'astryx-dracula/shared/chart-panel-style';
|
|
45
|
-
import {ChartLabel} from 'astryx-dracula/shared/chart-labels';
|
|
30
|
+
HStack,
|
|
31
|
+
Layout,
|
|
32
|
+
LayoutContent,
|
|
33
|
+
LayoutHeader,
|
|
34
|
+
StackItem,
|
|
35
|
+
VStack,
|
|
36
|
+
} from "@astryxdesign/core/Layout";
|
|
37
|
+
import { Link } from "@astryxdesign/core/Link";
|
|
38
|
+
import type { TableColumn } from "@astryxdesign/core/Table";
|
|
39
|
+
import { pixel, proportional, Table } from "@astryxdesign/core/Table";
|
|
40
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
41
|
+
import { Token } from "@astryxdesign/core/Token";
|
|
42
|
+
import { CHART_HUES } from "astryx-dracula/shared/chart-hues";
|
|
43
|
+
import { ChartLabel } from "astryx-dracula/shared/chart-labels";
|
|
44
|
+
import { ChartLegend, type ChartLegendEntry } from "astryx-dracula/shared/chart-legend";
|
|
45
|
+
import { CHART_PANEL_STYLE } from "astryx-dracula/shared/chart-panel-style";
|
|
46
|
+
import { Download, Filter, RotateCw } from "lucide-react";
|
|
47
|
+
import type { CSSProperties } from "react";
|
|
46
48
|
|
|
47
49
|
// ============= ICONS (verified lucide-react exports) =============
|
|
48
50
|
// Filter ← FunnelIcon, Download ← ArrowDownTrayIcon, RotateCw ← ArrowPathIcon.
|
|
49
51
|
|
|
50
52
|
// ============= DATA =============
|
|
51
53
|
type ProductName =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
| "Ads Manager"
|
|
55
|
+
| "Business Suite"
|
|
56
|
+
| "Commerce Manager"
|
|
57
|
+
| "Pages"
|
|
58
|
+
| "Messenger API"
|
|
59
|
+
| "Graph API"
|
|
60
|
+
| "WhatsApp Business"
|
|
61
|
+
| "Instagram API";
|
|
60
62
|
|
|
61
63
|
interface IncidentRow extends Record<string, unknown> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
id: string;
|
|
65
|
+
product: ProductName;
|
|
66
|
+
title: string;
|
|
67
|
+
status: "ongoing" | "identified" | "monitoring" | "resolved";
|
|
68
|
+
oncall: string;
|
|
69
|
+
startTime: string;
|
|
70
|
+
duration: string;
|
|
71
|
+
date: string;
|
|
72
|
+
dayOfWeek: number;
|
|
73
|
+
hour: number;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
const incidents: IncidentRow[] = [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
hour: 15,
|
|
263
|
-
},
|
|
77
|
+
// Active — ongoing
|
|
78
|
+
{
|
|
79
|
+
id: "INC-4008",
|
|
80
|
+
product: "Business Suite",
|
|
81
|
+
title:
|
|
82
|
+
"Notification delivery delays affecting mobile push and email channels, queue saturation under investigation",
|
|
83
|
+
oncall: "Carlos Mendez",
|
|
84
|
+
status: "ongoing",
|
|
85
|
+
startTime: "09:15",
|
|
86
|
+
duration: "ongoing",
|
|
87
|
+
date: "2025-01-15",
|
|
88
|
+
dayOfWeek: 3,
|
|
89
|
+
hour: 9,
|
|
90
|
+
},
|
|
91
|
+
// Tue (2) 3pm — the only major outage cluster (3 incidents)
|
|
92
|
+
{
|
|
93
|
+
id: "INC-4001",
|
|
94
|
+
product: "Ads Manager",
|
|
95
|
+
title:
|
|
96
|
+
"Campaign creation timing out for ~12% of advertisers in EMEA region, root caused to elevated DB write latency",
|
|
97
|
+
oncall: "Sarah Chen",
|
|
98
|
+
status: "resolved",
|
|
99
|
+
startTime: "15:10",
|
|
100
|
+
duration: "2h 15m",
|
|
101
|
+
date: "2025-01-14",
|
|
102
|
+
dayOfWeek: 2,
|
|
103
|
+
hour: 15,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "INC-4002",
|
|
107
|
+
product: "Graph API",
|
|
108
|
+
title:
|
|
109
|
+
"Elevated 5xx error rates on /me and /me/accounts endpoints affecting third-party integrations globally",
|
|
110
|
+
oncall: "David Kim",
|
|
111
|
+
status: "resolved",
|
|
112
|
+
startTime: "15:30",
|
|
113
|
+
duration: "3h 20m",
|
|
114
|
+
date: "2025-01-14",
|
|
115
|
+
dayOfWeek: 2,
|
|
116
|
+
hour: 15,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "INC-4003",
|
|
120
|
+
product: "Instagram API",
|
|
121
|
+
title:
|
|
122
|
+
"Media upload failures for image and video content via /media endpoint affecting ~8% of publishers",
|
|
123
|
+
oncall: "Priya Sharma",
|
|
124
|
+
status: "resolved",
|
|
125
|
+
startTime: "15:05",
|
|
126
|
+
duration: "1h 50m",
|
|
127
|
+
date: "2025-01-14",
|
|
128
|
+
dayOfWeek: 2,
|
|
129
|
+
hour: 15,
|
|
130
|
+
},
|
|
131
|
+
// Tue (2) 4pm — spillover (2 incidents)
|
|
132
|
+
{
|
|
133
|
+
id: "INC-4004",
|
|
134
|
+
product: "Ads Manager",
|
|
135
|
+
title:
|
|
136
|
+
"Audience insights tab returning empty results due to cache invalidation cascade from earlier outage",
|
|
137
|
+
oncall: "Marcus Rivera",
|
|
138
|
+
status: "resolved",
|
|
139
|
+
startTime: "16:00",
|
|
140
|
+
duration: "45m",
|
|
141
|
+
date: "2025-01-14",
|
|
142
|
+
dayOfWeek: 2,
|
|
143
|
+
hour: 16,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "INC-4005",
|
|
147
|
+
product: "WhatsApp Business",
|
|
148
|
+
title:
|
|
149
|
+
"Outbound message delivery delays of 2-5 minutes for high-volume senders due to queue backpressure",
|
|
150
|
+
oncall: "Elena Volkov",
|
|
151
|
+
status: "resolved",
|
|
152
|
+
startTime: "16:20",
|
|
153
|
+
duration: "1h 15m",
|
|
154
|
+
date: "2025-01-14",
|
|
155
|
+
dayOfWeek: 2,
|
|
156
|
+
hour: 16,
|
|
157
|
+
},
|
|
158
|
+
// Thu (4) 10am — single incident
|
|
159
|
+
{
|
|
160
|
+
id: "INC-4006",
|
|
161
|
+
product: "Commerce Manager",
|
|
162
|
+
title:
|
|
163
|
+
"Product catalog sync failures from Shopify and BigCommerce integrations after schema migration deployed",
|
|
164
|
+
oncall: "Noah Williams",
|
|
165
|
+
status: "resolved",
|
|
166
|
+
startTime: "10:15",
|
|
167
|
+
duration: "2h 40m",
|
|
168
|
+
date: "2025-01-16",
|
|
169
|
+
dayOfWeek: 4,
|
|
170
|
+
hour: 10,
|
|
171
|
+
},
|
|
172
|
+
// Fri (5) 4pm — single incident
|
|
173
|
+
{
|
|
174
|
+
id: "INC-4007",
|
|
175
|
+
product: "Messenger API",
|
|
176
|
+
title:
|
|
177
|
+
"Webhook delivery failures for subscribed page events, retries succeeded but signaled false alerts",
|
|
178
|
+
oncall: "Mei Lin",
|
|
179
|
+
status: "resolved",
|
|
180
|
+
startTime: "16:30",
|
|
181
|
+
duration: "45m",
|
|
182
|
+
date: "2025-01-17",
|
|
183
|
+
dayOfWeek: 5,
|
|
184
|
+
hour: 16,
|
|
185
|
+
},
|
|
186
|
+
// Active incident
|
|
187
|
+
// Scattered minor blips — light pink cells
|
|
188
|
+
{
|
|
189
|
+
id: "INC-4009",
|
|
190
|
+
product: "Pages",
|
|
191
|
+
title:
|
|
192
|
+
"Image upload timeouts for posts larger than 4MB, auto-recovered after CDN region failover",
|
|
193
|
+
oncall: "Fatima Al-Rashid",
|
|
194
|
+
status: "resolved",
|
|
195
|
+
startTime: "11:00",
|
|
196
|
+
duration: "20m",
|
|
197
|
+
date: "2025-01-12",
|
|
198
|
+
dayOfWeek: 0,
|
|
199
|
+
hour: 11,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "INC-4010",
|
|
203
|
+
product: "Graph API",
|
|
204
|
+
title:
|
|
205
|
+
"Intermittent 503 errors on a single load balancer in us-east-1, auto-healed by health check rotation",
|
|
206
|
+
oncall: "Lucas Andersson",
|
|
207
|
+
status: "resolved",
|
|
208
|
+
startTime: "14:30",
|
|
209
|
+
duration: "15m",
|
|
210
|
+
date: "2025-01-13",
|
|
211
|
+
dayOfWeek: 1,
|
|
212
|
+
hour: 14,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "INC-4011",
|
|
216
|
+
product: "WhatsApp Business",
|
|
217
|
+
title: "Template message rejections due to stale category cache for newly approved templates",
|
|
218
|
+
oncall: "Sofia Garcia",
|
|
219
|
+
status: "resolved",
|
|
220
|
+
startTime: "13:00",
|
|
221
|
+
duration: "30m",
|
|
222
|
+
date: "2025-01-16",
|
|
223
|
+
dayOfWeek: 4,
|
|
224
|
+
hour: 13,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "INC-4012",
|
|
228
|
+
product: "Commerce Manager",
|
|
229
|
+
title:
|
|
230
|
+
"Inventory count mismatch between checkout and catalog services for ~200 SKUs in test region",
|
|
231
|
+
oncall: "Raj Kapoor",
|
|
232
|
+
status: "resolved",
|
|
233
|
+
startTime: "12:00",
|
|
234
|
+
duration: "1h 00m",
|
|
235
|
+
date: "2025-01-17",
|
|
236
|
+
dayOfWeek: 5,
|
|
237
|
+
hour: 12,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "INC-4013",
|
|
241
|
+
product: "Instagram API",
|
|
242
|
+
title: "Story insights data delayed by 30-60 minutes for accounts with >100K followers",
|
|
243
|
+
oncall: "Emma Thompson",
|
|
244
|
+
status: "resolved",
|
|
245
|
+
startTime: "10:00",
|
|
246
|
+
duration: "45m",
|
|
247
|
+
date: "2025-01-18",
|
|
248
|
+
dayOfWeek: 6,
|
|
249
|
+
hour: 10,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "INC-4014",
|
|
253
|
+
product: "Business Suite",
|
|
254
|
+
title:
|
|
255
|
+
"Slow inbox loading times (>3s p95) for accounts with large message histories, index rebuild in progress",
|
|
256
|
+
oncall: "Andre Santos",
|
|
257
|
+
status: "resolved",
|
|
258
|
+
startTime: "15:00",
|
|
259
|
+
duration: "25m",
|
|
260
|
+
date: "2025-01-15",
|
|
261
|
+
dayOfWeek: 3,
|
|
262
|
+
hour: 15,
|
|
263
|
+
},
|
|
264
264
|
];
|
|
265
265
|
|
|
266
|
-
const DAYS = [
|
|
267
|
-
|
|
268
|
-
'Jan 13',
|
|
269
|
-
'Jan 14',
|
|
270
|
-
'Jan 15',
|
|
271
|
-
'Jan 16',
|
|
272
|
-
'Jan 17',
|
|
273
|
-
'Jan 18',
|
|
274
|
-
];
|
|
275
|
-
const HOURS = [
|
|
276
|
-
'9am',
|
|
277
|
-
'10am',
|
|
278
|
-
'11am',
|
|
279
|
-
'12pm',
|
|
280
|
-
'1pm',
|
|
281
|
-
'2pm',
|
|
282
|
-
'3pm',
|
|
283
|
-
'4pm',
|
|
284
|
-
'5pm',
|
|
285
|
-
];
|
|
266
|
+
const DAYS = ["Jan 12", "Jan 13", "Jan 14", "Jan 15", "Jan 16", "Jan 17", "Jan 18"];
|
|
267
|
+
const HOURS = ["9am", "10am", "11am", "12pm", "1pm", "2pm", "3pm", "4pm", "5pm"];
|
|
286
268
|
|
|
287
269
|
function buildHeatmapData(data: IncidentRow[]) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}),
|
|
296
|
-
);
|
|
270
|
+
return HOURS.flatMap((hour, hi) =>
|
|
271
|
+
DAYS.map((day, di) => {
|
|
272
|
+
const hourValue = 9 + hi;
|
|
273
|
+
const count = data.filter((o) => o.dayOfWeek === di && o.hour === hourValue).length;
|
|
274
|
+
return { day, hour, incidents: count };
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
297
277
|
}
|
|
298
278
|
|
|
299
|
-
const STATUS_TOKEN_COLOR: Record<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
identified: 'yellow',
|
|
305
|
-
monitoring: 'cyan',
|
|
306
|
-
resolved: 'green',
|
|
279
|
+
const STATUS_TOKEN_COLOR: Record<IncidentRow["status"], "red" | "yellow" | "cyan" | "green"> = {
|
|
280
|
+
ongoing: "red",
|
|
281
|
+
identified: "yellow",
|
|
282
|
+
monitoring: "cyan",
|
|
283
|
+
resolved: "green",
|
|
307
284
|
};
|
|
308
285
|
|
|
309
|
-
const STATUS_LABEL: Record<IncidentRow[
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
286
|
+
const STATUS_LABEL: Record<IncidentRow["status"], string> = {
|
|
287
|
+
ongoing: "Ongoing",
|
|
288
|
+
identified: "Identified",
|
|
289
|
+
monitoring: "Monitoring",
|
|
290
|
+
resolved: "Resolved",
|
|
314
291
|
};
|
|
315
292
|
|
|
316
293
|
const columns: TableColumn<IncidentRow>[] = [
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{item.date}
|
|
401
|
-
</Text>
|
|
402
|
-
),
|
|
403
|
-
},
|
|
294
|
+
{
|
|
295
|
+
key: "id",
|
|
296
|
+
header: "Incident",
|
|
297
|
+
width: pixel(96),
|
|
298
|
+
renderCell: (item: IncidentRow) => (
|
|
299
|
+
<Link href="#/templates/table-page-heatmap-status" isStandalone>
|
|
300
|
+
{item.id}
|
|
301
|
+
</Link>
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "product",
|
|
306
|
+
header: "Product",
|
|
307
|
+
width: proportional(2, { minWidth: 120 }),
|
|
308
|
+
renderCell: (item: IncidentRow) => (
|
|
309
|
+
<Text type="body" maxLines={1}>
|
|
310
|
+
{item.product}
|
|
311
|
+
</Text>
|
|
312
|
+
),
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
key: "title",
|
|
316
|
+
header: "Description",
|
|
317
|
+
width: proportional(5, { minWidth: 160 }),
|
|
318
|
+
renderCell: (item: IncidentRow) => (
|
|
319
|
+
<Text type="body" maxLines={2}>
|
|
320
|
+
{item.title}
|
|
321
|
+
</Text>
|
|
322
|
+
),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
key: "startTime",
|
|
326
|
+
header: "Started",
|
|
327
|
+
width: pixel(88),
|
|
328
|
+
renderCell: (item: IncidentRow) => (
|
|
329
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
330
|
+
{item.startTime}
|
|
331
|
+
</Text>
|
|
332
|
+
),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: "duration",
|
|
336
|
+
header: "Duration",
|
|
337
|
+
width: pixel(96),
|
|
338
|
+
renderCell: (item: IncidentRow) => (
|
|
339
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
340
|
+
{item.duration}
|
|
341
|
+
</Text>
|
|
342
|
+
),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
key: "oncall",
|
|
346
|
+
header: "On-call",
|
|
347
|
+
width: proportional(2, { minWidth: 120 }),
|
|
348
|
+
renderCell: (item: IncidentRow) => (
|
|
349
|
+
<HStack gap={2} vAlign="center">
|
|
350
|
+
<Avatar name={item.oncall} size="sm" />
|
|
351
|
+
<StackItem size="fill">
|
|
352
|
+
<Text type="body" maxLines={1}>
|
|
353
|
+
{item.oncall}
|
|
354
|
+
</Text>
|
|
355
|
+
</StackItem>
|
|
356
|
+
</HStack>
|
|
357
|
+
),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
key: "status",
|
|
361
|
+
header: "Status",
|
|
362
|
+
width: pixel(120),
|
|
363
|
+
renderCell: (item: IncidentRow) => (
|
|
364
|
+
<Token size="sm" color={STATUS_TOKEN_COLOR[item.status]} label={STATUS_LABEL[item.status]} />
|
|
365
|
+
),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
key: "date",
|
|
369
|
+
header: "Date",
|
|
370
|
+
width: pixel(112),
|
|
371
|
+
renderCell: (item: IncidentRow) => (
|
|
372
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
373
|
+
{item.date}
|
|
374
|
+
</Text>
|
|
375
|
+
),
|
|
376
|
+
},
|
|
404
377
|
];
|
|
405
378
|
|
|
406
379
|
// ============= HEATMAP (hand SVG, Dracula ramp) =============
|
|
@@ -411,107 +384,107 @@ const columns: TableColumn<IncidentRow>[] = [
|
|
|
411
384
|
// literal here is what let a --dracula-* primitive reach a chart mark: the
|
|
412
385
|
// dictionary itself held primitives, so "use the module" was not available.
|
|
413
386
|
function heatFill(count: number): string {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
387
|
+
if (count >= 3) {
|
|
388
|
+
return CHART_HUES.red;
|
|
389
|
+
}
|
|
390
|
+
if (count === 2) {
|
|
391
|
+
return CHART_HUES.orange;
|
|
392
|
+
}
|
|
393
|
+
if (count === 1) {
|
|
394
|
+
return CHART_HUES.green;
|
|
395
|
+
}
|
|
396
|
+
// Zero is a magnitude, not a severity, so it wears the surface tier rather
|
|
397
|
+
// than a data hue -- which is why the legend type needs a background arm.
|
|
398
|
+
return "var(--color-background-card)";
|
|
426
399
|
}
|
|
427
400
|
|
|
428
401
|
// The zero swatch and its cell MUST move together: the legend is how a reader
|
|
429
402
|
// learns what a colour means, so a legend that disagrees with the grid teaches
|
|
430
403
|
// them the wrong mapping.
|
|
431
404
|
const HEAT_LEGEND: readonly ChartLegendEntry[] = [
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
405
|
+
{ label: "0", color: "var(--color-background-card)" },
|
|
406
|
+
{ label: "1", color: CHART_HUES.green },
|
|
407
|
+
{ label: "2", color: CHART_HUES.orange },
|
|
408
|
+
{ label: "3+", color: CHART_HUES.red },
|
|
436
409
|
];
|
|
437
410
|
|
|
438
411
|
function OutageHeatmap() {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
{/* Every cell prints its count, empty ones included, so
|
|
412
|
+
const heatmapData = buildHeatmapData(incidents);
|
|
413
|
+
const cellW = 56;
|
|
414
|
+
const cellH = 24;
|
|
415
|
+
const gap = 4;
|
|
416
|
+
const labelW = 44;
|
|
417
|
+
const labelH = 22;
|
|
418
|
+
const W = labelW + DAYS.length * (cellW + gap);
|
|
419
|
+
const H = labelH + HOURS.length * (cellH + gap);
|
|
420
|
+
const countFor = (day: string, hour: string) =>
|
|
421
|
+
heatmapData.find((d) => d.day === day && d.hour === hour)?.incidents ?? 0;
|
|
422
|
+
return (
|
|
423
|
+
<VStack gap={3}>
|
|
424
|
+
<Card padding={4} style={CHART_PANEL_STYLE}>
|
|
425
|
+
<svg
|
|
426
|
+
viewBox={`0 0 ${W} ${H}`}
|
|
427
|
+
width="100%"
|
|
428
|
+
role="img"
|
|
429
|
+
aria-label="Incidents by day and hour, January 12 to 18"
|
|
430
|
+
>
|
|
431
|
+
{DAYS.map((day, di) => (
|
|
432
|
+
<ChartLabel key={day} x={labelW + di * (cellW + gap) + cellW / 2} y={14}>
|
|
433
|
+
{day}
|
|
434
|
+
</ChartLabel>
|
|
435
|
+
))}
|
|
436
|
+
{HOURS.map((hour, hi) => (
|
|
437
|
+
<g key={hour}>
|
|
438
|
+
<ChartLabel
|
|
439
|
+
x={labelW - 6}
|
|
440
|
+
y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
|
|
441
|
+
textAnchor="end"
|
|
442
|
+
>
|
|
443
|
+
{hour}
|
|
444
|
+
</ChartLabel>
|
|
445
|
+
{DAYS.map((day, di) => {
|
|
446
|
+
const count = countFor(day, hour);
|
|
447
|
+
return (
|
|
448
|
+
<g key={day}>
|
|
449
|
+
<rect
|
|
450
|
+
x={labelW + di * (cellW + gap)}
|
|
451
|
+
y={labelH + hi * (cellH + gap)}
|
|
452
|
+
width={cellW}
|
|
453
|
+
height={cellH}
|
|
454
|
+
rx={4}
|
|
455
|
+
fill={heatFill(count)}
|
|
456
|
+
/>
|
|
457
|
+
{/* Every cell prints its count, empty ones included, so
|
|
486
458
|
the ramp reinforces the number instead of carrying the
|
|
487
459
|
reading on its own. */}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
460
|
+
<ChartLabel
|
|
461
|
+
x={labelW + di * (cellW + gap) + cellW / 2}
|
|
462
|
+
y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
|
|
463
|
+
// On-fill ink follows the cell it sits on, so it takes
|
|
464
|
+
// the --color-on-* arm for that status rather than one
|
|
465
|
+
// fixed token. --color-on-dark is the wrong target here:
|
|
466
|
+
// it is #F8F8F2, near-white, which would fail on the
|
|
467
|
+
// saturated green/orange/red fills.
|
|
468
|
+
fill={
|
|
469
|
+
count >= 3
|
|
470
|
+
? "var(--color-on-error)"
|
|
471
|
+
: count === 2
|
|
472
|
+
? "var(--color-on-warning)"
|
|
473
|
+
: count === 1
|
|
474
|
+
? "var(--color-on-success)"
|
|
475
|
+
: undefined
|
|
476
|
+
}
|
|
477
|
+
>
|
|
478
|
+
{count}
|
|
479
|
+
</ChartLabel>
|
|
480
|
+
</g>
|
|
481
|
+
);
|
|
482
|
+
})}
|
|
483
|
+
</g>
|
|
484
|
+
))}
|
|
485
|
+
</svg>
|
|
486
|
+
</Card>
|
|
487
|
+
{/* Legend rows go through shared/chart-legend.tsx. The ramp itself stays
|
|
515
488
|
local: it encodes SEVERITY (0/1/2/3+ against green/orange/red), which
|
|
516
489
|
is a status vocabulary, not a magnitude. A sequential magnitude ramp
|
|
517
490
|
would invert this one — on a dark page a high count reads lighter,
|
|
@@ -519,18 +492,18 @@ function OutageHeatmap() {
|
|
|
519
492
|
1..5 steps remain in tokens.css for a consumer that genuinely needs
|
|
520
493
|
one; nothing in this kit does, which is why the module that wrapped
|
|
521
494
|
them is gone. */}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
495
|
+
<ChartLegend
|
|
496
|
+
entries={HEAT_LEGEND}
|
|
497
|
+
gap={2}
|
|
498
|
+
swatchSize={12}
|
|
499
|
+
caption={
|
|
500
|
+
<Text type="supporting" color="secondary">
|
|
501
|
+
Incidents by day and hour · Jan 12–18
|
|
502
|
+
</Text>
|
|
503
|
+
}
|
|
504
|
+
/>
|
|
505
|
+
</VStack>
|
|
506
|
+
);
|
|
534
507
|
}
|
|
535
508
|
|
|
536
509
|
// ============= PAGE =============
|
|
@@ -541,56 +514,54 @@ function OutageHeatmap() {
|
|
|
541
514
|
// the scroll. LayoutContent is overflow:auto, so the definite height lands as a
|
|
542
515
|
// content-pane scroller rather than a clip. Same shape as dashboard.tsx and
|
|
543
516
|
// editor.tsx:302.
|
|
544
|
-
const pageStyle: CSSProperties = {height:
|
|
517
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
545
518
|
|
|
546
519
|
export default function HeatmapTable() {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
<VStack gap={4}>
|
|
580
|
-
<OutageHeatmap />
|
|
520
|
+
return (
|
|
521
|
+
<Layout
|
|
522
|
+
style={pageStyle}
|
|
523
|
+
height="fill"
|
|
524
|
+
header={
|
|
525
|
+
<LayoutHeader hasDivider padding={6}>
|
|
526
|
+
<HStack gap={2} vAlign="center">
|
|
527
|
+
<StackItem size="fill">
|
|
528
|
+
<Heading level={1} type="display-2">
|
|
529
|
+
Status
|
|
530
|
+
</Heading>
|
|
531
|
+
</StackItem>
|
|
532
|
+
<IconButton
|
|
533
|
+
label="Filter"
|
|
534
|
+
icon={<Icon icon={Filter} size="sm" />}
|
|
535
|
+
variant="ghost"
|
|
536
|
+
tooltip="Filter"
|
|
537
|
+
/>
|
|
538
|
+
<IconButton
|
|
539
|
+
label="Export"
|
|
540
|
+
icon={<Icon icon={Download} size="sm" />}
|
|
541
|
+
variant="ghost"
|
|
542
|
+
tooltip="Export"
|
|
543
|
+
/>
|
|
544
|
+
<Button label="Refresh" variant="secondary" icon={<Icon icon={RotateCw} size="sm" />} />
|
|
545
|
+
</HStack>
|
|
546
|
+
</LayoutHeader>
|
|
547
|
+
}
|
|
548
|
+
content={
|
|
549
|
+
<LayoutContent padding={3}>
|
|
550
|
+
<VStack gap={4}>
|
|
551
|
+
<OutageHeatmap />
|
|
581
552
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
553
|
+
<Table<IncidentRow>
|
|
554
|
+
data={incidents}
|
|
555
|
+
columns={columns}
|
|
556
|
+
idKey="id"
|
|
557
|
+
density="balanced"
|
|
558
|
+
dividers="rows"
|
|
559
|
+
textOverflow="truncate"
|
|
560
|
+
hasHover
|
|
561
|
+
/>
|
|
562
|
+
</VStack>
|
|
563
|
+
</LayoutContent>
|
|
564
|
+
}
|
|
565
|
+
/>
|
|
566
|
+
);
|
|
596
567
|
}
|