astryx-dracula 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRAND.md +30 -18
- package/README.md +17 -12
- 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 +66 -33
- package/templates/ai-chat.tsx +36 -19
- package/templates/blank.tsx +4 -3
- package/templates/centered-hero.tsx +14 -125
- package/templates/classic-gallery.tsx +20 -66
- package/templates/contact-form.tsx +59 -39
- package/templates/dashboard-portfolio.tsx +143 -226
- package/templates/dashboard.tsx +122 -122
- package/templates/detail-page.tsx +95 -91
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +83 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +29 -19
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +52 -74
- package/templates/editor.tsx +112 -84
- package/templates/file-explorer.tsx +131 -84
- package/templates/form-two-column.tsx +61 -125
- package/templates/gallery-hero.tsx +15 -171
- package/templates/ide.tsx +22 -20
- package/templates/incident-console.tsx +104 -70
- package/templates/kanban-board.tsx +99 -42
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +37 -42
- package/templates/login-card.tsx +58 -87
- package/templates/login-split.tsx +88 -126
- package/templates/login-sso.tsx +105 -64
- package/templates/login.tsx +107 -53
- package/templates/messaging-shell.tsx +171 -144
- package/templates/mixed-gallery.tsx +29 -130
- package/templates/payment-form.tsx +368 -290
- package/templates/product-detail.tsx +39 -58
- package/templates/product-gallery.tsx +30 -41
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +135 -282
- package/templates/settings-sidebar.tsx +178 -268
- package/templates/settings.tsx +41 -14
- package/templates/shell-nav.tsx +31 -24
- package/templates/shell-side-nav.tsx +29 -20
- package/templates/shell-top-nav.tsx +80 -44
- package/templates/side-gallery.tsx +38 -94
- package/templates/table-grouped.tsx +62 -34
- package/templates/table-page-chart.tsx +23 -302
- package/templates/table-page-heatmap-status.tsx +156 -92
- package/templates/table-page-shoe-store-heatmap.tsx +18 -293
- package/templates/table-page.tsx +23 -7
- package/templates/table.tsx +23 -8
- 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 +299 -237
- package/theme.css +76 -14
- package/tokens.css +425 -30
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > (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] > 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=
|
|
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))))
|
|
4
4
|
|
|
5
5
|
import {useState, useMemo, type CSSProperties} from 'react';
|
|
6
6
|
import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
@@ -9,9 +9,11 @@ import {List, ListItem} from '@astryxdesign/core/List';
|
|
|
9
9
|
import {HStack, VStack} from '@astryxdesign/core/Layout';
|
|
10
10
|
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
11
11
|
import {Text} from '@astryxdesign/core/Text';
|
|
12
|
+
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
12
13
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
13
14
|
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
14
15
|
import {Section} from '@astryxdesign/core/Section';
|
|
16
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
15
17
|
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
16
18
|
import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
|
|
17
19
|
import {
|
|
@@ -264,13 +266,85 @@ const page: CSSProperties = {height: '100dvh'};
|
|
|
264
266
|
const columnRow: CSSProperties = {overflowX: 'auto', overflowY: 'hidden'};
|
|
265
267
|
const scrollable: CSSProperties = {overflowY: 'auto'};
|
|
266
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.
|
|
267
272
|
const detailColumn: CSSProperties = {
|
|
268
273
|
flexGrow: 1,
|
|
269
274
|
flexShrink: 0,
|
|
270
|
-
flexBasis: 320,
|
|
271
275
|
};
|
|
272
276
|
const controlsScroll: CSSProperties = {overflowX: 'auto'};
|
|
273
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
|
+
|
|
274
348
|
function findItem(items: FileSystemItem[], id: string): FileSystemItem | null {
|
|
275
349
|
for (const item of items) {
|
|
276
350
|
if (item.id === id) {
|
|
@@ -299,16 +373,27 @@ export default function FileExplorer() {
|
|
|
299
373
|
]);
|
|
300
374
|
|
|
301
375
|
const isNarrow = useMediaQuery('(max-width: 768px)');
|
|
302
|
-
const isPhone =
|
|
376
|
+
const isPhone = isNarrow;
|
|
303
377
|
|
|
304
378
|
const columns = useMemo(() => {
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
});
|
|
307
391
|
let currentItems: FileSystemItem[] = FILESYSTEM;
|
|
308
392
|
for (let i = 0; i < selectedPath.length; i++) {
|
|
309
393
|
const selected = currentItems.find(item => item.id === selectedPath[i]);
|
|
310
394
|
if (selected?.children && selected.children.length > 0) {
|
|
311
395
|
cols.push({
|
|
396
|
+
id: selectedPath[i],
|
|
312
397
|
items: selected.children,
|
|
313
398
|
selectedId: selectedPath[i + 1] ?? null,
|
|
314
399
|
});
|
|
@@ -383,76 +468,6 @@ export default function FileExplorer() {
|
|
|
383
468
|
</Text>
|
|
384
469
|
</>
|
|
385
470
|
);
|
|
386
|
-
const viewSwitcher = (
|
|
387
|
-
<SegmentedControl
|
|
388
|
-
value="column"
|
|
389
|
-
onChange={() => {}}
|
|
390
|
-
label="View mode">
|
|
391
|
-
<SegmentedControlItem
|
|
392
|
-
value="grid"
|
|
393
|
-
label="Grid"
|
|
394
|
-
icon={<Icon icon={LayoutGrid} size="sm" />}
|
|
395
|
-
isLabelHidden
|
|
396
|
-
/>
|
|
397
|
-
<SegmentedControlItem
|
|
398
|
-
value="list"
|
|
399
|
-
label="List"
|
|
400
|
-
icon={<Icon icon={Rows3} size="sm" />}
|
|
401
|
-
isLabelHidden
|
|
402
|
-
/>
|
|
403
|
-
<SegmentedControlItem
|
|
404
|
-
value="column"
|
|
405
|
-
label="Column"
|
|
406
|
-
icon={<Icon icon={Columns} size="sm" />}
|
|
407
|
-
isLabelHidden
|
|
408
|
-
/>
|
|
409
|
-
<SegmentedControlItem
|
|
410
|
-
value="gallery"
|
|
411
|
-
label="Gallery"
|
|
412
|
-
icon={<Icon icon={Table} size="sm" />}
|
|
413
|
-
isLabelHidden
|
|
414
|
-
/>
|
|
415
|
-
</SegmentedControl>
|
|
416
|
-
);
|
|
417
|
-
const fileActions = (
|
|
418
|
-
<>
|
|
419
|
-
<IconButton
|
|
420
|
-
variant="ghost"
|
|
421
|
-
size="sm"
|
|
422
|
-
icon={<Icon icon={SlidersHorizontal} size="sm" />}
|
|
423
|
-
label="Group"
|
|
424
|
-
tooltip="Group"
|
|
425
|
-
/>
|
|
426
|
-
<IconButton
|
|
427
|
-
variant="ghost"
|
|
428
|
-
size="sm"
|
|
429
|
-
icon={<Icon icon={Share2} size="sm" />}
|
|
430
|
-
label="Share"
|
|
431
|
-
tooltip="Share"
|
|
432
|
-
/>
|
|
433
|
-
<IconButton
|
|
434
|
-
variant="ghost"
|
|
435
|
-
size="sm"
|
|
436
|
-
icon={<Icon icon={Tag} size="sm" />}
|
|
437
|
-
label="Tags"
|
|
438
|
-
tooltip="Tags"
|
|
439
|
-
/>
|
|
440
|
-
<IconButton
|
|
441
|
-
variant="ghost"
|
|
442
|
-
size="sm"
|
|
443
|
-
icon={<Icon icon={Ellipsis} size="sm" />}
|
|
444
|
-
label="More"
|
|
445
|
-
tooltip="More"
|
|
446
|
-
/>
|
|
447
|
-
<IconButton
|
|
448
|
-
variant="ghost"
|
|
449
|
-
size="sm"
|
|
450
|
-
icon={<Icon icon={Search} size="sm" />}
|
|
451
|
-
label="Search"
|
|
452
|
-
tooltip="Search"
|
|
453
|
-
/>
|
|
454
|
-
</>
|
|
455
|
-
);
|
|
456
471
|
|
|
457
472
|
return (
|
|
458
473
|
<Layout
|
|
@@ -473,7 +488,13 @@ export default function FileExplorer() {
|
|
|
473
488
|
variant="transparent"
|
|
474
489
|
padding={2}
|
|
475
490
|
dividers={['bottom']}>
|
|
476
|
-
<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}>
|
|
477
498
|
{viewSwitcher}
|
|
478
499
|
{fileActions}
|
|
479
500
|
</HStack>
|
|
@@ -483,6 +504,9 @@ export default function FileExplorer() {
|
|
|
483
504
|
}
|
|
484
505
|
content={
|
|
485
506
|
<LayoutContent padding={0} isScrollable={false}>
|
|
507
|
+
<VisuallyHidden as="h1">
|
|
508
|
+
File Explorer
|
|
509
|
+
</VisuallyHidden>
|
|
486
510
|
<HStack height="100%" style={columnRow}>
|
|
487
511
|
{visibleColumns.map((col, colIndex) => {
|
|
488
512
|
const trueIndex = columnOffset + colIndex;
|
|
@@ -490,12 +514,22 @@ export default function FileExplorer() {
|
|
|
490
514
|
trueIndex < columns.length - 1 || selectedFile != null;
|
|
491
515
|
return (
|
|
492
516
|
<Section
|
|
493
|
-
key={
|
|
517
|
+
key={col.id}
|
|
494
518
|
width={240}
|
|
495
519
|
padding={2}
|
|
496
520
|
variant="transparent"
|
|
497
521
|
dividers={showDivider ? ['end'] : undefined}
|
|
498
|
-
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
|
+
) : (
|
|
499
533
|
<List density="compact" hasDividers={false}>
|
|
500
534
|
{col.items.map(item => {
|
|
501
535
|
const isSelected = col.selectedId === item.id;
|
|
@@ -539,18 +573,29 @@ export default function FileExplorer() {
|
|
|
539
573
|
);
|
|
540
574
|
})}
|
|
541
575
|
</List>
|
|
576
|
+
)}
|
|
542
577
|
</Section>
|
|
543
578
|
);
|
|
544
579
|
})}
|
|
545
580
|
{selectedFile && (
|
|
546
581
|
<Section
|
|
547
|
-
|
|
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}
|
|
548
590
|
variant="transparent"
|
|
549
|
-
style={{...scrollable, ...detailColumn}}
|
|
591
|
+
style={{...scrollable, ...detailColumn}}
|
|
592
|
+
role="region"
|
|
593
|
+
aria-label={`Details for ${selectedFile.name}`}
|
|
594
|
+
tabIndex={0}>
|
|
550
595
|
<VStack gap={4} hAlign="center">
|
|
551
596
|
<Avatar name={selectedFile.name} size={96} />
|
|
552
597
|
<VStack gap={1} hAlign="center">
|
|
553
|
-
<Text type="
|
|
598
|
+
<Text type="body" weight="semibold" maxLines={1}>
|
|
554
599
|
{selectedFile.name}
|
|
555
600
|
</Text>
|
|
556
601
|
<Text type="supporting" color="secondary">
|
|
@@ -559,13 +604,15 @@ export default function FileExplorer() {
|
|
|
559
604
|
</VStack>
|
|
560
605
|
<MetadataList title="Information">
|
|
561
606
|
<MetadataListItem label="Created">
|
|
562
|
-
March 28, 2026 at 2:15 PM
|
|
607
|
+
<Text type="body">March 28, 2026 at 2:15 PM</Text>
|
|
563
608
|
</MetadataListItem>
|
|
564
609
|
<MetadataListItem label="Modified">
|
|
565
|
-
Yesterday, 10:27 PM
|
|
610
|
+
<Text type="body">Yesterday, 10:27 PM</Text>
|
|
566
611
|
</MetadataListItem>
|
|
567
612
|
<MetadataListItem label="Kind">
|
|
568
|
-
|
|
613
|
+
<Text type="body">
|
|
614
|
+
{getFileExtension(selectedFile.name)} Document
|
|
615
|
+
</Text>
|
|
569
616
|
</MetadataListItem>
|
|
570
617
|
</MetadataList>
|
|
571
618
|
</VStack>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// Ctr > S.transparent[p=10] > V[g=10] > (G[c={min:
|
|
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))
|
|
4
4
|
|
|
5
5
|
import {useState, type CSSProperties} from 'react';
|
|
6
6
|
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
@@ -8,6 +8,7 @@ import {Center} from '@astryxdesign/core/Center';
|
|
|
8
8
|
import {Section} from '@astryxdesign/core/Section';
|
|
9
9
|
import {Grid} from '@astryxdesign/core/Grid';
|
|
10
10
|
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
11
|
+
import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
|
|
11
12
|
import {Button} from '@astryxdesign/core/Button';
|
|
12
13
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
13
14
|
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
@@ -17,91 +18,10 @@ import {Link} from '@astryxdesign/core/Link';
|
|
|
17
18
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
18
19
|
import {Card} from '@astryxdesign/core/Card';
|
|
19
20
|
import {Selector} from '@astryxdesign/core/Selector';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
style={illustrationImg}
|
|
25
|
-
viewBox="0 0 400 300"
|
|
26
|
-
role="img"
|
|
27
|
-
aria-label="Moonlit castle where the night crew works">
|
|
28
|
-
<title>Castle office after dark</title>
|
|
29
|
-
<rect width="400" height="300" fill="var(--dracula-bg-dark)" />
|
|
30
|
-
<g fill="var(--dracula-purple)">
|
|
31
|
-
<circle cx="40" cy="40" r="2.5" />
|
|
32
|
-
<circle cx="120" cy="70" r="2" />
|
|
33
|
-
<circle cx="210" cy="36" r="2.5" />
|
|
34
|
-
<circle cx="330" cy="60" r="2" />
|
|
35
|
-
</g>
|
|
36
|
-
<g fill="var(--dracula-cyan)">
|
|
37
|
-
<circle cx="80" cy="55" r="2" />
|
|
38
|
-
<circle cx="170" cy="60" r="2" />
|
|
39
|
-
<circle cx="290" cy="36" r="2.5" />
|
|
40
|
-
</g>
|
|
41
|
-
<circle cx="315" cy="70" r="34" fill="var(--dracula-yellow)" />
|
|
42
|
-
<circle
|
|
43
|
-
cx="304"
|
|
44
|
-
cy="61"
|
|
45
|
-
r="6"
|
|
46
|
-
fill="var(--dracula-orange)"
|
|
47
|
-
opacity={0.5}
|
|
48
|
-
/>
|
|
49
|
-
<g
|
|
50
|
-
fill="none"
|
|
51
|
-
stroke="var(--dracula-purple)"
|
|
52
|
-
strokeWidth={3}
|
|
53
|
-
strokeLinecap="round">
|
|
54
|
-
<path d="M90 100 q9 -9 18 0 q9 -9 18 0" />
|
|
55
|
-
<path d="M180 84 q9 -9 18 0 q9 -9 18 0" />
|
|
56
|
-
</g>
|
|
57
|
-
<path
|
|
58
|
-
d="M0 220 Q140 180 260 210 T400 200 V300 H0 Z"
|
|
59
|
-
fill="var(--dracula-selection)"
|
|
60
|
-
/>
|
|
61
|
-
<path
|
|
62
|
-
d="M0 255 Q160 225 320 250 T400 245 V300 H0 Z"
|
|
63
|
-
fill="var(--dracula-bg-light)"
|
|
64
|
-
/>
|
|
65
|
-
<g>
|
|
66
|
-
<rect
|
|
67
|
-
x="140"
|
|
68
|
-
y="150"
|
|
69
|
-
width="120"
|
|
70
|
-
height="100"
|
|
71
|
-
rx={4}
|
|
72
|
-
fill="var(--dracula-bg-lighter)"
|
|
73
|
-
/>
|
|
74
|
-
<rect
|
|
75
|
-
x="122"
|
|
76
|
-
y="122"
|
|
77
|
-
width="38"
|
|
78
|
-
height="128"
|
|
79
|
-
rx={4}
|
|
80
|
-
fill="var(--dracula-bg-lighter)"
|
|
81
|
-
/>
|
|
82
|
-
<rect
|
|
83
|
-
x="240"
|
|
84
|
-
y="122"
|
|
85
|
-
width="38"
|
|
86
|
-
height="128"
|
|
87
|
-
rx={4}
|
|
88
|
-
fill="var(--dracula-bg-lighter)"
|
|
89
|
-
/>
|
|
90
|
-
<g fill="var(--dracula-yellow)">
|
|
91
|
-
<rect x="156" y="172" width="12" height="18" rx={2} />
|
|
92
|
-
<rect x="186" y="172" width="12" height="18" rx={2} />
|
|
93
|
-
<rect x="216" y="172" width="12" height="18" rx={2} />
|
|
94
|
-
<rect x="132" y="146" width="9" height="12" rx={2} />
|
|
95
|
-
<rect x="251" y="146" width="9" height="12" rx={2} />
|
|
96
|
-
</g>
|
|
97
|
-
<path
|
|
98
|
-
d="M185 250 v-24 a15 15 0 0 1 30 0 v24 Z"
|
|
99
|
-
fill="var(--dracula-bg-dark)"
|
|
100
|
-
/>
|
|
101
|
-
</g>
|
|
102
|
-
</svg>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
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';
|
|
105
25
|
|
|
106
26
|
const INQUIRY_REASONS = [
|
|
107
27
|
'New business',
|
|
@@ -128,17 +48,12 @@ const CONTACT_COLUMNS = [
|
|
|
128
48
|
{label: 'Press & partnerships', email: 'press@castle.dracula'},
|
|
129
49
|
];
|
|
130
50
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
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).
|
|
133
54
|
const pageStyle: CSSProperties = {
|
|
134
55
|
minHeight: '100%',
|
|
135
56
|
};
|
|
136
|
-
const illustrationImg: CSSProperties = {
|
|
137
|
-
width: '100%',
|
|
138
|
-
height: '100%',
|
|
139
|
-
display: 'block',
|
|
140
|
-
borderRadius: 'var(--radius-container)',
|
|
141
|
-
};
|
|
142
57
|
|
|
143
58
|
/**
|
|
144
59
|
* Form (Two-column) — marketing contact form template.
|
|
@@ -147,7 +62,12 @@ const illustrationImg: CSSProperties = {
|
|
|
147
62
|
* Top: two-column — left has headline + description + illustration,
|
|
148
63
|
* right has the contact form on a card.
|
|
149
64
|
* Bottom: three-column contact info strip.
|
|
150
|
-
*
|
|
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.
|
|
151
71
|
*/
|
|
152
72
|
export default function TwoColumnForm() {
|
|
153
73
|
const [fullName, setFullName] = useState('');
|
|
@@ -168,14 +88,18 @@ export default function TwoColumnForm() {
|
|
|
168
88
|
}
|
|
169
89
|
: {};
|
|
170
90
|
|
|
91
|
+
const isValid =
|
|
92
|
+
fullName.trim() !== '' &&
|
|
93
|
+
email.trim() !== '' &&
|
|
94
|
+
details.trim() !== '';
|
|
95
|
+
|
|
171
96
|
const handleSubmit = () => setSubmitted(true);
|
|
172
97
|
|
|
173
98
|
return (
|
|
174
99
|
<Center style={pageStyle}>
|
|
175
100
|
<Section maxWidth={1100} width="100%" padding={10} variant="transparent">
|
|
176
101
|
<VStack gap={10}>
|
|
177
|
-
{
|
|
178
|
-
<Grid columns={{minWidth: 320}} align="center" gap={10}>
|
|
102
|
+
<Grid columns={{minWidth: 280, max: 2}} align="center" gap={10}>
|
|
179
103
|
<VStack gap={6}>
|
|
180
104
|
<VStack gap={3}>
|
|
181
105
|
<Heading level={1} type="display-1">
|
|
@@ -186,18 +110,18 @@ export default function TwoColumnForm() {
|
|
|
186
110
|
the best path through the night.
|
|
187
111
|
</Text>
|
|
188
112
|
</VStack>
|
|
189
|
-
<AspectRatio ratio={4 / 3}>
|
|
190
|
-
<
|
|
113
|
+
<AspectRatio ratio={4 / 3} style={galleryImageClip}>
|
|
114
|
+
<SceneCastle variant="card" />
|
|
191
115
|
</AspectRatio>
|
|
192
116
|
</VStack>
|
|
193
117
|
|
|
194
|
-
<Card padding={
|
|
118
|
+
<Card padding={4}>
|
|
195
119
|
<VStack gap={4}>
|
|
196
|
-
<
|
|
120
|
+
<Heading level={2}>Your details</Heading>
|
|
197
121
|
<TextInput
|
|
198
122
|
label="Full name"
|
|
199
|
-
|
|
200
|
-
placeholder="Full name
|
|
123
|
+
isRequired
|
|
124
|
+
placeholder="Full name"
|
|
201
125
|
value={fullName}
|
|
202
126
|
onChange={setFullName}
|
|
203
127
|
status={
|
|
@@ -209,8 +133,8 @@ export default function TwoColumnForm() {
|
|
|
209
133
|
<Grid columns={{minWidth: 180}} gap={3}>
|
|
210
134
|
<TextInput
|
|
211
135
|
label="Email"
|
|
212
|
-
|
|
213
|
-
placeholder="Email
|
|
136
|
+
isRequired
|
|
137
|
+
placeholder="Email"
|
|
214
138
|
value={email}
|
|
215
139
|
onChange={setEmail}
|
|
216
140
|
status={
|
|
@@ -221,7 +145,6 @@ export default function TwoColumnForm() {
|
|
|
221
145
|
/>
|
|
222
146
|
<TextInput
|
|
223
147
|
label="Company name"
|
|
224
|
-
isLabelHidden
|
|
225
148
|
placeholder="Company name"
|
|
226
149
|
value={company}
|
|
227
150
|
onChange={setCompany}
|
|
@@ -230,14 +153,12 @@ export default function TwoColumnForm() {
|
|
|
230
153
|
<Grid columns={{minWidth: 180}} gap={3}>
|
|
231
154
|
<TextInput
|
|
232
155
|
label="Job title"
|
|
233
|
-
isLabelHidden
|
|
234
156
|
placeholder="Job title"
|
|
235
157
|
value={jobTitle}
|
|
236
158
|
onChange={setJobTitle}
|
|
237
159
|
/>
|
|
238
160
|
<TextInput
|
|
239
161
|
label="Phone number"
|
|
240
|
-
isLabelHidden
|
|
241
162
|
placeholder="Phone number"
|
|
242
163
|
value={phone}
|
|
243
164
|
onChange={setPhone}
|
|
@@ -247,18 +168,27 @@ export default function TwoColumnForm() {
|
|
|
247
168
|
<VStack gap={2}>
|
|
248
169
|
<Text type="label">What are you reaching out about?</Text>
|
|
249
170
|
<HStack gap={2} wrap="wrap">
|
|
250
|
-
{INQUIRY_REASONS.map(reason =>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
})}
|
|
262
192
|
</HStack>
|
|
263
193
|
</VStack>
|
|
264
194
|
<Selector
|
|
@@ -266,12 +196,12 @@ export default function TwoColumnForm() {
|
|
|
266
196
|
options={BUDGET_OPTIONS}
|
|
267
197
|
value={budget}
|
|
268
198
|
onChange={setBudget}
|
|
269
|
-
placeholder="Select a budget range
|
|
199
|
+
placeholder="Select a budget range…"
|
|
270
200
|
/>
|
|
271
201
|
<TextArea
|
|
272
202
|
label="Project details"
|
|
273
|
-
|
|
274
|
-
placeholder="Project details
|
|
203
|
+
isRequired
|
|
204
|
+
placeholder="Project details"
|
|
275
205
|
value={details}
|
|
276
206
|
onChange={setDetails}
|
|
277
207
|
status={
|
|
@@ -280,6 +210,13 @@ export default function TwoColumnForm() {
|
|
|
280
210
|
: undefined
|
|
281
211
|
}
|
|
282
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
|
+
)}
|
|
283
220
|
{/* hAlign="stretch" = full-width button workaround; Button
|
|
284
221
|
has no full-width prop (#2600). */}
|
|
285
222
|
<VStack hAlign="stretch">
|
|
@@ -293,7 +230,6 @@ export default function TwoColumnForm() {
|
|
|
293
230
|
</Card>
|
|
294
231
|
</Grid>
|
|
295
232
|
|
|
296
|
-
{/* Contact strip; stacks below ~440px. */}
|
|
297
233
|
<VStack gap={6}>
|
|
298
234
|
<Divider />
|
|
299
235
|
<Grid columns={{minWidth: 200}} gap={6}>
|
|
@@ -302,7 +238,7 @@ export default function TwoColumnForm() {
|
|
|
302
238
|
<Text type="supporting" color="secondary">
|
|
303
239
|
{col.label}
|
|
304
240
|
</Text>
|
|
305
|
-
<Link href={`mailto:${col.email}`}
|
|
241
|
+
<Link href={`mailto:${col.email}`}>
|
|
306
242
|
{col.email}
|
|
307
243
|
</Link>
|
|
308
244
|
</VStack>
|