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,4 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
+
// L > (Tbar[size=sm] > (H > IB"Go back" + IB"Go forward" + Tx"Folder"[t=label]) + (SG"View mode" > SGI*4) + (H > IB"Group" + IB"Share" + IB"Tags" + IB"More" + IB"Search")) + (LC[p=0 !scroll] > (Hd"File Explorer"[level=1] + H[h=fill] > (S.transparent[p=2 w=240 dv=[end]] > UL[density=compact !hasDividers] > (LI > Ic + Tx"Item"[t=body])*3)*3 + (S.transparent[p=6] > V[g=4 a=center] > Av[size=96] + (V[g=1 a=center] > Tx"Name"[t=body] + Tx"Kind"[t=supporting]) + (ML"Information" > MLI*3))))
|
|
2
4
|
|
|
3
5
|
import {useState, useMemo, type CSSProperties} from 'react';
|
|
4
6
|
import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
@@ -7,9 +9,11 @@ import {List, ListItem} from '@astryxdesign/core/List';
|
|
|
7
9
|
import {HStack, VStack} from '@astryxdesign/core/Layout';
|
|
8
10
|
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
9
11
|
import {Text} from '@astryxdesign/core/Text';
|
|
12
|
+
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
10
13
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
11
14
|
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
12
15
|
import {Section} from '@astryxdesign/core/Section';
|
|
16
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
13
17
|
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
14
18
|
import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
|
|
15
19
|
import {
|
|
@@ -262,13 +266,85 @@ const page: CSSProperties = {height: '100dvh'};
|
|
|
262
266
|
const columnRow: CSSProperties = {overflowX: 'auto', overflowY: 'hidden'};
|
|
263
267
|
const scrollable: CSSProperties = {overflowY: 'auto'};
|
|
264
268
|
const fixedColumn: CSSProperties = {flexShrink: 0};
|
|
269
|
+
// The column's 320px resting width rides on Section's `width` prop; grow
|
|
270
|
+
// lets it absorb whatever the fixed columns leave. Inline styles are for
|
|
271
|
+
// what props cannot express.
|
|
265
272
|
const detailColumn: CSSProperties = {
|
|
266
273
|
flexGrow: 1,
|
|
267
274
|
flexShrink: 0,
|
|
268
|
-
flexBasis: 320,
|
|
269
275
|
};
|
|
270
276
|
const controlsScroll: CSSProperties = {overflowX: 'auto'};
|
|
271
277
|
|
|
278
|
+
// Static toolbar content, hoisted out of the render path: neither element
|
|
279
|
+
// reads component state, so rebuilding them per render buys nothing.
|
|
280
|
+
const viewSwitcher = (
|
|
281
|
+
<SegmentedControl value="column" onChange={() => {}} label="View mode">
|
|
282
|
+
<SegmentedControlItem
|
|
283
|
+
value="grid"
|
|
284
|
+
label="Grid"
|
|
285
|
+
icon={<Icon icon={LayoutGrid} size="sm" />}
|
|
286
|
+
isLabelHidden
|
|
287
|
+
/>
|
|
288
|
+
<SegmentedControlItem
|
|
289
|
+
value="list"
|
|
290
|
+
label="List"
|
|
291
|
+
icon={<Icon icon={Rows3} size="sm" />}
|
|
292
|
+
isLabelHidden
|
|
293
|
+
/>
|
|
294
|
+
<SegmentedControlItem
|
|
295
|
+
value="column"
|
|
296
|
+
label="Column"
|
|
297
|
+
icon={<Icon icon={Columns} size="sm" />}
|
|
298
|
+
isLabelHidden
|
|
299
|
+
/>
|
|
300
|
+
<SegmentedControlItem
|
|
301
|
+
value="gallery"
|
|
302
|
+
label="Gallery"
|
|
303
|
+
icon={<Icon icon={Table} size="sm" />}
|
|
304
|
+
isLabelHidden
|
|
305
|
+
/>
|
|
306
|
+
</SegmentedControl>
|
|
307
|
+
);
|
|
308
|
+
const fileActions = (
|
|
309
|
+
<>
|
|
310
|
+
<IconButton
|
|
311
|
+
variant="ghost"
|
|
312
|
+
size="sm"
|
|
313
|
+
icon={<Icon icon={SlidersHorizontal} size="sm" />}
|
|
314
|
+
label="Group"
|
|
315
|
+
tooltip="Group"
|
|
316
|
+
/>
|
|
317
|
+
<IconButton
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="sm"
|
|
320
|
+
icon={<Icon icon={Share2} size="sm" />}
|
|
321
|
+
label="Share"
|
|
322
|
+
tooltip="Share"
|
|
323
|
+
/>
|
|
324
|
+
<IconButton
|
|
325
|
+
variant="ghost"
|
|
326
|
+
size="sm"
|
|
327
|
+
icon={<Icon icon={Tag} size="sm" />}
|
|
328
|
+
label="Tags"
|
|
329
|
+
tooltip="Tags"
|
|
330
|
+
/>
|
|
331
|
+
<IconButton
|
|
332
|
+
variant="ghost"
|
|
333
|
+
size="sm"
|
|
334
|
+
icon={<Icon icon={Ellipsis} size="sm" />}
|
|
335
|
+
label="More"
|
|
336
|
+
tooltip="More"
|
|
337
|
+
/>
|
|
338
|
+
<IconButton
|
|
339
|
+
variant="ghost"
|
|
340
|
+
size="sm"
|
|
341
|
+
icon={<Icon icon={Search} size="sm" />}
|
|
342
|
+
label="Search"
|
|
343
|
+
tooltip="Search"
|
|
344
|
+
/>
|
|
345
|
+
</>
|
|
346
|
+
);
|
|
347
|
+
|
|
272
348
|
function findItem(items: FileSystemItem[], id: string): FileSystemItem | null {
|
|
273
349
|
for (const item of items) {
|
|
274
350
|
if (item.id === id) {
|
|
@@ -297,16 +373,27 @@ export default function FileExplorer() {
|
|
|
297
373
|
]);
|
|
298
374
|
|
|
299
375
|
const isNarrow = useMediaQuery('(max-width: 768px)');
|
|
300
|
-
const isPhone =
|
|
376
|
+
const isPhone = isNarrow;
|
|
301
377
|
|
|
302
378
|
const columns = useMemo(() => {
|
|
303
|
-
|
|
304
|
-
|
|
379
|
+
// `id` names the folder a column lists, so the React key stays put when
|
|
380
|
+
// narrow mode slices the row down to the last visible column.
|
|
381
|
+
const cols: {
|
|
382
|
+
id: string;
|
|
383
|
+
items: FileSystemItem[];
|
|
384
|
+
selectedId: string | null;
|
|
385
|
+
}[] = [];
|
|
386
|
+
cols.push({
|
|
387
|
+
id: 'root',
|
|
388
|
+
items: FILESYSTEM,
|
|
389
|
+
selectedId: selectedPath[0] ?? null,
|
|
390
|
+
});
|
|
305
391
|
let currentItems: FileSystemItem[] = FILESYSTEM;
|
|
306
392
|
for (let i = 0; i < selectedPath.length; i++) {
|
|
307
393
|
const selected = currentItems.find(item => item.id === selectedPath[i]);
|
|
308
394
|
if (selected?.children && selected.children.length > 0) {
|
|
309
395
|
cols.push({
|
|
396
|
+
id: selectedPath[i],
|
|
310
397
|
items: selected.children,
|
|
311
398
|
selectedId: selectedPath[i + 1] ?? null,
|
|
312
399
|
});
|
|
@@ -381,76 +468,6 @@ export default function FileExplorer() {
|
|
|
381
468
|
</Text>
|
|
382
469
|
</>
|
|
383
470
|
);
|
|
384
|
-
const viewSwitcher = (
|
|
385
|
-
<SegmentedControl
|
|
386
|
-
value="column"
|
|
387
|
-
onChange={() => {}}
|
|
388
|
-
label="View mode">
|
|
389
|
-
<SegmentedControlItem
|
|
390
|
-
value="grid"
|
|
391
|
-
label="Grid"
|
|
392
|
-
icon={<Icon icon={LayoutGrid} size="sm" />}
|
|
393
|
-
isLabelHidden
|
|
394
|
-
/>
|
|
395
|
-
<SegmentedControlItem
|
|
396
|
-
value="list"
|
|
397
|
-
label="List"
|
|
398
|
-
icon={<Icon icon={Rows3} size="sm" />}
|
|
399
|
-
isLabelHidden
|
|
400
|
-
/>
|
|
401
|
-
<SegmentedControlItem
|
|
402
|
-
value="column"
|
|
403
|
-
label="Column"
|
|
404
|
-
icon={<Icon icon={Columns} size="sm" />}
|
|
405
|
-
isLabelHidden
|
|
406
|
-
/>
|
|
407
|
-
<SegmentedControlItem
|
|
408
|
-
value="gallery"
|
|
409
|
-
label="Gallery"
|
|
410
|
-
icon={<Icon icon={Table} size="sm" />}
|
|
411
|
-
isLabelHidden
|
|
412
|
-
/>
|
|
413
|
-
</SegmentedControl>
|
|
414
|
-
);
|
|
415
|
-
const fileActions = (
|
|
416
|
-
<>
|
|
417
|
-
<IconButton
|
|
418
|
-
variant="ghost"
|
|
419
|
-
size="sm"
|
|
420
|
-
icon={<Icon icon={SlidersHorizontal} size="sm" />}
|
|
421
|
-
label="Group"
|
|
422
|
-
tooltip="Group"
|
|
423
|
-
/>
|
|
424
|
-
<IconButton
|
|
425
|
-
variant="ghost"
|
|
426
|
-
size="sm"
|
|
427
|
-
icon={<Icon icon={Share2} size="sm" />}
|
|
428
|
-
label="Share"
|
|
429
|
-
tooltip="Share"
|
|
430
|
-
/>
|
|
431
|
-
<IconButton
|
|
432
|
-
variant="ghost"
|
|
433
|
-
size="sm"
|
|
434
|
-
icon={<Icon icon={Tag} size="sm" />}
|
|
435
|
-
label="Tags"
|
|
436
|
-
tooltip="Tags"
|
|
437
|
-
/>
|
|
438
|
-
<IconButton
|
|
439
|
-
variant="ghost"
|
|
440
|
-
size="sm"
|
|
441
|
-
icon={<Icon icon={Ellipsis} size="sm" />}
|
|
442
|
-
label="More"
|
|
443
|
-
tooltip="More"
|
|
444
|
-
/>
|
|
445
|
-
<IconButton
|
|
446
|
-
variant="ghost"
|
|
447
|
-
size="sm"
|
|
448
|
-
icon={<Icon icon={Search} size="sm" />}
|
|
449
|
-
label="Search"
|
|
450
|
-
tooltip="Search"
|
|
451
|
-
/>
|
|
452
|
-
</>
|
|
453
|
-
);
|
|
454
471
|
|
|
455
472
|
return (
|
|
456
473
|
<Layout
|
|
@@ -471,7 +488,13 @@ export default function FileExplorer() {
|
|
|
471
488
|
variant="transparent"
|
|
472
489
|
padding={2}
|
|
473
490
|
dividers={['bottom']}>
|
|
474
|
-
<HStack
|
|
491
|
+
<HStack
|
|
492
|
+
gap={2}
|
|
493
|
+
vAlign="center"
|
|
494
|
+
style={controlsScroll}
|
|
495
|
+
role="region"
|
|
496
|
+
aria-label="View and file actions"
|
|
497
|
+
tabIndex={0}>
|
|
475
498
|
{viewSwitcher}
|
|
476
499
|
{fileActions}
|
|
477
500
|
</HStack>
|
|
@@ -481,6 +504,9 @@ export default function FileExplorer() {
|
|
|
481
504
|
}
|
|
482
505
|
content={
|
|
483
506
|
<LayoutContent padding={0} isScrollable={false}>
|
|
507
|
+
<VisuallyHidden as="h1">
|
|
508
|
+
File Explorer
|
|
509
|
+
</VisuallyHidden>
|
|
484
510
|
<HStack height="100%" style={columnRow}>
|
|
485
511
|
{visibleColumns.map((col, colIndex) => {
|
|
486
512
|
const trueIndex = columnOffset + colIndex;
|
|
@@ -488,12 +514,22 @@ export default function FileExplorer() {
|
|
|
488
514
|
trueIndex < columns.length - 1 || selectedFile != null;
|
|
489
515
|
return (
|
|
490
516
|
<Section
|
|
491
|
-
key={
|
|
517
|
+
key={col.id}
|
|
492
518
|
width={240}
|
|
493
519
|
padding={2}
|
|
494
520
|
variant="transparent"
|
|
495
521
|
dividers={showDivider ? ['end'] : undefined}
|
|
496
|
-
style={{...scrollable, ...fixedColumn}}
|
|
522
|
+
style={{...scrollable, ...fixedColumn}}
|
|
523
|
+
role="region"
|
|
524
|
+
aria-label={`Folder column ${trueIndex + 1}`}
|
|
525
|
+
tabIndex={0}>
|
|
526
|
+
{col.items.length === 0 ? (
|
|
527
|
+
<EmptyState
|
|
528
|
+
title="Quiet in the crypt"
|
|
529
|
+
description="This folder holds no scrolls yet."
|
|
530
|
+
isCompact
|
|
531
|
+
/>
|
|
532
|
+
) : (
|
|
497
533
|
<List density="compact" hasDividers={false}>
|
|
498
534
|
{col.items.map(item => {
|
|
499
535
|
const isSelected = col.selectedId === item.id;
|
|
@@ -537,33 +573,46 @@ export default function FileExplorer() {
|
|
|
537
573
|
);
|
|
538
574
|
})}
|
|
539
575
|
</List>
|
|
576
|
+
)}
|
|
540
577
|
</Section>
|
|
541
578
|
);
|
|
542
579
|
})}
|
|
543
580
|
{selectedFile && (
|
|
544
581
|
<Section
|
|
545
|
-
|
|
582
|
+
width={320}
|
|
583
|
+
// 16px, not 24. The sibling column at :519 is padding={2}
|
|
584
|
+
// (8px) because it holds a dense compact List, and these two
|
|
585
|
+
// sit in one HStack (:510) as columns of the same view --
|
|
586
|
+
// 24 beside 8 made the compact column look broken rather
|
|
587
|
+
// than compact. 16 is the outer-inset value this repo already
|
|
588
|
+
// uses for exactly this (scaffolds.md new-site recipe).
|
|
589
|
+
padding={4}
|
|
546
590
|
variant="transparent"
|
|
547
|
-
style={{...scrollable, ...detailColumn}}
|
|
591
|
+
style={{...scrollable, ...detailColumn}}
|
|
592
|
+
role="region"
|
|
593
|
+
aria-label={`Details for ${selectedFile.name}`}
|
|
594
|
+
tabIndex={0}>
|
|
548
595
|
<VStack gap={4} hAlign="center">
|
|
549
596
|
<Avatar name={selectedFile.name} size={96} />
|
|
550
597
|
<VStack gap={1} hAlign="center">
|
|
551
|
-
<Text type="
|
|
598
|
+
<Text type="body" weight="semibold" maxLines={1}>
|
|
552
599
|
{selectedFile.name}
|
|
553
600
|
</Text>
|
|
554
|
-
<Text type="supporting">
|
|
601
|
+
<Text type="supporting" color="secondary">
|
|
555
602
|
{getFileExtension(selectedFile.name)} Document
|
|
556
603
|
</Text>
|
|
557
604
|
</VStack>
|
|
558
605
|
<MetadataList title="Information">
|
|
559
606
|
<MetadataListItem label="Created">
|
|
560
|
-
March 28, 2026 at 2:15 PM
|
|
607
|
+
<Text type="body">March 28, 2026 at 2:15 PM</Text>
|
|
561
608
|
</MetadataListItem>
|
|
562
609
|
<MetadataListItem label="Modified">
|
|
563
|
-
Yesterday, 10:27 PM
|
|
610
|
+
<Text type="body">Yesterday, 10:27 PM</Text>
|
|
564
611
|
</MetadataListItem>
|
|
565
612
|
<MetadataListItem label="Kind">
|
|
566
|
-
|
|
613
|
+
<Text type="body">
|
|
614
|
+
{getFileExtension(selectedFile.name)} Document
|
|
615
|
+
</Text>
|
|
567
616
|
</MetadataListItem>
|
|
568
617
|
</MetadataList>
|
|
569
618
|
</VStack>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
+
// Ctr > S.transparent[p=10] > V[g=10] > (G[c={min:280,max:2} g=10] > (V[g=6] > (V[g=3] > Hd"Let's conjure together"[level=1 t=display-1] + Tx"Tell us what you're brewing"[t=body]) + AR) + (C[p=4] > V[g=4] > Hd"Your details"[level=2] + TI"Full name"[req] + (G[c={min:180} g=3] > TI"Email"[req] + TI"Company") + (G[c={min:180} g=3] > TI"Job title" + TI"Phone") + (V[g=2] > Tx"Reason"[t=label] + (H[g=2] > Tk"Reason"*7)) + SE"Budget" + TA"Project details" + B.primary"Send it into the night")) + (V[g=6] > D + (G[c={min:200} g=6] > (V[g=1 a=center] > Tx"General"[t=supporting] + Lk"hello@castle.dracula")*3))
|
|
2
4
|
|
|
3
5
|
import {useState, type CSSProperties} from 'react';
|
|
4
6
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
@@ -6,6 +8,7 @@ import {Center} from '@astryxdesign/core/Center';
|
|
|
6
8
|
import {Section} from '@astryxdesign/core/Section';
|
|
7
9
|
import {Grid} from '@astryxdesign/core/Grid';
|
|
8
10
|
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
11
|
+
import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
|
|
9
12
|
import {Button} from '@astryxdesign/core/Button';
|
|
10
13
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
11
14
|
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
@@ -15,91 +18,10 @@ import {Link} from '@astryxdesign/core/Link';
|
|
|
15
18
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
16
19
|
import {Card} from '@astryxdesign/core/Card';
|
|
17
20
|
import {Selector} from '@astryxdesign/core/Selector';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
style={illustrationImg}
|
|
23
|
-
viewBox="0 0 400 300"
|
|
24
|
-
role="img"
|
|
25
|
-
aria-label="Moonlit castle where the night crew works">
|
|
26
|
-
<title>Castle office after dark</title>
|
|
27
|
-
<rect width="400" height="300" fill="var(--dracula-bg-dark)" />
|
|
28
|
-
<g fill="var(--dracula-purple)">
|
|
29
|
-
<circle cx="40" cy="40" r="2.5" />
|
|
30
|
-
<circle cx="120" cy="70" r="2" />
|
|
31
|
-
<circle cx="210" cy="36" r="2.5" />
|
|
32
|
-
<circle cx="330" cy="60" r="2" />
|
|
33
|
-
</g>
|
|
34
|
-
<g fill="var(--dracula-cyan)">
|
|
35
|
-
<circle cx="80" cy="55" r="2" />
|
|
36
|
-
<circle cx="170" cy="60" r="2" />
|
|
37
|
-
<circle cx="290" cy="36" r="2.5" />
|
|
38
|
-
</g>
|
|
39
|
-
<circle cx="315" cy="70" r="34" fill="var(--dracula-yellow)" />
|
|
40
|
-
<circle
|
|
41
|
-
cx="304"
|
|
42
|
-
cy="61"
|
|
43
|
-
r="6"
|
|
44
|
-
fill="var(--dracula-orange)"
|
|
45
|
-
opacity={0.5}
|
|
46
|
-
/>
|
|
47
|
-
<g
|
|
48
|
-
fill="none"
|
|
49
|
-
stroke="var(--dracula-purple)"
|
|
50
|
-
strokeWidth={3}
|
|
51
|
-
strokeLinecap="round">
|
|
52
|
-
<path d="M90 100 q9 -9 18 0 q9 -9 18 0" />
|
|
53
|
-
<path d="M180 84 q9 -9 18 0 q9 -9 18 0" />
|
|
54
|
-
</g>
|
|
55
|
-
<path
|
|
56
|
-
d="M0 220 Q140 180 260 210 T400 200 V300 H0 Z"
|
|
57
|
-
fill="var(--dracula-selection)"
|
|
58
|
-
/>
|
|
59
|
-
<path
|
|
60
|
-
d="M0 255 Q160 225 320 250 T400 245 V300 H0 Z"
|
|
61
|
-
fill="var(--dracula-bg-light)"
|
|
62
|
-
/>
|
|
63
|
-
<g>
|
|
64
|
-
<rect
|
|
65
|
-
x="140"
|
|
66
|
-
y="150"
|
|
67
|
-
width="120"
|
|
68
|
-
height="100"
|
|
69
|
-
rx={4}
|
|
70
|
-
fill="var(--dracula-bg-lighter)"
|
|
71
|
-
/>
|
|
72
|
-
<rect
|
|
73
|
-
x="122"
|
|
74
|
-
y="122"
|
|
75
|
-
width="38"
|
|
76
|
-
height="128"
|
|
77
|
-
rx={4}
|
|
78
|
-
fill="var(--dracula-bg-lighter)"
|
|
79
|
-
/>
|
|
80
|
-
<rect
|
|
81
|
-
x="240"
|
|
82
|
-
y="122"
|
|
83
|
-
width="38"
|
|
84
|
-
height="128"
|
|
85
|
-
rx={4}
|
|
86
|
-
fill="var(--dracula-bg-lighter)"
|
|
87
|
-
/>
|
|
88
|
-
<g fill="var(--dracula-yellow)">
|
|
89
|
-
<rect x="156" y="172" width="12" height="18" rx={2} />
|
|
90
|
-
<rect x="186" y="172" width="12" height="18" rx={2} />
|
|
91
|
-
<rect x="216" y="172" width="12" height="18" rx={2} />
|
|
92
|
-
<rect x="132" y="146" width="9" height="12" rx={2} />
|
|
93
|
-
<rect x="251" y="146" width="9" height="12" rx={2} />
|
|
94
|
-
</g>
|
|
95
|
-
<path
|
|
96
|
-
d="M185 250 v-24 a15 15 0 0 1 30 0 v24 Z"
|
|
97
|
-
fill="var(--dracula-bg-dark)"
|
|
98
|
-
/>
|
|
99
|
-
</g>
|
|
100
|
-
</svg>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
21
|
+
import {Banner} from '@astryxdesign/core/Banner';
|
|
22
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
23
|
+
import {Check} from 'lucide-react';
|
|
24
|
+
import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
|
|
103
25
|
|
|
104
26
|
const INQUIRY_REASONS = [
|
|
105
27
|
'New business',
|
|
@@ -126,17 +48,12 @@ const CONTACT_COLUMNS = [
|
|
|
126
48
|
{label: 'Press & partnerships', email: 'press@castle.dracula'},
|
|
127
49
|
];
|
|
128
50
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
51
|
+
// Castle illustration lives in shared/scene-castle (`card` variant:
|
|
52
|
+
// harvest-yellow moon, fg-stroke bats). Fill + radius-clip live in
|
|
53
|
+
// shared/gallery-image (no Image primitive, #2582).
|
|
131
54
|
const pageStyle: CSSProperties = {
|
|
132
55
|
minHeight: '100%',
|
|
133
56
|
};
|
|
134
|
-
const illustrationImg: CSSProperties = {
|
|
135
|
-
width: '100%',
|
|
136
|
-
height: '100%',
|
|
137
|
-
display: 'block',
|
|
138
|
-
borderRadius: 'var(--radius-container)',
|
|
139
|
-
};
|
|
140
57
|
|
|
141
58
|
/**
|
|
142
59
|
* Form (Two-column) — marketing contact form template.
|
|
@@ -145,7 +62,12 @@ const illustrationImg: CSSProperties = {
|
|
|
145
62
|
* Top: two-column — left has headline + description + illustration,
|
|
146
63
|
* right has the contact form on a card.
|
|
147
64
|
* Bottom: three-column contact info strip.
|
|
148
|
-
*
|
|
65
|
+
*
|
|
66
|
+
* Responsive contract (both grids reflow on their own; no JS breakpoints):
|
|
67
|
+
* Form grid (min 280px tracks, capped at 2 columns): two columns from a
|
|
68
|
+
* 680px viewport up, one column below that.
|
|
69
|
+
* Contact strip (min 200px tracks): three columns from 728px, two columns
|
|
70
|
+
* from 504px, one column below.
|
|
149
71
|
*/
|
|
150
72
|
export default function TwoColumnForm() {
|
|
151
73
|
const [fullName, setFullName] = useState('');
|
|
@@ -166,14 +88,18 @@ export default function TwoColumnForm() {
|
|
|
166
88
|
}
|
|
167
89
|
: {};
|
|
168
90
|
|
|
91
|
+
const isValid =
|
|
92
|
+
fullName.trim() !== '' &&
|
|
93
|
+
email.trim() !== '' &&
|
|
94
|
+
details.trim() !== '';
|
|
95
|
+
|
|
169
96
|
const handleSubmit = () => setSubmitted(true);
|
|
170
97
|
|
|
171
98
|
return (
|
|
172
99
|
<Center style={pageStyle}>
|
|
173
100
|
<Section maxWidth={1100} width="100%" padding={10} variant="transparent">
|
|
174
101
|
<VStack gap={10}>
|
|
175
|
-
{
|
|
176
|
-
<Grid columns={{minWidth: 320}} align="center" gap={10}>
|
|
102
|
+
<Grid columns={{minWidth: 280, max: 2}} align="center" gap={10}>
|
|
177
103
|
<VStack gap={6}>
|
|
178
104
|
<VStack gap={3}>
|
|
179
105
|
<Heading level={1} type="display-1">
|
|
@@ -184,18 +110,18 @@ export default function TwoColumnForm() {
|
|
|
184
110
|
the best path through the night.
|
|
185
111
|
</Text>
|
|
186
112
|
</VStack>
|
|
187
|
-
<AspectRatio ratio={4 / 3}>
|
|
188
|
-
<
|
|
113
|
+
<AspectRatio ratio={4 / 3} style={galleryImageClip}>
|
|
114
|
+
<SceneCastle variant="card" />
|
|
189
115
|
</AspectRatio>
|
|
190
116
|
</VStack>
|
|
191
117
|
|
|
192
|
-
<Card padding={
|
|
118
|
+
<Card padding={4}>
|
|
193
119
|
<VStack gap={4}>
|
|
194
|
-
<
|
|
120
|
+
<Heading level={2}>Your details</Heading>
|
|
195
121
|
<TextInput
|
|
196
122
|
label="Full name"
|
|
197
|
-
|
|
198
|
-
placeholder="Full name
|
|
123
|
+
isRequired
|
|
124
|
+
placeholder="Full name"
|
|
199
125
|
value={fullName}
|
|
200
126
|
onChange={setFullName}
|
|
201
127
|
status={
|
|
@@ -207,8 +133,8 @@ export default function TwoColumnForm() {
|
|
|
207
133
|
<Grid columns={{minWidth: 180}} gap={3}>
|
|
208
134
|
<TextInput
|
|
209
135
|
label="Email"
|
|
210
|
-
|
|
211
|
-
placeholder="Email
|
|
136
|
+
isRequired
|
|
137
|
+
placeholder="Email"
|
|
212
138
|
value={email}
|
|
213
139
|
onChange={setEmail}
|
|
214
140
|
status={
|
|
@@ -219,7 +145,6 @@ export default function TwoColumnForm() {
|
|
|
219
145
|
/>
|
|
220
146
|
<TextInput
|
|
221
147
|
label="Company name"
|
|
222
|
-
isLabelHidden
|
|
223
148
|
placeholder="Company name"
|
|
224
149
|
value={company}
|
|
225
150
|
onChange={setCompany}
|
|
@@ -228,14 +153,12 @@ export default function TwoColumnForm() {
|
|
|
228
153
|
<Grid columns={{minWidth: 180}} gap={3}>
|
|
229
154
|
<TextInput
|
|
230
155
|
label="Job title"
|
|
231
|
-
isLabelHidden
|
|
232
156
|
placeholder="Job title"
|
|
233
157
|
value={jobTitle}
|
|
234
158
|
onChange={setJobTitle}
|
|
235
159
|
/>
|
|
236
160
|
<TextInput
|
|
237
161
|
label="Phone number"
|
|
238
|
-
isLabelHidden
|
|
239
162
|
placeholder="Phone number"
|
|
240
163
|
value={phone}
|
|
241
164
|
onChange={setPhone}
|
|
@@ -245,18 +168,27 @@ export default function TwoColumnForm() {
|
|
|
245
168
|
<VStack gap={2}>
|
|
246
169
|
<Text type="label">What are you reaching out about?</Text>
|
|
247
170
|
<HStack gap={2} wrap="wrap">
|
|
248
|
-
{INQUIRY_REASONS.map(reason =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
171
|
+
{INQUIRY_REASONS.map(reason => {
|
|
172
|
+
const isSelected = inquiryReason === reason;
|
|
173
|
+
return (
|
|
174
|
+
<Token
|
|
175
|
+
key={reason}
|
|
176
|
+
label={reason}
|
|
177
|
+
color={isSelected ? 'yellow' : 'default'}
|
|
178
|
+
icon={
|
|
179
|
+
isSelected ? (
|
|
180
|
+
<Icon icon={Check} size="xsm" color="inherit" />
|
|
181
|
+
) : undefined
|
|
182
|
+
}
|
|
183
|
+
description={isSelected ? 'Selected' : undefined}
|
|
184
|
+
onClick={() =>
|
|
185
|
+
setInquiryReason(prev =>
|
|
186
|
+
prev === reason ? '' : reason,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
260
192
|
</HStack>
|
|
261
193
|
</VStack>
|
|
262
194
|
<Selector
|
|
@@ -264,12 +196,12 @@ export default function TwoColumnForm() {
|
|
|
264
196
|
options={BUDGET_OPTIONS}
|
|
265
197
|
value={budget}
|
|
266
198
|
onChange={setBudget}
|
|
267
|
-
placeholder="Select a budget range
|
|
199
|
+
placeholder="Select a budget range…"
|
|
268
200
|
/>
|
|
269
201
|
<TextArea
|
|
270
202
|
label="Project details"
|
|
271
|
-
|
|
272
|
-
placeholder="Project details
|
|
203
|
+
isRequired
|
|
204
|
+
placeholder="Project details"
|
|
273
205
|
value={details}
|
|
274
206
|
onChange={setDetails}
|
|
275
207
|
status={
|
|
@@ -278,6 +210,13 @@ export default function TwoColumnForm() {
|
|
|
278
210
|
: undefined
|
|
279
211
|
}
|
|
280
212
|
/>
|
|
213
|
+
{submitted && isValid && (
|
|
214
|
+
<Banner
|
|
215
|
+
status="success"
|
|
216
|
+
title="Sent into the night"
|
|
217
|
+
description="The coven received your message and will answer after dark."
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
281
220
|
{/* hAlign="stretch" = full-width button workaround; Button
|
|
282
221
|
has no full-width prop (#2600). */}
|
|
283
222
|
<VStack hAlign="stretch">
|
|
@@ -291,7 +230,6 @@ export default function TwoColumnForm() {
|
|
|
291
230
|
</Card>
|
|
292
231
|
</Grid>
|
|
293
232
|
|
|
294
|
-
{/* Contact strip; stacks below ~440px. */}
|
|
295
233
|
<VStack gap={6}>
|
|
296
234
|
<Divider />
|
|
297
235
|
<Grid columns={{minWidth: 200}} gap={6}>
|
|
@@ -300,7 +238,7 @@ export default function TwoColumnForm() {
|
|
|
300
238
|
<Text type="supporting" color="secondary">
|
|
301
239
|
{col.label}
|
|
302
240
|
</Text>
|
|
303
|
-
<Link href={`mailto:${col.email}`}
|
|
241
|
+
<Link href={`mailto:${col.email}`}>
|
|
304
242
|
{col.email}
|
|
305
243
|
</Link>
|
|
306
244
|
</VStack>
|