@medalsocial/meda 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar/calendar-toolbar.d.ts +2 -0
- package/dist/calendar/calendar-toolbar.js +22 -0
- package/dist/calendar/calendar.d.ts +2 -0
- package/dist/calendar/calendar.js +55 -0
- package/dist/calendar/day-view.d.ts +2 -0
- package/dist/calendar/day-view.js +40 -0
- package/dist/calendar/index.d.ts +6 -0
- package/dist/calendar/index.js +7 -0
- package/dist/calendar/internal/date.d.ts +26 -0
- package/dist/calendar/internal/date.js +107 -0
- package/dist/calendar/internal/day-cell.d.ts +15 -0
- package/dist/calendar/internal/day-cell.js +38 -0
- package/dist/calendar/internal/event-pill.d.ts +13 -0
- package/dist/calendar/internal/event-pill.js +30 -0
- package/dist/calendar/internal/time-slot.d.ts +17 -0
- package/dist/calendar/internal/time-slot.js +23 -0
- package/dist/calendar/month-view.d.ts +2 -0
- package/dist/calendar/month-view.js +46 -0
- package/dist/calendar/public.d.ts +6 -0
- package/dist/calendar/public.js +8 -0
- package/dist/calendar/types.d.ts +113 -0
- package/dist/calendar/types.js +15 -0
- package/dist/calendar/week-view.d.ts +2 -0
- package/dist/calendar/week-view.js +39 -0
- package/dist/chat/turn-card.js +10 -5
- package/dist/email-builder/block-error-boundary.d.ts +16 -0
- package/dist/email-builder/block-error-boundary.js +21 -0
- package/dist/email-builder/block-palette.d.ts +7 -0
- package/dist/email-builder/block-palette.js +22 -0
- package/dist/email-builder/block-registry.d.ts +15 -0
- package/dist/email-builder/block-registry.js +157 -0
- package/dist/email-builder/block-renderer.d.ts +5 -0
- package/dist/email-builder/block-renderer.js +40 -0
- package/dist/email-builder/block-renderers/button.d.ts +4 -0
- package/dist/email-builder/block-renderers/button.js +26 -0
- package/dist/email-builder/block-renderers/columns.d.ts +5 -0
- package/dist/email-builder/block-renderers/columns.js +27 -0
- package/dist/email-builder/block-renderers/divider.d.ts +4 -0
- package/dist/email-builder/block-renderers/divider.js +14 -0
- package/dist/email-builder/block-renderers/footer.d.ts +4 -0
- package/dist/email-builder/block-renderers/footer.js +12 -0
- package/dist/email-builder/block-renderers/heading.d.ts +4 -0
- package/dist/email-builder/block-renderers/heading.js +19 -0
- package/dist/email-builder/block-renderers/image.d.ts +4 -0
- package/dist/email-builder/block-renderers/image.js +18 -0
- package/dist/email-builder/block-renderers/social.d.ts +4 -0
- package/dist/email-builder/block-renderers/social.js +19 -0
- package/dist/email-builder/block-renderers/spacer.d.ts +4 -0
- package/dist/email-builder/block-renderers/spacer.js +5 -0
- package/dist/email-builder/block-renderers/text.d.ts +4 -0
- package/dist/email-builder/block-renderers/text.js +17 -0
- package/dist/email-builder/builder-canvas.d.ts +15 -0
- package/dist/email-builder/builder-canvas.js +28 -0
- package/dist/email-builder/builder-header.d.ts +9 -0
- package/dist/email-builder/builder-header.js +8 -0
- package/dist/email-builder/builder-left-tabs.d.ts +11 -0
- package/dist/email-builder/builder-left-tabs.js +20 -0
- package/dist/email-builder/email-builder.d.ts +3 -0
- package/dist/email-builder/email-builder.js +94 -0
- package/dist/email-builder/envelope-card.d.ts +7 -0
- package/dist/email-builder/envelope-card.js +16 -0
- package/dist/email-builder/floating-bar.d.ts +13 -0
- package/dist/email-builder/floating-bar.js +9 -0
- package/dist/email-builder/index.d.ts +4 -0
- package/dist/email-builder/index.js +12 -0
- package/dist/email-builder/internal/builder-state-utils.d.ts +7 -0
- package/dist/email-builder/internal/builder-state-utils.js +53 -0
- package/dist/email-builder/internal/dnd-wrapper.d.ts +10 -0
- package/dist/email-builder/internal/dnd-wrapper.js +9 -0
- package/dist/email-builder/internal/email-font-options.d.ts +5 -0
- package/dist/email-builder/internal/email-font-options.js +12 -0
- package/dist/email-builder/mobile-drawers.d.ts +10 -0
- package/dist/email-builder/mobile-drawers.js +7 -0
- package/dist/email-builder/mobile-tab-bar.d.ts +10 -0
- package/dist/email-builder/mobile-tab-bar.js +9 -0
- package/dist/email-builder/property-editors/_shared.d.ts +58 -0
- package/dist/email-builder/property-editors/_shared.js +54 -0
- package/dist/email-builder/property-editors/button-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/button-editor.js +10 -0
- package/dist/email-builder/property-editors/columns-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/columns-editor.js +15 -0
- package/dist/email-builder/property-editors/divider-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/divider-editor.js +10 -0
- package/dist/email-builder/property-editors/footer-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/footer-editor.js +6 -0
- package/dist/email-builder/property-editors/heading-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/heading-editor.js +16 -0
- package/dist/email-builder/property-editors/image-editor.d.ts +8 -0
- package/dist/email-builder/property-editors/image-editor.js +17 -0
- package/dist/email-builder/property-editors/social-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/social-editor.js +18 -0
- package/dist/email-builder/property-editors/spacer-editor.d.ts +7 -0
- package/dist/email-builder/property-editors/spacer-editor.js +6 -0
- package/dist/email-builder/property-editors/text-editor.d.ts +13 -0
- package/dist/email-builder/property-editors/text-editor.js +8 -0
- package/dist/email-builder/property-inspector.d.ts +11 -0
- package/dist/email-builder/property-inspector.js +42 -0
- package/dist/email-builder/public.d.ts +1 -0
- package/dist/email-builder/public.js +3 -0
- package/dist/email-builder/render-to-email-html.d.ts +11 -0
- package/dist/email-builder/render-to-email-html.js +220 -0
- package/dist/email-builder/starter-shell.d.ts +3 -0
- package/dist/email-builder/starter-shell.js +21 -0
- package/dist/email-builder/types.d.ts +182 -0
- package/dist/email-builder/types.js +41 -0
- package/dist/email-builder/view-controls.d.ts +10 -0
- package/dist/email-builder/view-controls.js +12 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/format-time.js +2 -3
- package/dist/marketing/index.d.ts +10 -1
- package/dist/marketing/index.js +9 -0
- package/dist/marketing/marketing-bento-card.d.ts +2 -0
- package/dist/marketing/marketing-bento-card.js +24 -0
- package/dist/marketing/marketing-bento-grid.d.ts +2 -0
- package/dist/marketing/marketing-bento-grid.js +12 -0
- package/dist/marketing/marketing-faq.d.ts +2 -0
- package/dist/marketing/marketing-faq.js +12 -0
- package/dist/marketing/marketing-footer.d.ts +2 -0
- package/dist/marketing/marketing-footer.js +5 -0
- package/dist/marketing/marketing-header-logo.d.ts +10 -0
- package/dist/marketing/marketing-header-logo.js +6 -0
- package/dist/marketing/marketing-header.d.ts +2 -0
- package/dist/marketing/marketing-header.js +26 -0
- package/dist/marketing/marketing-nav-item.d.ts +4 -0
- package/dist/marketing/marketing-nav-item.js +10 -0
- package/dist/marketing/marketing-shell.d.ts +2 -0
- package/dist/marketing/marketing-shell.js +5 -0
- package/dist/marketing/marketing-trust-bar.d.ts +2 -0
- package/dist/marketing/marketing-trust-bar.js +5 -0
- package/dist/marketing/public.d.ts +1 -1
- package/dist/marketing/types.d.ts +99 -0
- package/dist/post-preview/index.d.ts +4 -0
- package/dist/post-preview/index.js +7 -0
- package/dist/post-preview/internal/avatar.d.ts +12 -0
- package/dist/post-preview/internal/avatar.js +21 -0
- package/dist/post-preview/internal/format-content.d.ts +12 -0
- package/dist/post-preview/internal/format-content.js +32 -0
- package/dist/post-preview/internal/platform-meta.d.ts +10 -0
- package/dist/post-preview/internal/platform-meta.js +18 -0
- package/dist/post-preview/internal/textarea.d.ts +8 -0
- package/dist/post-preview/internal/textarea.js +12 -0
- package/dist/post-preview/internal/use-swipe.d.ts +17 -0
- package/dist/post-preview/internal/use-swipe.js +29 -0
- package/dist/post-preview/platforms/bluesky.d.ts +15 -0
- package/dist/post-preview/platforms/bluesky.js +21 -0
- package/dist/post-preview/platforms/discord.d.ts +44 -0
- package/dist/post-preview/platforms/discord.js +34 -0
- package/dist/post-preview/platforms/facebook.d.ts +23 -0
- package/dist/post-preview/platforms/facebook.js +26 -0
- package/dist/post-preview/platforms/generic.d.ts +22 -0
- package/dist/post-preview/platforms/generic.js +30 -0
- package/dist/post-preview/platforms/google-business.d.ts +18 -0
- package/dist/post-preview/platforms/google-business.js +24 -0
- package/dist/post-preview/platforms/index.d.ts +24 -0
- package/dist/post-preview/platforms/index.js +13 -0
- package/dist/post-preview/platforms/instagram.d.ts +31 -0
- package/dist/post-preview/platforms/instagram.js +104 -0
- package/dist/post-preview/platforms/linkedin.d.ts +52 -0
- package/dist/post-preview/platforms/linkedin.js +140 -0
- package/dist/post-preview/platforms/telegram.d.ts +46 -0
- package/dist/post-preview/platforms/telegram.js +44 -0
- package/dist/post-preview/platforms/threads.d.ts +19 -0
- package/dist/post-preview/platforms/threads.js +26 -0
- package/dist/post-preview/platforms/tiktok.d.ts +18 -0
- package/dist/post-preview/platforms/tiktok.js +25 -0
- package/dist/post-preview/platforms/twitter.d.ts +40 -0
- package/dist/post-preview/platforms/twitter.js +137 -0
- package/dist/post-preview/platforms/youtube.d.ts +17 -0
- package/dist/post-preview/platforms/youtube.js +23 -0
- package/dist/post-preview/post-preview.d.ts +29 -0
- package/dist/post-preview/post-preview.js +70 -0
- package/dist/post-preview/public.d.ts +1 -0
- package/dist/post-preview/public.js +6 -0
- package/dist/post-preview/types.d.ts +82 -0
- package/dist/post-preview/types.js +1 -0
- package/dist/primitives/filter-rail.js +17 -2
- package/dist/shell/command-palette.js +2 -0
- package/dist/shell/drag-mode-banner.js +4 -0
- package/dist/shell/extras/shell-scrollable-content.js +2 -2
- package/dist/shell/extras/workbench-layout.js +24 -13
- package/dist/shell/internal/mobile-bottom-nav.js +1 -0
- package/dist/shell/internal/mobile-drawers.js +5 -0
- package/dist/shell/rail-drop-slot.js +20 -3
- package/dist/shell/rail-drop-zones.js +2 -0
- package/dist/shell/right-panel.js +10 -1
- package/dist/shell/shell-header.js +7 -1
- package/dist/shell/theme-next-themes.js +7 -2
- package/dist/shell/theme.js +7 -1
- package/dist/shell/use-shell-viewport.js +2 -0
- package/dist/timeline/lane-timeline.js +1 -1
- package/dist/timeline/scrub-bar.js +1 -1
- package/dist/timeline/timeline-tape.js +8 -5
- package/dist/voice/voice-orb-scene.js +4 -4
- package/dist/voice/voice-status-pill.js +8 -7
- package/dist/workflow-builder/edges/default-edge.d.ts +10 -0
- package/dist/workflow-builder/edges/default-edge.js +21 -0
- package/dist/workflow-builder/edges/index.d.ts +3 -0
- package/dist/workflow-builder/edges/index.js +5 -0
- package/dist/workflow-builder/index.d.ts +5 -0
- package/dist/workflow-builder/index.js +12 -0
- package/dist/workflow-builder/internal/trigger-icons.d.ts +4 -0
- package/dist/workflow-builder/internal/trigger-icons.js +43 -0
- package/dist/workflow-builder/internal/trigger-sentence.d.ts +14 -0
- package/dist/workflow-builder/internal/trigger-sentence.js +50 -0
- package/dist/workflow-builder/nodes/action-node.d.ts +3 -0
- package/dist/workflow-builder/nodes/action-node.js +7 -0
- package/dist/workflow-builder/nodes/base-node.d.ts +20 -0
- package/dist/workflow-builder/nodes/base-node.js +18 -0
- package/dist/workflow-builder/nodes/condition-node.d.ts +3 -0
- package/dist/workflow-builder/nodes/condition-node.js +11 -0
- package/dist/workflow-builder/nodes/delay-node.d.ts +3 -0
- package/dist/workflow-builder/nodes/delay-node.js +7 -0
- package/dist/workflow-builder/nodes/end-node.d.ts +3 -0
- package/dist/workflow-builder/nodes/end-node.js +7 -0
- package/dist/workflow-builder/nodes/index.d.ts +13 -0
- package/dist/workflow-builder/nodes/index.js +22 -0
- package/dist/workflow-builder/nodes/trigger-node.d.ts +3 -0
- package/dist/workflow-builder/nodes/trigger-node.js +8 -0
- package/dist/workflow-builder/public.d.ts +1 -0
- package/dist/workflow-builder/public.js +4 -0
- package/dist/workflow-builder/types.d.ts +140 -0
- package/dist/workflow-builder/types.js +18 -0
- package/dist/workflow-builder/workflow-builder.d.ts +10 -0
- package/dist/workflow-builder/workflow-builder.js +22 -0
- package/dist/workflow-builder/workflow-canvas.d.ts +26 -0
- package/dist/workflow-builder/workflow-canvas.js +29 -0
- package/dist/workflow-builder/workflow-card-compact.d.ts +2 -0
- package/dist/workflow-builder/workflow-card-compact.js +22 -0
- package/dist/workflow-builder/workflow-card.d.ts +2 -0
- package/dist/workflow-builder/workflow-card.js +44 -0
- package/dist/workflow-builder/workflow-header.d.ts +2 -0
- package/dist/workflow-builder/workflow-header.js +32 -0
- package/dist/workflow-builder/workflow-toolbox.d.ts +2 -0
- package/dist/workflow-builder/workflow-toolbox.js +19 -0
- package/package.json +72 -50
- package/dist/styles/theme.test.ts +0 -41
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DevicePreview, EmailBuilderLabels } from './types.js';
|
|
2
|
+
interface BuilderHeaderProps {
|
|
3
|
+
device: DevicePreview;
|
|
4
|
+
onDeviceChange: (next: DevicePreview) => void;
|
|
5
|
+
onExport: () => void;
|
|
6
|
+
labels: EmailBuilderLabels;
|
|
7
|
+
}
|
|
8
|
+
export declare function BuilderHeader({ device, onDeviceChange, onExport, labels }: BuilderHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Download } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { ViewControls } from './view-controls.js';
|
|
6
|
+
export function BuilderHeader({ device, onDeviceChange, onExport, labels }) {
|
|
7
|
+
return (_jsxs("header", { "data-slot": "email-builder-header", className: "flex h-12 items-center justify-between gap-2 border-border border-b bg-background px-4", children: [_jsx("div", { className: "font-semibold text-foreground text-sm", children: labels.preview }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ViewControls, { device: device, onDeviceChange: onDeviceChange, desktopLabel: labels.desktopPreview, mobileLabel: labels.mobilePreview }), _jsxs("button", { type: "button", onClick: onExport, className: cn('inline-flex h-9 items-center gap-2 rounded-md border border-input bg-background px-3 font-medium text-sm', 'hover:bg-accent hover:text-accent-foreground'), children: [_jsx(Download, { className: "size-4", "aria-hidden": true }), labels.exportHtml] })] })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { EmailBuilderLabels } from './types.js';
|
|
3
|
+
interface BuilderLeftTabsProps {
|
|
4
|
+
labels: EmailBuilderLabels;
|
|
5
|
+
blocksContent: ReactNode;
|
|
6
|
+
designContent?: ReactNode;
|
|
7
|
+
envelopeContent: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
/** Vertical tab strip + content panel for the left sidebar. */
|
|
10
|
+
export declare function BuilderLeftTabs({ labels, blocksContent, designContent, envelopeContent, }: BuilderLeftTabsProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/** Vertical tab strip + content panel for the left sidebar. */
|
|
6
|
+
export function BuilderLeftTabs({ labels, blocksContent, designContent, envelopeContent, }) {
|
|
7
|
+
const [tab, setTab] = useState('blocks');
|
|
8
|
+
const tabs = [
|
|
9
|
+
{ id: 'blocks', label: labels.blocksTab, content: blocksContent },
|
|
10
|
+
{ id: 'envelope', label: labels.envelopeTab, content: envelopeContent },
|
|
11
|
+
];
|
|
12
|
+
if (designContent) {
|
|
13
|
+
tabs.splice(1, 0, { id: 'design', label: labels.designTab, content: designContent });
|
|
14
|
+
}
|
|
15
|
+
/* v8 ignore next -- fallback to tabs[0] is unreachable; tab state is always set to a valid TabId */
|
|
16
|
+
const active = tabs.find((t) => t.id === tab) ?? tabs[0];
|
|
17
|
+
return (_jsxs("div", { "data-slot": "email-builder-left-tabs", className: "flex h-full flex-col", children: [_jsx("div", { role: "tablist", className: "flex border-border border-b", children: tabs.map((t) => (_jsx("button", { type: "button", role: "tab", "aria-selected": t.id === tab, onClick: () => setTab(t.id), className: cn('flex h-11 flex-1 items-center justify-center text-sm transition-colors', t.id === tab
|
|
18
|
+
? 'border-primary border-b-2 font-medium text-foreground'
|
|
19
|
+
: 'text-muted-foreground hover:text-foreground'), children: t.label }, t.id))) }), _jsx("div", { role: "tabpanel", className: "flex-1 overflow-y-auto", children: active?.content })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type EmailBuilderProps } from './types.js';
|
|
2
|
+
export declare function EmailBuilder({ document, onDocumentChange, brand, renderMediaPicker, renderSavedBlocks, renderTextEditor, onExportHtml, labels: labelsProp, className, initialDevice, }: EmailBuilderProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export { renderToEmailHtml } from './render-to-email-html.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { BlockPalette } from './block-palette.js';
|
|
6
|
+
import { createBlock } from './block-registry.js';
|
|
7
|
+
import { BuilderCanvas } from './builder-canvas.js';
|
|
8
|
+
import { BuilderHeader } from './builder-header.js';
|
|
9
|
+
import { BuilderLeftTabs } from './builder-left-tabs.js';
|
|
10
|
+
import { EnvelopeCard } from './envelope-card.js';
|
|
11
|
+
import { FloatingBar } from './floating-bar.js';
|
|
12
|
+
import { addBlock, duplicateBlock, moveBlock, removeBlock, updateBlockProps, } from './internal/builder-state-utils.js';
|
|
13
|
+
import { BuilderDndWrapper } from './internal/dnd-wrapper.js';
|
|
14
|
+
import { MobileDrawer } from './mobile-drawers.js';
|
|
15
|
+
import { MobileTabBar } from './mobile-tab-bar.js';
|
|
16
|
+
import { PropertyInspector } from './property-inspector.js';
|
|
17
|
+
import { renderToEmailHtml } from './render-to-email-html.js';
|
|
18
|
+
import { defaultEmailBuilderLabels, } from './types.js';
|
|
19
|
+
export function EmailBuilder({ document, onDocumentChange, brand, renderMediaPicker, renderSavedBlocks, renderTextEditor, onExportHtml, labels: labelsProp, className, initialDevice = 'desktop', }) {
|
|
20
|
+
const labels = useMemo(() => ({ ...defaultEmailBuilderLabels, ...(labelsProp ?? {}) }), [labelsProp]);
|
|
21
|
+
const [selectedBlockId, setSelectedBlockId] = useState(null);
|
|
22
|
+
const [device, setDevice] = useState(initialDevice);
|
|
23
|
+
const [mobilePane, setMobilePane] = useState(null);
|
|
24
|
+
const selectedBlock = useMemo(() => document.blocks.find((b) => b.id === selectedBlockId) ?? null, [document.blocks, selectedBlockId]);
|
|
25
|
+
const handleAddBlock = useCallback((kind) => {
|
|
26
|
+
const block = createBlock(kind);
|
|
27
|
+
onDocumentChange(addBlock(document, block));
|
|
28
|
+
setSelectedBlockId(block.id);
|
|
29
|
+
setMobilePane(null);
|
|
30
|
+
}, [document, onDocumentChange]);
|
|
31
|
+
const handleSavedBlockPick = useCallback((block) => {
|
|
32
|
+
onDocumentChange(addBlock(document, block));
|
|
33
|
+
setSelectedBlockId(block.id);
|
|
34
|
+
setMobilePane(null);
|
|
35
|
+
}, [document, onDocumentChange]);
|
|
36
|
+
const handlePropChange = useCallback((blockId, patch) => {
|
|
37
|
+
onDocumentChange(updateBlockProps(document, blockId, patch));
|
|
38
|
+
}, [document, onDocumentChange]);
|
|
39
|
+
const handleDelete = useCallback((blockId) => {
|
|
40
|
+
onDocumentChange(removeBlock(document, blockId));
|
|
41
|
+
setSelectedBlockId(null);
|
|
42
|
+
}, [document, onDocumentChange]);
|
|
43
|
+
const handleDuplicate = useCallback((blockId) => {
|
|
44
|
+
onDocumentChange(duplicateBlock(document, blockId));
|
|
45
|
+
}, [document, onDocumentChange]);
|
|
46
|
+
const handleMove = useCallback((blockId, delta) => {
|
|
47
|
+
const idx = document.blocks.findIndex((b) => b.id === blockId);
|
|
48
|
+
/* v8 ignore next -- defensive guard; floating bar only renders for blocks that exist in document */
|
|
49
|
+
if (idx === -1)
|
|
50
|
+
return;
|
|
51
|
+
onDocumentChange(moveBlock(document, idx, idx + delta));
|
|
52
|
+
}, [document, onDocumentChange]);
|
|
53
|
+
const handleDragEnd = useCallback((event) => {
|
|
54
|
+
const { active, over } = event;
|
|
55
|
+
if (!over || active.id === over.id)
|
|
56
|
+
return;
|
|
57
|
+
const fromIndex = document.blocks.findIndex((b) => b.id === active.id);
|
|
58
|
+
const toIndex = document.blocks.findIndex((b) => b.id === over.id);
|
|
59
|
+
if (fromIndex === -1 || toIndex === -1)
|
|
60
|
+
return;
|
|
61
|
+
onDocumentChange(moveBlock(document, fromIndex, toIndex));
|
|
62
|
+
}, [document, onDocumentChange]);
|
|
63
|
+
const handleExport = useCallback(() => {
|
|
64
|
+
const html = renderToEmailHtml(document, brand ? { brand } : undefined);
|
|
65
|
+
if (onExportHtml) {
|
|
66
|
+
onExportHtml(html);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
/* v8 ignore next -- typeof window guard is always true in jsdom; SSR-only branch */
|
|
70
|
+
if (typeof window !== 'undefined') {
|
|
71
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
72
|
+
const url = URL.createObjectURL(blob);
|
|
73
|
+
const a = window.document.createElement('a');
|
|
74
|
+
a.href = url;
|
|
75
|
+
a.download = `${document.envelope?.subject ?? 'email'}.html`;
|
|
76
|
+
a.click();
|
|
77
|
+
URL.revokeObjectURL(url);
|
|
78
|
+
}
|
|
79
|
+
}, [brand, document, onExportHtml]);
|
|
80
|
+
const blockIds = useMemo(() => document.blocks.map((b) => b.id), [document.blocks]);
|
|
81
|
+
const blocksTabContent = (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(BlockPalette, { onPick: handleAddBlock }), renderSavedBlocks ? (_jsx("div", { "data-slot": "email-builder-saved-blocks-slot", className: "border-border border-t p-2", children: renderSavedBlocks({ onPick: handleSavedBlockPick }) })) : null] }));
|
|
82
|
+
const envelopeTabContent = (_jsx(EnvelopeCard, { envelope: document.envelope, onChange: (envelope) => onDocumentChange({ ...document, envelope }) }));
|
|
83
|
+
return (_jsxs("div", { "data-slot": "email-builder", className: cn('relative flex h-full min-h-[600px] w-full flex-col bg-background', className), children: [_jsx(BuilderHeader, { device: device, onDeviceChange: setDevice, onExport: handleExport, labels: labels }), _jsx(BuilderDndWrapper, { blockIds: blockIds, onDragEnd: handleDragEnd, children: _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("aside", { "data-slot": "email-builder-left", "aria-label": labels.blocksTab, className: "hidden w-72 shrink-0 border-border border-r md:block", children: _jsx(BuilderLeftTabs, { labels: labels, blocksContent: blocksTabContent, envelopeContent: envelopeTabContent }) }), _jsx("main", { className: "min-w-0 flex-1", children: _jsx(BuilderCanvas, { blocks: document.blocks, selectedBlockId: selectedBlockId, onSelectBlock: setSelectedBlockId, device: device, labels: labels, renderFloatingBar: (block) => (_jsx(FloatingBar, { labels: labels, canMoveUp: document.blocks[0]?.id !== block.id, canMoveDown: document.blocks[document.blocks.length - 1]?.id !== block.id, onMoveUp: () => handleMove(block.id, -1), onMoveDown: () => handleMove(block.id, 1), onDuplicate: () => handleDuplicate(block.id), onDelete: () => handleDelete(block.id) })) }) }), _jsx("aside", { "data-slot": "email-builder-right", "aria-label": labels.openInspector, className: "hidden w-80 shrink-0 overflow-y-auto border-border border-l md:block", children: _jsx(PropertyInspector, { block: selectedBlock, onChange: (id, patch) => handlePropChange(id, patch), emptyContent: labels.inspectorEmpty, renderMediaPicker: renderMediaPicker, renderTextEditor: renderTextEditor }) })] }) }), _jsx(MobileTabBar, { labels: labels, onOpenBlocks: () => setMobilePane('blocks'), onOpenInspector: () => setMobilePane('inspector'), onOpenSettings: () => setMobilePane('settings') }), _jsx(MobileDrawer, { open: mobilePane === 'blocks',
|
|
84
|
+
/* v8 ignore next -- vaul close events (swipe/overlay/escape) don't fire in jsdom */
|
|
85
|
+
onOpenChange: (o) => !o && setMobilePane(null), title: labels.blocksTab, children: blocksTabContent }), _jsx(MobileDrawer, { open: mobilePane === 'inspector',
|
|
86
|
+
/* v8 ignore next -- vaul close events (swipe/overlay/escape) don't fire in jsdom */
|
|
87
|
+
onOpenChange: (o) => !o && setMobilePane(null), title: labels.openInspector, children: _jsx(PropertyInspector, { block: selectedBlock,
|
|
88
|
+
/* v8 ignore next -- onChange is identical to the right-rail's; mobile drawer portal content is not exercised in jsdom */
|
|
89
|
+
onChange: (id, patch) => handlePropChange(id, patch), emptyContent: labels.inspectorEmpty, renderMediaPicker: renderMediaPicker, renderTextEditor: renderTextEditor }) }), _jsx(MobileDrawer, { open: mobilePane === 'settings',
|
|
90
|
+
/* v8 ignore next -- vaul close events (swipe/overlay/escape) don't fire in jsdom */
|
|
91
|
+
onOpenChange: (o) => !o && setMobilePane(null), title: labels.envelopeTab, children: envelopeTabContent })] }));
|
|
92
|
+
}
|
|
93
|
+
// Re-exported for convenience.
|
|
94
|
+
export { renderToEmailHtml } from './render-to-email-html.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EmailEnvelope } from './types.js';
|
|
2
|
+
interface EnvelopeCardProps {
|
|
3
|
+
envelope: EmailEnvelope | undefined;
|
|
4
|
+
onChange: (next: EmailEnvelope) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function EnvelopeCard({ envelope, onChange }: EnvelopeCardProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
const fieldClass = 'w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring';
|
|
5
|
+
export function EnvelopeCard({ envelope, onChange }) {
|
|
6
|
+
const e = envelope ?? {};
|
|
7
|
+
const update = (patch) => onChange({ ...e, ...patch });
|
|
8
|
+
const ids = {
|
|
9
|
+
subject: useId(),
|
|
10
|
+
fromName: useId(),
|
|
11
|
+
fromEmail: useId(),
|
|
12
|
+
replyTo: useId(),
|
|
13
|
+
preheader: useId(),
|
|
14
|
+
};
|
|
15
|
+
return (_jsxs("div", { "data-slot": "email-builder-envelope", className: "flex flex-col gap-2 p-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: ids.subject, className: "font-medium text-foreground text-xs", children: "Subject" }), _jsx("input", { id: ids.subject, type: "text", value: e.subject ?? '', onChange: (ev) => update({ subject: ev.target.value }), className: fieldClass })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: ids.fromName, className: "font-medium text-foreground text-xs", children: "From name" }), _jsx("input", { id: ids.fromName, type: "text", value: e.fromName ?? '', onChange: (ev) => update({ fromName: ev.target.value }), className: fieldClass })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: ids.fromEmail, className: "font-medium text-foreground text-xs", children: "From email" }), _jsx("input", { id: ids.fromEmail, type: "email", value: e.fromEmail ?? '', onChange: (ev) => update({ fromEmail: ev.target.value }), className: fieldClass })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: ids.replyTo, className: "font-medium text-foreground text-xs", children: "Reply-to" }), _jsx("input", { id: ids.replyTo, type: "email", value: e.replyTo ?? '', onChange: (ev) => update({ replyTo: ev.target.value }), className: fieldClass })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: ids.preheader, className: "font-medium text-foreground text-xs", children: "Preheader" }), _jsx("input", { id: ids.preheader, type: "text", value: e.preheader ?? '', onChange: (ev) => update({ preheader: ev.target.value }), className: fieldClass })] })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EmailBuilderLabels } from './types.js';
|
|
2
|
+
interface FloatingBarProps {
|
|
3
|
+
labels: EmailBuilderLabels;
|
|
4
|
+
canMoveUp: boolean;
|
|
5
|
+
canMoveDown: boolean;
|
|
6
|
+
onMoveUp: () => void;
|
|
7
|
+
onMoveDown: () => void;
|
|
8
|
+
onDuplicate: () => void;
|
|
9
|
+
onDelete: () => void;
|
|
10
|
+
}
|
|
11
|
+
/** Per-block floating action bar. Rendered next to the selected block. */
|
|
12
|
+
export declare function FloatingBar({ labels, canMoveUp, canMoveDown, onMoveUp, onMoveDown, onDuplicate, onDelete, }: FloatingBarProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown, ChevronUp, Copy, Trash2 } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/** Per-block floating action bar. Rendered next to the selected block. */
|
|
6
|
+
export function FloatingBar({ labels, canMoveUp, canMoveDown, onMoveUp, onMoveDown, onDuplicate, onDelete, }) {
|
|
7
|
+
const btnClass = 'inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40';
|
|
8
|
+
return (_jsxs("div", { "data-slot": "email-builder-floating-bar", className: "inline-flex rounded-md border border-input bg-background p-0.5 shadow-sm", children: [_jsx("button", { type: "button", onClick: onMoveUp, disabled: !canMoveUp, "aria-label": labels.moveUp, className: btnClass, children: _jsx(ChevronUp, { className: "size-4", "aria-hidden": true }) }), _jsx("button", { type: "button", onClick: onMoveDown, disabled: !canMoveDown, "aria-label": labels.moveDown, className: btnClass, children: _jsx(ChevronDown, { className: "size-4", "aria-hidden": true }) }), _jsx("button", { type: "button", onClick: onDuplicate, "aria-label": labels.duplicateBlock, className: btnClass, children: _jsx(Copy, { className: "size-4", "aria-hidden": true }) }), _jsx("button", { type: "button", onClick: onDelete, "aria-label": labels.deleteBlock, className: cn(btnClass, 'hover:bg-destructive/10 hover:text-destructive'), children: _jsx(Trash2, { className: "size-4", "aria-hidden": true }) })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { EmailBuilder } from './email-builder.js';
|
|
2
|
+
export { type RenderToEmailHtmlOptions, renderToEmailHtml, } from './render-to-email-html.js';
|
|
3
|
+
export { createStarterDocument } from './starter-shell.js';
|
|
4
|
+
export { type Alignment, BLOCK_KINDS, type BlockKind, type BlockPropsMap, type ButtonBlockProps, type ColumnLayout, type ColumnsBlockProps, type DevicePreview, type DividerBlockProps, defaultEmailBuilderLabels, type EmailBlock, type EmailBrand, type EmailBuilderLabels, type EmailBuilderProps, type EmailDocument, type EmailEnvelope, type FooterBlockProps, type HeadingBlockProps, type ImageBlockProps, type MediaPickerContext, type SavedBlocksContext, type SocialBlockProps, type SocialLink, type SpacerBlockProps, type SpacingValue, type TextBlockProps, } from './types.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// open/meda/src/email-builder/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Subpath barrel for `@medalsocial/meda/email-builder`.
|
|
4
|
+
//
|
|
5
|
+
// Single top-level `EmailBuilder` component. Heavy/optional integrations
|
|
6
|
+
// (rich text editor, media browser, saved blocks library) are injected via
|
|
7
|
+
// render-prop slots — see `EmailBuilderProps`. Block primitives, palettes,
|
|
8
|
+
// inspectors, and renderers remain internal implementation details.
|
|
9
|
+
export { EmailBuilder } from './email-builder.js';
|
|
10
|
+
export { renderToEmailHtml, } from './render-to-email-html.js';
|
|
11
|
+
export { createStarterDocument } from './starter-shell.js';
|
|
12
|
+
export { BLOCK_KINDS, defaultEmailBuilderLabels, } from './types.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BlockKind, BlockPropsMap, EmailBlock, EmailDocument } from '../types.js';
|
|
2
|
+
export declare function addBlock(doc: EmailDocument, block: EmailBlock, atIndex?: number): EmailDocument;
|
|
3
|
+
export declare function removeBlock(doc: EmailDocument, blockId: string): EmailDocument;
|
|
4
|
+
export declare function moveBlock(doc: EmailDocument, fromIndex: number, toIndex: number): EmailDocument;
|
|
5
|
+
export declare function updateBlockProps<K extends BlockKind>(doc: EmailDocument, blockId: string, patch: Partial<BlockPropsMap[K]>): EmailDocument;
|
|
6
|
+
export declare function duplicateBlock(doc: EmailDocument, blockId: string): EmailDocument;
|
|
7
|
+
export declare function findBlock(doc: EmailDocument, blockId: string): EmailBlock | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
let __idCounter = 0;
|
|
2
|
+
function genId(prefix = 'blk') {
|
|
3
|
+
__idCounter += 1;
|
|
4
|
+
return `${prefix}_${Date.now().toString(36)}_${__idCounter.toString(36)}`;
|
|
5
|
+
}
|
|
6
|
+
export function addBlock(doc, block, atIndex) {
|
|
7
|
+
const blocks = [...doc.blocks];
|
|
8
|
+
const idx = atIndex ?? blocks.length;
|
|
9
|
+
blocks.splice(idx, 0, block);
|
|
10
|
+
return { ...doc, blocks };
|
|
11
|
+
}
|
|
12
|
+
export function removeBlock(doc, blockId) {
|
|
13
|
+
return { ...doc, blocks: doc.blocks.filter((b) => b.id !== blockId) };
|
|
14
|
+
}
|
|
15
|
+
export function moveBlock(doc, fromIndex, toIndex) {
|
|
16
|
+
if (fromIndex === toIndex)
|
|
17
|
+
return doc;
|
|
18
|
+
const blocks = [...doc.blocks];
|
|
19
|
+
const [moved] = blocks.splice(fromIndex, 1);
|
|
20
|
+
if (!moved)
|
|
21
|
+
return doc;
|
|
22
|
+
blocks.splice(toIndex, 0, moved);
|
|
23
|
+
return { ...doc, blocks };
|
|
24
|
+
}
|
|
25
|
+
export function updateBlockProps(doc, blockId, patch) {
|
|
26
|
+
return {
|
|
27
|
+
...doc,
|
|
28
|
+
blocks: doc.blocks.map((b) => b.id === blockId
|
|
29
|
+
? { ...b, props: { ...b.props, ...patch } }
|
|
30
|
+
: b),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function duplicateBlock(doc, blockId) {
|
|
34
|
+
const idx = doc.blocks.findIndex((b) => b.id === blockId);
|
|
35
|
+
if (idx === -1)
|
|
36
|
+
return doc;
|
|
37
|
+
const original = doc.blocks[idx];
|
|
38
|
+
/* v8 ignore next -- defensive guard; findIndex guarantees idx is valid */
|
|
39
|
+
if (!original)
|
|
40
|
+
return doc;
|
|
41
|
+
const clone = {
|
|
42
|
+
...original,
|
|
43
|
+
id: genId(original.kind),
|
|
44
|
+
props: { ...original.props },
|
|
45
|
+
children: original.children?.map((col) => col.map((b) => ({ ...b, id: genId(b.kind) }))),
|
|
46
|
+
};
|
|
47
|
+
const blocks = [...doc.blocks];
|
|
48
|
+
blocks.splice(idx + 1, 0, clone);
|
|
49
|
+
return { ...doc, blocks };
|
|
50
|
+
}
|
|
51
|
+
export function findBlock(doc, blockId) {
|
|
52
|
+
return doc.blocks.find((b) => b.id === blockId) ?? null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DragEndEvent } from '@dnd-kit/core';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
interface BuilderDndWrapperProps {
|
|
4
|
+
blockIds: string[];
|
|
5
|
+
onDragEnd: (event: DragEndEvent) => void;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Wraps the canvas in a vertical-list sortable DndContext. */
|
|
9
|
+
export declare function BuilderDndWrapper({ blockIds, onDragEnd, children }: BuilderDndWrapperProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core';
|
|
4
|
+
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
5
|
+
/** Wraps the canvas in a vertical-list sortable DndContext. */
|
|
6
|
+
export function BuilderDndWrapper({ blockIds, onDragEnd, children }) {
|
|
7
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor));
|
|
8
|
+
return (_jsx(DndContext, { sensors: sensors, onDragEnd: onDragEnd, children: _jsx(SortableContext, { items: blockIds, strategy: verticalListSortingStrategy, children: children }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const EMAIL_FONT_OPTIONS = [
|
|
2
|
+
{ label: 'Inherit', value: '' },
|
|
3
|
+
{
|
|
4
|
+
label: 'System',
|
|
5
|
+
value: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
6
|
+
},
|
|
7
|
+
{ label: 'Arial', value: 'Arial, Helvetica, sans-serif' },
|
|
8
|
+
{ label: 'Georgia', value: 'Georgia, "Times New Roman", serif' },
|
|
9
|
+
{ label: 'Helvetica', value: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
|
10
|
+
{ label: 'Times', value: '"Times New Roman", Times, serif' },
|
|
11
|
+
{ label: 'Courier', value: '"Courier New", Courier, monospace' },
|
|
12
|
+
];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
interface MobileDrawerProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
title: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Generic bottom-sheet drawer used for the mobile palette/inspector. */
|
|
9
|
+
export declare function MobileDrawer({ open, onOpenChange, title, children }: MobileDrawerProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../components/ui/drawer.js';
|
|
4
|
+
/** Generic bottom-sheet drawer used for the mobile palette/inspector. */
|
|
5
|
+
export function MobileDrawer({ open, onOpenChange, title, children }) {
|
|
6
|
+
return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, children: _jsxs(DrawerContent, { children: [_jsx(DrawerHeader, { children: _jsx(DrawerTitle, { children: title }) }), _jsx("div", { className: "max-h-[70vh] overflow-y-auto px-4 pb-6", children: children })] }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EmailBuilderLabels } from './types.js';
|
|
2
|
+
interface MobileTabBarProps {
|
|
3
|
+
labels: EmailBuilderLabels;
|
|
4
|
+
onOpenBlocks: () => void;
|
|
5
|
+
onOpenInspector: () => void;
|
|
6
|
+
onOpenSettings: () => void;
|
|
7
|
+
}
|
|
8
|
+
/** Bottom-of-screen tab bar visible on mobile. */
|
|
9
|
+
export declare function MobileTabBar({ labels, onOpenBlocks, onOpenInspector, onOpenSettings, }: MobileTabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { LayoutPanelLeft, Settings, SlidersHorizontal } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/** Bottom-of-screen tab bar visible on mobile. */
|
|
6
|
+
export function MobileTabBar({ labels, onOpenBlocks, onOpenInspector, onOpenSettings, }) {
|
|
7
|
+
const btnClass = 'flex flex-1 flex-col items-center justify-center gap-1 py-2 text-muted-foreground hover:text-foreground';
|
|
8
|
+
return (_jsxs("div", { "data-slot": "email-builder-mobile-tab-bar", className: cn('fixed inset-x-0 bottom-0 z-40 flex items-stretch border-border border-t bg-background md:hidden'), children: [_jsxs("button", { type: "button", onClick: onOpenBlocks, className: btnClass, children: [_jsx(LayoutPanelLeft, { className: "size-5", "aria-hidden": true }), _jsx("span", { className: "text-xs", children: labels.openBlocks })] }), _jsxs("button", { type: "button", onClick: onOpenInspector, className: btnClass, children: [_jsx(SlidersHorizontal, { className: "size-5", "aria-hidden": true }), _jsx("span", { className: "text-xs", children: labels.openInspector })] }), _jsxs("button", { type: "button", onClick: onOpenSettings, className: btnClass, children: [_jsx(Settings, { className: "size-5", "aria-hidden": true }), _jsx("span", { className: "text-xs", children: labels.openSettings })] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Alignment, SpacingValue } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a labeled control. If `htmlFor` is set, the label uses it. Otherwise
|
|
5
|
+
* the field auto-injects `aria-label` on the immediate child element so axe
|
|
6
|
+
* passes without the consumer manually wiring ids everywhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Field({ label, htmlFor, children, }: {
|
|
9
|
+
label: string;
|
|
10
|
+
htmlFor?: string;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function TextInput({ id, value, onChange, placeholder, 'aria-label': ariaLabel, }: {
|
|
14
|
+
id?: string;
|
|
15
|
+
value: string;
|
|
16
|
+
onChange: (next: string) => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
'aria-label'?: string;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare function TextAreaInput({ id, value, onChange, rows, 'aria-label': ariaLabel, }: {
|
|
21
|
+
id?: string;
|
|
22
|
+
value: string;
|
|
23
|
+
onChange: (next: string) => void;
|
|
24
|
+
rows?: number;
|
|
25
|
+
'aria-label'?: string;
|
|
26
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export declare function NumberInput({ id, value, onChange, min, max, step, 'aria-label': ariaLabel, }: {
|
|
28
|
+
id?: string;
|
|
29
|
+
value: number;
|
|
30
|
+
onChange: (next: number) => void;
|
|
31
|
+
min?: number;
|
|
32
|
+
max?: number;
|
|
33
|
+
step?: number;
|
|
34
|
+
'aria-label'?: string;
|
|
35
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export declare function ColorInput({ id, value, onChange, }: {
|
|
37
|
+
id?: string;
|
|
38
|
+
value: string;
|
|
39
|
+
onChange: (next: string) => void;
|
|
40
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function SelectInput<T extends string>({ id, value, options, onChange, 'aria-label': ariaLabel, }: {
|
|
42
|
+
id?: string;
|
|
43
|
+
value: T;
|
|
44
|
+
options: {
|
|
45
|
+
value: T;
|
|
46
|
+
label: string;
|
|
47
|
+
}[];
|
|
48
|
+
onChange: (next: T) => void;
|
|
49
|
+
'aria-label'?: string;
|
|
50
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
export declare function AlignmentToggle({ value, onChange, }: {
|
|
52
|
+
value: Alignment;
|
|
53
|
+
onChange: (next: Alignment) => void;
|
|
54
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
55
|
+
export declare function SpacingEditor({ value, onChange, }: {
|
|
56
|
+
value: SpacingValue;
|
|
57
|
+
onChange: (next: SpacingValue) => void;
|
|
58
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, isValidElement } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a labeled control. If `htmlFor` is set, the label uses it. Otherwise
|
|
7
|
+
* the field auto-injects `aria-label` on the immediate child element so axe
|
|
8
|
+
* passes without the consumer manually wiring ids everywhere.
|
|
9
|
+
*/
|
|
10
|
+
export function Field({ label, htmlFor, children, }) {
|
|
11
|
+
const enhanced = !htmlFor && isValidElement(children)
|
|
12
|
+
? cloneElement(children, {
|
|
13
|
+
'aria-label': label,
|
|
14
|
+
})
|
|
15
|
+
: children;
|
|
16
|
+
return (_jsxs("div", { "data-slot": "email-builder-field", className: "flex flex-col gap-1.5", children: [_jsx("label", { htmlFor: htmlFor, className: "font-medium text-foreground text-xs", children: label }), enhanced] }));
|
|
17
|
+
}
|
|
18
|
+
export function TextInput({ id, value, onChange, placeholder, 'aria-label': ariaLabel, }) {
|
|
19
|
+
return (_jsx("input", { id: id, type: "text", value: value, placeholder: placeholder, "aria-label": ariaLabel, onChange: (e) => onChange(e.target.value), className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" }));
|
|
20
|
+
}
|
|
21
|
+
export function TextAreaInput({ id, value, onChange, rows = 4, 'aria-label': ariaLabel, }) {
|
|
22
|
+
return (_jsx("textarea", { id: id, value: value, rows: rows, "aria-label": ariaLabel, onChange: (e) => onChange(e.target.value), className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" }));
|
|
23
|
+
}
|
|
24
|
+
export function NumberInput({ id, value, onChange, min, max, step = 1, 'aria-label': ariaLabel, }) {
|
|
25
|
+
return (_jsx("input", { id: id, type: "number", value: value, min: min, max: max, step: step, "aria-label": ariaLabel, onChange: (e) => {
|
|
26
|
+
const n = Number(e.target.value);
|
|
27
|
+
/* v8 ignore next -- number inputs sanitize non-numeric input in real browsers; jsdom always coerces to 0 */
|
|
28
|
+
if (!Number.isNaN(n))
|
|
29
|
+
onChange(n);
|
|
30
|
+
}, className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" }));
|
|
31
|
+
}
|
|
32
|
+
export function ColorInput({ id, value, onChange, }) {
|
|
33
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: id, type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), className: "h-8 w-10 cursor-pointer rounded border border-input bg-transparent", "aria-label": "Color picker" }), _jsx("input", { type: "text", value: value, "aria-label": "Color value (hex)", onChange: (e) => onChange(e.target.value), className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" })] }));
|
|
34
|
+
}
|
|
35
|
+
export function SelectInput({ id, value, options, onChange, 'aria-label': ariaLabel, }) {
|
|
36
|
+
return (_jsx("select", { id: id, value: value, "aria-label": ariaLabel, onChange: (e) => onChange(e.target.value), className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring", children: options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) }));
|
|
37
|
+
}
|
|
38
|
+
export function AlignmentToggle({ value, onChange, }) {
|
|
39
|
+
const options = ['left', 'center', 'right'];
|
|
40
|
+
return (_jsx("div", { role: "radiogroup", "aria-label": "Alignment", className: "inline-flex rounded-md border border-input p-0.5", children: options.map((opt) => (_jsx("button", { type: "button", role: "radio", "aria-checked": value === opt, onClick: () => onChange(opt), className: cn('rounded px-3 py-1.5 text-xs capitalize', value === opt
|
|
41
|
+
? 'bg-primary text-primary-foreground'
|
|
42
|
+
: 'text-muted-foreground hover:bg-accent'), children: opt }, opt))) }));
|
|
43
|
+
}
|
|
44
|
+
export function SpacingEditor({ value, onChange, }) {
|
|
45
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-2", children: ['top', 'right', 'bottom', 'left'].map((side) => {
|
|
46
|
+
const label = (side[0]?.toUpperCase() + side.slice(1));
|
|
47
|
+
return (_jsxs("div", { "data-slot": "email-builder-field", className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "font-medium text-foreground text-xs", children: label }), _jsx("input", { type: "number", value: value[side], min: 0, "aria-label": `${label} padding`, onChange: (e) => {
|
|
48
|
+
const n = Number(e.target.value);
|
|
49
|
+
/* v8 ignore next -- number inputs sanitize non-numeric input in real browsers; jsdom always coerces to 0 */
|
|
50
|
+
if (!Number.isNaN(n))
|
|
51
|
+
onChange({ ...value, [side]: n });
|
|
52
|
+
}, className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" })] }, side));
|
|
53
|
+
}) }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ButtonBlockProps } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: ButtonBlockProps;
|
|
4
|
+
onChange: (patch: Partial<ButtonBlockProps>) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function ButtonPropertyEditor({ value, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlignmentToggle, ColorInput, Field, NumberInput, SelectInput, SpacingEditor, TextInput, } from './_shared.js';
|
|
4
|
+
export function ButtonPropertyEditor({ value, onChange }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "email-builder-property-button", className: "flex flex-col gap-3", children: [_jsx(Field, { label: "Text", htmlFor: "btn-text", children: _jsx(TextInput, { id: "btn-text", value: value.text, onChange: (text) => onChange({ text }) }) }), _jsx(Field, { label: "URL", htmlFor: "btn-url", children: _jsx(TextInput, { id: "btn-url", value: value.url, onChange: (url) => onChange({ url }) }) }), _jsx(Field, { label: "Background color", children: _jsx(ColorInput, { value: value.backgroundColor, onChange: (backgroundColor) => onChange({ backgroundColor }) }) }), _jsx(Field, { label: "Text color", children: _jsx(ColorInput, { value: value.textColor, onChange: (textColor) => onChange({ textColor }) }) }), _jsx(Field, { label: "Border radius (px)", children: _jsx(NumberInput, { value: value.borderRadius, min: 0, max: 64, onChange: (borderRadius) => onChange({ borderRadius }) }) }), _jsx(Field, { label: "Size", children: _jsx(SelectInput, { value: value.size, options: [
|
|
6
|
+
{ value: 'sm', label: 'Small' },
|
|
7
|
+
{ value: 'md', label: 'Medium' },
|
|
8
|
+
{ value: 'lg', label: 'Large' },
|
|
9
|
+
], onChange: (size) => onChange({ size }) }) }), _jsx(Field, { label: "Alignment", children: _jsx(AlignmentToggle, { value: value.alignment, onChange: (alignment) => onChange({ alignment }) }) }), _jsx(Field, { label: "Full width", children: _jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.fullWidth, onChange: (e) => onChange({ fullWidth: e.target.checked }) }), "Stretch to container width"] }) }), _jsx(Field, { label: "Padding", children: _jsx(SpacingEditor, { value: value.padding, onChange: (padding) => onChange({ padding }) }) })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ColumnsBlockProps } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: ColumnsBlockProps;
|
|
4
|
+
onChange: (patch: Partial<ColumnsBlockProps>) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function ColumnsPropertyEditor({ value, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ColorInput, Field, NumberInput, SelectInput, SpacingEditor } from './_shared.js';
|
|
4
|
+
export function ColumnsPropertyEditor({ value, onChange }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "email-builder-property-columns", className: "flex flex-col gap-3", children: [_jsx(Field, { label: "Layout", children: _jsx(SelectInput, { value: value.layout, options: [
|
|
6
|
+
{ value: '50-50', label: '2 columns (50/50)' },
|
|
7
|
+
{ value: '33-67', label: '2 columns (33/67)' },
|
|
8
|
+
{ value: '67-33', label: '2 columns (67/33)' },
|
|
9
|
+
{ value: '33-33-33', label: '3 columns' },
|
|
10
|
+
], onChange: (layout) => onChange({ layout }) }) }), _jsx(Field, { label: "Vertical alignment", children: _jsx(SelectInput, { value: value.verticalAlignment, options: [
|
|
11
|
+
{ value: 'top', label: 'Top' },
|
|
12
|
+
{ value: 'middle', label: 'Middle' },
|
|
13
|
+
{ value: 'bottom', label: 'Bottom' },
|
|
14
|
+
], onChange: (verticalAlignment) => onChange({ verticalAlignment }) }) }), _jsx(Field, { label: "Background color", children: _jsx(ColorInput, { value: value.backgroundColor === 'transparent' ? '#ffffff' : value.backgroundColor, onChange: (backgroundColor) => onChange({ backgroundColor }) }) }), _jsx(Field, { label: "Gap (px)", children: _jsx(NumberInput, { value: value.gap, min: 0, max: 64, onChange: (gap) => onChange({ gap }) }) }), _jsx(Field, { label: "Mobile stacking", children: _jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.mobileStacking, onChange: (e) => onChange({ mobileStacking: e.target.checked }) }), "Stack columns vertically on mobile"] }) }), _jsx(Field, { label: "Padding", children: _jsx(SpacingEditor, { value: value.padding, onChange: (padding) => onChange({ padding }) }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DividerBlockProps } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: DividerBlockProps;
|
|
4
|
+
onChange: (patch: Partial<DividerBlockProps>) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function DividerPropertyEditor({ value, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ColorInput, Field, NumberInput, SelectInput, SpacingEditor } from './_shared.js';
|
|
4
|
+
export function DividerPropertyEditor({ value, onChange }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "email-builder-property-divider", className: "flex flex-col gap-3", children: [_jsx(Field, { label: "Color", children: _jsx(ColorInput, { value: value.color, onChange: (color) => onChange({ color }) }) }), _jsx(Field, { label: "Thickness (px)", children: _jsx(NumberInput, { value: value.thickness, min: 1, max: 20, onChange: (thickness) => onChange({ thickness }) }) }), _jsx(Field, { label: "Width (%)", children: _jsx(NumberInput, { value: value.width, min: 1, max: 100, onChange: (width) => onChange({ width }) }) }), _jsx(Field, { label: "Style", children: _jsx(SelectInput, { value: value.style, options: [
|
|
6
|
+
{ value: 'solid', label: 'Solid' },
|
|
7
|
+
{ value: 'dashed', label: 'Dashed' },
|
|
8
|
+
{ value: 'dotted', label: 'Dotted' },
|
|
9
|
+
], onChange: (style) => onChange({ style }) }) }), _jsx(Field, { label: "Padding", children: _jsx(SpacingEditor, { value: value.padding, onChange: (padding) => onChange({ padding }) }) })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FooterBlockProps } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: FooterBlockProps;
|
|
4
|
+
onChange: (patch: Partial<FooterBlockProps>) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function FooterPropertyEditor({ value, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlignmentToggle, ColorInput, Field, NumberInput, SpacingEditor, TextAreaInput, TextInput, } from './_shared.js';
|
|
4
|
+
export function FooterPropertyEditor({ value, onChange }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "email-builder-property-footer", className: "flex flex-col gap-3", children: [_jsx(Field, { label: "Company name", children: _jsx(TextInput, { value: value.companyName, onChange: (companyName) => onChange({ companyName }) }) }), _jsx(Field, { label: "Address", children: _jsx(TextInput, { value: value.address, onChange: (address) => onChange({ address }) }) }), _jsx(Field, { label: "Custom text", children: _jsx(TextAreaInput, { value: value.customText, onChange: (customText) => onChange({ customText }) }) }), _jsx(Field, { label: "Unsubscribe link text", children: _jsx(TextInput, { value: value.unsubscribeText, onChange: (unsubscribeText) => onChange({ unsubscribeText }) }) }), _jsx(Field, { label: "Text color", children: _jsx(ColorInput, { value: value.textColor, onChange: (textColor) => onChange({ textColor }) }) }), _jsx(Field, { label: "Font size (px)", children: _jsx(NumberInput, { value: value.fontSize, min: 8, max: 20, onChange: (fontSize) => onChange({ fontSize }) }) }), _jsx(Field, { label: "Alignment", children: _jsx(AlignmentToggle, { value: value.alignment, onChange: (alignment) => onChange({ alignment }) }) }), _jsx(Field, { label: "Padding", children: _jsx(SpacingEditor, { value: value.padding, onChange: (padding) => onChange({ padding }) }) })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HeadingBlockProps } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: HeadingBlockProps;
|
|
4
|
+
onChange: (patch: Partial<HeadingBlockProps>) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function HeadingPropertyEditor({ value, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|