astryx-dracula 0.1.3 → 0.2.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 +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useState} from 'react';
|
|
4
|
+
import {AppShell} from '@astryxdesign/core/AppShell';
|
|
5
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
6
|
+
import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core/Layout';
|
|
7
|
+
import {
|
|
8
|
+
SideNav,
|
|
9
|
+
SideNavHeading,
|
|
10
|
+
SideNavItem,
|
|
11
|
+
SideNavSection,
|
|
12
|
+
} from '@astryxdesign/core/SideNav';
|
|
13
|
+
import {NavIcon} from '@astryxdesign/core/NavIcon';
|
|
14
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
15
|
+
import type {IconType} from '@astryxdesign/core/Icon';
|
|
16
|
+
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
17
|
+
import type {StatusDotVariant} from '@astryxdesign/core/StatusDot';
|
|
18
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
19
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
20
|
+
import {VStack, HStack} from '@astryxdesign/core/Stack';
|
|
21
|
+
import {
|
|
22
|
+
Sparkles,
|
|
23
|
+
Plus,
|
|
24
|
+
Search,
|
|
25
|
+
BookOpen,
|
|
26
|
+
Settings,
|
|
27
|
+
CircleUserRound,
|
|
28
|
+
User,
|
|
29
|
+
Building2,
|
|
30
|
+
CodeXml,
|
|
31
|
+
} from 'lucide-react';
|
|
32
|
+
|
|
33
|
+
type Conversation = {
|
|
34
|
+
label: string;
|
|
35
|
+
status: StatusDotVariant;
|
|
36
|
+
statusLabel: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type Workspace = {
|
|
40
|
+
name: string;
|
|
41
|
+
icon: IconType;
|
|
42
|
+
chats: Conversation[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const WORKSPACES: Workspace[] = [
|
|
46
|
+
{
|
|
47
|
+
name: 'Personal',
|
|
48
|
+
icon: User,
|
|
49
|
+
chats: [
|
|
50
|
+
{
|
|
51
|
+
label: 'Weekend trip planning',
|
|
52
|
+
status: 'success',
|
|
53
|
+
statusLabel: 'Active',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Recipe ideas for the week',
|
|
57
|
+
status: 'neutral',
|
|
58
|
+
statusLabel: 'Idle',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: 'Book recommendations',
|
|
62
|
+
status: 'warning',
|
|
63
|
+
statusLabel: 'Needs review',
|
|
64
|
+
},
|
|
65
|
+
{label: 'Home workout plan', status: 'neutral', statusLabel: 'Idle'},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Acme Corp',
|
|
70
|
+
icon: Building2,
|
|
71
|
+
chats: [
|
|
72
|
+
{label: 'Q3 roadmap draft', status: 'accent', statusLabel: 'In progress'},
|
|
73
|
+
{
|
|
74
|
+
label: 'Customer onboarding flow',
|
|
75
|
+
status: 'success',
|
|
76
|
+
statusLabel: 'Active',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: 'Pricing strategy review',
|
|
80
|
+
status: 'warning',
|
|
81
|
+
statusLabel: 'Needs review',
|
|
82
|
+
},
|
|
83
|
+
{label: 'Standup summary', status: 'neutral', statusLabel: 'Idle'},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Open Source',
|
|
88
|
+
icon: CodeXml,
|
|
89
|
+
chats: [
|
|
90
|
+
{
|
|
91
|
+
label: 'StyleX migration notes',
|
|
92
|
+
status: 'accent',
|
|
93
|
+
statusLabel: 'In progress',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: 'Skeleton loading states',
|
|
97
|
+
status: 'success',
|
|
98
|
+
statusLabel: 'Active',
|
|
99
|
+
},
|
|
100
|
+
{label: 'Accessibility audit', status: 'error', statusLabel: 'Blocked'},
|
|
101
|
+
{label: 'Release notes v4.0', status: 'neutral', statusLabel: 'Idle'},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const SELECTED_CHAT = 'StyleX migration notes';
|
|
107
|
+
// Same-route hash: demo links stay focusable anchors without escaping the
|
|
108
|
+
// template through the hash router (bare "#" would drop back to the home page).
|
|
109
|
+
const SELF_HASH = '#/templates/shell-side-nav';
|
|
110
|
+
|
|
111
|
+
const MESSAGES = [
|
|
112
|
+
{role: 'assistant', width: '78%', height: 104},
|
|
113
|
+
{role: 'user', width: '48%', height: 48},
|
|
114
|
+
{role: 'assistant', width: '64%', height: 132},
|
|
115
|
+
{role: 'user', width: '38%', height: 40},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function ConversationItem({
|
|
119
|
+
label,
|
|
120
|
+
status,
|
|
121
|
+
statusLabel,
|
|
122
|
+
isSelected,
|
|
123
|
+
onSelect,
|
|
124
|
+
}: {
|
|
125
|
+
label: string;
|
|
126
|
+
status: StatusDotVariant;
|
|
127
|
+
statusLabel: string;
|
|
128
|
+
isSelected?: boolean;
|
|
129
|
+
onSelect: () => void;
|
|
130
|
+
}) {
|
|
131
|
+
// The hover-only MoreMenu was four dead actions unreachable by keyboard and
|
|
132
|
+
// touch, so it goes: the status dot is always visible (its label doubles as
|
|
133
|
+
// the tooltip), and selecting a conversation actually switches selection.
|
|
134
|
+
return (
|
|
135
|
+
<SideNavItem
|
|
136
|
+
label={label}
|
|
137
|
+
href={SELF_HASH}
|
|
138
|
+
isSelected={isSelected}
|
|
139
|
+
onClick={onSelect}
|
|
140
|
+
endContent={<StatusDot variant={status} label={statusLabel} />}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default function ShellSideNav() {
|
|
146
|
+
const [selectedChat, setSelectedChat] = useState(SELECTED_CHAT);
|
|
147
|
+
const [draft, setDraft] = useState('');
|
|
148
|
+
return (
|
|
149
|
+
<AppShell
|
|
150
|
+
contentPadding={0}
|
|
151
|
+
sideNav={
|
|
152
|
+
<SideNav
|
|
153
|
+
collapsible
|
|
154
|
+
resizable={{defaultWidth: 300, minWidth: 220, maxWidth: 420}}
|
|
155
|
+
header={
|
|
156
|
+
<SideNavHeading
|
|
157
|
+
heading="Night Owl"
|
|
158
|
+
icon={<NavIcon icon={<Icon icon={Sparkles} size="sm" />} />}
|
|
159
|
+
headingHref={SELF_HASH}
|
|
160
|
+
/>
|
|
161
|
+
}
|
|
162
|
+
footer={
|
|
163
|
+
<SideNavSection title="Account" isHeaderHidden>
|
|
164
|
+
<SideNavItem label="Settings" icon={Settings} href={SELF_HASH} />
|
|
165
|
+
<SideNavItem
|
|
166
|
+
label="Sarah Chen"
|
|
167
|
+
icon={CircleUserRound}
|
|
168
|
+
href={SELF_HASH}
|
|
169
|
+
/>
|
|
170
|
+
</SideNavSection>
|
|
171
|
+
}>
|
|
172
|
+
<SideNavSection title="Menu" isHeaderHidden>
|
|
173
|
+
<SideNavItem label="New chat" icon={Plus} href={SELF_HASH} />
|
|
174
|
+
<SideNavItem label="Search" icon={Search} href={SELF_HASH} />
|
|
175
|
+
<SideNavItem label="Library" icon={BookOpen} href={SELF_HASH} />
|
|
176
|
+
</SideNavSection>
|
|
177
|
+
<Divider />
|
|
178
|
+
<SideNavSection title="Workspaces" isHeaderHidden>
|
|
179
|
+
{WORKSPACES.map(workspace => (
|
|
180
|
+
<SideNavItem
|
|
181
|
+
key={workspace.name}
|
|
182
|
+
label={workspace.name}
|
|
183
|
+
icon={workspace.icon}
|
|
184
|
+
collapsible={{defaultIsCollapsed: false}}>
|
|
185
|
+
<VStack gap={0.5}>
|
|
186
|
+
{workspace.chats.map(chat => (
|
|
187
|
+
<ConversationItem
|
|
188
|
+
key={chat.label}
|
|
189
|
+
label={chat.label}
|
|
190
|
+
status={chat.status}
|
|
191
|
+
statusLabel={chat.statusLabel}
|
|
192
|
+
isSelected={chat.label === selectedChat}
|
|
193
|
+
onSelect={() => setSelectedChat(chat.label)}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
</VStack>
|
|
197
|
+
</SideNavItem>
|
|
198
|
+
))}
|
|
199
|
+
</SideNavSection>
|
|
200
|
+
</SideNav>
|
|
201
|
+
}>
|
|
202
|
+
<Layout
|
|
203
|
+
height="fill"
|
|
204
|
+
contentWidth={768}
|
|
205
|
+
content={
|
|
206
|
+
<LayoutContent padding={6}>
|
|
207
|
+
<VStack gap={5}>
|
|
208
|
+
{MESSAGES.map((message, mi) => (
|
|
209
|
+
<HStack
|
|
210
|
+
key={mi}
|
|
211
|
+
hAlign={message.role === 'assistant' ? 'start' : 'end'}>
|
|
212
|
+
<Card
|
|
213
|
+
variant="muted"
|
|
214
|
+
padding={0}
|
|
215
|
+
width={message.width}
|
|
216
|
+
height={message.height}
|
|
217
|
+
/>
|
|
218
|
+
</HStack>
|
|
219
|
+
))}
|
|
220
|
+
</VStack>
|
|
221
|
+
</LayoutContent>
|
|
222
|
+
}
|
|
223
|
+
footer={
|
|
224
|
+
<LayoutFooter>
|
|
225
|
+
<TextInput
|
|
226
|
+
label="Message Night Owl"
|
|
227
|
+
isLabelHidden
|
|
228
|
+
placeholder="Message Night Owl…"
|
|
229
|
+
value={draft}
|
|
230
|
+
onChange={setDraft}
|
|
231
|
+
width="100%"
|
|
232
|
+
/>
|
|
233
|
+
</LayoutFooter>
|
|
234
|
+
}
|
|
235
|
+
/>
|
|
236
|
+
</AppShell>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import type {CSSProperties} from 'react';
|
|
4
|
+
import {AppShell} from '@astryxdesign/core/AppShell';
|
|
5
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
6
|
+
import {
|
|
7
|
+
TopNav,
|
|
8
|
+
TopNavHeading,
|
|
9
|
+
TopNavItem,
|
|
10
|
+
TopNavMegaMenu,
|
|
11
|
+
TopNavMegaMenuItem,
|
|
12
|
+
TopNavMegaMenuFeaturedCard,
|
|
13
|
+
useTopNavRenderMode,
|
|
14
|
+
} from '@astryxdesign/core/TopNav';
|
|
15
|
+
import {NavIcon} from '@astryxdesign/core/NavIcon';
|
|
16
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
17
|
+
import type {IconType} from '@astryxdesign/core/Icon';
|
|
18
|
+
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
19
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
20
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
21
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
22
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
23
|
+
import {Stack, VStack} from '@astryxdesign/core/Stack';
|
|
24
|
+
import {
|
|
25
|
+
ShoppingBag,
|
|
26
|
+
ShoppingCart,
|
|
27
|
+
Search,
|
|
28
|
+
Sparkles,
|
|
29
|
+
SwatchBook,
|
|
30
|
+
Tag,
|
|
31
|
+
House,
|
|
32
|
+
Smile,
|
|
33
|
+
BadgePercent,
|
|
34
|
+
Gift,
|
|
35
|
+
Cloud,
|
|
36
|
+
Zap,
|
|
37
|
+
Sun,
|
|
38
|
+
Star,
|
|
39
|
+
Flame,
|
|
40
|
+
Globe,
|
|
41
|
+
Moon,
|
|
42
|
+
} from 'lucide-react';
|
|
43
|
+
|
|
44
|
+
// Cap + center the page body so wide screens show whitespace gutters.
|
|
45
|
+
const contentMax: CSSProperties = {maxWidth: 1100, marginInline: 'auto'};
|
|
46
|
+
// Same-route hash: demo links stay focusable anchors without escaping the
|
|
47
|
+
// template through the hash router (bare "#" would drop back to the home page).
|
|
48
|
+
const SELF_HASH = '#/templates/shell-top-nav';
|
|
49
|
+
// Lock both mega-menu panels to an identical size. Without this, Shop and
|
|
50
|
+
// Brands size to their own content (different widths); since both anchor to
|
|
51
|
+
// the centered nav, switching between them resizes the panel — which reads
|
|
52
|
+
// as flashing/jumping. Fixed item + featured widths make the panels
|
|
53
|
+
// pixel-identical so the transition is seamless.
|
|
54
|
+
const megaItems: CSSProperties = {gridColumn: '1 / -1', width: 520};
|
|
55
|
+
const megaFeatured: CSSProperties = {width: 240};
|
|
56
|
+
|
|
57
|
+
type MegaItem = {name: string; tagline: string; icon: IconType};
|
|
58
|
+
|
|
59
|
+
// Shop and Brands each render 8 items — the mega menu's built-in 2-column grid
|
|
60
|
+
// lays them out as 2 columns × 4 rows, alongside a featured card.
|
|
61
|
+
const SHOP_ITEMS: MegaItem[] = [
|
|
62
|
+
{name: 'New Arrivals', tagline: 'The latest drops', icon: Sparkles},
|
|
63
|
+
{name: 'Womenswear', tagline: 'Dresses, knitwear & more', icon: SwatchBook},
|
|
64
|
+
{name: 'Menswear', tagline: 'Shirts, tailoring & more', icon: Tag},
|
|
65
|
+
{name: 'Home', tagline: 'Bedding, lighting & décor', icon: House},
|
|
66
|
+
{
|
|
67
|
+
name: 'Beauty',
|
|
68
|
+
tagline: 'Skincare, fragrance & makeup',
|
|
69
|
+
icon: Smile,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Accessories',
|
|
73
|
+
tagline: 'Bags, hats & sunglasses',
|
|
74
|
+
icon: ShoppingBag,
|
|
75
|
+
},
|
|
76
|
+
{name: 'Sale', tagline: 'Up to 50% off', icon: BadgePercent},
|
|
77
|
+
{name: 'Gift Cards', tagline: 'The perfect present', icon: Gift},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const BRAND_ITEMS: MegaItem[] = [
|
|
81
|
+
{name: 'Aether', tagline: 'Performance essentials', icon: Sparkles},
|
|
82
|
+
{name: 'Northwind', tagline: 'Outdoor & technical', icon: Cloud},
|
|
83
|
+
{name: 'Loomwell', tagline: 'Everyday knitwear', icon: Zap},
|
|
84
|
+
{name: 'Verdant', tagline: 'Sustainable basics', icon: Sun},
|
|
85
|
+
{name: 'Studio Mara', tagline: 'Modern tailoring', icon: Star},
|
|
86
|
+
{name: 'Atelier Kos', tagline: 'Limited ateliers', icon: Flame},
|
|
87
|
+
{name: 'Rue & Co', tagline: 'City streetwear', icon: Globe},
|
|
88
|
+
{name: 'Halden', tagline: 'Minimal staples', icon: Moon},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const CATEGORY_TILES = [
|
|
92
|
+
'New Arrivals',
|
|
93
|
+
'Womenswear',
|
|
94
|
+
'Menswear',
|
|
95
|
+
'Home & Living',
|
|
96
|
+
'Beauty',
|
|
97
|
+
'Accessories',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Wraps the 8 items in a fixed-width 2-column grid so every mega menu's item
|
|
101
|
+
// area is exactly the same width regardless of its content.
|
|
102
|
+
function MegaItems({items}: {items: MegaItem[]}) {
|
|
103
|
+
// In the mobile drawer the fixed 520px panel would overflow the ~350px
|
|
104
|
+
// drawer, and the 2-column grid overlaps item text there — so the drawer
|
|
105
|
+
// gets a natural-width single column. Desktop keeps the lock.
|
|
106
|
+
const isDrawer = useTopNavRenderMode() === 'drawer';
|
|
107
|
+
if (isDrawer) {
|
|
108
|
+
return (
|
|
109
|
+
<VStack gap={1}>
|
|
110
|
+
{items.map(item => (
|
|
111
|
+
<TopNavMegaMenuItem
|
|
112
|
+
key={item.name}
|
|
113
|
+
title={item.name}
|
|
114
|
+
description={item.tagline}
|
|
115
|
+
icon={<Icon icon={item.icon} size="md" color="secondary" />}
|
|
116
|
+
href={SELF_HASH}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</VStack>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
<Stack style={megaItems}>
|
|
124
|
+
<Grid columns={2} gap={2}>
|
|
125
|
+
{items.map(item => (
|
|
126
|
+
<TopNavMegaMenuItem
|
|
127
|
+
key={item.name}
|
|
128
|
+
title={item.name}
|
|
129
|
+
description={item.tagline}
|
|
130
|
+
icon={<Icon icon={item.icon} size="md" color="secondary" />}
|
|
131
|
+
href={SELF_HASH}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
</Grid>
|
|
135
|
+
</Stack>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pins the featured card to a fixed width so both panels match exactly.
|
|
140
|
+
function MegaFeatured(props: {
|
|
141
|
+
title: string;
|
|
142
|
+
description: string;
|
|
143
|
+
linkLabel: string;
|
|
144
|
+
linkHref: string;
|
|
145
|
+
}) {
|
|
146
|
+
const isDrawer = useTopNavRenderMode() === 'drawer';
|
|
147
|
+
return (
|
|
148
|
+
<Stack style={isDrawer ? undefined : megaFeatured}>
|
|
149
|
+
<TopNavMegaMenuFeaturedCard {...props} />
|
|
150
|
+
</Stack>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default function ShellTopNav() {
|
|
155
|
+
// Below ~640px the mobile bar (heading + actions + toggle) overflows 390px
|
|
156
|
+
// viewports, pushing the nav toggle off-screen. Dropping the text Sign in
|
|
157
|
+
// button there restores room for search, checkout, and the toggle.
|
|
158
|
+
const isCompact = useMediaQuery('(max-width: 640px)');
|
|
159
|
+
return (
|
|
160
|
+
<AppShell
|
|
161
|
+
variant="surface"
|
|
162
|
+
contentPadding={6}
|
|
163
|
+
topNav={
|
|
164
|
+
<TopNav
|
|
165
|
+
label="Nocturne storefront navigation"
|
|
166
|
+
heading={
|
|
167
|
+
<TopNavHeading
|
|
168
|
+
heading="Nocturne"
|
|
169
|
+
logo={
|
|
170
|
+
<NavIcon icon={<Icon icon={ShoppingBag} size="sm" />} />
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
centerContent={
|
|
175
|
+
<>
|
|
176
|
+
<TopNavMegaMenu
|
|
177
|
+
label="Shop"
|
|
178
|
+
items={<MegaItems items={SHOP_ITEMS} />}
|
|
179
|
+
featured={
|
|
180
|
+
<MegaFeatured
|
|
181
|
+
title="The Midnight Edit"
|
|
182
|
+
description="Layered staples in moonlit purples."
|
|
183
|
+
linkLabel="Shop the edit"
|
|
184
|
+
linkHref={SELF_HASH}
|
|
185
|
+
/>
|
|
186
|
+
}
|
|
187
|
+
/>
|
|
188
|
+
<TopNavMegaMenu
|
|
189
|
+
label="Brands"
|
|
190
|
+
items={<MegaItems items={BRAND_ITEMS} />}
|
|
191
|
+
featured={
|
|
192
|
+
<MegaFeatured
|
|
193
|
+
title="Meet Studio Mara"
|
|
194
|
+
description="Modern tailoring, made to last."
|
|
195
|
+
linkLabel="Discover the label"
|
|
196
|
+
linkHref={SELF_HASH}
|
|
197
|
+
/>
|
|
198
|
+
}
|
|
199
|
+
/>
|
|
200
|
+
<TopNavItem label="Sale" href={SELF_HASH} />
|
|
201
|
+
<TopNavItem label="Service" href={SELF_HASH} />
|
|
202
|
+
</>
|
|
203
|
+
}
|
|
204
|
+
endContent={
|
|
205
|
+
<>
|
|
206
|
+
<IconButton
|
|
207
|
+
label="Search products"
|
|
208
|
+
tooltip="Search"
|
|
209
|
+
variant="ghost"
|
|
210
|
+
icon={<Icon icon={Search} size="sm" />}
|
|
211
|
+
/>
|
|
212
|
+
{!isCompact && <Button label="Sign in" variant="ghost" />}
|
|
213
|
+
<Button
|
|
214
|
+
label="Checkout"
|
|
215
|
+
variant="primary"
|
|
216
|
+
tooltip="Cart, 3 items"
|
|
217
|
+
icon={<Icon icon={ShoppingCart} size="sm" />}
|
|
218
|
+
endContent={<Badge label={3} />}
|
|
219
|
+
/>
|
|
220
|
+
</>
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
}>
|
|
224
|
+
<VStack gap={10} style={contentMax}>
|
|
225
|
+
<Card variant="muted" padding={0} width="100%" height={360} />
|
|
226
|
+
|
|
227
|
+
{[0, 1, 2].map(section => (
|
|
228
|
+
<VStack key={section} gap={4}>
|
|
229
|
+
<Card variant="muted" padding={0} width={200} height={24} />
|
|
230
|
+
<Grid columns={{minWidth: 160, repeat: 'fit'}} gap={4}>
|
|
231
|
+
{CATEGORY_TILES.map(tile => (
|
|
232
|
+
<VStack key={tile} gap={2}>
|
|
233
|
+
<Card variant="muted" padding={0} width="100%" height={120} />
|
|
234
|
+
<Card variant="muted" padding={0} width="60%" height={14} />
|
|
235
|
+
</VStack>
|
|
236
|
+
))}
|
|
237
|
+
</Grid>
|
|
238
|
+
</VStack>
|
|
239
|
+
))}
|
|
240
|
+
</VStack>
|
|
241
|
+
</AppShell>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import type {CSSProperties} from 'react';
|
|
4
|
+
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
5
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
6
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
7
|
+
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
8
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
9
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
10
|
+
|
|
11
|
+
// NOTE: The only custom styling here is gallery fill + corner radius. It
|
|
12
|
+
// exists because Astryx has no image primitive — AspectRatio exposes no
|
|
13
|
+
// objectFit or radius props and there's no Image. Tracked in issue #2582;
|
|
14
|
+
// replace these with component props once it lands.
|
|
15
|
+
// Fills the AspectRatio box. No objectFit prop on AspectRatio (#2582).
|
|
16
|
+
const galleryImage: CSSProperties = {
|
|
17
|
+
width: '100%',
|
|
18
|
+
height: '100%',
|
|
19
|
+
display: 'block',
|
|
20
|
+
};
|
|
21
|
+
// Rounds the gallery corners. No radius prop on AspectRatio (#2582);
|
|
22
|
+
// overflow clip masks the SVG scene to the rounded corners.
|
|
23
|
+
const galleryImageClip: CSSProperties = {
|
|
24
|
+
borderRadius: 'var(--radius-container)',
|
|
25
|
+
overflow: 'clip',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ─── Gallery Data ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
// One Dracula accent per tile from the fixed categorical vocabulary.
|
|
31
|
+
const GALLERY_SCENES = [
|
|
32
|
+
{alt: 'Moonlit ridge trail under a harvest moon', hue: 'var(--dracula-cyan)'},
|
|
33
|
+
{alt: 'Late portrait in violet lamplight', hue: 'var(--dracula-pink)'},
|
|
34
|
+
{
|
|
35
|
+
alt: 'Lamplit reading nook after midnight',
|
|
36
|
+
hue: 'var(--dracula-yellow)',
|
|
37
|
+
},
|
|
38
|
+
{alt: 'Fog rolling over the night pines', hue: 'var(--dracula-purple)'},
|
|
39
|
+
{alt: 'Dancer caught mid-step in stage pink', hue: 'var(--dracula-pink)'},
|
|
40
|
+
{
|
|
41
|
+
alt: 'Kitchen table set for a midnight feast',
|
|
42
|
+
hue: 'var(--dracula-orange)',
|
|
43
|
+
},
|
|
44
|
+
{alt: 'Castle silhouette over sleeping hills', hue: 'var(--dracula-cyan)'},
|
|
45
|
+
{alt: 'Attic window glowing amber at 2am', hue: 'var(--dracula-yellow)'},
|
|
46
|
+
{alt: 'Cellar shelves lined with bottled dusk', hue: 'var(--dracula-green)'},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// ─── Gallery Art ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function NightScene({alt, hue, index}: {alt: string; hue: string; index: number}) {
|
|
52
|
+
const moonX = 90 + ((index * 53) % 220);
|
|
53
|
+
const moonY = 62 + ((index * 29) % 60);
|
|
54
|
+
const hillA = 190 + ((index * 13) % 40);
|
|
55
|
+
const hillB = 215 + ((index * 17) % 40);
|
|
56
|
+
return (
|
|
57
|
+
<svg
|
|
58
|
+
viewBox="0 0 400 300"
|
|
59
|
+
preserveAspectRatio="xMidYMid slice"
|
|
60
|
+
style={galleryImage}
|
|
61
|
+
role="img"
|
|
62
|
+
aria-label={alt}>
|
|
63
|
+
<rect width="400" height="300" fill="var(--dracula-bg-light)" />
|
|
64
|
+
<g fill="var(--dracula-purple)" opacity={0.55}>
|
|
65
|
+
<circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
|
|
66
|
+
<circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
|
|
67
|
+
<circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
|
|
68
|
+
<circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
|
|
69
|
+
</g>
|
|
70
|
+
<circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
|
|
71
|
+
<circle
|
|
72
|
+
cx={moonX - 12}
|
|
73
|
+
cy={moonY - 8}
|
|
74
|
+
r={28}
|
|
75
|
+
fill="var(--dracula-bg-light)"
|
|
76
|
+
opacity={0.55}
|
|
77
|
+
/>
|
|
78
|
+
<path
|
|
79
|
+
d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
|
|
80
|
+
fill="var(--dracula-current-line)"
|
|
81
|
+
/>
|
|
82
|
+
<path
|
|
83
|
+
d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
|
|
84
|
+
fill="var(--dracula-bg)"
|
|
85
|
+
/>
|
|
86
|
+
<g
|
|
87
|
+
transform={`translate(${60 + ((index * 41) % 280)} ${hillB - 34})`}
|
|
88
|
+
fill="none"
|
|
89
|
+
stroke={hue}
|
|
90
|
+
strokeWidth="4"
|
|
91
|
+
strokeLinecap="round"
|
|
92
|
+
strokeLinejoin="round">
|
|
93
|
+
<rect x="-22" y="-22" width="44" height="44" rx={4} />
|
|
94
|
+
<circle cx="9" cy="-9" r="2" fill={hue} stroke="none" />
|
|
95
|
+
<path d="M-17 15 L-4 0 L5 9 L10 4 L17 12" />
|
|
96
|
+
</g>
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Stat Block ─────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function StatBlock({value, label}: {value: string; label: string}) {
|
|
104
|
+
return (
|
|
105
|
+
<VStack gap={0}>
|
|
106
|
+
<Text type="large" weight="bold" hasTabularNumbers>
|
|
107
|
+
{value}
|
|
108
|
+
</Text>
|
|
109
|
+
<Text type="supporting" color="secondary">
|
|
110
|
+
{label}
|
|
111
|
+
</Text>
|
|
112
|
+
</VStack>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Image Grid ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function ImageGrid() {
|
|
119
|
+
return (
|
|
120
|
+
<Grid columns={3} gap={3}>
|
|
121
|
+
{GALLERY_SCENES.map((scene, index) => (
|
|
122
|
+
<AspectRatio key={scene.alt} ratio={1} style={galleryImageClip}>
|
|
123
|
+
<NightScene alt={scene.alt} hue={scene.hue} index={index} />
|
|
124
|
+
</AspectRatio>
|
|
125
|
+
))}
|
|
126
|
+
</Grid>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Main Page ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export default function SideGallery() {
|
|
133
|
+
return (
|
|
134
|
+
<Layout
|
|
135
|
+
height="fill"
|
|
136
|
+
contentWidth={1400}
|
|
137
|
+
content={
|
|
138
|
+
<LayoutContent padding={6}>
|
|
139
|
+
<Grid columns={{minWidth: 360, repeat: 'fit'}} gap={8} align="center">
|
|
140
|
+
{/* Left side: Text + CTA */}
|
|
141
|
+
<VStack gap={6} vAlign="center">
|
|
142
|
+
<VStack gap={3}>
|
|
143
|
+
<Text type="supporting" color="secondary" weight="semibold">
|
|
144
|
+
AFTER DARK
|
|
145
|
+
</Text>
|
|
146
|
+
<Heading level={1}>
|
|
147
|
+
Make every night a little more spellbinding, one small ritual
|
|
148
|
+
at a time.
|
|
149
|
+
</Heading>
|
|
150
|
+
<Text type="body" color="secondary">
|
|
151
|
+
The smallest rituals are the ones that matter most. A little
|
|
152
|
+
lamplight that catches your eye and makes you pause;
|
|
153
|
+
that's what turns an ordinary evening into something
|
|
154
|
+
worth remembering.
|
|
155
|
+
</Text>
|
|
156
|
+
</VStack>
|
|
157
|
+
|
|
158
|
+
<HStack gap={3} vAlign="center">
|
|
159
|
+
<Button label="Explore the night" variant="primary" />
|
|
160
|
+
</HStack>
|
|
161
|
+
|
|
162
|
+
<VStack gap={4}>
|
|
163
|
+
<Divider />
|
|
164
|
+
<HStack gap={6}>
|
|
165
|
+
<StatBlock value="12k+" label="Night shots" />
|
|
166
|
+
<StatBlock value="350+" label="Dark builds" />
|
|
167
|
+
<StatBlock value="8yrs" label="Years prowling" />
|
|
168
|
+
</HStack>
|
|
169
|
+
</VStack>
|
|
170
|
+
</VStack>
|
|
171
|
+
|
|
172
|
+
{/* Right side: Image Grid */}
|
|
173
|
+
<ImageGrid />
|
|
174
|
+
</Grid>
|
|
175
|
+
</LayoutContent>
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|