@fufulog/brevomorphic-cms-sdk 1.0.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/INTEGRATION.md +101 -0
- package/README.md +336 -0
- package/dist/brevoClient.d.ts +53 -0
- package/dist/brevoClient.js +141 -0
- package/dist/controller.d.ts +46 -0
- package/dist/controller.js +173 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/repository.d.ts +34 -0
- package/dist/repository.js +224 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.js +185 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +1 -0
- package/env.example +25 -0
- package/package.json +35 -0
- package/prd.md +135 -0
- package/react/BrevoWysiwyg.css +233 -0
- package/react/BrevoWysiwyg.tsx +388 -0
- package/src/brevoClient.ts +171 -0
- package/src/controller.ts +186 -0
- package/src/index.ts +5 -0
- package/src/repository.ts +229 -0
- package/src/service.ts +221 -0
- package/src/types.ts +77 -0
- package/svelte/BrevoWysiwyg.svelte +572 -0
- package/tests/sdk.test.ts +239 -0
- package/tsconfig.json +16 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface BrevoSender {
|
|
2
|
+
id?: number;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BrevoTemplate {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
htmlContent?: string;
|
|
14
|
+
sender?: BrevoSender;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LocalMapping {
|
|
18
|
+
id?: number | string;
|
|
19
|
+
template_id: number;
|
|
20
|
+
event_name: string;
|
|
21
|
+
is_active: boolean;
|
|
22
|
+
updated_at?: Date | string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CombinedTemplate {
|
|
26
|
+
templateId: number;
|
|
27
|
+
templateName: string;
|
|
28
|
+
subject: string;
|
|
29
|
+
eventName: string;
|
|
30
|
+
isActive: boolean; // synced from local state & remote
|
|
31
|
+
sender?: BrevoSender;
|
|
32
|
+
htmlContent?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SQLClient {
|
|
36
|
+
query: (sql: string, params: any[]) => Promise<any>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FirestoreClient {
|
|
40
|
+
collection: (collectionPath: string) => any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SDKConfig {
|
|
44
|
+
brevoApiKey: string;
|
|
45
|
+
defaultSender: {
|
|
46
|
+
name?: string;
|
|
47
|
+
email: string;
|
|
48
|
+
};
|
|
49
|
+
dbType: 'postgres' | 'mysql' | 'firestore';
|
|
50
|
+
/**
|
|
51
|
+
* For 'postgres': an object satisfying { query: (sql, params) => Promise<{ rows: any[] }> } or equivalent
|
|
52
|
+
* For 'mysql': an object satisfying { query: (sql, params) => Promise<[any[], any]> } or equivalent
|
|
53
|
+
* For 'firestore': an instance of Firebase Firestore DB
|
|
54
|
+
*/
|
|
55
|
+
dbClient: any;
|
|
56
|
+
/**
|
|
57
|
+
* Table name for postgres/mysql, or Collection path for Firestore.
|
|
58
|
+
* Defaults to 'email_event_templates'
|
|
59
|
+
*/
|
|
60
|
+
tableNameOrCollection?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CreateTemplateInput {
|
|
64
|
+
name?: string;
|
|
65
|
+
subject?: string;
|
|
66
|
+
senderEmail?: string;
|
|
67
|
+
senderName?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface UpdateTemplateInput {
|
|
71
|
+
templateName: string;
|
|
72
|
+
subject: string;
|
|
73
|
+
sender: BrevoSender;
|
|
74
|
+
htmlContent: string;
|
|
75
|
+
eventName: string;
|
|
76
|
+
isActive: boolean;
|
|
77
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
BrevoWysiwyg.svelte — WYSIWYG Email Template Editor
|
|
3
|
+
|
|
4
|
+
Drop-in Svelte component for the brevoCMS SDK.
|
|
5
|
+
Provides rich-text editing with Source toggle, formatting toolbar,
|
|
6
|
+
and Brevo variable injection. Does NOT escape Twig brackets.
|
|
7
|
+
|
|
8
|
+
Props:
|
|
9
|
+
value (string) — HTML content (bind:value for two-way binding)
|
|
10
|
+
variables (array) — [{ label: string, key: string }] for variable injection
|
|
11
|
+
placeholder (string) — Placeholder text when editor is empty
|
|
12
|
+
disabled (boolean) — Disables the editor
|
|
13
|
+
|
|
14
|
+
Events:
|
|
15
|
+
on:change — Fires with { detail: string } containing raw HTML
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
<BrevoWysiwyg
|
|
19
|
+
bind:value={htmlContent}
|
|
20
|
+
variables={[
|
|
21
|
+
{ label: 'Customer Name', key: 'params.name' },
|
|
22
|
+
{ label: 'Ticket Code', key: 'params.ticket_code' }
|
|
23
|
+
]}
|
|
24
|
+
on:change={(e) => console.log(e.detail)}
|
|
25
|
+
/>
|
|
26
|
+
-->
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
import { onMount, createEventDispatcher, tick } from 'svelte';
|
|
30
|
+
|
|
31
|
+
export let value = '';
|
|
32
|
+
export let variables = [];
|
|
33
|
+
export let placeholder = 'Start designing your email template...';
|
|
34
|
+
export let disabled = false;
|
|
35
|
+
|
|
36
|
+
const dispatch = createEventDispatcher();
|
|
37
|
+
|
|
38
|
+
let editorEl;
|
|
39
|
+
let sourceEl;
|
|
40
|
+
let isSourceMode = false;
|
|
41
|
+
let sourceCode = '';
|
|
42
|
+
let showVariableMenu = false;
|
|
43
|
+
let showHeadingMenu = false;
|
|
44
|
+
let isFocused = false;
|
|
45
|
+
|
|
46
|
+
// Toolbar state tracking
|
|
47
|
+
let activeFormats = {
|
|
48
|
+
bold: false,
|
|
49
|
+
italic: false,
|
|
50
|
+
underline: false,
|
|
51
|
+
strikeThrough: false,
|
|
52
|
+
insertOrderedList: false,
|
|
53
|
+
insertUnorderedList: false,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
if (value && editorEl) {
|
|
58
|
+
editorEl.innerHTML = value;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function syncFromEditor() {
|
|
63
|
+
if (!editorEl || isSourceMode) return;
|
|
64
|
+
// Extract raw innerHTML — NO sanitization of { } % characters
|
|
65
|
+
const raw = editorEl.innerHTML;
|
|
66
|
+
// Restore any accidentally encoded Twig brackets
|
|
67
|
+
const cleaned = raw
|
|
68
|
+
.replace(/{/g, '{')
|
|
69
|
+
.replace(/}/g, '}')
|
|
70
|
+
.replace(/{/g, '{')
|
|
71
|
+
.replace(/}/g, '}')
|
|
72
|
+
.replace(/%/g, '%')
|
|
73
|
+
.replace(/%/g, '%');
|
|
74
|
+
value = cleaned;
|
|
75
|
+
dispatch('change', cleaned);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function syncFromSource() {
|
|
79
|
+
sourceCode = sourceEl?.value ?? sourceCode;
|
|
80
|
+
value = sourceCode;
|
|
81
|
+
dispatch('change', value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function toggleSourceMode() {
|
|
85
|
+
if (isSourceMode) {
|
|
86
|
+
// Switching to Visual — load source into editor
|
|
87
|
+
value = sourceCode;
|
|
88
|
+
isSourceMode = false;
|
|
89
|
+
await tick();
|
|
90
|
+
if (editorEl) editorEl.innerHTML = value;
|
|
91
|
+
} else {
|
|
92
|
+
// Switching to Source — serialize editor to textarea
|
|
93
|
+
syncFromEditor();
|
|
94
|
+
sourceCode = value;
|
|
95
|
+
isSourceMode = true;
|
|
96
|
+
}
|
|
97
|
+
showVariableMenu = false;
|
|
98
|
+
showHeadingMenu = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function execCmd(command, val = null) {
|
|
102
|
+
if (isSourceMode || disabled) return;
|
|
103
|
+
editorEl?.focus();
|
|
104
|
+
document.execCommand(command, false, val);
|
|
105
|
+
syncFromEditor();
|
|
106
|
+
updateActiveFormats();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function updateActiveFormats() {
|
|
110
|
+
activeFormats = {
|
|
111
|
+
bold: document.queryCommandState('bold'),
|
|
112
|
+
italic: document.queryCommandState('italic'),
|
|
113
|
+
underline: document.queryCommandState('underline'),
|
|
114
|
+
strikeThrough: document.queryCommandState('strikeThrough'),
|
|
115
|
+
insertOrderedList: document.queryCommandState('insertOrderedList'),
|
|
116
|
+
insertUnorderedList: document.queryCommandState('insertUnorderedList'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function insertHeading(tag) {
|
|
121
|
+
execCmd('formatBlock', tag);
|
|
122
|
+
showHeadingMenu = false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertLink() {
|
|
126
|
+
if (isSourceMode || disabled) return;
|
|
127
|
+
const url = prompt('Enter URL:');
|
|
128
|
+
if (url) execCmd('createLink', url);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function insertVariable(variable) {
|
|
132
|
+
if (disabled) return;
|
|
133
|
+
const twigExpr = `{{ ${variable.key} }}`;
|
|
134
|
+
|
|
135
|
+
if (isSourceMode && sourceEl) {
|
|
136
|
+
// Insert into textarea at cursor position
|
|
137
|
+
const start = sourceEl.selectionStart;
|
|
138
|
+
const end = sourceEl.selectionEnd;
|
|
139
|
+
const before = sourceCode.substring(0, start);
|
|
140
|
+
const after = sourceCode.substring(end);
|
|
141
|
+
sourceCode = before + twigExpr + after;
|
|
142
|
+
sourceEl.value = sourceCode;
|
|
143
|
+
value = sourceCode;
|
|
144
|
+
dispatch('change', value);
|
|
145
|
+
// Restore cursor after inserted text
|
|
146
|
+
tick().then(() => {
|
|
147
|
+
sourceEl.selectionStart = sourceEl.selectionEnd = start + twigExpr.length;
|
|
148
|
+
sourceEl.focus();
|
|
149
|
+
});
|
|
150
|
+
} else if (editorEl) {
|
|
151
|
+
// Insert into contentEditable at cursor position
|
|
152
|
+
editorEl.focus();
|
|
153
|
+
// Use insertHTML to preserve Twig brackets exactly
|
|
154
|
+
document.execCommand('insertHTML', false, twigExpr);
|
|
155
|
+
syncFromEditor();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
showVariableMenu = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function handleEditorInput() {
|
|
162
|
+
syncFromEditor();
|
|
163
|
+
updateActiveFormats();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleEditorKeyup() {
|
|
167
|
+
updateActiveFormats();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleEditorMouseup() {
|
|
171
|
+
updateActiveFormats();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function closeMenus(e) {
|
|
175
|
+
if (!e.target.closest('.brevo-dropdown')) {
|
|
176
|
+
showVariableMenu = false;
|
|
177
|
+
showHeadingMenu = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<svelte:window on:click={closeMenus} />
|
|
183
|
+
|
|
184
|
+
<div class="brevo-wysiwyg" class:brevo-wysiwyg--disabled={disabled} class:brevo-wysiwyg--focused={isFocused}>
|
|
185
|
+
<!-- Toolbar -->
|
|
186
|
+
<div class="brevo-toolbar">
|
|
187
|
+
<div class="brevo-toolbar__group">
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
class="brevo-btn"
|
|
191
|
+
class:brevo-btn--active={activeFormats.bold}
|
|
192
|
+
title="Bold"
|
|
193
|
+
on:click={() => execCmd('bold')}
|
|
194
|
+
{disabled}
|
|
195
|
+
><strong>B</strong></button>
|
|
196
|
+
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
class="brevo-btn"
|
|
200
|
+
class:brevo-btn--active={activeFormats.italic}
|
|
201
|
+
title="Italic"
|
|
202
|
+
on:click={() => execCmd('italic')}
|
|
203
|
+
{disabled}
|
|
204
|
+
><em>I</em></button>
|
|
205
|
+
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
class="brevo-btn"
|
|
209
|
+
class:brevo-btn--active={activeFormats.underline}
|
|
210
|
+
title="Underline"
|
|
211
|
+
on:click={() => execCmd('underline')}
|
|
212
|
+
{disabled}
|
|
213
|
+
><u>U</u></button>
|
|
214
|
+
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
class="brevo-btn"
|
|
218
|
+
class:brevo-btn--active={activeFormats.strikeThrough}
|
|
219
|
+
title="Strikethrough"
|
|
220
|
+
on:click={() => execCmd('strikeThrough')}
|
|
221
|
+
{disabled}
|
|
222
|
+
><s>S</s></button>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div class="brevo-toolbar__divider"></div>
|
|
226
|
+
|
|
227
|
+
<!-- Heading dropdown -->
|
|
228
|
+
<div class="brevo-dropdown">
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
class="brevo-btn"
|
|
232
|
+
title="Headings"
|
|
233
|
+
on:click|stopPropagation={() => { showHeadingMenu = !showHeadingMenu; showVariableMenu = false; }}
|
|
234
|
+
{disabled}
|
|
235
|
+
>H ▾</button>
|
|
236
|
+
|
|
237
|
+
{#if showHeadingMenu}
|
|
238
|
+
<div class="brevo-dropdown__menu">
|
|
239
|
+
<button type="button" on:click={() => insertHeading('h1')}>Heading 1</button>
|
|
240
|
+
<button type="button" on:click={() => insertHeading('h2')}>Heading 2</button>
|
|
241
|
+
<button type="button" on:click={() => insertHeading('h3')}>Heading 3</button>
|
|
242
|
+
<button type="button" on:click={() => insertHeading('p')}>Paragraph</button>
|
|
243
|
+
</div>
|
|
244
|
+
{/if}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="brevo-toolbar__divider"></div>
|
|
248
|
+
|
|
249
|
+
<div class="brevo-toolbar__group">
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
class="brevo-btn"
|
|
253
|
+
class:brevo-btn--active={activeFormats.insertUnorderedList}
|
|
254
|
+
title="Bullet List"
|
|
255
|
+
on:click={() => execCmd('insertUnorderedList')}
|
|
256
|
+
{disabled}
|
|
257
|
+
>• List</button>
|
|
258
|
+
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="brevo-btn"
|
|
262
|
+
class:brevo-btn--active={activeFormats.insertOrderedList}
|
|
263
|
+
title="Numbered List"
|
|
264
|
+
on:click={() => execCmd('insertOrderedList')}
|
|
265
|
+
{disabled}
|
|
266
|
+
>1. List</button>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div class="brevo-toolbar__divider"></div>
|
|
270
|
+
|
|
271
|
+
<div class="brevo-toolbar__group">
|
|
272
|
+
<button type="button" class="brevo-btn" title="Align Left" on:click={() => execCmd('justifyLeft')} {disabled}>⫷</button>
|
|
273
|
+
<button type="button" class="brevo-btn" title="Align Center" on:click={() => execCmd('justifyCenter')} {disabled}>⫿</button>
|
|
274
|
+
<button type="button" class="brevo-btn" title="Align Right" on:click={() => execCmd('justifyRight')} {disabled}>⫸</button>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="brevo-toolbar__divider"></div>
|
|
278
|
+
|
|
279
|
+
<button type="button" class="brevo-btn" title="Insert Link" on:click={insertLink} {disabled}>🔗</button>
|
|
280
|
+
|
|
281
|
+
<!-- Variable injector -->
|
|
282
|
+
{#if variables.length > 0}
|
|
283
|
+
<div class="brevo-toolbar__divider"></div>
|
|
284
|
+
<div class="brevo-dropdown">
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
class="brevo-btn brevo-btn--accent"
|
|
288
|
+
title="Insert Variable"
|
|
289
|
+
on:click|stopPropagation={() => { showVariableMenu = !showVariableMenu; showHeadingMenu = false; }}
|
|
290
|
+
{disabled}
|
|
291
|
+
>{{ }} Insert Variable ▾</button>
|
|
292
|
+
|
|
293
|
+
{#if showVariableMenu}
|
|
294
|
+
<div class="brevo-dropdown__menu brevo-dropdown__menu--variables">
|
|
295
|
+
{#each variables as variable}
|
|
296
|
+
<button type="button" on:click={() => insertVariable(variable)}>
|
|
297
|
+
<span class="brevo-var-label">{variable.label}</span>
|
|
298
|
+
<code class="brevo-var-key">{'{{ ' + variable.key + ' }}'}</code>
|
|
299
|
+
</button>
|
|
300
|
+
{/each}
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
</div>
|
|
304
|
+
{/if}
|
|
305
|
+
|
|
306
|
+
<!-- Source toggle (right-aligned) -->
|
|
307
|
+
<div class="brevo-toolbar__spacer"></div>
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
class="brevo-btn brevo-btn--source"
|
|
311
|
+
class:brevo-btn--active={isSourceMode}
|
|
312
|
+
title="Toggle Source View"
|
|
313
|
+
on:click={toggleSourceMode}
|
|
314
|
+
{disabled}
|
|
315
|
+
>{isSourceMode ? '✎ Visual' : '</> Source'}</button>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- Editor Area -->
|
|
319
|
+
<div class="brevo-editor-wrapper">
|
|
320
|
+
{#if isSourceMode}
|
|
321
|
+
<textarea
|
|
322
|
+
bind:this={sourceEl}
|
|
323
|
+
bind:value={sourceCode}
|
|
324
|
+
class="brevo-source"
|
|
325
|
+
on:input={syncFromSource}
|
|
326
|
+
on:focus={() => (isFocused = true)}
|
|
327
|
+
on:blur={() => (isFocused = false)}
|
|
328
|
+
{placeholder}
|
|
329
|
+
{disabled}
|
|
330
|
+
spellcheck="false"
|
|
331
|
+
></textarea>
|
|
332
|
+
{:else}
|
|
333
|
+
<div
|
|
334
|
+
bind:this={editorEl}
|
|
335
|
+
class="brevo-editor"
|
|
336
|
+
contenteditable={!disabled}
|
|
337
|
+
role="textbox"
|
|
338
|
+
tabindex="0"
|
|
339
|
+
aria-multiline="true"
|
|
340
|
+
aria-label="Email template editor"
|
|
341
|
+
data-placeholder={placeholder}
|
|
342
|
+
on:input={handleEditorInput}
|
|
343
|
+
on:keyup={handleEditorKeyup}
|
|
344
|
+
on:mouseup={handleEditorMouseup}
|
|
345
|
+
on:focus={() => (isFocused = true)}
|
|
346
|
+
on:blur={() => (isFocused = false)}
|
|
347
|
+
></div>
|
|
348
|
+
{/if}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<style>
|
|
353
|
+
/* ── Container ── */
|
|
354
|
+
.brevo-wysiwyg {
|
|
355
|
+
--brevo-bg: #1a1a2e;
|
|
356
|
+
--brevo-surface: #16213e;
|
|
357
|
+
--brevo-border: #2a2a4a;
|
|
358
|
+
--brevo-border-focus: #6366f1;
|
|
359
|
+
--brevo-text: #e2e8f0;
|
|
360
|
+
--brevo-text-muted: #94a3b8;
|
|
361
|
+
--brevo-accent: #6366f1;
|
|
362
|
+
--brevo-accent-hover: #818cf8;
|
|
363
|
+
--brevo-toolbar-bg: #0f1629;
|
|
364
|
+
--brevo-btn-hover: #2a2a4a;
|
|
365
|
+
--brevo-btn-active: #3730a3;
|
|
366
|
+
--brevo-radius: 8px;
|
|
367
|
+
--brevo-font: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
368
|
+
|
|
369
|
+
font-family: var(--brevo-font);
|
|
370
|
+
border: 1.5px solid var(--brevo-border);
|
|
371
|
+
border-radius: var(--brevo-radius);
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
background: var(--brevo-bg);
|
|
374
|
+
transition: border-color 0.2s ease;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.brevo-wysiwyg--focused {
|
|
378
|
+
border-color: var(--brevo-border-focus);
|
|
379
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.brevo-wysiwyg--disabled {
|
|
383
|
+
opacity: 0.55;
|
|
384
|
+
pointer-events: none;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* ── Toolbar ── */
|
|
388
|
+
.brevo-toolbar {
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
gap: 2px;
|
|
393
|
+
padding: 6px 8px;
|
|
394
|
+
background: var(--brevo-toolbar-bg);
|
|
395
|
+
border-bottom: 1px solid var(--brevo-border);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.brevo-toolbar__group {
|
|
399
|
+
display: flex;
|
|
400
|
+
gap: 2px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.brevo-toolbar__divider {
|
|
404
|
+
width: 1px;
|
|
405
|
+
height: 22px;
|
|
406
|
+
background: var(--brevo-border);
|
|
407
|
+
margin: 0 4px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.brevo-toolbar__spacer {
|
|
411
|
+
flex: 1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ── Buttons ── */
|
|
415
|
+
.brevo-btn {
|
|
416
|
+
display: inline-flex;
|
|
417
|
+
align-items: center;
|
|
418
|
+
gap: 4px;
|
|
419
|
+
padding: 5px 10px;
|
|
420
|
+
border: none;
|
|
421
|
+
border-radius: 5px;
|
|
422
|
+
background: transparent;
|
|
423
|
+
color: var(--brevo-text-muted);
|
|
424
|
+
font-family: var(--brevo-font);
|
|
425
|
+
font-size: 13px;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
transition: all 0.15s ease;
|
|
428
|
+
white-space: nowrap;
|
|
429
|
+
line-height: 1.4;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.brevo-btn:hover {
|
|
433
|
+
background: var(--brevo-btn-hover);
|
|
434
|
+
color: var(--brevo-text);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.brevo-btn--active {
|
|
438
|
+
background: var(--brevo-btn-active);
|
|
439
|
+
color: #fff;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.brevo-btn--accent {
|
|
443
|
+
color: var(--brevo-accent);
|
|
444
|
+
font-weight: 500;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.brevo-btn--accent:hover {
|
|
448
|
+
color: var(--brevo-accent-hover);
|
|
449
|
+
background: rgba(99, 102, 241, 0.12);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.brevo-btn--source {
|
|
453
|
+
font-size: 12px;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
letter-spacing: 0.02em;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* ── Dropdowns ── */
|
|
459
|
+
.brevo-dropdown {
|
|
460
|
+
position: relative;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.brevo-dropdown__menu {
|
|
464
|
+
position: absolute;
|
|
465
|
+
top: calc(100% + 4px);
|
|
466
|
+
left: 0;
|
|
467
|
+
min-width: 160px;
|
|
468
|
+
padding: 4px;
|
|
469
|
+
background: var(--brevo-surface);
|
|
470
|
+
border: 1px solid var(--brevo-border);
|
|
471
|
+
border-radius: 6px;
|
|
472
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
473
|
+
z-index: 100;
|
|
474
|
+
animation: brevo-dropdown-in 0.12s ease-out;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.brevo-dropdown__menu--variables {
|
|
478
|
+
min-width: 260px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
@keyframes brevo-dropdown-in {
|
|
482
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
483
|
+
to { opacity: 1; transform: translateY(0); }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.brevo-dropdown__menu button {
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
justify-content: space-between;
|
|
490
|
+
width: 100%;
|
|
491
|
+
padding: 7px 10px;
|
|
492
|
+
border: none;
|
|
493
|
+
border-radius: 4px;
|
|
494
|
+
background: transparent;
|
|
495
|
+
color: var(--brevo-text);
|
|
496
|
+
font-family: var(--brevo-font);
|
|
497
|
+
font-size: 13px;
|
|
498
|
+
cursor: pointer;
|
|
499
|
+
text-align: left;
|
|
500
|
+
gap: 8px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.brevo-dropdown__menu button:hover {
|
|
504
|
+
background: var(--brevo-btn-hover);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.brevo-var-label {
|
|
508
|
+
flex: 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.brevo-var-key {
|
|
512
|
+
font-size: 11px;
|
|
513
|
+
color: var(--brevo-accent);
|
|
514
|
+
background: rgba(99, 102, 241, 0.1);
|
|
515
|
+
padding: 2px 6px;
|
|
516
|
+
border-radius: 3px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* ── Editor ── */
|
|
520
|
+
.brevo-editor-wrapper {
|
|
521
|
+
background: var(--brevo-bg);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.brevo-editor {
|
|
525
|
+
min-height: 320px;
|
|
526
|
+
max-height: 600px;
|
|
527
|
+
overflow-y: auto;
|
|
528
|
+
padding: 20px 24px;
|
|
529
|
+
color: var(--brevo-text);
|
|
530
|
+
font-size: 15px;
|
|
531
|
+
line-height: 1.7;
|
|
532
|
+
outline: none;
|
|
533
|
+
word-wrap: break-word;
|
|
534
|
+
overflow-wrap: break-word;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.brevo-editor:empty::before {
|
|
538
|
+
content: attr(data-placeholder);
|
|
539
|
+
color: var(--brevo-text-muted);
|
|
540
|
+
opacity: 0.5;
|
|
541
|
+
pointer-events: none;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.brevo-editor :global(h1) { font-size: 1.8em; font-weight: 700; margin: 0.4em 0; }
|
|
545
|
+
.brevo-editor :global(h2) { font-size: 1.4em; font-weight: 600; margin: 0.4em 0; }
|
|
546
|
+
.brevo-editor :global(h3) { font-size: 1.15em; font-weight: 600; margin: 0.4em 0; }
|
|
547
|
+
.brevo-editor :global(a) { color: var(--brevo-accent-hover); text-decoration: underline; }
|
|
548
|
+
.brevo-editor :global(ul),
|
|
549
|
+
.brevo-editor :global(ol) { padding-left: 1.6em; }
|
|
550
|
+
|
|
551
|
+
/* ── Source ── */
|
|
552
|
+
.brevo-source {
|
|
553
|
+
width: 100%;
|
|
554
|
+
min-height: 320px;
|
|
555
|
+
max-height: 600px;
|
|
556
|
+
padding: 16px 20px;
|
|
557
|
+
border: none;
|
|
558
|
+
background: #0d1117;
|
|
559
|
+
color: #c9d1d9;
|
|
560
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
561
|
+
font-size: 13px;
|
|
562
|
+
line-height: 1.65;
|
|
563
|
+
resize: vertical;
|
|
564
|
+
outline: none;
|
|
565
|
+
tab-size: 2;
|
|
566
|
+
box-sizing: border-box;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.brevo-source::placeholder {
|
|
570
|
+
color: #484f58;
|
|
571
|
+
}
|
|
572
|
+
</style>
|