astryx-dracula 0.6.2 → 0.7.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 +54 -3
- package/README.md +11 -4
- package/USAGE.md +1 -1
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +26 -4
- package/astryx-dracula.variants.d.ts +2 -2
- package/package.json +11 -11
- package/shared/auth-chrome-config.ts +0 -4
- package/shared/auth-chrome.tsx +2 -3
- package/shared/chaptered-doc-config.ts +3 -2
- package/shared/order-desk.tsx +52 -54
- package/shared/revenue-chart.tsx +1 -1
- package/shared/scene-castle.tsx +21 -13
- package/templates/ai-chat-landing.tsx +6 -9
- package/templates/ai-chat.tsx +5 -1
- package/templates/centered-hero.tsx +2 -2
- package/templates/classic-gallery.tsx +5 -0
- package/templates/contact-form.tsx +9 -0
- package/templates/dashboard-portfolio.tsx +17 -4
- package/templates/dashboard.tsx +5 -5
- package/templates/detail-page.tsx +20 -28
- package/templates/documentation-design.tsx +3 -0
- package/templates/documentation-technical.tsx +8 -5
- package/templates/documentation.tsx +13 -6
- package/templates/form-two-column.tsx +11 -2
- package/templates/form-wizard.template.mjs +15 -0
- package/templates/form-wizard.tsx +878 -0
- package/templates/gallery-hero.tsx +10 -4
- package/templates/ide.tsx +177 -96
- package/templates/incident-console.tsx +15 -5
- package/templates/kanban-board.tsx +46 -4
- package/templates/library.tsx +12 -3
- package/templates/login-split.tsx +3 -3
- package/templates/login-sso.tsx +9 -17
- package/templates/messaging-shell.tsx +109 -46
- package/templates/mixed-gallery.tsx +6 -0
- package/templates/payment-form.tsx +29 -28
- package/templates/product-detail.tsx +12 -3
- package/templates/product-gallery.tsx +8 -3
- package/templates/product-tour.tsx +11 -7
- package/templates/settings-dialog.tsx +5 -0
- package/templates/settings-sidebar.tsx +603 -584
- package/templates/settings.tsx +5 -2
- package/templates/shell-nav.tsx +23 -3
- package/templates/shell-side-nav.tsx +94 -81
- package/templates/shell-top-nav.tsx +98 -86
- package/templates/table-grouped.tsx +146 -132
- package/templates/{table-page-shoe-store-heatmap.template.mjs → table-page-shoe-store.template.mjs} +1 -1
- package/templates/{table-page-shoe-store-heatmap.tsx → table-page-shoe-store.tsx} +1 -1
- package/templates/table-page.tsx +8 -0
- package/templates/table-tree.template.mjs +16 -0
- package/templates/table-tree.tsx +707 -0
- package/templates/tech-report.tsx +8 -5
- package/templates/theme-showcase.tsx +18 -11
- package/templates/work-item-detail.template.mjs +8 -0
- package/templates/work-item-detail.tsx +1049 -0
- package/theme.css +38 -3
- package/theme.css.d.ts +2 -2
- package/tokens.css +41 -4
package/templates/login-sso.tsx
CHANGED
|
@@ -14,10 +14,7 @@ import { HStack, VStack } from "@astryxdesign/core/Layout";
|
|
|
14
14
|
import { Link } from "@astryxdesign/core/Link";
|
|
15
15
|
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
16
16
|
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
17
|
-
import {
|
|
18
|
-
inputAutoComplete,
|
|
19
|
-
authPageStyle as pageStyle,
|
|
20
|
-
} from "astryx-dracula/shared/auth-chrome-config";
|
|
17
|
+
import { authPageStyle as pageStyle } from "astryx-dracula/shared/auth-chrome-config";
|
|
21
18
|
import {
|
|
22
19
|
AUTH_EMAIL_PLACEHOLDER,
|
|
23
20
|
AUTH_ERROR_MESSAGE,
|
|
@@ -41,14 +38,13 @@ import { useState, useTransition } from "react";
|
|
|
41
38
|
|
|
42
39
|
type SSOProvider = {
|
|
43
40
|
name: string;
|
|
44
|
-
abbr: string;
|
|
45
41
|
};
|
|
46
42
|
const SSO_PROVIDERS: Record<string, SSOProvider> = {
|
|
47
|
-
"google.com": { name: "Google Workspace"
|
|
48
|
-
"microsoft.com": { name: "Microsoft Entra ID"
|
|
49
|
-
"okta.com": { name: "Okta"
|
|
50
|
-
"meta.com": { name: "Meta SSO"
|
|
51
|
-
"apple.com": { name: "Apple Business"
|
|
43
|
+
"google.com": { name: "Google Workspace" },
|
|
44
|
+
"microsoft.com": { name: "Microsoft Entra ID" },
|
|
45
|
+
"okta.com": { name: "Okta" },
|
|
46
|
+
"meta.com": { name: "Meta SSO" },
|
|
47
|
+
"apple.com": { name: "Apple Business" },
|
|
52
48
|
};
|
|
53
49
|
|
|
54
50
|
function getProvider(email: string) {
|
|
@@ -136,7 +132,7 @@ export default function LoginSso() {
|
|
|
136
132
|
label="Work email"
|
|
137
133
|
isLabelHidden
|
|
138
134
|
type="email"
|
|
139
|
-
|
|
135
|
+
autoComplete="email"
|
|
140
136
|
placeholder={AUTH_EMAIL_PLACEHOLDER}
|
|
141
137
|
value={email}
|
|
142
138
|
onChange={(v: string) => {
|
|
@@ -144,11 +140,7 @@ export default function LoginSso() {
|
|
|
144
140
|
setError(null);
|
|
145
141
|
}}
|
|
146
142
|
size="lg"
|
|
147
|
-
|
|
148
|
-
if (e.key === "Enter") {
|
|
149
|
-
handleContinue();
|
|
150
|
-
}
|
|
151
|
-
}}
|
|
143
|
+
onEnter={handleContinue}
|
|
152
144
|
status={error ? { type: "error", message: error } : undefined}
|
|
153
145
|
/>
|
|
154
146
|
|
|
@@ -246,7 +238,7 @@ export default function LoginSso() {
|
|
|
246
238
|
<TextInput
|
|
247
239
|
label="Password"
|
|
248
240
|
type="password"
|
|
249
|
-
|
|
241
|
+
autoComplete="current-password"
|
|
250
242
|
value={password}
|
|
251
243
|
size="lg"
|
|
252
244
|
onChange={(v: string) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
3
|
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
4
|
// human drift check — update it with the file, never alone):
|
|
5
|
-
// L > (LP[w=68] > V[g=2] > Av + IB*4 + SI[fill] + IB"Settings") + (LP[w=260] > V > (H[g=2] > (SI[fill] > Hd"Messages"[level=1]) + IB"New message") + (V[g=0] > TI"Jump to…") + (SI[fill] > (UL[density=compact edge=flush !hasDividers] > (Tx"Channels"[t=label] + LI*4)) + (UL[density=compact edge=flush !hasDividers] > (Tx"Direct messages"[t=label] + LI*3)))) + (LC[p=0] > V > (H[g=3] > Ic + Hd"design-systems"[level=2] + (SI[fill] > Tx"Component APIs, tokens, and release coordination"[t=body]) + SD"12 online" + IB"Members") + D + (SI[fill] > ChL > (ChML[density=balanced] > ChSM"Tuesday, June 30" + ChSM"Sasha Ortiz joined #design-systems" + (ChM > ChBu) + (ChM > ChBu*2) + (ChM > ChBu*2) + (ChM > ChBu) + (ChM > ChBu)) + ChC"Message #design-systems" + EmptyState"No messages yet")) + (LP[w=340] > V > (H[g=2] > (SI[fill] > (H[g=2] > Tx"Thread"[weight=semibold] + Tx"#design-systems"[t=supporting])) + IB"Close thread") + D + (SI[fill] > ChML[density=compact] > (ChM > ChBu) + ChSM"2 replies" + (ChM > ChBu)*2) + (V[g=0] > ChC"Reply in thread…")))
|
|
5
|
+
// L > (LP[w=68] > V[g=2] > Av + IB*4 + SI[fill] + IB"Settings") + (LP[w=260] > V > (H[g=2] > (SI[fill] > Hd"Messages"[level=1]) + IB"New message") + (V[g=0] > TI"Jump to…") + (SI[fill] > (UL[density=compact edge=flush !hasDividers] > (Tx"Channels"[t=label] + LI*4)) + (UL[density=compact edge=flush !hasDividers] > (Tx"Direct messages"[t=label] + LI*3)))) + (LC[p=0] > V > (H[g=3] > Ic + Hd"design-systems"[level=2] + (SI[fill] > Tx"Component APIs, tokens, and release coordination"[t=body]) + SD"12 online" + IB"Open thread" + IB"Members") + D + (SI[fill] > ChL > (ChML[density=balanced] > ChSM"Tuesday, June 30" + ChSM"Sasha Ortiz joined #design-systems" + (ChM > ChBu) + (ChM > ChBu*2) + (ChM > ChBu*2) + (ChM > ChBu) + (ChM > ChBu)) + ChC"Message #design-systems" + EmptyState"No messages yet")) + (LP[w=340] > V > (H[g=2] > (SI[fill] > (H[g=2] > Tx"Thread"[weight=semibold] + Tx"#design-systems"[t=supporting])) + IB"Close thread") + D + (SI[fill] > ChML[density=compact] > (ChM > ChBu) + ChSM"2 replies" + (ChM > ChBu)*2) + (V[g=0] > ChC"Reply in thread…")) ;; Drawer"Thread"[side=end w=340] > (DrawerHeader"Thread" + (V > (SI[fill] > ChML[density=compact] > (ChM > ChBu) + ChSM"2 replies" + (ChM > ChBu)*2) + (V[g=0] > ChC"Reply in thread…")))
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Messaging Shell — Slack-style column frame for team messaging tools. Frame (desktop, left to right): (Frame/responsive/container: see XLE header above.)
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ChatSystemMessage,
|
|
21
21
|
} from "@astryxdesign/core/Chat";
|
|
22
22
|
import { Divider } from "@astryxdesign/core/Divider";
|
|
23
|
+
import { Drawer, DrawerHeader } from "@astryxdesign/core/Drawer";
|
|
23
24
|
import { EmptyState } from "@astryxdesign/core/EmptyState";
|
|
24
25
|
import { useMediaQuery } from "@astryxdesign/core/hooks";
|
|
25
26
|
import { Icon } from "@astryxdesign/core/Icon";
|
|
@@ -38,6 +39,7 @@ import { StatusDot } from "@astryxdesign/core/StatusDot";
|
|
|
38
39
|
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
39
40
|
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
40
41
|
import { Timestamp } from "@astryxdesign/core/Timestamp";
|
|
42
|
+
import { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden";
|
|
41
43
|
// ============= ICONS (verified lucide-react exports) =============
|
|
42
44
|
// Bell ← BellIcon, Bookmark ← BookmarkIcon, MessagesSquare ← ChatBubbleLeftRightIcon,
|
|
43
45
|
// Settings ← Cog6ToothIcon, Hash ← HashtagIcon, House ← HomeIcon, Inbox ← InboxIcon,
|
|
@@ -420,6 +422,10 @@ export default function MessagingShell() {
|
|
|
420
422
|
const [selectedChannelId, setSelectedChannelId] = useState("design-systems");
|
|
421
423
|
const [selectedDmId, setSelectedDmId] = useState<string | null>(null);
|
|
422
424
|
const [isThreadOpen, setIsThreadOpen] = useState(true);
|
|
425
|
+
// Below 1024 the thread is a Drawer instead of a column, and that overlay
|
|
426
|
+
// owns its own open state: loaded narrow, it starts closed and the message
|
|
427
|
+
// header is what opens it.
|
|
428
|
+
const [isThreadDrawerOpen, setIsThreadDrawerOpen] = useState(false);
|
|
423
429
|
const [searchQuery, setSearchQuery] = useState("");
|
|
424
430
|
|
|
425
431
|
// Responsive contract (see file header).
|
|
@@ -439,6 +445,16 @@ export default function MessagingShell() {
|
|
|
439
445
|
);
|
|
440
446
|
|
|
441
447
|
const showThreadPanel = isThreadOpen && !isThreadHidden;
|
|
448
|
+
// One control drives both presentations — the docked column at 1024 and
|
|
449
|
+
// above, the Drawer below it — so closing the thread is never one-way.
|
|
450
|
+
const isThreadVisible = isThreadHidden ? isThreadDrawerOpen : showThreadPanel;
|
|
451
|
+
const toggleThread = () => {
|
|
452
|
+
if (isThreadHidden) {
|
|
453
|
+
setIsThreadDrawerOpen((isOpen) => !isOpen);
|
|
454
|
+
} else {
|
|
455
|
+
setIsThreadOpen((isOpen) => !isOpen);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
442
458
|
|
|
443
459
|
const workspaceRail = (
|
|
444
460
|
<VStack gap={2} style={railStyle}>
|
|
@@ -450,7 +466,6 @@ export default function MessagingShell() {
|
|
|
450
466
|
tooltip={item.label}
|
|
451
467
|
icon={<Icon icon={item.icon} size="sm" color="inherit" />}
|
|
452
468
|
variant={item.id === "home" ? "secondary" : "ghost"}
|
|
453
|
-
onClick={() => {}}
|
|
454
469
|
/>
|
|
455
470
|
))}
|
|
456
471
|
<StackItem size="fill" />
|
|
@@ -459,7 +474,6 @@ export default function MessagingShell() {
|
|
|
459
474
|
tooltip="Settings"
|
|
460
475
|
icon={<Icon icon={Settings} size="sm" color="inherit" />}
|
|
461
476
|
variant="ghost"
|
|
462
|
-
onClick={() => {}}
|
|
463
477
|
/>
|
|
464
478
|
</VStack>
|
|
465
479
|
);
|
|
@@ -476,7 +490,6 @@ export default function MessagingShell() {
|
|
|
476
490
|
icon={<Icon icon={SquarePen} size="sm" color="inherit" />}
|
|
477
491
|
variant="ghost"
|
|
478
492
|
size="sm"
|
|
479
|
-
onClick={() => {}}
|
|
480
493
|
/>
|
|
481
494
|
</HStack>
|
|
482
495
|
<VStack gap={0} style={sidebarSearchStyle}>
|
|
@@ -563,6 +576,14 @@ export default function MessagingShell() {
|
|
|
563
576
|
const messageStream = (
|
|
564
577
|
<Stack direction="vertical" style={streamColumnStyle}>
|
|
565
578
|
<HStack gap={3} style={streamHeaderStyle}>
|
|
579
|
+
{/* The sidebar carries the page h1 and is hidden at 768 and below; the
|
|
580
|
+
stream keeps the outline intact so a phone still has a top-level
|
|
581
|
+
heading to navigate by. */}
|
|
582
|
+
{isSidebarHidden && (
|
|
583
|
+
<VisuallyHidden>
|
|
584
|
+
<Heading level={1}>Messages</Heading>
|
|
585
|
+
</VisuallyHidden>
|
|
586
|
+
)}
|
|
566
587
|
<Icon icon={Hash} size="sm" color="secondary" />
|
|
567
588
|
<Heading level={2}>{selectedChannel.name}</Heading>
|
|
568
589
|
<StackItem size="fill" style={streamTopicStyle}>
|
|
@@ -571,13 +592,20 @@ export default function MessagingShell() {
|
|
|
571
592
|
</Text>
|
|
572
593
|
</StackItem>
|
|
573
594
|
<StatusDot variant="success" label="12 online" />
|
|
595
|
+
<IconButton
|
|
596
|
+
label={isThreadVisible ? "Close thread" : "Open thread"}
|
|
597
|
+
tooltip={isThreadVisible ? "Close thread" : "Open thread"}
|
|
598
|
+
icon={<Icon icon={isThreadVisible ? X : MessagesSquare} size="sm" color="inherit" />}
|
|
599
|
+
variant="ghost"
|
|
600
|
+
size="sm"
|
|
601
|
+
onClick={toggleThread}
|
|
602
|
+
/>
|
|
574
603
|
<IconButton
|
|
575
604
|
label="Members"
|
|
576
605
|
tooltip="Members"
|
|
577
606
|
icon={<Icon icon={Users} size="sm" color="inherit" />}
|
|
578
607
|
variant="ghost"
|
|
579
608
|
size="sm"
|
|
580
|
-
onClick={() => {}}
|
|
581
609
|
/>
|
|
582
610
|
</HStack>
|
|
583
611
|
<Divider />
|
|
@@ -609,6 +637,31 @@ export default function MessagingShell() {
|
|
|
609
637
|
</Stack>
|
|
610
638
|
);
|
|
611
639
|
|
|
640
|
+
// The thread body is shared by the docked column and the narrow Drawer, so
|
|
641
|
+
// the two presentations of one thread cannot drift apart.
|
|
642
|
+
const threadBody = (
|
|
643
|
+
<>
|
|
644
|
+
<StackItem
|
|
645
|
+
size="fill"
|
|
646
|
+
style={threadScrollStyle}
|
|
647
|
+
role="region"
|
|
648
|
+
aria-label="Thread messages"
|
|
649
|
+
tabIndex={0}
|
|
650
|
+
>
|
|
651
|
+
<ChatMessageList density="compact">
|
|
652
|
+
<ThreadMessage reply={THREAD[0]} />
|
|
653
|
+
<ChatSystemMessage variant="divider">{THREAD.length - 1} replies</ChatSystemMessage>
|
|
654
|
+
{THREAD.slice(1).map((reply) => (
|
|
655
|
+
<ThreadMessage key={reply.id} reply={reply} />
|
|
656
|
+
))}
|
|
657
|
+
</ChatMessageList>
|
|
658
|
+
</StackItem>
|
|
659
|
+
<VStack gap={0} style={threadComposerStyle}>
|
|
660
|
+
<ChatComposer density="compact" placeholder="Reply in thread…" onSubmit={() => {}} />
|
|
661
|
+
</VStack>
|
|
662
|
+
</>
|
|
663
|
+
);
|
|
664
|
+
|
|
612
665
|
const threadPanel = (
|
|
613
666
|
<Stack direction="vertical" style={threadColumnStyle}>
|
|
614
667
|
<HStack gap={2} style={threadHeaderStyle}>
|
|
@@ -630,51 +683,61 @@ export default function MessagingShell() {
|
|
|
630
683
|
/>
|
|
631
684
|
</HStack>
|
|
632
685
|
<Divider />
|
|
633
|
-
|
|
634
|
-
size="fill"
|
|
635
|
-
style={threadScrollStyle}
|
|
636
|
-
role="region"
|
|
637
|
-
aria-label="Thread messages"
|
|
638
|
-
tabIndex={0}
|
|
639
|
-
>
|
|
640
|
-
<ChatMessageList density="compact">
|
|
641
|
-
<ThreadMessage reply={THREAD[0]} />
|
|
642
|
-
<ChatSystemMessage variant="divider">{THREAD.length - 1} replies</ChatSystemMessage>
|
|
643
|
-
{THREAD.slice(1).map((reply) => (
|
|
644
|
-
<ThreadMessage key={reply.id} reply={reply} />
|
|
645
|
-
))}
|
|
646
|
-
</ChatMessageList>
|
|
647
|
-
</StackItem>
|
|
648
|
-
<VStack gap={0} style={threadComposerStyle}>
|
|
649
|
-
<ChatComposer density="compact" placeholder="Reply in thread…" onSubmit={() => {}} />
|
|
650
|
-
</VStack>
|
|
686
|
+
{threadBody}
|
|
651
687
|
</Stack>
|
|
652
688
|
);
|
|
653
689
|
|
|
654
690
|
return (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
{
|
|
662
|
-
|
|
663
|
-
{!isSidebarHidden && (
|
|
664
|
-
<LayoutPanel width={260} padding={0}>
|
|
665
|
-
{channelSidebar}
|
|
691
|
+
<>
|
|
692
|
+
<Layout
|
|
693
|
+
height="fill"
|
|
694
|
+
style={rootStyle}
|
|
695
|
+
start={
|
|
696
|
+
<>
|
|
697
|
+
<LayoutPanel width={68} padding={0}>
|
|
698
|
+
{workspaceRail}
|
|
666
699
|
</LayoutPanel>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
700
|
+
{!isSidebarHidden && (
|
|
701
|
+
<LayoutPanel width={260} padding={0}>
|
|
702
|
+
{channelSidebar}
|
|
703
|
+
</LayoutPanel>
|
|
704
|
+
)}
|
|
705
|
+
</>
|
|
706
|
+
}
|
|
707
|
+
end={
|
|
708
|
+
showThreadPanel ? (
|
|
709
|
+
<LayoutPanel width={340} padding={0}>
|
|
710
|
+
{threadPanel}
|
|
711
|
+
</LayoutPanel>
|
|
712
|
+
) : undefined
|
|
713
|
+
}
|
|
714
|
+
content={<LayoutContent padding={0}>{messageStream}</LayoutContent>}
|
|
715
|
+
/>
|
|
716
|
+
{/* Below 1024 the thread is a Drawer, not a column: it overlays the
|
|
717
|
+
stream rather than compressing it. Non-modal (hasScrim={false}) so the
|
|
718
|
+
conversation behind stays selectable — the point of a thread.
|
|
719
|
+
DrawerHeader renders the close button because it is given
|
|
720
|
+
onOpenChange. `padding={0}` because the thread body brings its own
|
|
721
|
+
gutters, exactly as the docked column does. */}
|
|
722
|
+
<Drawer
|
|
723
|
+
side="end"
|
|
724
|
+
label="Thread"
|
|
725
|
+
isOpen={isThreadHidden && isThreadDrawerOpen}
|
|
726
|
+
onOpenChange={setIsThreadDrawerOpen}
|
|
727
|
+
hasScrim={false}
|
|
728
|
+
width={340}
|
|
729
|
+
padding={0}
|
|
730
|
+
>
|
|
731
|
+
<Stack direction="vertical" style={threadColumnStyle}>
|
|
732
|
+
<DrawerHeader
|
|
733
|
+
title="Thread"
|
|
734
|
+
subtitle={`#${selectedChannel.name}`}
|
|
735
|
+
onOpenChange={setIsThreadDrawerOpen}
|
|
736
|
+
hasDivider
|
|
737
|
+
/>
|
|
738
|
+
{threadBody}
|
|
739
|
+
</Stack>
|
|
740
|
+
</Drawer>
|
|
741
|
+
</>
|
|
679
742
|
);
|
|
680
743
|
}
|
|
@@ -11,10 +11,15 @@ import { Heading, Text } from "@astryxdesign/core/Text";
|
|
|
11
11
|
import { galleryImageClip } from "astryx-dracula/shared/gallery-image";
|
|
12
12
|
import type { SceneHue } from "astryx-dracula/shared/scene-hues";
|
|
13
13
|
import { SceneTile } from "astryx-dracula/shared/scene-tile";
|
|
14
|
+
import type { CSSProperties } from "react";
|
|
14
15
|
|
|
15
16
|
// ─── Styles ────────────────────────────────────────────────────────────────
|
|
16
17
|
// Image fill + radius live in shared/gallery-image (no Image primitive in
|
|
17
18
|
// Astryx, #2582). Tile art lives in shared/scene-tile (large fork).
|
|
19
|
+
// Definite ancestor for `Layout height="fill"`: 100dvh, not minHeight: '100%'
|
|
20
|
+
// (a percentage min-height against an indefinite block computes to 0 — CSS 2.1
|
|
21
|
+
// 10.5). Same shape as side-gallery.tsx.
|
|
22
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
18
23
|
|
|
19
24
|
// ─── Gallery Data ───────────────────────────────────────────────────────────
|
|
20
25
|
|
|
@@ -57,6 +62,7 @@ function GalleryCard({ image, ratio }: { image: GalleryImage; ratio: number }) {
|
|
|
57
62
|
export default function MixedGallery() {
|
|
58
63
|
return (
|
|
59
64
|
<Layout
|
|
65
|
+
style={pageStyle}
|
|
60
66
|
height="fill"
|
|
61
67
|
contentWidth={1400}
|
|
62
68
|
content={
|
|
@@ -24,6 +24,7 @@ import { StatusDot } from "@astryxdesign/core/StatusDot";
|
|
|
24
24
|
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
25
25
|
import { TextArea } from "@astryxdesign/core/TextArea";
|
|
26
26
|
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
27
|
+
import { formatMoney } from "astryx-dracula/shared/format-money";
|
|
27
28
|
import type { SceneHue } from "astryx-dracula/shared/scene-hues";
|
|
28
29
|
import { SceneTile } from "astryx-dracula/shared/scene-tile";
|
|
29
30
|
import { CircleCheck, Lock, ShieldCheck, Truck } from "lucide-react";
|
|
@@ -126,13 +127,20 @@ const ORDER_ITEMS = [
|
|
|
126
127
|
|
|
127
128
|
const TAX_RATE = 0.08;
|
|
128
129
|
const FREE_SHIPPING_THRESHOLD = 300;
|
|
129
|
-
|
|
130
|
+
// The rates the summary and the RadioList labels both read.
|
|
131
|
+
const SHIPPING_STANDARD = 4.95;
|
|
132
|
+
const SHIPPING_EXPEDITED = 9.95;
|
|
130
133
|
|
|
131
134
|
// ── Styles ────────────────────────────────────────────────────────────────────
|
|
132
135
|
// Plain inline styles using Astryx design-token CSS variables (declared at
|
|
133
136
|
// :root by `@astryxdesign/core/astryx.css`). No StyleX compiler required.
|
|
134
137
|
|
|
135
138
|
const fullWidth: CSSProperties = { width: "100%" };
|
|
139
|
+
// Definite ancestor for `Layout height="fill"`: 100dvh, not minHeight: '100%'
|
|
140
|
+
// (a percentage min-height against an indefinite block computes to 0 — CSS 2.1
|
|
141
|
+
// 10.5). LayoutContent is overflow:auto, so the height lands as a content-pane
|
|
142
|
+
// scroller. Same shape as side-gallery.tsx.
|
|
143
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
136
144
|
// Form column flex-basis so the two checkout columns share width evenly.
|
|
137
145
|
const formColBasis: CSSProperties = { flexBasis: 0 };
|
|
138
146
|
// Space the Order Summary content below its collapsible trigger title.
|
|
@@ -203,7 +211,7 @@ function OrderLineItem({
|
|
|
203
211
|
)}
|
|
204
212
|
</HStack>
|
|
205
213
|
<Text type="body" hasTabularNumbers>
|
|
206
|
-
{
|
|
214
|
+
{formatMoney(item.price)}
|
|
207
215
|
</Text>
|
|
208
216
|
</HStack>
|
|
209
217
|
<Text type="supporting" color="secondary">
|
|
@@ -219,8 +227,8 @@ function OrderLineItem({
|
|
|
219
227
|
isIntegerOnly
|
|
220
228
|
/>
|
|
221
229
|
{/* Editing the order in place — an action, never a destination. */}
|
|
222
|
-
<Button label="Remove" variant="ghost"
|
|
223
|
-
<Button label="Save" variant="ghost"
|
|
230
|
+
<Button label="Remove" variant="ghost" />
|
|
231
|
+
<Button label="Save" variant="ghost" />
|
|
224
232
|
</HStack>
|
|
225
233
|
</VStack>
|
|
226
234
|
</StackItem>
|
|
@@ -285,7 +293,7 @@ function OrderTotalSection({
|
|
|
285
293
|
Subtotal
|
|
286
294
|
</Text>
|
|
287
295
|
<Text type="body" hasTabularNumbers>
|
|
288
|
-
{
|
|
296
|
+
{formatMoney(subtotal)}
|
|
289
297
|
</Text>
|
|
290
298
|
</HStack>
|
|
291
299
|
<HStack hAlign="between" vAlign="center">
|
|
@@ -293,7 +301,7 @@ function OrderTotalSection({
|
|
|
293
301
|
Shipping
|
|
294
302
|
</Text>
|
|
295
303
|
<Text type="body" hasTabularNumbers>
|
|
296
|
-
{
|
|
304
|
+
{formatMoney(shipping)}
|
|
297
305
|
</Text>
|
|
298
306
|
</HStack>
|
|
299
307
|
<HStack hAlign="between" vAlign="center">
|
|
@@ -301,7 +309,7 @@ function OrderTotalSection({
|
|
|
301
309
|
Tax
|
|
302
310
|
</Text>
|
|
303
311
|
<Text type="body" hasTabularNumbers>
|
|
304
|
-
{
|
|
312
|
+
{formatMoney(tax)}
|
|
305
313
|
</Text>
|
|
306
314
|
</HStack>
|
|
307
315
|
</VStack>
|
|
@@ -309,13 +317,13 @@ function OrderTotalSection({
|
|
|
309
317
|
<HStack hAlign="between" vAlign="center">
|
|
310
318
|
<Text type="large">Total</Text>
|
|
311
319
|
<Text type="large" hasTabularNumbers>
|
|
312
|
-
{
|
|
320
|
+
{formatMoney(total)}
|
|
313
321
|
</Text>
|
|
314
322
|
</HStack>
|
|
315
323
|
<Banner
|
|
316
324
|
status="info"
|
|
317
325
|
icon={<Icon icon={Truck} size="sm" />}
|
|
318
|
-
title=
|
|
326
|
+
title={`Free shipping on orders over $${FREE_SHIPPING_THRESHOLD}`}
|
|
319
327
|
/>
|
|
320
328
|
</VStack>
|
|
321
329
|
);
|
|
@@ -410,8 +418,11 @@ export default function PaymentForm() {
|
|
|
410
418
|
(sum, item) => sum + item.price * (quantities[item.id] ?? item.qty),
|
|
411
419
|
0,
|
|
412
420
|
);
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
// The summary and the RadioList labels read the same rate constants, so a
|
|
422
|
+
// price change cannot leave the radio advertising a value the order does
|
|
423
|
+
// not charge.
|
|
424
|
+
const shippingRate = deliveryMethod === "expedited" ? SHIPPING_EXPEDITED : SHIPPING_STANDARD;
|
|
425
|
+
const shipping = subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : shippingRate;
|
|
415
426
|
const tax = Math.round(subtotal * TAX_RATE * 100) / 100;
|
|
416
427
|
const total = subtotal + shipping + tax;
|
|
417
428
|
|
|
@@ -444,6 +455,7 @@ export default function PaymentForm() {
|
|
|
444
455
|
|
|
445
456
|
return (
|
|
446
457
|
<Layout
|
|
458
|
+
style={pageStyle}
|
|
447
459
|
height="fill"
|
|
448
460
|
content={
|
|
449
461
|
<LayoutContent padding={0}>
|
|
@@ -471,7 +483,7 @@ export default function PaymentForm() {
|
|
|
471
483
|
<VStack gap={1}>
|
|
472
484
|
<HStack gap={2} hAlign="between" vAlign="center">
|
|
473
485
|
<Heading level={2}>Sign in to check out</Heading>
|
|
474
|
-
<Button label="Sign in" variant="secondary"
|
|
486
|
+
<Button label="Sign in" variant="secondary" />
|
|
475
487
|
</HStack>
|
|
476
488
|
<Text type="body" color="secondary">
|
|
477
489
|
Sign in to track your order and save your information for faster checkout.
|
|
@@ -612,7 +624,7 @@ export default function PaymentForm() {
|
|
|
612
624
|
label="Standard (3–7 business days)"
|
|
613
625
|
endContent={
|
|
614
626
|
<Text type="body" weight="semibold" hasTabularNumbers>
|
|
615
|
-
|
|
627
|
+
{formatMoney(SHIPPING_STANDARD)}
|
|
616
628
|
</Text>
|
|
617
629
|
}
|
|
618
630
|
/>
|
|
@@ -621,7 +633,7 @@ export default function PaymentForm() {
|
|
|
621
633
|
label="Expedited (1–2 business days)"
|
|
622
634
|
endContent={
|
|
623
635
|
<Text type="body" weight="semibold" hasTabularNumbers>
|
|
624
|
-
|
|
636
|
+
{formatMoney(SHIPPING_EXPEDITED)}
|
|
625
637
|
</Text>
|
|
626
638
|
}
|
|
627
639
|
/>
|
|
@@ -641,19 +653,9 @@ export default function PaymentForm() {
|
|
|
641
653
|
<VStack gap={3}>
|
|
642
654
|
<Grid columns={isMobile ? 1 : 2} gap={3}>
|
|
643
655
|
{/* PayPal */}
|
|
644
|
-
<Button
|
|
645
|
-
label="PayPal"
|
|
646
|
-
variant="secondary"
|
|
647
|
-
size="lg"
|
|
648
|
-
onClick={() => {}}
|
|
649
|
-
/>
|
|
656
|
+
<Button label="PayPal" variant="secondary" size="lg" />
|
|
650
657
|
{/* Google Pay */}
|
|
651
|
-
<Button
|
|
652
|
-
label="Google Pay"
|
|
653
|
-
variant="secondary"
|
|
654
|
-
size="lg"
|
|
655
|
-
onClick={() => {}}
|
|
656
|
-
/>
|
|
658
|
+
<Button label="Google Pay" variant="secondary" size="lg" />
|
|
657
659
|
</Grid>
|
|
658
660
|
</VStack>
|
|
659
661
|
|
|
@@ -867,7 +869,7 @@ export default function PaymentForm() {
|
|
|
867
869
|
controlStyle={fullWidth}
|
|
868
870
|
/>
|
|
869
871
|
</StackItem>
|
|
870
|
-
<Button label="Apply" variant="secondary" size="lg"
|
|
872
|
+
<Button label="Apply" variant="secondary" size="lg" />
|
|
871
873
|
</HStack>
|
|
872
874
|
</VStack>
|
|
873
875
|
|
|
@@ -923,7 +925,6 @@ export default function PaymentForm() {
|
|
|
923
925
|
variant="secondary"
|
|
924
926
|
size="lg"
|
|
925
927
|
style={fullWidth}
|
|
926
|
-
onClick={() => {}}
|
|
927
928
|
/>
|
|
928
929
|
</VStack>
|
|
929
930
|
<Divider />
|
|
@@ -34,6 +34,11 @@ const stickyInfo: CSSProperties = {
|
|
|
34
34
|
top: "var(--spacing-8)",
|
|
35
35
|
alignSelf: "start",
|
|
36
36
|
};
|
|
37
|
+
// Definite ancestor for `Layout height="fill"`: 100dvh, not minHeight: '100%'
|
|
38
|
+
// (a percentage min-height against an indefinite block computes to 0 — CSS 2.1
|
|
39
|
+
// 10.5). LayoutContent is overflow:auto, so the height lands as a content-pane
|
|
40
|
+
// scroller. Same shape as side-gallery.tsx and dashboard.tsx.
|
|
41
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
37
42
|
|
|
38
43
|
import type { SceneHue } from "astryx-dracula/shared/scene-hues";
|
|
39
44
|
import { Minus, Plus } from "lucide-react";
|
|
@@ -62,6 +67,10 @@ const PRODUCT = {
|
|
|
62
67
|
name: "Midnight Ceremony Mug & Plate Set",
|
|
63
68
|
price: 89.0,
|
|
64
69
|
originalPrice: 119.0,
|
|
70
|
+
// The review count is data, so it lives with the product rather than as a
|
|
71
|
+
// numeral at the render site.
|
|
72
|
+
rating: 4.3,
|
|
73
|
+
reviewCount: 128,
|
|
65
74
|
description:
|
|
66
75
|
"A hand-thrown mug and plate set that brings quiet warmth to every midnight meal. The mug sits easy in the hand with a generous 12 oz capacity, while the 8-inch plate works for everything from toast to tapas. Each piece is kiln-fired at 2,300\u00B0F for a finish that resists chips and stains. Subtle variations in the reactive glaze pool like night mist, so no two sets are exactly alike. Dishwasher and microwave safe.",
|
|
67
76
|
composition:
|
|
@@ -87,7 +96,6 @@ const FINISHES = [
|
|
|
87
96
|
// ─── Image Gallery ──────────────────────────────────────────────────────────
|
|
88
97
|
function ImageGallery({ selected, onSelect }: { selected: number; onSelect: (i: number) => void }) {
|
|
89
98
|
const heroHue: SceneHue = IMAGES[selected];
|
|
90
|
-
const thumbnails = IMAGES;
|
|
91
99
|
|
|
92
100
|
return (
|
|
93
101
|
<VStack gap={3}>
|
|
@@ -95,7 +103,7 @@ function ImageGallery({ selected, onSelect }: { selected: number; onSelect: (i:
|
|
|
95
103
|
<SceneTile label={PRODUCT.name} hue={heroHue} size="lg" />
|
|
96
104
|
</AspectRatio>
|
|
97
105
|
<Grid columns={3} gap={2}>
|
|
98
|
-
{
|
|
106
|
+
{IMAGES.map((hue, i) => (
|
|
99
107
|
<AspectRatio key={hue} ratio={1}>
|
|
100
108
|
<SelectableCard
|
|
101
109
|
label={`Product image ${i + 1}`}
|
|
@@ -134,7 +142,7 @@ function ProductInfo() {
|
|
|
134
142
|
<Heading level={1} type="display-2">
|
|
135
143
|
{PRODUCT.name}
|
|
136
144
|
</Heading>
|
|
137
|
-
<StarRating rating={
|
|
145
|
+
<StarRating rating={PRODUCT.rating} count={PRODUCT.reviewCount} />
|
|
138
146
|
<HStack gap={2} vAlign="center">
|
|
139
147
|
<Text type="large" hasTabularNumbers>
|
|
140
148
|
{formatMoney(PRODUCT.price)}
|
|
@@ -286,6 +294,7 @@ export default function ProductDetail() {
|
|
|
286
294
|
|
|
287
295
|
return (
|
|
288
296
|
<Layout
|
|
297
|
+
style={pageStyle}
|
|
289
298
|
height="fill"
|
|
290
299
|
contentWidth={1200}
|
|
291
300
|
content={
|
|
@@ -11,13 +11,19 @@ import { Grid } from "@astryxdesign/core/Grid";
|
|
|
11
11
|
import { Icon } from "@astryxdesign/core/Icon";
|
|
12
12
|
import { Layout, LayoutContent, VStack } from "@astryxdesign/core/Layout";
|
|
13
13
|
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
14
|
+
import { formatMoney } from "astryx-dracula/shared/format-money";
|
|
14
15
|
import type { SceneHue } from "astryx-dracula/shared/scene-hues";
|
|
15
16
|
import { SceneTile } from "astryx-dracula/shared/scene-tile";
|
|
16
17
|
import { ArrowRight } from "lucide-react";
|
|
18
|
+
import type { CSSProperties } from "react";
|
|
17
19
|
|
|
18
20
|
// ─── Styles ─────────────────────────────────────────────────────────────────
|
|
19
21
|
// Product tile art lives in shared/scene-tile (large fork); there is no Image
|
|
20
22
|
// primitive to fill the AspectRatio box with `object-fit` (#2582).
|
|
23
|
+
// Definite ancestor for `Layout height="fill"`: 100dvh, not minHeight: '100%'
|
|
24
|
+
// (a percentage min-height against an indefinite block computes to 0 — CSS 2.1
|
|
25
|
+
// 10.5). Same shape as side-gallery.tsx.
|
|
26
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
21
27
|
|
|
22
28
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
23
29
|
|
|
@@ -75,8 +81,6 @@ const PRODUCTS: Product[] = [
|
|
|
75
81
|
},
|
|
76
82
|
];
|
|
77
83
|
|
|
78
|
-
const fmt = (n: number) => `$${n.toFixed(2)}`;
|
|
79
|
-
|
|
80
84
|
// ─── Product Card ───────────────────────────────────────────────────────────
|
|
81
85
|
|
|
82
86
|
function ProductCard({ product }: { product: Product }) {
|
|
@@ -93,7 +97,7 @@ function ProductCard({ product }: { product: Product }) {
|
|
|
93
97
|
{product.description}
|
|
94
98
|
</Text>
|
|
95
99
|
<Text type="large" hasTabularNumbers>
|
|
96
|
-
{
|
|
100
|
+
{formatMoney(product.price)}
|
|
97
101
|
</Text>
|
|
98
102
|
</VStack>
|
|
99
103
|
</VStack>
|
|
@@ -105,6 +109,7 @@ function ProductCard({ product }: { product: Product }) {
|
|
|
105
109
|
export default function ProductGallery() {
|
|
106
110
|
return (
|
|
107
111
|
<Layout
|
|
112
|
+
style={pageStyle}
|
|
108
113
|
height="fill"
|
|
109
114
|
contentWidth={1200}
|
|
110
115
|
content={
|