@voila.dev/ui-email-block-editor 1.1.9 → 1.1.10
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/package.json +1 -1
- package/src/blocks/article-block.tsx +15 -15
- package/src/blocks/block-definitions.tsx +1 -1
- package/src/blocks/button-block.tsx +10 -10
- package/src/blocks/divider-block.tsx +1 -1
- package/src/blocks/grid-block.tsx +6 -6
- package/src/blocks/heading-block.tsx +7 -7
- package/src/blocks/image-block.tsx +18 -18
- package/src/blocks/list-block.tsx +14 -15
- package/src/blocks/offer-block.tsx +26 -26
- package/src/blocks/paragraph-block.tsx +4 -4
- package/src/blocks/product-block.tsx +19 -19
- package/src/blocks/rating-block.tsx +14 -14
- package/src/blocks/stat-block.tsx +10 -10
- package/src/blocks/table-block.tsx +15 -15
- package/src/document/types.ts +2 -2
- package/src/email-block-editor.tsx +3 -3
- package/src/lib/use-media-query.ts +1 -1
- package/src/sections/add-block-menu.tsx +2 -2
- package/src/sections/block-options/alignment-option.tsx +5 -5
- package/src/sections/block-options/block-option-row.tsx +3 -3
- package/src/sections/block-options/money-option.tsx +1 -1
- package/src/sections/block-options/text-option.tsx +2 -2
- package/src/sections/block-toolbar.tsx +12 -12
- package/src/sections/editor-canvas.tsx +3 -3
- package/src/sections/editor-sidebar.tsx +3 -3
- package/src/sections/link-popover.tsx +5 -5
- package/src/sections/preview-toggle.tsx +2 -2
- package/src/theme.ts +3 -3
|
@@ -21,7 +21,7 @@ import { EMAIL_COLOR } from "#/theme.ts";
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* A catalogue item on the shared card shell: visual, name, description, price
|
|
24
|
-
* with an optional struck-through
|
|
24
|
+
* with an optional struck-through base price, and a call to action.
|
|
25
25
|
*/
|
|
26
26
|
function ProductBlockView({
|
|
27
27
|
block,
|
|
@@ -30,17 +30,17 @@ function ProductBlockView({
|
|
|
30
30
|
return (
|
|
31
31
|
<EmailCardShell image={block.image}>
|
|
32
32
|
<BlockTextInput
|
|
33
|
-
ariaLabel="
|
|
33
|
+
ariaLabel="Product name"
|
|
34
34
|
value={block.name}
|
|
35
|
-
placeholder="
|
|
35
|
+
placeholder="Product name"
|
|
36
36
|
onChange={(name) => onChange({ ...block, name })}
|
|
37
37
|
className="font-bold text-[17px] leading-[1.3]"
|
|
38
38
|
style={{ color: EMAIL_COLOR.brand }}
|
|
39
39
|
/>
|
|
40
40
|
<textarea
|
|
41
|
-
aria-label="
|
|
41
|
+
aria-label="Product description"
|
|
42
42
|
value={block.description}
|
|
43
|
-
placeholder="
|
|
43
|
+
placeholder="The product description."
|
|
44
44
|
rows={2}
|
|
45
45
|
onChange={(event) =>
|
|
46
46
|
onChange({ ...block, description: event.target.value })
|
|
@@ -75,9 +75,9 @@ function ProductBlockSettings({
|
|
|
75
75
|
}: EmailBlockComponentProps<EmailEditorProductBlock>) {
|
|
76
76
|
return (
|
|
77
77
|
<>
|
|
78
|
-
<BlockOptionSection title="
|
|
78
|
+
<BlockOptionSection title="Content">
|
|
79
79
|
<TextOption
|
|
80
|
-
label="
|
|
80
|
+
label="Name"
|
|
81
81
|
value={block.name}
|
|
82
82
|
onChange={(name) => onChange({ ...block, name })}
|
|
83
83
|
/>
|
|
@@ -87,12 +87,12 @@ function ProductBlockSettings({
|
|
|
87
87
|
onChange={(description) => onChange({ ...block, description })}
|
|
88
88
|
/>
|
|
89
89
|
<MoneyOption
|
|
90
|
-
label="
|
|
90
|
+
label="Price"
|
|
91
91
|
value={block.price}
|
|
92
92
|
onChange={(price) => onChange({ ...block, price })}
|
|
93
93
|
/>
|
|
94
94
|
<ToggleOption
|
|
95
|
-
label="
|
|
95
|
+
label="Struck-through base price"
|
|
96
96
|
checked={block.compareAtPrice !== null}
|
|
97
97
|
onChange={(enabled) =>
|
|
98
98
|
onChange({
|
|
@@ -100,11 +100,11 @@ function ProductBlockSettings({
|
|
|
100
100
|
compareAtPrice: enabled ? { ...block.price } : null,
|
|
101
101
|
})
|
|
102
102
|
}
|
|
103
|
-
description="
|
|
103
|
+
description="Shows a struck-through price next to the current price."
|
|
104
104
|
/>
|
|
105
105
|
{block.compareAtPrice === null ? null : (
|
|
106
106
|
<MoneyOption
|
|
107
|
-
label="
|
|
107
|
+
label="Base price"
|
|
108
108
|
value={block.compareAtPrice}
|
|
109
109
|
onChange={(compareAtPrice) =>
|
|
110
110
|
onChange({ ...block, compareAtPrice })
|
|
@@ -112,9 +112,9 @@ function ProductBlockSettings({
|
|
|
112
112
|
/>
|
|
113
113
|
)}
|
|
114
114
|
</BlockOptionSection>
|
|
115
|
-
<BlockOptionSection title="
|
|
115
|
+
<BlockOptionSection title="Appearance">
|
|
116
116
|
<TextOption
|
|
117
|
-
label="
|
|
117
|
+
label="Image URL"
|
|
118
118
|
value={block.image.src}
|
|
119
119
|
onChange={(src) =>
|
|
120
120
|
onChange({ ...block, image: { ...block.image, src } })
|
|
@@ -122,20 +122,20 @@ function ProductBlockSettings({
|
|
|
122
122
|
placeholder="https://"
|
|
123
123
|
/>
|
|
124
124
|
<TextOption
|
|
125
|
-
label="
|
|
125
|
+
label="Alt text"
|
|
126
126
|
value={block.image.alt}
|
|
127
127
|
onChange={(alt) =>
|
|
128
128
|
onChange({ ...block, image: { ...block.image, alt } })
|
|
129
129
|
}
|
|
130
130
|
/>
|
|
131
131
|
</BlockOptionSection>
|
|
132
|
-
<BlockOptionSection title="
|
|
132
|
+
<BlockOptionSection title="Link">
|
|
133
133
|
<TextOption
|
|
134
|
-
label="
|
|
134
|
+
label="Button label"
|
|
135
135
|
value={block.buttonLabel}
|
|
136
136
|
onChange={(buttonLabel) => onChange({ ...block, buttonLabel })}
|
|
137
|
-
placeholder="
|
|
138
|
-
description="
|
|
137
|
+
placeholder="Order now"
|
|
138
|
+
description="Leave empty for a card without a button."
|
|
139
139
|
/>
|
|
140
140
|
<LinkOption
|
|
141
141
|
value={block.href}
|
|
@@ -148,7 +148,7 @@ function ProductBlockSettings({
|
|
|
148
148
|
|
|
149
149
|
export const productBlockDefinition: EmailBlockDefinition<EmailEditorProductBlock> =
|
|
150
150
|
{
|
|
151
|
-
label: "
|
|
151
|
+
label: "Product",
|
|
152
152
|
icon: TagIcon,
|
|
153
153
|
View: ProductBlockView,
|
|
154
154
|
Settings: ProductBlockSettings,
|
|
@@ -23,8 +23,8 @@ const STYLE_OPTIONS: ReadonlyArray<{
|
|
|
23
23
|
readonly value: EmailEditorRatingStyle;
|
|
24
24
|
readonly label: string;
|
|
25
25
|
}> = [
|
|
26
|
-
{ value: "filled", label: "
|
|
27
|
-
{ value: "outline", label: "
|
|
26
|
+
{ value: "filled", label: "Filled stars" },
|
|
27
|
+
{ value: "outline", label: "Outlined stars" },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -42,7 +42,7 @@ function RatingBlockView({
|
|
|
42
42
|
spans={block.question}
|
|
43
43
|
onChange={(question) => onChange({ ...block, question })}
|
|
44
44
|
ariaLabel="Question"
|
|
45
|
-
placeholder="
|
|
45
|
+
placeholder="How did your last project go?"
|
|
46
46
|
className="text-center text-[16px] leading-[1.6]"
|
|
47
47
|
style={{ fontFamily: EMAIL_FONT, color: EMAIL_COLOR.ink }}
|
|
48
48
|
/>
|
|
@@ -75,25 +75,25 @@ function RatingBlockSettings({
|
|
|
75
75
|
}: EmailBlockComponentProps<EmailEditorRatingBlock>) {
|
|
76
76
|
return (
|
|
77
77
|
<>
|
|
78
|
-
<BlockOptionSection title="
|
|
78
|
+
<BlockOptionSection title="Content">
|
|
79
79
|
<p className="text-muted-foreground text-xs">
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
The question is typed and formatted directly on the block, like a
|
|
81
|
+
paragraph.
|
|
82
82
|
</p>
|
|
83
83
|
<TextOption
|
|
84
|
-
label="
|
|
84
|
+
label="Low end of the scale"
|
|
85
85
|
value={block.lowLabel}
|
|
86
86
|
onChange={(lowLabel) => onChange({ ...block, lowLabel })}
|
|
87
|
-
placeholder="
|
|
87
|
+
placeholder="Not at all"
|
|
88
88
|
/>
|
|
89
89
|
<TextOption
|
|
90
|
-
label="
|
|
90
|
+
label="High end of the scale"
|
|
91
91
|
value={block.highLabel}
|
|
92
92
|
onChange={(highLabel) => onChange({ ...block, highLabel })}
|
|
93
|
-
placeholder="
|
|
93
|
+
placeholder="Absolutely"
|
|
94
94
|
/>
|
|
95
95
|
</BlockOptionSection>
|
|
96
|
-
<BlockOptionSection title="
|
|
96
|
+
<BlockOptionSection title="Appearance">
|
|
97
97
|
<SelectOption
|
|
98
98
|
label="Style"
|
|
99
99
|
value={block.style}
|
|
@@ -101,11 +101,11 @@ function RatingBlockSettings({
|
|
|
101
101
|
onChange={(style) => onChange({ ...block, style })}
|
|
102
102
|
/>
|
|
103
103
|
</BlockOptionSection>
|
|
104
|
-
<BlockOptionSection title="
|
|
104
|
+
<BlockOptionSection title="Link">
|
|
105
105
|
<LinkOption
|
|
106
106
|
value={block.href}
|
|
107
107
|
onChange={(href) => onChange({ ...block, href })}
|
|
108
|
-
description="
|
|
108
|
+
description="Each star points to this address with rating=1 through rating=5 appended: the five scores are therefore counted separately in the click statistics."
|
|
109
109
|
/>
|
|
110
110
|
</BlockOptionSection>
|
|
111
111
|
</>
|
|
@@ -114,7 +114,7 @@ function RatingBlockSettings({
|
|
|
114
114
|
|
|
115
115
|
export const ratingBlockDefinition: EmailBlockDefinition<EmailEditorRatingBlock> =
|
|
116
116
|
{
|
|
117
|
-
label: "
|
|
117
|
+
label: "Rating",
|
|
118
118
|
icon: StarIcon,
|
|
119
119
|
View: RatingBlockView,
|
|
120
120
|
Settings: RatingBlockSettings,
|
|
@@ -37,7 +37,7 @@ function StatBlockView({
|
|
|
37
37
|
style={{ textAlign, fontFamily: EMAIL_FONT }}
|
|
38
38
|
>
|
|
39
39
|
<BlockTextInput
|
|
40
|
-
ariaLabel="
|
|
40
|
+
ariaLabel="Value"
|
|
41
41
|
value={block.value}
|
|
42
42
|
placeholder="128"
|
|
43
43
|
onChange={(value) => onChange({ ...block, value })}
|
|
@@ -45,9 +45,9 @@ function StatBlockView({
|
|
|
45
45
|
style={{ color: EMAIL_COLOR.brand, textAlign }}
|
|
46
46
|
/>
|
|
47
47
|
<BlockTextInput
|
|
48
|
-
ariaLabel="
|
|
48
|
+
ariaLabel="Label"
|
|
49
49
|
value={block.label}
|
|
50
|
-
placeholder="
|
|
50
|
+
placeholder="Projects delivered"
|
|
51
51
|
onChange={(label) => onChange({ ...block, label })}
|
|
52
52
|
className="font-semibold text-[12px] uppercase leading-[1.4] tracking-[0.04em]"
|
|
53
53
|
style={{ color: EMAIL_COLOR.muted, textAlign }}
|
|
@@ -59,7 +59,7 @@ function StatBlockView({
|
|
|
59
59
|
<BlockTextInput
|
|
60
60
|
ariaLabel="Description"
|
|
61
61
|
value={block.description}
|
|
62
|
-
placeholder="Description (
|
|
62
|
+
placeholder="Description (optional)"
|
|
63
63
|
onChange={(description) => onChange({ ...block, description })}
|
|
64
64
|
className="text-[14px] leading-[1.5]"
|
|
65
65
|
style={{ color: EMAIL_COLOR.ink, textAlign }}
|
|
@@ -75,18 +75,18 @@ function StatBlockSettings({
|
|
|
75
75
|
}: EmailBlockComponentProps<EmailEditorStatBlock>) {
|
|
76
76
|
return (
|
|
77
77
|
<>
|
|
78
|
-
<BlockOptionSection title="
|
|
78
|
+
<BlockOptionSection title="Content">
|
|
79
79
|
<TextOption
|
|
80
|
-
label="
|
|
80
|
+
label="Value"
|
|
81
81
|
value={block.value}
|
|
82
82
|
onChange={(value) => onChange({ ...block, value })}
|
|
83
83
|
placeholder="128"
|
|
84
84
|
/>
|
|
85
85
|
<TextOption
|
|
86
|
-
label="
|
|
86
|
+
label="Label"
|
|
87
87
|
value={block.label}
|
|
88
88
|
onChange={(label) => onChange({ ...block, label })}
|
|
89
|
-
placeholder="
|
|
89
|
+
placeholder="Projects delivered"
|
|
90
90
|
/>
|
|
91
91
|
<TextAreaOption
|
|
92
92
|
label="Description"
|
|
@@ -95,7 +95,7 @@ function StatBlockSettings({
|
|
|
95
95
|
rows={2}
|
|
96
96
|
/>
|
|
97
97
|
</BlockOptionSection>
|
|
98
|
-
<BlockOptionSection title="
|
|
98
|
+
<BlockOptionSection title="Appearance">
|
|
99
99
|
<AlignmentOption
|
|
100
100
|
value={block.align}
|
|
101
101
|
onChange={(align) => onChange({ ...block, align })}
|
|
@@ -106,7 +106,7 @@ function StatBlockSettings({
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export const statBlockDefinition: EmailBlockDefinition<EmailEditorStatBlock> = {
|
|
109
|
-
label: "
|
|
109
|
+
label: "Key figure",
|
|
110
110
|
icon: ChartBarIcon,
|
|
111
111
|
View: StatBlockView,
|
|
112
112
|
Settings: StatBlockSettings,
|
|
@@ -21,8 +21,8 @@ const ALIGN_OPTIONS: ReadonlyArray<{
|
|
|
21
21
|
readonly value: EmailEditorTableColumn["align"];
|
|
22
22
|
readonly label: string;
|
|
23
23
|
}> = [
|
|
24
|
-
{ value: "left", label: "
|
|
25
|
-
{ value: "right", label: "
|
|
24
|
+
{ value: "left", label: "Left" },
|
|
25
|
+
{ value: "right", label: "Right" },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
/** Mirrors `emailLineItemsTable`'s `flushEdges`: the first and last columns
|
|
@@ -84,9 +84,9 @@ function TableBlockView({
|
|
|
84
84
|
}}
|
|
85
85
|
>
|
|
86
86
|
<BlockTextInput
|
|
87
|
-
ariaLabel={`
|
|
87
|
+
ariaLabel={`Column ${columnIndex + 1} title`}
|
|
88
88
|
value={column.label}
|
|
89
|
-
placeholder="
|
|
89
|
+
placeholder="Column"
|
|
90
90
|
onChange={(label) =>
|
|
91
91
|
onChange({
|
|
92
92
|
...block,
|
|
@@ -116,7 +116,7 @@ function TableBlockView({
|
|
|
116
116
|
style={{ borderBottom: `1px solid ${EMAIL_COLOR.border}` }}
|
|
117
117
|
>
|
|
118
118
|
<BlockTextInput
|
|
119
|
-
ariaLabel={`
|
|
119
|
+
ariaLabel={`Row ${rowIndex + 1}, column ${columnIndex + 1}`}
|
|
120
120
|
value={cell}
|
|
121
121
|
onChange={(value) => setCell(rowIndex, columnIndex, value)}
|
|
122
122
|
className="text-[14px] leading-[1.4]"
|
|
@@ -147,7 +147,7 @@ function TableColumnSettings({
|
|
|
147
147
|
<div key={index} className="flex items-end gap-2">
|
|
148
148
|
<div className="flex-1">
|
|
149
149
|
<SelectOption
|
|
150
|
-
label={column.label.trim() || `
|
|
150
|
+
label={column.label.trim() || `Column ${index + 1}`}
|
|
151
151
|
value={column.align}
|
|
152
152
|
options={ALIGN_OPTIONS}
|
|
153
153
|
onChange={(align) =>
|
|
@@ -163,7 +163,7 @@ function TableColumnSettings({
|
|
|
163
163
|
<Button
|
|
164
164
|
variant="ghost"
|
|
165
165
|
size="icon-sm"
|
|
166
|
-
aria-label={`
|
|
166
|
+
aria-label={`Remove column ${index + 1}`}
|
|
167
167
|
disabled={block.columns.length === 1}
|
|
168
168
|
onClick={() =>
|
|
169
169
|
onChange({
|
|
@@ -191,7 +191,7 @@ function TableColumnSettings({
|
|
|
191
191
|
}
|
|
192
192
|
>
|
|
193
193
|
<PlusIcon aria-hidden />
|
|
194
|
-
|
|
194
|
+
Add a column
|
|
195
195
|
</Button>
|
|
196
196
|
</>
|
|
197
197
|
);
|
|
@@ -203,16 +203,16 @@ function TableBlockSettings({
|
|
|
203
203
|
}: EmailBlockComponentProps<EmailEditorTableBlock>) {
|
|
204
204
|
return (
|
|
205
205
|
<>
|
|
206
|
-
<BlockOptionSection title="
|
|
206
|
+
<BlockOptionSection title="Content">
|
|
207
207
|
<div className="flex items-center justify-between gap-2 text-sm">
|
|
208
208
|
<span className="text-muted-foreground">
|
|
209
|
-
{block.rows.length}
|
|
209
|
+
{block.rows.length} row{block.rows.length > 1 ? "s" : ""}
|
|
210
210
|
</span>
|
|
211
211
|
<div className="flex gap-1">
|
|
212
212
|
<Button
|
|
213
213
|
variant="ghost"
|
|
214
214
|
size="icon-sm"
|
|
215
|
-
aria-label="
|
|
215
|
+
aria-label="Remove the last row"
|
|
216
216
|
disabled={block.rows.length === 1}
|
|
217
217
|
onClick={() =>
|
|
218
218
|
onChange({ ...block, rows: block.rows.slice(0, -1) })
|
|
@@ -231,14 +231,14 @@ function TableBlockSettings({
|
|
|
231
231
|
}
|
|
232
232
|
>
|
|
233
233
|
<PlusIcon aria-hidden />
|
|
234
|
-
|
|
234
|
+
Row
|
|
235
235
|
</Button>
|
|
236
236
|
</div>
|
|
237
237
|
</div>
|
|
238
238
|
</BlockOptionSection>
|
|
239
|
-
<BlockOptionSection title="
|
|
239
|
+
<BlockOptionSection title="Appearance">
|
|
240
240
|
<ToggleOption
|
|
241
|
-
label="
|
|
241
|
+
label="Header row"
|
|
242
242
|
checked={block.headerRow}
|
|
243
243
|
onChange={(headerRow) => onChange({ ...block, headerRow })}
|
|
244
244
|
/>
|
|
@@ -250,7 +250,7 @@ function TableBlockSettings({
|
|
|
250
250
|
|
|
251
251
|
export const tableBlockDefinition: EmailBlockDefinition<EmailEditorTableBlock> =
|
|
252
252
|
{
|
|
253
|
-
label: "
|
|
253
|
+
label: "Table",
|
|
254
254
|
icon: TableIcon,
|
|
255
255
|
View: TableBlockView,
|
|
256
256
|
Settings: TableBlockSettings,
|
package/src/document/types.ts
CHANGED
|
@@ -190,7 +190,7 @@ export interface EmailEditorProductBlock {
|
|
|
190
190
|
readonly description: string;
|
|
191
191
|
readonly image: EmailEditorCardImage;
|
|
192
192
|
readonly price: EmailEditorMoney;
|
|
193
|
-
/** The struck-through
|
|
193
|
+
/** The struck-through base price, mirroring the shop catalogue's
|
|
194
194
|
* `publicPriceHtCents`. Null when the product is not discounted. */
|
|
195
195
|
readonly compareAtPrice: EmailEditorMoney | null;
|
|
196
196
|
readonly href: string;
|
|
@@ -205,7 +205,7 @@ export interface EmailEditorOfferBlock {
|
|
|
205
205
|
readonly description: string;
|
|
206
206
|
readonly image: EmailEditorCardImage;
|
|
207
207
|
readonly price: EmailEditorMoney;
|
|
208
|
-
/**
|
|
208
|
+
/** "per month", "per year"; empty for a one-off price. */
|
|
209
209
|
readonly period: string;
|
|
210
210
|
readonly features: ReadonlyArray<string>;
|
|
211
211
|
readonly buttonLabel: string;
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "#/sections/editor-sidebar.tsx";
|
|
24
24
|
import { PreviewToggle } from "#/sections/preview-toggle.tsx";
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/** The block settings panel as a bottom sheet, for the viewports where a 280px
|
|
27
27
|
* column would put the options a screenful away from their block. */
|
|
28
28
|
function BlockSettingsSheet({
|
|
29
29
|
open,
|
|
@@ -42,9 +42,9 @@ function BlockSettingsSheet({
|
|
|
42
42
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
43
43
|
<DrawerContent>
|
|
44
44
|
<DrawerHeader>
|
|
45
|
-
<DrawerTitle>
|
|
45
|
+
<DrawerTitle>Block settings</DrawerTitle>
|
|
46
46
|
<DrawerDescription className="sr-only">
|
|
47
|
-
Options
|
|
47
|
+
Options for the selected block.
|
|
48
48
|
</DrawerDescription>
|
|
49
49
|
</DrawerHeader>
|
|
50
50
|
<div className="flex flex-col gap-4 overflow-y-auto px-4 pb-8">
|
|
@@ -31,7 +31,7 @@ export const useCoarsePointer = (): boolean =>
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Below Tailwind's `lg`, the 280px settings column no longer fits beside the
|
|
34
|
-
* canvas, so
|
|
34
|
+
* canvas, so the block settings panel moves into a bottom sheet reachable from the
|
|
35
35
|
* selected block's toolbar.
|
|
36
36
|
*/
|
|
37
37
|
export const useCompactEditorLayout = (): boolean =>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type { EmailEditorBlockType } from "#/document/types.ts";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The
|
|
17
|
+
* The "Add a block" menu, fed by the block registry. The default trigger
|
|
18
18
|
* is the outlined empty-state button; the block toolbar passes its own icon
|
|
19
19
|
* trigger instead.
|
|
20
20
|
*/
|
|
@@ -35,7 +35,7 @@ export function AddBlockMenu({
|
|
|
35
35
|
trigger ?? (
|
|
36
36
|
<Button variant="outline" size="sm">
|
|
37
37
|
<PlusIcon aria-hidden />
|
|
38
|
-
|
|
38
|
+
Add a block
|
|
39
39
|
</Button>
|
|
40
40
|
)
|
|
41
41
|
}
|
|
@@ -13,28 +13,28 @@ const ALIGNMENTS: ReadonlyArray<{
|
|
|
13
13
|
}> = [
|
|
14
14
|
{
|
|
15
15
|
value: "left",
|
|
16
|
-
label: "
|
|
16
|
+
label: "Align left",
|
|
17
17
|
icon: <TextAlignLeftIcon aria-hidden />,
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
value: "center",
|
|
21
|
-
label: "
|
|
21
|
+
label: "Align center",
|
|
22
22
|
icon: <TextAlignCenterIcon aria-hidden />,
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
value: "right",
|
|
26
|
-
label: "
|
|
26
|
+
label: "Align right",
|
|
27
27
|
icon: <TextAlignRightIcon aria-hidden />,
|
|
28
28
|
},
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The one alignment control. Every block that can be aligned uses this exact
|
|
33
|
-
* segmented control, so
|
|
33
|
+
* segmented control, so "Alignment" means the same thing and looks the same
|
|
34
34
|
* everywhere (§1.2 of the editor plan).
|
|
35
35
|
*/
|
|
36
36
|
export function AlignmentOption({
|
|
37
|
-
label = "
|
|
37
|
+
label = "Alignment",
|
|
38
38
|
value,
|
|
39
39
|
onChange,
|
|
40
40
|
}: {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* One labelled option in
|
|
10
|
+
* One labelled option in the block settings panel. Every block setting goes
|
|
11
11
|
* through this row rather than hand-rolling `useId()` + `Label` + control, so
|
|
12
12
|
* spacing, label typography and description placement cannot drift from block
|
|
13
13
|
* to block.
|
|
@@ -58,8 +58,8 @@ export function BlockOptionRow({
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* A titled group of rows. §1.4 of the editor plan: once a block carries more
|
|
61
|
-
* than a handful of options they are split into
|
|
62
|
-
*
|
|
61
|
+
* than a handful of options they are split into "Content", "Appearance" and
|
|
62
|
+
* "Link", always in that order.
|
|
63
63
|
*/
|
|
64
64
|
export function BlockOptionSection({
|
|
65
65
|
title,
|
|
@@ -61,12 +61,12 @@ export function TextAreaOption({
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* The one destination-URL control. Always
|
|
64
|
+
* The one destination-URL control. Always "Link (URL)", always
|
|
65
65
|
* `type="url"` with the `https://` placeholder, so a link field is
|
|
66
66
|
* recognisable at a glance in any block (§1.2 of the editor plan).
|
|
67
67
|
*/
|
|
68
68
|
export function LinkOption({
|
|
69
|
-
label = "
|
|
69
|
+
label = "Link (URL)",
|
|
70
70
|
value,
|
|
71
71
|
onChange,
|
|
72
72
|
description,
|
|
@@ -141,7 +141,7 @@ function StructureControls({
|
|
|
141
141
|
<Button
|
|
142
142
|
variant="ghost"
|
|
143
143
|
size={coarsePointer ? "icon" : "icon-sm"}
|
|
144
|
-
aria-label="
|
|
144
|
+
aria-label="Add a block"
|
|
145
145
|
className={toolbarButtonClassName(coarsePointer)}
|
|
146
146
|
>
|
|
147
147
|
<PlusIcon aria-hidden />
|
|
@@ -151,7 +151,7 @@ function StructureControls({
|
|
|
151
151
|
<Button
|
|
152
152
|
variant="ghost"
|
|
153
153
|
size={coarsePointer ? "icon" : "icon-sm"}
|
|
154
|
-
aria-label="
|
|
154
|
+
aria-label="Move block"
|
|
155
155
|
className={cn(
|
|
156
156
|
toolbarButtonClassName(coarsePointer),
|
|
157
157
|
"cursor-grab touch-none active:cursor-grabbing",
|
|
@@ -164,7 +164,7 @@ function StructureControls({
|
|
|
164
164
|
</Button>
|
|
165
165
|
{onSelectContainer ? (
|
|
166
166
|
<ToolbarIconButton
|
|
167
|
-
label="
|
|
167
|
+
label="Select the column row"
|
|
168
168
|
coarsePointer={coarsePointer}
|
|
169
169
|
onClick={onSelectContainer}
|
|
170
170
|
>
|
|
@@ -190,7 +190,7 @@ function RichTextControls({
|
|
|
190
190
|
<>
|
|
191
191
|
<ToolbarSeparator />
|
|
192
192
|
<ToolbarIconButton
|
|
193
|
-
label="
|
|
193
|
+
label="Bold"
|
|
194
194
|
active={active.has("bold")}
|
|
195
195
|
coarsePointer={coarsePointer}
|
|
196
196
|
onMouseDown={keepSelection}
|
|
@@ -199,7 +199,7 @@ function RichTextControls({
|
|
|
199
199
|
<TextBIcon aria-hidden />
|
|
200
200
|
</ToolbarIconButton>
|
|
201
201
|
<ToolbarIconButton
|
|
202
|
-
label="
|
|
202
|
+
label="Italic"
|
|
203
203
|
active={active.has("italic")}
|
|
204
204
|
coarsePointer={coarsePointer}
|
|
205
205
|
onMouseDown={keepSelection}
|
|
@@ -208,7 +208,7 @@ function RichTextControls({
|
|
|
208
208
|
<TextItalicIcon aria-hidden />
|
|
209
209
|
</ToolbarIconButton>
|
|
210
210
|
<ToolbarIconButton
|
|
211
|
-
label="
|
|
211
|
+
label="Underline"
|
|
212
212
|
active={active.has("underline")}
|
|
213
213
|
coarsePointer={coarsePointer}
|
|
214
214
|
onMouseDown={keepSelection}
|
|
@@ -253,7 +253,7 @@ function SelectionLinkButton({
|
|
|
253
253
|
<Button
|
|
254
254
|
variant="ghost"
|
|
255
255
|
size={coarsePointer ? "icon" : "icon-sm"}
|
|
256
|
-
aria-label="
|
|
256
|
+
aria-label="Insert a link"
|
|
257
257
|
aria-pressed={active || undefined}
|
|
258
258
|
className={toolbarButtonClassName(coarsePointer, active)}
|
|
259
259
|
onMouseDown={keepSelection}
|
|
@@ -295,7 +295,7 @@ function SelectionLinkButton({
|
|
|
295
295
|
/**
|
|
296
296
|
* The controls of the selected block: add-below, drag handle, inline
|
|
297
297
|
* formatting when the block holds rich text, duplicate, delete, plus a
|
|
298
|
-
*
|
|
298
|
+
* Settings button when the settings live in a bottom sheet rather than in
|
|
299
299
|
* the sidebar. App-chrome styling on purpose — the toolbar is editor UI, not
|
|
300
300
|
* part of the email. Only ever rendered for the selected block, so a single
|
|
301
301
|
* toolbar is visible at a time.
|
|
@@ -303,7 +303,7 @@ function SelectionLinkButton({
|
|
|
303
303
|
* At 44px per target the full set is wider than a 390px viewport, so the row
|
|
304
304
|
* wraps under a touch pointer rather than scrolling: it sits in the flow there
|
|
305
305
|
* (see the canvas), so a second line costs nothing, while a scrolling row
|
|
306
|
-
* would hide
|
|
306
|
+
* would hide Delete behind an edge with no affordance.
|
|
307
307
|
*/
|
|
308
308
|
export function BlockToolbar({
|
|
309
309
|
handle,
|
|
@@ -353,7 +353,7 @@ export function BlockToolbar({
|
|
|
353
353
|
<ToolbarSeparator />
|
|
354
354
|
{onOpenSettings ? (
|
|
355
355
|
<ToolbarIconButton
|
|
356
|
-
label="
|
|
356
|
+
label="Block settings"
|
|
357
357
|
coarsePointer={coarsePointer}
|
|
358
358
|
onClick={onOpenSettings}
|
|
359
359
|
>
|
|
@@ -361,14 +361,14 @@ export function BlockToolbar({
|
|
|
361
361
|
</ToolbarIconButton>
|
|
362
362
|
) : null}
|
|
363
363
|
<ToolbarIconButton
|
|
364
|
-
label="
|
|
364
|
+
label="Duplicate block"
|
|
365
365
|
coarsePointer={coarsePointer}
|
|
366
366
|
onClick={onDuplicate}
|
|
367
367
|
>
|
|
368
368
|
<CopyIcon aria-hidden />
|
|
369
369
|
</ToolbarIconButton>
|
|
370
370
|
<ToolbarIconButton
|
|
371
|
-
label="
|
|
371
|
+
label="Delete block"
|
|
372
372
|
coarsePointer={coarsePointer}
|
|
373
373
|
onClick={onRemove}
|
|
374
374
|
>
|
|
@@ -53,7 +53,7 @@ interface CanvasContext {
|
|
|
53
53
|
readonly onOpenSettings?: () => void;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** The dashed
|
|
56
|
+
/** The dashed add slot of a grid cell. */
|
|
57
57
|
function GridAddCell({
|
|
58
58
|
gridId,
|
|
59
59
|
context,
|
|
@@ -325,7 +325,7 @@ export function EditorCanvas({
|
|
|
325
325
|
preview: EmailEditorPreview;
|
|
326
326
|
onUploadImage?: (file: File) => Promise<string>;
|
|
327
327
|
/** Present when the settings live in a sheet rather than in the sidebar;
|
|
328
|
-
* each selected block's toolbar then offers a
|
|
328
|
+
* each selected block's toolbar then offers a Settings button. */
|
|
329
329
|
onOpenSettings?: () => void;
|
|
330
330
|
headerSlot?: ReactNode;
|
|
331
331
|
footerSlot?: ReactNode;
|
|
@@ -387,7 +387,7 @@ export function EditorCanvas({
|
|
|
387
387
|
className="text-[14px]"
|
|
388
388
|
style={{ color: EMAIL_COLOR.muted, fontFamily: EMAIL_FONT }}
|
|
389
389
|
>
|
|
390
|
-
|
|
390
|
+
Your email is empty.
|
|
391
391
|
</p>
|
|
392
392
|
<AddBlockMenu
|
|
393
393
|
onAdd={(type) => dispatch({ type: "add", blockType: type })}
|