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,5 +1,25 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > (LH[divider] > H[g2 a=center] > Hd"Status"[level=1] + IB"Filter"[variant=ghost] + IB"Export"[variant=ghost] + B.secondary"Refresh") + (LC[p3] > V[g4] > (V[g3] > C[p4] + Tx"Incidents by day and hour"[t=supporting] + (H[g4] > (H[g2] > Tx[t=supporting])*4)) + (T[hover] > (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")*5))
2
4
 
5
+ /**
6
+ * Status — the incident heatmap for the week plus the log behind it.
7
+ *
8
+ * Frame: page header (title + icon actions) | content column (heatmap, table).
9
+ *
10
+ * Container policy: the day/hour heatmap is a single Card widget; incidents
11
+ * render as dense rows in one edge-to-edge table. Every heat cell prints its
12
+ * count, so the green/orange/red ramp reinforces the numbers rather than
13
+ * carrying the reading on its own, and status is a Token instead of a badge.
14
+ *
15
+ * Responsive contract:
16
+ * no media queries — the heatmap is an SVG that scales to its column, so it
17
+ * reflows at any width. The table declares a ~910px floor across its eight
18
+ * columns and truncates cells (description to two lines), so narrower
19
+ * viewports scroll the table horizontally instead of widening the page.
20
+ */
21
+
22
+ import type {CSSProperties} from 'react';
3
23
  import {
4
24
  VStack,
5
25
  HStack,
@@ -13,12 +33,16 @@ import {Button} from '@astryxdesign/core/Button';
13
33
  import {IconButton} from '@astryxdesign/core/IconButton';
14
34
  import {Icon} from '@astryxdesign/core/Icon';
15
35
  import {Avatar} from '@astryxdesign/core/Avatar';
16
- import {Badge} from '@astryxdesign/core/Badge';
36
+ import {Token} from '@astryxdesign/core/Token';
17
37
  import {Card} from '@astryxdesign/core/Card';
18
38
  import {Link} from '@astryxdesign/core/Link';
19
39
  import {Table, proportional, pixel} from '@astryxdesign/core/Table';
20
40
  import type {TableColumn} from '@astryxdesign/core/Table';
21
41
  import {Filter, Download, RotateCw} from 'lucide-react';
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';
22
46
 
23
47
  // ============= ICONS (verified lucide-react exports) =============
24
48
  // Filter ← FunnelIcon, Download ← ArrowDownTrayIcon, RotateCw ← ArrowPathIcon.
@@ -56,7 +80,7 @@ const incidents: IncidentRow[] = [
56
80
  id: 'INC-4008',
57
81
  product: 'Business Suite',
58
82
  title:
59
- 'Notification delivery delays affecting mobile push and email channels — queue saturation under investigation',
83
+ 'Notification delivery delays affecting mobile push and email channels, queue saturation under investigation',
60
84
  severity: 'major',
61
85
  oncall: 'Carlos Mendez',
62
86
  status: 'ongoing',
@@ -71,7 +95,7 @@ const incidents: IncidentRow[] = [
71
95
  id: 'INC-4001',
72
96
  product: 'Ads Manager',
73
97
  title:
74
- 'Campaign creation timing out for ~12% of advertisers in EMEA region — root caused to elevated DB write latency',
98
+ 'Campaign creation timing out for ~12% of advertisers in EMEA region, root caused to elevated DB write latency',
75
99
  severity: 'critical',
76
100
  oncall: 'Sarah Chen',
77
101
  status: 'resolved',
@@ -158,7 +182,7 @@ const incidents: IncidentRow[] = [
158
182
  id: 'INC-4007',
159
183
  product: 'Messenger API',
160
184
  title:
161
- 'Webhook delivery failures for subscribed page events — retries succeeded but signaled false alerts',
185
+ 'Webhook delivery failures for subscribed page events, retries succeeded but signaled false alerts',
162
186
  severity: 'minor',
163
187
  oncall: 'Mei Lin',
164
188
  status: 'resolved',
@@ -174,7 +198,7 @@ const incidents: IncidentRow[] = [
174
198
  id: 'INC-4009',
175
199
  product: 'Pages',
176
200
  title:
177
- 'Image upload timeouts for posts larger than 4MB — auto-recovered after CDN region failover',
201
+ 'Image upload timeouts for posts larger than 4MB, auto-recovered after CDN region failover',
178
202
  severity: 'minor',
179
203
  oncall: 'Fatima Al-Rashid',
180
204
  status: 'resolved',
@@ -188,7 +212,7 @@ const incidents: IncidentRow[] = [
188
212
  id: 'INC-4010',
189
213
  product: 'Graph API',
190
214
  title:
191
- 'Intermittent 503 errors on a single load balancer in us-east-1 — auto-healed by health check rotation',
215
+ 'Intermittent 503 errors on a single load balancer in us-east-1, auto-healed by health check rotation',
192
216
  severity: 'minor',
193
217
  oncall: 'Lucas Andersson',
194
218
  status: 'resolved',
@@ -244,7 +268,7 @@ const incidents: IncidentRow[] = [
244
268
  id: 'INC-4014',
245
269
  product: 'Business Suite',
246
270
  title:
247
- 'Slow inbox loading times (>3s p95) for accounts with large message histories — index rebuild in progress',
271
+ 'Slow inbox loading times (>3s p95) for accounts with large message histories, index rebuild in progress',
248
272
  severity: 'minor',
249
273
  oncall: 'Andre Santos',
250
274
  status: 'resolved',
@@ -289,19 +313,28 @@ function buildHeatmapData(data: IncidentRow[]) {
289
313
  );
290
314
  }
291
315
 
292
- const statusVariant: Record<string, 'error' | 'warning' | 'info' | 'success'> =
293
- {
294
- ongoing: 'error',
295
- identified: 'warning',
296
- monitoring: 'info',
297
- resolved: 'success',
298
- };
316
+ const STATUS_TOKEN_COLOR: Record<
317
+ IncidentRow['status'],
318
+ 'red' | 'yellow' | 'cyan' | 'green'
319
+ > = {
320
+ ongoing: 'red',
321
+ identified: 'yellow',
322
+ monitoring: 'cyan',
323
+ resolved: 'green',
324
+ };
325
+
326
+ const STATUS_LABEL: Record<IncidentRow['status'], string> = {
327
+ ongoing: 'Ongoing',
328
+ identified: 'Identified',
329
+ monitoring: 'Monitoring',
330
+ resolved: 'Resolved',
331
+ };
299
332
 
300
333
  const columns: TableColumn<IncidentRow>[] = [
301
334
  {
302
335
  key: 'id',
303
336
  header: 'Incident',
304
- width: pixel(110),
337
+ width: pixel(96),
305
338
  renderCell: (item: IncidentRow) => (
306
339
  <Link href="#/templates/table-page-heatmap-status" isStandalone>
307
340
  {item.id}
@@ -311,21 +344,29 @@ const columns: TableColumn<IncidentRow>[] = [
311
344
  {
312
345
  key: 'product',
313
346
  header: 'Product',
314
- width: proportional(2),
315
- renderCell: (item: IncidentRow) => <Text type="body">{item.product}</Text>,
347
+ width: proportional(2, {minWidth: 120}),
348
+ renderCell: (item: IncidentRow) => (
349
+ <Text type="body" maxLines={1}>
350
+ {item.product}
351
+ </Text>
352
+ ),
316
353
  },
317
354
  {
318
355
  key: 'title',
319
356
  header: 'Description',
320
- width: proportional(5),
321
- renderCell: (item: IncidentRow) => <Text type="body">{item.title}</Text>,
357
+ width: proportional(5, {minWidth: 160}),
358
+ renderCell: (item: IncidentRow) => (
359
+ <Text type="body" maxLines={2}>
360
+ {item.title}
361
+ </Text>
362
+ ),
322
363
  },
323
364
  {
324
365
  key: 'startTime',
325
366
  header: 'Started',
326
- width: pixel(96),
367
+ width: pixel(88),
327
368
  renderCell: (item: IncidentRow) => (
328
- <Text type="body" hasTabularNumbers>
369
+ <Text type="body" hasTabularNumbers maxLines={1}>
329
370
  {item.startTime}
330
371
  </Text>
331
372
  ),
@@ -333,9 +374,9 @@ const columns: TableColumn<IncidentRow>[] = [
333
374
  {
334
375
  key: 'duration',
335
376
  header: 'Duration',
336
- width: pixel(100),
377
+ width: pixel(96),
337
378
  renderCell: (item: IncidentRow) => (
338
- <Text type="body" hasTabularNumbers>
379
+ <Text type="body" hasTabularNumbers maxLines={1}>
339
380
  {item.duration}
340
381
  </Text>
341
382
  ),
@@ -343,11 +384,15 @@ const columns: TableColumn<IncidentRow>[] = [
343
384
  {
344
385
  key: 'oncall',
345
386
  header: 'On-call',
346
- width: proportional(2),
387
+ width: proportional(2, {minWidth: 120}),
347
388
  renderCell: (item: IncidentRow) => (
348
389
  <HStack gap={2} vAlign="center">
349
390
  <Avatar name={item.oncall} size="sm" />
350
- <Text type="body">{item.oncall}</Text>
391
+ <StackItem size="fill">
392
+ <Text type="body" maxLines={1}>
393
+ {item.oncall}
394
+ </Text>
395
+ </StackItem>
351
396
  </HStack>
352
397
  ),
353
398
  },
@@ -356,18 +401,19 @@ const columns: TableColumn<IncidentRow>[] = [
356
401
  header: 'Status',
357
402
  width: pixel(120),
358
403
  renderCell: (item: IncidentRow) => (
359
- <Badge
360
- label={item.status.charAt(0).toUpperCase() + item.status.slice(1)}
361
- variant={statusVariant[item.status]}
404
+ <Token
405
+ size="sm"
406
+ color={STATUS_TOKEN_COLOR[item.status]}
407
+ label={STATUS_LABEL[item.status]}
362
408
  />
363
409
  ),
364
410
  },
365
411
  {
366
412
  key: 'date',
367
413
  header: 'Date',
368
- width: pixel(110),
414
+ width: pixel(112),
369
415
  renderCell: (item: IncidentRow) => (
370
- <Text type="body" hasTabularNumbers>
416
+ <Text type="body" hasTabularNumbers maxLines={1}>
371
417
  {item.date}
372
418
  </Text>
373
419
  ),
@@ -378,24 +424,32 @@ const columns: TableColumn<IncidentRow>[] = [
378
424
 
379
425
  // Incident-count ramp: quiet days rest on the Dracula surface, busier cells
380
426
  // climb green → orange → red.
427
+ // Delegates to CHART_HUES, the sanctioned categorical source. Writing the
428
+ // literal here is what let a --dracula-* primitive reach a chart mark: the
429
+ // dictionary itself held primitives, so "use the module" was not available.
381
430
  function heatFill(count: number): string {
382
431
  if (count >= 3) {
383
- return 'var(--dracula-red)';
432
+ return CHART_HUES.red;
384
433
  }
385
434
  if (count === 2) {
386
- return 'var(--dracula-orange)';
435
+ return CHART_HUES.orange;
387
436
  }
388
437
  if (count === 1) {
389
- return 'var(--dracula-green)';
438
+ return CHART_HUES.green;
390
439
  }
391
- return 'var(--dracula-bg-light)';
440
+ // Zero is a magnitude, not a severity, so it wears the surface tier rather
441
+ // than a data hue -- which is why the legend type needs a background arm.
442
+ return 'var(--color-background-card)';
392
443
  }
393
444
 
394
- const HEAT_LEGEND = [
395
- {label: '0', fill: 'var(--dracula-bg-light)'},
396
- {label: '1', fill: 'var(--dracula-green)'},
397
- {label: '2', fill: 'var(--dracula-orange)'},
398
- {label: '3+', fill: 'var(--dracula-red)'},
445
+ // The zero swatch and its cell MUST move together: the legend is how a reader
446
+ // learns what a colour means, so a legend that disagrees with the grid teaches
447
+ // them the wrong mapping.
448
+ const HEAT_LEGEND: readonly ChartLegendEntry[] = [
449
+ {label: '0', color: 'var(--color-background-card)'},
450
+ {label: '1', color: CHART_HUES.green},
451
+ {label: '2', color: CHART_HUES.orange},
452
+ {label: '3+', color: CHART_HUES.red},
399
453
  ];
400
454
 
401
455
  function OutageHeatmap() {
@@ -411,40 +465,28 @@ function OutageHeatmap() {
411
465
  heatmapData.find(d => d.day === day && d.hour === hour)?.incidents ?? 0;
412
466
  return (
413
467
  <VStack gap={3}>
414
- <Card
415
- padding={3}
416
- style={{
417
- backgroundColor: 'var(--color-background)',
418
- border: 'var(--border-width) solid var(--color-separator)',
419
- }}>
468
+ <Card padding={4} style={CHART_PANEL_STYLE}>
420
469
  <svg
421
470
  viewBox={`0 0 ${W} ${H}`}
422
471
  width="100%"
423
472
  role="img"
424
473
  aria-label="Incidents by day and hour, January 12 to 18">
425
474
  {DAYS.map((day, di) => (
426
- <text
475
+ <ChartLabel
427
476
  key={day}
428
477
  x={labelW + di * (cellW + gap) + cellW / 2}
429
- y={14}
430
- textAnchor="middle"
431
- fontSize={9}
432
- fill="var(--color-text-paragraph)"
433
- fontFamily="var(--font-family-mono)">
478
+ y={14}>
434
479
  {day}
435
- </text>
480
+ </ChartLabel>
436
481
  ))}
437
482
  {HOURS.map((hour, hi) => (
438
483
  <g key={hour}>
439
- <text
484
+ <ChartLabel
440
485
  x={labelW - 6}
441
486
  y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
442
- textAnchor="end"
443
- fontSize={9}
444
- fill="var(--color-text-paragraph)"
445
- fontFamily="var(--font-family-mono)">
487
+ textAnchor="end">
446
488
  {hour}
447
- </text>
489
+ </ChartLabel>
448
490
  {DAYS.map((day, di) => {
449
491
  const count = countFor(day, hour);
450
492
  return (
@@ -457,17 +499,28 @@ function OutageHeatmap() {
457
499
  rx={4}
458
500
  fill={heatFill(count)}
459
501
  />
460
- {count > 0 && (
461
- <text
462
- x={labelW + di * (cellW + gap) + cellW / 2}
463
- y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
464
- textAnchor="middle"
465
- fontSize={9}
466
- fill="var(--dracula-bg-dark)"
467
- fontFamily="var(--font-family-mono)">
468
- {count}
469
- </text>
470
- )}
502
+ {/* Every cell prints its count, empty ones included, so
503
+ the ramp reinforces the number instead of carrying the
504
+ reading on its own. */}
505
+ <ChartLabel
506
+ x={labelW + di * (cellW + gap) + cellW / 2}
507
+ y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
508
+ // On-fill ink follows the cell it sits on, so it takes
509
+ // the --color-on-* arm for that status rather than one
510
+ // fixed token. --color-on-dark is the wrong target here:
511
+ // it is #F8F8F2, near-white, which would fail on the
512
+ // saturated green/orange/red fills.
513
+ fill={
514
+ count >= 3
515
+ ? 'var(--color-on-error)'
516
+ : count === 2
517
+ ? 'var(--color-on-warning)'
518
+ : count === 1
519
+ ? 'var(--color-on-success)'
520
+ : undefined
521
+ }>
522
+ {count}
523
+ </ChartLabel>
471
524
  </g>
472
525
  );
473
526
  })}
@@ -475,40 +528,48 @@ function OutageHeatmap() {
475
528
  ))}
476
529
  </svg>
477
530
  </Card>
478
- <Text type="supporting" color="secondary">
479
- Incidents by day and hour · Jan 12–18
480
- </Text>
481
- <HStack gap={4} vAlign="center">
482
- {HEAT_LEGEND.map(entry => (
483
- <HStack key={entry.label} gap={2} vAlign="center">
484
- <svg
485
- width={12}
486
- height={12}
487
- role="img"
488
- aria-label={`${entry.label} incidents`}>
489
- <rect width={12} height={12} rx={4} fill={entry.fill} />
490
- </svg>
491
- <Text type="supporting" color="secondary" hasTabularNumbers>
492
- {entry.label}
493
- </Text>
494
- </HStack>
495
- ))}
496
- </HStack>
531
+ {/* Legend rows go through shared/chart-legend.tsx. The ramp itself stays
532
+ local: it encodes SEVERITY (0/1/2/3+ against green/orange/red), which
533
+ is a status vocabulary, not a magnitude. A sequential magnitude ramp
534
+ would invert this one — on a dark page a high count reads lighter,
535
+ which is the opposite of "more is worse" here. The --color-data-<family>-
536
+ 1..5 steps remain in tokens.css for a consumer that genuinely needs
537
+ one; nothing in this kit does, which is why the module that wrapped
538
+ them is gone. */}
539
+ <ChartLegend
540
+ entries={HEAT_LEGEND}
541
+ gap={2}
542
+ swatchSize={12}
543
+ caption={
544
+ <Text type="supporting" color="secondary">
545
+ Incidents by day and hour · Jan 12–18
546
+ </Text>
547
+ }
548
+ />
497
549
  </VStack>
498
550
  );
499
551
  }
500
552
 
501
553
  // ============= PAGE =============
502
554
 
555
+ // A definite ancestor for `Layout height="fill"`. 100dvh, not minHeight: '100%':
556
+ // a percentage min-height against an indefinite containing block computes to 0
557
+ // (CSS 2.1 10.5), so the soft failure would survive and the document would keep
558
+ // the scroll. LayoutContent is overflow:auto, so the definite height lands as a
559
+ // content-pane scroller rather than a clip. Same shape as dashboard.tsx and
560
+ // editor.tsx:302.
561
+ const pageStyle: CSSProperties = {height: '100dvh'};
562
+
503
563
  export default function HeatmapTable() {
504
564
  return (
505
565
  <Layout
566
+ style={pageStyle}
506
567
  height="fill"
507
568
  header={
508
- <LayoutHeader hasDivider>
569
+ <LayoutHeader hasDivider padding={6}>
509
570
  <HStack gap={2} vAlign="center">
510
571
  <StackItem size="fill">
511
- <Heading level={1}>Status</Heading>
572
+ <Heading level={1} type="display-2">Status</Heading>
512
573
  </StackItem>
513
574
  <IconButton
514
575
  label="Filter"
@@ -522,7 +583,11 @@ export default function HeatmapTable() {
522
583
  variant="ghost"
523
584
  tooltip="Export"
524
585
  />
525
- <Button label="Refresh" icon={<Icon icon={RotateCw} size="sm" />} />
586
+ <Button
587
+ label="Refresh"
588
+ variant="secondary"
589
+ icon={<Icon icon={RotateCw} size="sm" />}
590
+ />
526
591
  </HStack>
527
592
  </LayoutHeader>
528
593
  }
@@ -537,6 +602,7 @@ export default function HeatmapTable() {
537
602
  idKey="id"
538
603
  density="balanced"
539
604
  dividers="rows"
605
+ textOverflow="truncate"
540
606
  hasHover
541
607
  />
542
608
  </VStack>