astryx-dracula 0.5.0 → 0.6.2
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 +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
package/templates/editor.tsx
CHANGED
|
@@ -1,287 +1,282 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from 'react';
|
|
15
|
-
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
16
|
-
import {Button} from '@astryxdesign/core/Button';
|
|
17
|
-
import {SelectableCard} from '@astryxdesign/core/SelectableCard';
|
|
18
|
-
import {Center} from '@astryxdesign/core/Center';
|
|
19
|
-
import {Dialog} from '@astryxdesign/core/Dialog';
|
|
20
|
-
import {Divider} from '@astryxdesign/core/Divider';
|
|
21
|
-
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
5
|
+
// L[h=fill] > (LP[w=320 p=0 divider] > V[g=4] > (S.transparent[p=4] > V[g=4] > (TI"Page title" + Hd"Page Editor"[level=1 t=display-2]) + Tbar) + (V[g=4] > (V[g=0] > (TL > Tab"Blocks"! + Tab"Properties") + D) + (S.transparent[p=4] > (V[g=2] > (V[g=1] > Hd"Add Block"[level=3] + UL) + (V[g=1] > Hd"Layers"[level=3] + UL)) + (V[g=4] > TI"Heading" + TA"Description")))) + (LC[p=8] > V[g=4] > SC*3) ;; Dlg[variant=fullscreen p=0] > L[h=fill] > (LH[divider] > (H[g=3 j=between a=center] > Hd"Customize"[level=3] + IB"Close")) + (LC[p=0] > (V[g=4] > (V[g=0] > (TL > Tab"Blocks"! + Tab"Properties") + D) + (S.transparent[p=4] > (V[g=2] > (V[g=1] > Hd"Add Block"[level=3] + UL) + (V[g=1] > Hd"Layers"[level=3] + UL)) + (V[g=4] > TI"Heading" + TA"Description")))))
|
|
6
|
+
|
|
7
|
+
import { Button } from "@astryxdesign/core/Button";
|
|
8
|
+
import { Center } from "@astryxdesign/core/Center";
|
|
9
|
+
import { Dialog } from "@astryxdesign/core/Dialog";
|
|
10
|
+
import { Divider } from "@astryxdesign/core/Divider";
|
|
11
|
+
import { EmptyState } from "@astryxdesign/core/EmptyState";
|
|
12
|
+
import { useMediaQuery } from "@astryxdesign/core/hooks";
|
|
13
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
22
14
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
15
|
+
HStack,
|
|
16
|
+
Layout,
|
|
17
|
+
LayoutContent,
|
|
18
|
+
LayoutHeader,
|
|
19
|
+
LayoutPanel,
|
|
20
|
+
VStack,
|
|
21
|
+
} from "@astryxdesign/core/Layout";
|
|
22
|
+
import { List, ListItem } from "@astryxdesign/core/List";
|
|
23
|
+
import { Section } from "@astryxdesign/core/Section";
|
|
24
|
+
import { SegmentedControl, SegmentedControlItem } from "@astryxdesign/core/SegmentedControl";
|
|
25
|
+
import { SelectableCard } from "@astryxdesign/core/SelectableCard";
|
|
26
|
+
import { Selector } from "@astryxdesign/core/Selector";
|
|
27
|
+
import { Tab, TabList } from "@astryxdesign/core/TabList";
|
|
28
|
+
import type { TableColumn } from "@astryxdesign/core/Table";
|
|
29
|
+
import { proportional, Table } from "@astryxdesign/core/Table";
|
|
30
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
31
|
+
import { TextArea } from "@astryxdesign/core/TextArea";
|
|
32
|
+
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
33
|
+
import { Toolbar } from "@astryxdesign/core/Toolbar";
|
|
35
34
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
Banknote,
|
|
36
|
+
CarFront,
|
|
37
|
+
ChevronDown,
|
|
38
|
+
ChevronUp,
|
|
39
|
+
CirclePlay,
|
|
40
|
+
CirclePlus,
|
|
41
|
+
Columns,
|
|
42
|
+
Eye,
|
|
43
|
+
FileText,
|
|
44
|
+
Image,
|
|
45
|
+
LayoutGrid,
|
|
46
|
+
Lock,
|
|
47
|
+
Megaphone,
|
|
48
|
+
Monitor,
|
|
49
|
+
MousePointerClick,
|
|
50
|
+
ShoppingBag,
|
|
51
|
+
ShoppingCart,
|
|
52
|
+
SlidersHorizontal,
|
|
53
|
+
Smartphone,
|
|
54
|
+
Sparkles,
|
|
55
|
+
Tablet,
|
|
56
|
+
Trash2,
|
|
57
|
+
X,
|
|
58
|
+
} from "lucide-react";
|
|
45
59
|
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Tablet,
|
|
58
|
-
Smartphone,
|
|
59
|
-
Eye,
|
|
60
|
-
SlidersHorizontal,
|
|
61
|
-
X,
|
|
62
|
-
CirclePlus,
|
|
63
|
-
ShoppingBag,
|
|
64
|
-
ShoppingCart,
|
|
65
|
-
Banknote,
|
|
66
|
-
CarFront,
|
|
67
|
-
CirclePlay,
|
|
68
|
-
Lock,
|
|
69
|
-
} from 'lucide-react';
|
|
70
|
-
|
|
71
|
-
type BlockType =
|
|
72
|
-
'hero' | 'text' | 'image' | 'button' | 'cards' | 'features' | 'cta';
|
|
60
|
+
type ComponentType,
|
|
61
|
+
type CSSProperties,
|
|
62
|
+
type KeyboardEvent,
|
|
63
|
+
type MouseEvent,
|
|
64
|
+
type ReactNode,
|
|
65
|
+
type SVGProps,
|
|
66
|
+
useCallback,
|
|
67
|
+
useState,
|
|
68
|
+
} from "react";
|
|
69
|
+
|
|
70
|
+
type BlockType = "hero" | "text" | "image" | "button" | "cards" | "features" | "cta";
|
|
73
71
|
|
|
74
72
|
interface Block {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
id: string;
|
|
74
|
+
type: BlockType;
|
|
75
|
+
label: string;
|
|
76
|
+
props: Record<string, unknown>;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
type ViewportSize =
|
|
82
|
-
type SidebarTab =
|
|
79
|
+
type ViewportSize = "desktop" | "tablet" | "phone";
|
|
80
|
+
type SidebarTab = "blocks" | "properties";
|
|
83
81
|
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
|
|
84
82
|
|
|
85
|
-
const BLOCK_META: Record<BlockType, {label: string; icon: IconComponent}> = {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
const BLOCK_META: Record<BlockType, { label: string; icon: IconComponent }> = {
|
|
84
|
+
hero: { label: "Hero", icon: LayoutGrid },
|
|
85
|
+
text: { label: "Text", icon: FileText },
|
|
86
|
+
image: { label: "Image", icon: Image },
|
|
87
|
+
button: { label: "Button", icon: MousePointerClick },
|
|
88
|
+
cards: { label: "Cards", icon: Columns },
|
|
89
|
+
features: { label: "Features", icon: Sparkles },
|
|
90
|
+
cta: { label: "CTA", icon: Megaphone },
|
|
93
91
|
};
|
|
94
92
|
|
|
95
93
|
type Transaction = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
category: string;
|
|
97
|
+
date: string;
|
|
98
|
+
amount: string;
|
|
99
|
+
isPositive?: boolean;
|
|
102
100
|
};
|
|
103
101
|
|
|
104
102
|
const CATEGORY_ICONS: Record<string, IconComponent> = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
"Food & Drink": ShoppingBag,
|
|
104
|
+
Groceries: ShoppingCart,
|
|
105
|
+
Income: Banknote,
|
|
106
|
+
Transport: CarFront,
|
|
107
|
+
Entertainment: CirclePlay,
|
|
110
108
|
};
|
|
111
109
|
|
|
112
110
|
// The minimums clear the 375px phone canvas once the preview card's 24px
|
|
113
111
|
// padding is taken out; the widest column carries the slack and every cell
|
|
114
112
|
// truncates rather than pushing the table past the card.
|
|
115
113
|
const TRANSACTION_COLUMNS: TableColumn<Transaction>[] = [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
114
|
+
{
|
|
115
|
+
key: "name",
|
|
116
|
+
header: "Transaction",
|
|
117
|
+
width: proportional(2, { minWidth: 104 }),
|
|
118
|
+
renderCell: (item: Transaction) => (
|
|
119
|
+
<HStack gap={3} vAlign="center">
|
|
120
|
+
<Icon icon={CATEGORY_ICONS[item.category] || Sparkles} />
|
|
121
|
+
<VStack gap={0}>
|
|
122
|
+
<Text type="body" weight="semibold">
|
|
123
|
+
{item.name}
|
|
124
|
+
</Text>
|
|
125
|
+
<Text type="supporting" color="secondary">
|
|
126
|
+
{item.category}
|
|
127
|
+
</Text>
|
|
128
|
+
</VStack>
|
|
129
|
+
</HStack>
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "date",
|
|
134
|
+
header: "Date",
|
|
135
|
+
width: proportional(1, { minWidth: 64 }),
|
|
136
|
+
renderCell: (item: Transaction) => (
|
|
137
|
+
<Text type="body" color="secondary" hasTabularNumbers>
|
|
138
|
+
{item.date}
|
|
139
|
+
</Text>
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "amount",
|
|
144
|
+
header: "Amount",
|
|
145
|
+
width: proportional(1, { minWidth: 88 }),
|
|
146
|
+
renderCell: (item: Transaction) => (
|
|
147
|
+
<Text type="body" weight="semibold" hasTabularNumbers>
|
|
148
|
+
{item.amount}
|
|
149
|
+
</Text>
|
|
150
|
+
),
|
|
151
|
+
},
|
|
154
152
|
];
|
|
155
153
|
|
|
156
154
|
const VIEWPORT_MAX: Record<ViewportSize, number> = {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
desktop: 960,
|
|
156
|
+
tablet: 768,
|
|
157
|
+
phone: 375,
|
|
160
158
|
};
|
|
161
159
|
|
|
162
|
-
|
|
163
160
|
const DEFAULT_BLOCKS: Block[] = [
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
},
|
|
161
|
+
{
|
|
162
|
+
id: "2",
|
|
163
|
+
type: "features",
|
|
164
|
+
label: "Recent Transactions",
|
|
165
|
+
props: {
|
|
166
|
+
heading: "Recent Transactions",
|
|
167
|
+
description: "Your latest account activity.",
|
|
168
|
+
items: [
|
|
169
|
+
{
|
|
170
|
+
id: "t1",
|
|
171
|
+
name: "Midnight Oil Coffee",
|
|
172
|
+
category: "Food & Drink",
|
|
173
|
+
date: "Today, 10:24 AM",
|
|
174
|
+
amount: "-$6.50",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "t2",
|
|
178
|
+
name: "Moonlit Market",
|
|
179
|
+
category: "Groceries",
|
|
180
|
+
date: "Yesterday",
|
|
181
|
+
amount: "-$142.30",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "t3",
|
|
185
|
+
name: "Night Shift Payout",
|
|
186
|
+
category: "Income",
|
|
187
|
+
date: "Oct 12",
|
|
188
|
+
amount: "+$4,200.00",
|
|
189
|
+
isPositive: true,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "t4",
|
|
193
|
+
name: "Night Owl Rides",
|
|
194
|
+
category: "Transport",
|
|
195
|
+
date: "Oct 11",
|
|
196
|
+
amount: "-$24.10",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "t5",
|
|
200
|
+
name: "Cryptflix Subscription",
|
|
201
|
+
category: "Entertainment",
|
|
202
|
+
date: "Oct 10",
|
|
203
|
+
amount: "-$19.99",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "3",
|
|
210
|
+
type: "text",
|
|
211
|
+
label: "Syncing State",
|
|
212
|
+
props: {
|
|
213
|
+
heading: "Syncing your accounts",
|
|
214
|
+
description: "We're pulling in your latest transactions.\nThis usually takes a few seconds.",
|
|
215
|
+
buttonLabel: "Cancel",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "4",
|
|
220
|
+
type: "cta",
|
|
221
|
+
label: "Trust Notice",
|
|
222
|
+
props: {
|
|
223
|
+
heading: "Adding devices from people you trust",
|
|
224
|
+
description:
|
|
225
|
+
"When you approve a request, you grant someone full access to your account. They'll be able to change reservations and send messages on your behalf.",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
232
228
|
];
|
|
233
229
|
|
|
234
230
|
function defaultProps(type: BlockType): Record<string, unknown> {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
231
|
+
switch (type) {
|
|
232
|
+
case "hero":
|
|
233
|
+
return {
|
|
234
|
+
heading: "Midnight at the Castle Gates",
|
|
235
|
+
subheading: "Gather the coven — the night is young and the candles are lit.",
|
|
236
|
+
buttonLabel: "Enter the night",
|
|
237
|
+
alignment: "center",
|
|
238
|
+
};
|
|
239
|
+
case "text":
|
|
240
|
+
return { content: "Enter your text here." };
|
|
241
|
+
case "image":
|
|
242
|
+
return {};
|
|
243
|
+
case "button":
|
|
244
|
+
return { label: "Button", variant: "primary", size: "md" };
|
|
245
|
+
case "cards":
|
|
246
|
+
return {
|
|
247
|
+
cards: [
|
|
248
|
+
{
|
|
249
|
+
title: "Pricing",
|
|
250
|
+
description: "Flexible plans for every team size.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
title: "Support",
|
|
254
|
+
description: "Get help whenever you need it.",
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
case "features":
|
|
259
|
+
return {
|
|
260
|
+
heading: "Activity",
|
|
261
|
+
description: "",
|
|
262
|
+
items: [
|
|
263
|
+
{
|
|
264
|
+
id: "t1",
|
|
265
|
+
name: "New Item",
|
|
266
|
+
category: "General",
|
|
267
|
+
date: "Today",
|
|
268
|
+
amount: "$0.00",
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
case "cta":
|
|
273
|
+
return {
|
|
274
|
+
heading: "Join the Midnight Coven",
|
|
275
|
+
description: "Claim your seat at the table — letters from the crypt, once a moon.",
|
|
276
|
+
primaryLabel: "Join the coven",
|
|
277
|
+
secondaryLabel: "Read the lore",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
285
280
|
}
|
|
286
281
|
|
|
287
282
|
// ---------------------------------------------------------------------------
|
|
@@ -296,12 +291,12 @@ function defaultProps(type: BlockType): Record<string, unknown> {
|
|
|
296
291
|
// against a definite height — and the host's <html>/<body> don't set one, so
|
|
297
292
|
// the layout anchors a definite viewport height itself. No background; the
|
|
298
293
|
// host owns the page surface.
|
|
299
|
-
const pageStyle: CSSProperties = {height:
|
|
294
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
300
295
|
// Canvas reflows to the chosen viewport width; VStack has no maxWidth prop.
|
|
301
296
|
const canvasStyle = (maxWidth: number): CSSProperties => ({
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
maxWidth,
|
|
298
|
+
width: "100%",
|
|
299
|
+
marginInline: "auto",
|
|
305
300
|
});
|
|
306
301
|
// `clickable` and `selectedCard` are gone: the eight block cards are core's
|
|
307
302
|
// `SelectableCard` now, which supplies the cursor, the hover overlay
|
|
@@ -313,12 +308,12 @@ const canvasStyle = (maxWidth: number): CSSProperties => ({
|
|
|
313
308
|
// ring colour is unchanged and still stands: Comment #6272A4 cannot reach
|
|
314
309
|
// 3:1 on #343746 at any alpha, and core's own selected ring has that ceiling.
|
|
315
310
|
// The sidebar keeps its width when the canvas is wider than the window.
|
|
316
|
-
const panelShrink: CSSProperties = {flexShrink: 0};
|
|
311
|
+
const panelShrink: CSSProperties = { flexShrink: 0 };
|
|
317
312
|
// Square muted chip behind the CTA icon — Center handles the centering
|
|
318
313
|
// and sizing; only the surface (radius + fill) needs custom CSS.
|
|
319
314
|
const iconCircle: CSSProperties = {
|
|
320
|
-
|
|
321
|
-
|
|
315
|
+
borderRadius: "var(--radius-element)",
|
|
316
|
+
backgroundColor: "var(--color-selection)",
|
|
322
317
|
};
|
|
323
318
|
|
|
324
319
|
// ---------------------------------------------------------------------------
|
|
@@ -329,132 +324,132 @@ const iconCircle: CSSProperties = {
|
|
|
329
324
|
// Hero labels it "Subheading" and adds its own button/alignment controls;
|
|
330
325
|
// the merge keeps the shared pair, not the per-branch extras.
|
|
331
326
|
function HeadingDescriptionGroup({
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
327
|
+
props,
|
|
328
|
+
onUpdate,
|
|
329
|
+
descriptionKey,
|
|
330
|
+
descriptionLabel,
|
|
336
331
|
}: {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
332
|
+
props: Record<string, unknown>;
|
|
333
|
+
onUpdate: (key: string, value: unknown) => void;
|
|
334
|
+
descriptionKey: string;
|
|
335
|
+
descriptionLabel: string;
|
|
341
336
|
}) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
337
|
+
return (
|
|
338
|
+
<>
|
|
339
|
+
<TextInput
|
|
340
|
+
label="Heading"
|
|
341
|
+
value={(props.heading as string) ?? ""}
|
|
342
|
+
onChange={(v: string) => onUpdate("heading", v)}
|
|
343
|
+
/>
|
|
344
|
+
<TextArea
|
|
345
|
+
label={descriptionLabel}
|
|
346
|
+
value={(props[descriptionKey] as string) ?? ""}
|
|
347
|
+
onChange={(v: string) => onUpdate(descriptionKey, v)}
|
|
348
|
+
/>
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
356
351
|
}
|
|
357
352
|
|
|
358
353
|
function PropertiesForm({
|
|
359
|
-
|
|
360
|
-
|
|
354
|
+
block,
|
|
355
|
+
onUpdate,
|
|
361
356
|
}: {
|
|
362
|
-
|
|
363
|
-
|
|
357
|
+
block: Block;
|
|
358
|
+
onUpdate: (key: string, value: unknown) => void;
|
|
364
359
|
}) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
360
|
+
const { type, props } = block;
|
|
361
|
+
|
|
362
|
+
switch (type) {
|
|
363
|
+
case "hero":
|
|
364
|
+
return (
|
|
365
|
+
<VStack gap={4}>
|
|
366
|
+
<HeadingDescriptionGroup
|
|
367
|
+
props={props}
|
|
368
|
+
onUpdate={onUpdate}
|
|
369
|
+
descriptionKey="subheading"
|
|
370
|
+
descriptionLabel="Subheading"
|
|
371
|
+
/>
|
|
372
|
+
<TextInput
|
|
373
|
+
label="Button Label"
|
|
374
|
+
value={(props.buttonLabel as string) ?? ""}
|
|
375
|
+
onChange={(v: string) => onUpdate("buttonLabel", v)}
|
|
376
|
+
/>
|
|
377
|
+
<Selector
|
|
378
|
+
label="Alignment"
|
|
379
|
+
value={(props.alignment as string) ?? "center"}
|
|
380
|
+
onChange={(v: string) => onUpdate("alignment", v)}
|
|
381
|
+
options={[
|
|
382
|
+
{ label: "Left", value: "left" },
|
|
383
|
+
{ label: "Center", value: "center" },
|
|
384
|
+
{ label: "Right", value: "right" },
|
|
385
|
+
]}
|
|
386
|
+
/>
|
|
387
|
+
</VStack>
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
case "text":
|
|
391
|
+
return (
|
|
392
|
+
<VStack gap={4}>
|
|
393
|
+
<HeadingDescriptionGroup
|
|
394
|
+
props={props}
|
|
395
|
+
onUpdate={onUpdate}
|
|
396
|
+
descriptionKey="description"
|
|
397
|
+
descriptionLabel="Description"
|
|
398
|
+
/>
|
|
399
|
+
<TextInput
|
|
400
|
+
label="Button Label"
|
|
401
|
+
value={(props.buttonLabel as string) ?? ""}
|
|
402
|
+
onChange={(v: string) => onUpdate("buttonLabel", v)}
|
|
403
|
+
/>
|
|
404
|
+
</VStack>
|
|
405
|
+
);
|
|
406
|
+
case "features":
|
|
407
|
+
case "cta":
|
|
408
|
+
return (
|
|
409
|
+
<VStack gap={4}>
|
|
410
|
+
<HeadingDescriptionGroup
|
|
411
|
+
props={props}
|
|
412
|
+
onUpdate={onUpdate}
|
|
413
|
+
descriptionKey="description"
|
|
414
|
+
descriptionLabel="Description"
|
|
415
|
+
/>
|
|
416
|
+
</VStack>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
case "button":
|
|
420
|
+
return (
|
|
421
|
+
<VStack gap={4}>
|
|
422
|
+
<TextInput
|
|
423
|
+
label="Label"
|
|
424
|
+
value={(props.label as string) ?? ""}
|
|
425
|
+
onChange={(v: string) => onUpdate("label", v)}
|
|
426
|
+
/>
|
|
427
|
+
<Selector
|
|
428
|
+
label="Variant"
|
|
429
|
+
value={(props.variant as string) ?? "primary"}
|
|
430
|
+
onChange={(v: string) => onUpdate("variant", v)}
|
|
431
|
+
options={[
|
|
432
|
+
{ label: "Primary", value: "primary" },
|
|
433
|
+
{ label: "Secondary", value: "secondary" },
|
|
434
|
+
{ label: "Ghost", value: "ghost" },
|
|
435
|
+
]}
|
|
436
|
+
/>
|
|
437
|
+
<Selector
|
|
438
|
+
label="Size"
|
|
439
|
+
value={(props.size as string) ?? "md"}
|
|
440
|
+
onChange={(v: string) => onUpdate("size", v)}
|
|
441
|
+
options={[
|
|
442
|
+
{ label: "Small", value: "sm" },
|
|
443
|
+
{ label: "Medium", value: "md" },
|
|
444
|
+
{ label: "Large", value: "lg" },
|
|
445
|
+
]}
|
|
446
|
+
/>
|
|
447
|
+
</VStack>
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
default:
|
|
451
|
+
return <EmptyState title="No configurable properties" isCompact />;
|
|
452
|
+
}
|
|
458
453
|
}
|
|
459
454
|
|
|
460
455
|
// ---------------------------------------------------------------------------
|
|
@@ -462,166 +457,139 @@ function PropertiesForm({
|
|
|
462
457
|
// ---------------------------------------------------------------------------
|
|
463
458
|
|
|
464
459
|
function BlockPreview({
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
460
|
+
block,
|
|
461
|
+
isSelected,
|
|
462
|
+
onSelect,
|
|
468
463
|
}: {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
464
|
+
block: Block;
|
|
465
|
+
isSelected: boolean;
|
|
466
|
+
onSelect: () => void;
|
|
472
467
|
}) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
</Center>
|
|
599
|
-
<VStack gap={1}>
|
|
600
|
-
<Heading level={3}>
|
|
601
|
-
{(props.heading as string) || 'Notice'}
|
|
602
|
-
</Heading>
|
|
603
|
-
<Text type="body" color="secondary">
|
|
604
|
-
{(props.description as string) ||
|
|
605
|
-
'Words from the crypt gather here…'}
|
|
606
|
-
</Text>
|
|
607
|
-
</VStack>
|
|
608
|
-
</HStack>
|
|
609
|
-
);
|
|
610
|
-
break;
|
|
611
|
-
|
|
612
|
-
default:
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return (
|
|
617
|
-
<SelectableCard
|
|
618
|
-
padding={4}
|
|
619
|
-
label={block.label}
|
|
620
|
-
isSelected={isSelected}
|
|
621
|
-
onChange={onSelect}>
|
|
622
|
-
{content}
|
|
623
|
-
</SelectableCard>
|
|
624
|
-
);
|
|
468
|
+
const { type, props } = block;
|
|
469
|
+
|
|
470
|
+
// One SelectableCard wrapper for every block; only the inner content
|
|
471
|
+
// switches. Keeps padding/label/selection in one place.
|
|
472
|
+
let content: ReactNode;
|
|
473
|
+
switch (type) {
|
|
474
|
+
case "hero":
|
|
475
|
+
content = (
|
|
476
|
+
<VStack gap={4}>
|
|
477
|
+
<Heading level={3}>{(props.heading as string) || "Hero Heading"}</Heading>
|
|
478
|
+
<Text type="body" color="secondary">
|
|
479
|
+
{(props.subheading as string) || "A whisper from the crypt…"}
|
|
480
|
+
</Text>
|
|
481
|
+
{(props.buttonLabel as string) && <Button label={props.buttonLabel as string} />}
|
|
482
|
+
</VStack>
|
|
483
|
+
);
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case "text":
|
|
487
|
+
content = props.heading ? (
|
|
488
|
+
<EmptyState
|
|
489
|
+
title={props.heading as string}
|
|
490
|
+
description={props.description as string}
|
|
491
|
+
icon={<Icon icon={FileText} color="secondary" />}
|
|
492
|
+
actions={
|
|
493
|
+
(props.buttonLabel as string) ? (
|
|
494
|
+
<Button label={props.buttonLabel as string} variant="secondary" />
|
|
495
|
+
) : undefined
|
|
496
|
+
}
|
|
497
|
+
/>
|
|
498
|
+
) : (
|
|
499
|
+
<Text type="body">{(props.content as string) || "Ink your midnight thoughts here…"}</Text>
|
|
500
|
+
);
|
|
501
|
+
break;
|
|
502
|
+
case "image":
|
|
503
|
+
content = (
|
|
504
|
+
<EmptyState
|
|
505
|
+
title="Image Block"
|
|
506
|
+
description="Drop an image or enter a URL"
|
|
507
|
+
icon={<Icon icon={Image} color="secondary" />}
|
|
508
|
+
isCompact
|
|
509
|
+
/>
|
|
510
|
+
);
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case "button":
|
|
514
|
+
content = (
|
|
515
|
+
<Center>
|
|
516
|
+
<Button
|
|
517
|
+
label={(props.label as string) || "Button"}
|
|
518
|
+
variant={(props.variant as "primary" | "secondary" | "ghost") || "primary"}
|
|
519
|
+
size={(props.size as "sm" | "md" | "lg") || "md"}
|
|
520
|
+
/>
|
|
521
|
+
</Center>
|
|
522
|
+
);
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case "features": {
|
|
526
|
+
const items = (props.items as Transaction[]) || [];
|
|
527
|
+
content = (
|
|
528
|
+
<VStack gap={4}>
|
|
529
|
+
<HStack gap={3} vAlign="start" hAlign="between">
|
|
530
|
+
<VStack gap={1}>
|
|
531
|
+
<Heading level={3}>{(props.heading as string) || "Features"}</Heading>
|
|
532
|
+
{(props.description as string) && (
|
|
533
|
+
<Text type="body" color="secondary">
|
|
534
|
+
{props.description as string}
|
|
535
|
+
</Text>
|
|
536
|
+
)}
|
|
537
|
+
</VStack>
|
|
538
|
+
<Button label="View All" variant="secondary" />
|
|
539
|
+
</HStack>
|
|
540
|
+
<Table
|
|
541
|
+
data={items}
|
|
542
|
+
columns={TRANSACTION_COLUMNS}
|
|
543
|
+
idKey="id"
|
|
544
|
+
hasHover
|
|
545
|
+
textOverflow="truncate"
|
|
546
|
+
/>
|
|
547
|
+
</VStack>
|
|
548
|
+
);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case "cards": {
|
|
553
|
+
const cardItems = (props.cards as Array<{ title: string; description: string }>) || [];
|
|
554
|
+
content = (
|
|
555
|
+
<VStack gap={4}>
|
|
556
|
+
<Heading level={3}>Cards</Heading>
|
|
557
|
+
<Divider />
|
|
558
|
+
<List density="balanced" hasDividers={false}>
|
|
559
|
+
{cardItems.map((card) => (
|
|
560
|
+
<ListItem key={card.title} label={card.title} description={card.description} />
|
|
561
|
+
))}
|
|
562
|
+
</List>
|
|
563
|
+
</VStack>
|
|
564
|
+
);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case "cta":
|
|
569
|
+
content = (
|
|
570
|
+
<HStack gap={4} vAlign="start">
|
|
571
|
+
<Center width={40} height={40} style={iconCircle}>
|
|
572
|
+
<Icon icon={Lock} color="secondary" />
|
|
573
|
+
</Center>
|
|
574
|
+
<VStack gap={1}>
|
|
575
|
+
<Heading level={3}>{(props.heading as string) || "Notice"}</Heading>
|
|
576
|
+
<Text type="body" color="secondary">
|
|
577
|
+
{(props.description as string) || "Words from the crypt gather here…"}
|
|
578
|
+
</Text>
|
|
579
|
+
</VStack>
|
|
580
|
+
</HStack>
|
|
581
|
+
);
|
|
582
|
+
break;
|
|
583
|
+
|
|
584
|
+
default:
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<SelectableCard padding={4} label={block.label} isSelected={isSelected} onChange={onSelect}>
|
|
590
|
+
{content}
|
|
591
|
+
</SelectableCard>
|
|
592
|
+
);
|
|
625
593
|
}
|
|
626
594
|
|
|
627
595
|
// ---------------------------------------------------------------------------
|
|
@@ -629,344 +597,343 @@ function BlockPreview({
|
|
|
629
597
|
// ---------------------------------------------------------------------------
|
|
630
598
|
|
|
631
599
|
export default function PageEditor() {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
);
|
|
600
|
+
const [blocks, setBlocks] = useState<Block[]>(DEFAULT_BLOCKS);
|
|
601
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
602
|
+
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("blocks");
|
|
603
|
+
const [pageTitle, setPageTitle] = useState("Page Editor");
|
|
604
|
+
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
|
605
|
+
const [viewport, setViewport] = useState<ViewportSize>("desktop");
|
|
606
|
+
// Mobile only: the customizations (tabs + Add Block/Layers) open in a
|
|
607
|
+
// fullscreen dialog over the preview, since there's no room to dock them.
|
|
608
|
+
const [isCustomizeOpen, setIsCustomizeOpen] = useState(false);
|
|
609
|
+
|
|
610
|
+
// On phones the editor stacks: the panel sits in the header slot (full width,
|
|
611
|
+
// above the canvas) showing just its toolbar, so the canvas isn't crushed
|
|
612
|
+
// beside a 320px sidebar. On desktop the full panel (tabs + lists) shows.
|
|
613
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
614
|
+
|
|
615
|
+
const selectedBlock = blocks.find((b) => b.id === selectedId) ?? null;
|
|
616
|
+
|
|
617
|
+
const updateBlockProp = useCallback((id: string, key: string, value: unknown) => {
|
|
618
|
+
setBlocks((prev) =>
|
|
619
|
+
prev.map((b) => (b.id === id ? { ...b, props: { ...b.props, [key]: value } } : b)),
|
|
620
|
+
);
|
|
621
|
+
}, []);
|
|
622
|
+
|
|
623
|
+
const moveBlock = useCallback((id: string, dir: -1 | 1) => {
|
|
624
|
+
setBlocks((prev) => {
|
|
625
|
+
const idx = prev.findIndex((b) => b.id === id);
|
|
626
|
+
if (idx < 0) {
|
|
627
|
+
return prev;
|
|
628
|
+
}
|
|
629
|
+
const target = idx + dir;
|
|
630
|
+
if (target < 0 || target >= prev.length) {
|
|
631
|
+
return prev;
|
|
632
|
+
}
|
|
633
|
+
const next = [...prev];
|
|
634
|
+
[next[idx], next[target]] = [next[target], next[idx]];
|
|
635
|
+
return next;
|
|
636
|
+
});
|
|
637
|
+
}, []);
|
|
638
|
+
|
|
639
|
+
const deleteBlock = useCallback(
|
|
640
|
+
(id: string) => {
|
|
641
|
+
setBlocks((prev) => prev.filter((b) => b.id !== id));
|
|
642
|
+
if (selectedId === id) {
|
|
643
|
+
setSelectedId(null);
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
[selectedId],
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const addBlock = useCallback((type: BlockType) => {
|
|
650
|
+
const id = crypto.randomUUID();
|
|
651
|
+
const newBlock: Block = {
|
|
652
|
+
id,
|
|
653
|
+
type,
|
|
654
|
+
label: BLOCK_META[type].label,
|
|
655
|
+
props: defaultProps(type),
|
|
656
|
+
};
|
|
657
|
+
setBlocks((prev) => [...prev, newBlock]);
|
|
658
|
+
setSelectedId(id);
|
|
659
|
+
setSidebarTab("properties");
|
|
660
|
+
}, []);
|
|
661
|
+
|
|
662
|
+
const selectBlock = useCallback(
|
|
663
|
+
(id: string) => {
|
|
664
|
+
setSelectedId((prev) => (prev === id ? null : id));
|
|
665
|
+
setSidebarTab("properties");
|
|
666
|
+
// On mobile, tapping a block on the canvas opens its properties dialog.
|
|
667
|
+
if (isMobile) {
|
|
668
|
+
setIsCustomizeOpen(true);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
[isMobile],
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// --- sidebar content ---
|
|
675
|
+
|
|
676
|
+
const blocksTabContent = (
|
|
677
|
+
<VStack gap={2}>
|
|
678
|
+
<VStack gap={1}>
|
|
679
|
+
<Heading level={3}>Add Block</Heading>
|
|
680
|
+
<List density="balanced" hasDividers={false}>
|
|
681
|
+
{(Object.keys(BLOCK_META) as BlockType[]).map((type) => (
|
|
682
|
+
<ListItem
|
|
683
|
+
key={type}
|
|
684
|
+
label={BLOCK_META[type].label}
|
|
685
|
+
startContent={<Icon icon={BLOCK_META[type].icon} color="secondary" />}
|
|
686
|
+
onClick={() => addBlock(type)}
|
|
687
|
+
/>
|
|
688
|
+
))}
|
|
689
|
+
</List>
|
|
690
|
+
</VStack>
|
|
691
|
+
|
|
692
|
+
<VStack gap={1}>
|
|
693
|
+
<Heading level={3}>Layers</Heading>
|
|
694
|
+
<List density="balanced" hasDividers={false}>
|
|
695
|
+
{blocks.map((block) => (
|
|
696
|
+
<ListItem
|
|
697
|
+
key={block.id}
|
|
698
|
+
label={block.label}
|
|
699
|
+
isSelected={block.id === selectedId}
|
|
700
|
+
onClick={() => selectBlock(block.id)}
|
|
701
|
+
startContent={<Icon icon={BLOCK_META[block.type].icon} color="secondary" />}
|
|
702
|
+
endContent={
|
|
703
|
+
<HStack gap={1}>
|
|
704
|
+
<Button
|
|
705
|
+
label="Move up"
|
|
706
|
+
icon={<Icon icon={ChevronUp} size="sm" />}
|
|
707
|
+
variant="ghost"
|
|
708
|
+
size="sm"
|
|
709
|
+
onClick={(e: MouseEvent) => {
|
|
710
|
+
e.stopPropagation();
|
|
711
|
+
moveBlock(block.id, -1);
|
|
712
|
+
}}
|
|
713
|
+
isIconOnly
|
|
714
|
+
/>
|
|
715
|
+
<Button
|
|
716
|
+
label="Move down"
|
|
717
|
+
icon={<Icon icon={ChevronDown} size="sm" />}
|
|
718
|
+
variant="ghost"
|
|
719
|
+
size="sm"
|
|
720
|
+
onClick={(e: MouseEvent) => {
|
|
721
|
+
e.stopPropagation();
|
|
722
|
+
moveBlock(block.id, 1);
|
|
723
|
+
}}
|
|
724
|
+
isIconOnly
|
|
725
|
+
/>
|
|
726
|
+
<Button
|
|
727
|
+
label="Delete"
|
|
728
|
+
icon={<Icon icon={Trash2} size="sm" />}
|
|
729
|
+
variant="ghost"
|
|
730
|
+
size="sm"
|
|
731
|
+
onClick={(e: MouseEvent) => {
|
|
732
|
+
e.stopPropagation();
|
|
733
|
+
deleteBlock(block.id);
|
|
734
|
+
}}
|
|
735
|
+
isIconOnly
|
|
736
|
+
/>
|
|
737
|
+
</HStack>
|
|
738
|
+
}
|
|
739
|
+
/>
|
|
740
|
+
))}
|
|
741
|
+
</List>
|
|
742
|
+
</VStack>
|
|
743
|
+
</VStack>
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
const propertiesTabContent = selectedBlock ? (
|
|
747
|
+
<PropertiesForm
|
|
748
|
+
block={selectedBlock}
|
|
749
|
+
onUpdate={(key, value) => updateBlockProp(selectedBlock.id, key, value)}
|
|
750
|
+
/>
|
|
751
|
+
) : (
|
|
752
|
+
<EmptyState
|
|
753
|
+
title="No block selected"
|
|
754
|
+
description="Select a block to edit its properties"
|
|
755
|
+
isCompact
|
|
756
|
+
/>
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
// Tabs + the active tab's content. Shown inline in the sidebar on desktop,
|
|
760
|
+
// and inside a fullscreen dialog on mobile.
|
|
761
|
+
const editingContent = (
|
|
762
|
+
<VStack gap={4}>
|
|
763
|
+
<VStack gap={0}>
|
|
764
|
+
<TabList
|
|
765
|
+
layout="fill"
|
|
766
|
+
value={sidebarTab}
|
|
767
|
+
onChange={(v: string) => setSidebarTab(v as SidebarTab)}
|
|
768
|
+
>
|
|
769
|
+
<Tab value="blocks" label="Blocks" />
|
|
770
|
+
<Tab value="properties" label="Properties" />
|
|
771
|
+
</TabList>
|
|
772
|
+
<Divider />
|
|
773
|
+
</VStack>
|
|
774
|
+
<Section variant="transparent" padding={4}>
|
|
775
|
+
{sidebarTab === "blocks" ? blocksTabContent : propertiesTabContent}
|
|
776
|
+
</Section>
|
|
777
|
+
</VStack>
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const sidebar = (
|
|
781
|
+
<LayoutPanel
|
|
782
|
+
hasDivider={!isMobile}
|
|
783
|
+
padding={0}
|
|
784
|
+
width={isMobile ? "100%" : 320}
|
|
785
|
+
style={panelShrink}
|
|
786
|
+
>
|
|
787
|
+
<VStack gap={4}>
|
|
788
|
+
{/* Panel Header — mobile/desktop branches are exclusive: exactly one h1. */}
|
|
789
|
+
<Section variant="transparent" padding={4}>
|
|
790
|
+
{isMobile ? (
|
|
791
|
+
// Mobile: the title, an Edit button that opens the customizations
|
|
792
|
+
// dialog, and the primary action.
|
|
793
|
+
<HStack gap={3} vAlign="center" hAlign="between">
|
|
794
|
+
<Heading level={1} type="display-2">
|
|
795
|
+
{pageTitle}
|
|
796
|
+
</Heading>
|
|
797
|
+
<HStack gap={2} vAlign="center">
|
|
798
|
+
<Button
|
|
799
|
+
label="Edit"
|
|
800
|
+
icon={<Icon icon={SlidersHorizontal} size="sm" />}
|
|
801
|
+
variant="ghost"
|
|
802
|
+
isIconOnly
|
|
803
|
+
onClick={() => setIsCustomizeOpen(true)}
|
|
804
|
+
/>
|
|
805
|
+
<Button label="Publish" variant="primary" />
|
|
806
|
+
</HStack>
|
|
807
|
+
</HStack>
|
|
808
|
+
) : (
|
|
809
|
+
<VStack gap={4}>
|
|
810
|
+
{isEditingTitle ? (
|
|
811
|
+
<TextInput
|
|
812
|
+
label="Page title"
|
|
813
|
+
isLabelHidden
|
|
814
|
+
value={pageTitle}
|
|
815
|
+
onChange={setPageTitle}
|
|
816
|
+
onKeyDown={(e: KeyboardEvent) => {
|
|
817
|
+
if (e.key === "Enter") {
|
|
818
|
+
setIsEditingTitle(false);
|
|
819
|
+
}
|
|
820
|
+
}}
|
|
821
|
+
hasAutoFocus
|
|
822
|
+
onBlur={() => setIsEditingTitle(false)}
|
|
823
|
+
/>
|
|
824
|
+
) : (
|
|
825
|
+
<Heading level={1} type="display-2">
|
|
826
|
+
{pageTitle}
|
|
827
|
+
</Heading>
|
|
828
|
+
)}
|
|
829
|
+
|
|
830
|
+
<Toolbar
|
|
831
|
+
label="Viewport and actions"
|
|
832
|
+
startContent={
|
|
833
|
+
<SegmentedControl
|
|
834
|
+
label="Viewport size"
|
|
835
|
+
value={viewport}
|
|
836
|
+
onChange={(v: string) => setViewport(v as ViewportSize)}
|
|
837
|
+
>
|
|
838
|
+
<SegmentedControlItem
|
|
839
|
+
value="desktop"
|
|
840
|
+
label="Desktop"
|
|
841
|
+
icon={<Icon icon={Monitor} size="sm" />}
|
|
842
|
+
isLabelHidden
|
|
843
|
+
/>
|
|
844
|
+
<SegmentedControlItem
|
|
845
|
+
value="tablet"
|
|
846
|
+
label="Tablet"
|
|
847
|
+
icon={<Icon icon={Tablet} size="sm" />}
|
|
848
|
+
isLabelHidden
|
|
849
|
+
/>
|
|
850
|
+
<SegmentedControlItem
|
|
851
|
+
value="phone"
|
|
852
|
+
label="Phone"
|
|
853
|
+
icon={<Icon icon={Smartphone} size="sm" />}
|
|
854
|
+
isLabelHidden
|
|
855
|
+
/>
|
|
856
|
+
</SegmentedControl>
|
|
857
|
+
}
|
|
858
|
+
endContent={
|
|
859
|
+
<HStack gap={2}>
|
|
860
|
+
<Button
|
|
861
|
+
label="Preview"
|
|
862
|
+
icon={<Icon icon={Eye} size="sm" />}
|
|
863
|
+
variant="ghost"
|
|
864
|
+
isIconOnly
|
|
865
|
+
/>
|
|
866
|
+
<Button label="Publish" variant="primary" />
|
|
867
|
+
</HStack>
|
|
868
|
+
}
|
|
869
|
+
/>
|
|
870
|
+
</VStack>
|
|
871
|
+
)}
|
|
872
|
+
</Section>
|
|
873
|
+
|
|
874
|
+
{!isMobile && editingContent}
|
|
875
|
+
</VStack>
|
|
876
|
+
</LayoutPanel>
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
return (
|
|
880
|
+
<>
|
|
881
|
+
<Layout
|
|
882
|
+
style={pageStyle}
|
|
883
|
+
height="fill"
|
|
884
|
+
header={isMobile ? sidebar : undefined}
|
|
885
|
+
start={isMobile ? undefined : sidebar}
|
|
886
|
+
content={
|
|
887
|
+
<LayoutContent padding={8}>
|
|
888
|
+
<VStack gap={4} style={canvasStyle(VIEWPORT_MAX[viewport])}>
|
|
889
|
+
{blocks.length > 0 ? (
|
|
890
|
+
blocks.map((block) => (
|
|
891
|
+
<BlockPreview
|
|
892
|
+
key={block.id}
|
|
893
|
+
block={block}
|
|
894
|
+
isSelected={block.id === selectedId}
|
|
895
|
+
onSelect={() => selectBlock(block.id)}
|
|
896
|
+
/>
|
|
897
|
+
))
|
|
898
|
+
) : (
|
|
899
|
+
<EmptyState
|
|
900
|
+
title="No blocks yet"
|
|
901
|
+
description="Add blocks from the sidebar to start building your page"
|
|
902
|
+
icon={<Icon icon={CirclePlus} color="secondary" />}
|
|
903
|
+
/>
|
|
904
|
+
)}
|
|
905
|
+
</VStack>
|
|
906
|
+
</LayoutContent>
|
|
907
|
+
}
|
|
908
|
+
/>
|
|
909
|
+
|
|
910
|
+
{/* Mobile: customizations open in a fullscreen dialog over the preview. */}
|
|
911
|
+
<Dialog
|
|
912
|
+
isOpen={isMobile && isCustomizeOpen}
|
|
913
|
+
onOpenChange={setIsCustomizeOpen}
|
|
914
|
+
variant="fullscreen"
|
|
915
|
+
purpose="info"
|
|
916
|
+
padding={0}
|
|
917
|
+
>
|
|
918
|
+
<Layout
|
|
919
|
+
height="fill"
|
|
920
|
+
header={
|
|
921
|
+
<LayoutHeader hasDivider>
|
|
922
|
+
<HStack gap={3} vAlign="center" hAlign="between">
|
|
923
|
+
<Heading level={3}>Customize</Heading>
|
|
924
|
+
<Button
|
|
925
|
+
label="Close"
|
|
926
|
+
icon={<Icon icon={X} size="sm" />}
|
|
927
|
+
variant="ghost"
|
|
928
|
+
isIconOnly
|
|
929
|
+
onClick={() => setIsCustomizeOpen(false)}
|
|
930
|
+
/>
|
|
931
|
+
</HStack>
|
|
932
|
+
</LayoutHeader>
|
|
933
|
+
}
|
|
934
|
+
content={<LayoutContent padding={0}>{editingContent}</LayoutContent>}
|
|
935
|
+
/>
|
|
936
|
+
</Dialog>
|
|
937
|
+
</>
|
|
938
|
+
);
|
|
972
939
|
}
|