@medalsocial/meda 2.0.0 → 2.1.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 +20 -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 +156 -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 +26 -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 +19 -0
- package/dist/email-builder/email-builder.d.ts +3 -0
- package/dist/email-builder/email-builder.js +84 -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 +52 -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 +52 -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 +219 -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/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 +31 -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 +32 -0
- package/dist/post-preview/platforms/facebook.d.ts +23 -0
- package/dist/post-preview/platforms/facebook.js +24 -0
- package/dist/post-preview/platforms/generic.d.ts +22 -0
- package/dist/post-preview/platforms/generic.js +28 -0
- package/dist/post-preview/platforms/google-business.d.ts +18 -0
- package/dist/post-preview/platforms/google-business.js +22 -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 +99 -0
- package/dist/post-preview/platforms/linkedin.d.ts +52 -0
- package/dist/post-preview/platforms/linkedin.js +131 -0
- package/dist/post-preview/platforms/telegram.d.ts +46 -0
- package/dist/post-preview/platforms/telegram.js +42 -0
- package/dist/post-preview/platforms/threads.d.ts +19 -0
- package/dist/post-preview/platforms/threads.js +24 -0
- package/dist/post-preview/platforms/tiktok.d.ts +18 -0
- package/dist/post-preview/platforms/tiktok.js +23 -0
- package/dist/post-preview/platforms/twitter.d.ts +40 -0
- package/dist/post-preview/platforms/twitter.js +132 -0
- package/dist/post-preview/platforms/youtube.d.ts +17 -0
- package/dist/post-preview/platforms/youtube.js +21 -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/extras/shell-scrollable-content.js +2 -2
- package/dist/shell/extras/workbench-layout.js +24 -13
- package/dist/shell/right-panel.js +10 -1
- package/dist/shell/shell-header.js +1 -1
- package/dist/shell/theme-next-themes.js +2 -2
- package/dist/shell/theme.js +7 -1
- 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 +25 -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 +31 -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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { formatDayOfWeek, formatHour, getDateKey, getHoursOfDay, getWeekDays, isToday, } from './internal/date.js';
|
|
6
|
+
import { TimeSlot } from './internal/time-slot.js';
|
|
7
|
+
import { DEFAULT_CALENDAR_LABELS } from './types.js';
|
|
8
|
+
export function WeekView({ date, events, onEventClick, onSlotClick, onDateClick, weekStartsOn = 0, locale = 'en-US', labels: labelOverrides, className, renderEvent, }) {
|
|
9
|
+
const labels = { ...DEFAULT_CALENDAR_LABELS, ...labelOverrides };
|
|
10
|
+
const weekDays = useMemo(() => getWeekDays(date, weekStartsOn), [date, weekStartsOn]);
|
|
11
|
+
const hours = useMemo(() => getHoursOfDay(), []);
|
|
12
|
+
const eventsByDateHour = useMemo(() => {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const event of events) {
|
|
15
|
+
if (event.allDay)
|
|
16
|
+
continue;
|
|
17
|
+
const dateKey = getDateKey(event.start);
|
|
18
|
+
const hour = event.start.getHours();
|
|
19
|
+
const key = `${dateKey}-${hour}`;
|
|
20
|
+
const existing = map.get(key) ?? [];
|
|
21
|
+
existing.push(event);
|
|
22
|
+
map.set(key, existing);
|
|
23
|
+
}
|
|
24
|
+
for (const [key, slot] of map.entries()) {
|
|
25
|
+
slot.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
26
|
+
map.set(key, slot);
|
|
27
|
+
}
|
|
28
|
+
return map;
|
|
29
|
+
}, [events]);
|
|
30
|
+
return (_jsxs("section", { "data-slot": "calendar-week-view", "aria-label": labels.weekView, className: cn('@container/calendar overflow-hidden rounded-lg border border-border bg-background', className), children: [_jsxs("div", { className: "sticky top-0 z-10 grid grid-cols-[64px_repeat(7,minmax(0,1fr))] border-border border-b bg-muted/50", children: [_jsx("div", { className: "border-border border-r px-2 py-3" }), weekDays.map((d) => {
|
|
31
|
+
const isTodayDate = isToday(d);
|
|
32
|
+
const isInteractive = typeof onDateClick === 'function';
|
|
33
|
+
const dayLabel = formatDayOfWeek(d, locale, 'short');
|
|
34
|
+
const node = (_jsxs(_Fragment, { children: [_jsx("div", { className: "font-medium text-muted-foreground text-[11px]", children: dayLabel }), _jsx("div", { className: cn('mt-0.5 font-semibold text-base', isTodayDate ? 'text-primary' : 'text-foreground'), children: d.getDate() })] }));
|
|
35
|
+
return isInteractive ? (_jsx("button", { type: "button", onClick: () => onDateClick?.(d), className: cn('min-h-[44px] border-border border-r px-2 py-3 text-center transition-colors last:border-r-0', 'hover:bg-accent/50', isTodayDate && 'bg-primary/5'), children: node }, `header-${getDateKey(d)}`)) : (_jsx("div", { className: cn('min-h-[44px] border-border border-r px-2 py-3 text-center last:border-r-0', isTodayDate && 'bg-primary/5'), children: node }, `header-${getDateKey(d)}`));
|
|
36
|
+
})] }), _jsx("div", {
|
|
37
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable region needs keyboard focus per WCAG (axe scrollable-region-focusable)
|
|
38
|
+
tabIndex: 0, className: "max-h-[36rem] overflow-y-auto focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: hours.map((hour) => (_jsxs("div", { className: "grid grid-cols-[64px_repeat(7,minmax(0,1fr))]", children: [_jsx("div", { className: "border-border border-r border-b px-2 py-2 text-right text-muted-foreground text-[11px]", children: formatHour(hour, locale) }), weekDays.map((d) => (_jsx(TimeSlot, { date: d, hour: hour, events: eventsByDateHour.get(`${getDateKey(d)}-${hour}`) ?? [], onSlotClick: onSlotClick, onEventClick: onEventClick, renderEvent: renderEvent, variant: "week" }, `slot-${getDateKey(d)}-${hour}`)))] }, `row-${hour}`))) })] }));
|
|
39
|
+
}
|
package/dist/chat/turn-card.js
CHANGED
|
@@ -13,11 +13,16 @@ export function TurnCard({ turn, startedAtRef, tz, onPlay, className }) {
|
|
|
13
13
|
const offset = turn.startedAt - startedAtRef;
|
|
14
14
|
// amber-700 (not -500) for the 11px system speaker label so it clears
|
|
15
15
|
// WCAG AA (4.5:1) against the card background.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
let speakerColor;
|
|
17
|
+
if (turn.speaker === 'assistant') {
|
|
18
|
+
speakerColor = 'text-primary';
|
|
19
|
+
}
|
|
20
|
+
else if (turn.speaker === 'system') {
|
|
21
|
+
speakerColor = 'text-amber-700';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
speakerColor = 'text-foreground';
|
|
25
|
+
}
|
|
21
26
|
return (_jsxs("div", { className: [
|
|
22
27
|
'grid grid-cols-[64px_1fr] gap-5 border-b border-border py-4 last:border-b-0',
|
|
23
28
|
className ?? '',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
blockId: string;
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
interface State {
|
|
7
|
+
error: Error | null;
|
|
8
|
+
}
|
|
9
|
+
/** Catches render errors inside an individual block so a broken block doesn't tear down the canvas. */
|
|
10
|
+
export declare class BlockErrorBoundary extends Component<Props, State> {
|
|
11
|
+
state: State;
|
|
12
|
+
static getDerivedStateFromError(error: Error): State;
|
|
13
|
+
componentDidCatch(_error: Error): void;
|
|
14
|
+
render(): ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Component } from 'react';
|
|
4
|
+
/** Catches render errors inside an individual block so a broken block doesn't tear down the canvas. */
|
|
5
|
+
export class BlockErrorBoundary extends Component {
|
|
6
|
+
state = { error: null };
|
|
7
|
+
static getDerivedStateFromError(error) {
|
|
8
|
+
return { error };
|
|
9
|
+
}
|
|
10
|
+
componentDidCatch(_error) {
|
|
11
|
+
// No-op. Consumers can subclass or wrap to add custom logging — we don't
|
|
12
|
+
// want to call console here (and trip the lint rule) in a public package.
|
|
13
|
+
}
|
|
14
|
+
render() {
|
|
15
|
+
if (this.state.error) {
|
|
16
|
+
return (_jsxs("div", { "data-slot": "email-builder-block-error", className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-destructive text-sm", role: "alert", children: ["Block failed to render: ", this.state.error.message] }));
|
|
17
|
+
}
|
|
18
|
+
return this.props.children;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BlockKind } from './types.js';
|
|
2
|
+
interface BlockPaletteProps {
|
|
3
|
+
onPick: (kind: BlockKind) => void;
|
|
4
|
+
}
|
|
5
|
+
/** Vertical button list of all available block kinds. */
|
|
6
|
+
export declare function BlockPalette({ onPick }: BlockPaletteProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlignLeft, Columns2, Image, Minus, MousePointerClick, Share2, Space, Type, } from 'lucide-react';
|
|
4
|
+
import { BLOCK_REGISTRY } from './block-registry.js';
|
|
5
|
+
const ICONS = {
|
|
6
|
+
heading: Type,
|
|
7
|
+
text: Type,
|
|
8
|
+
image: Image,
|
|
9
|
+
button: MousePointerClick,
|
|
10
|
+
divider: Minus,
|
|
11
|
+
spacer: Space,
|
|
12
|
+
columns: Columns2,
|
|
13
|
+
social: Share2,
|
|
14
|
+
footer: AlignLeft,
|
|
15
|
+
};
|
|
16
|
+
/** Vertical button list of all available block kinds. */
|
|
17
|
+
export function BlockPalette({ onPick }) {
|
|
18
|
+
return (_jsx("div", { "data-slot": "email-builder-palette", className: "flex flex-col gap-1.5 p-2", children: BLOCK_REGISTRY.map((meta) => {
|
|
19
|
+
const Icon = ICONS[meta.kind];
|
|
20
|
+
return (_jsxs("button", { type: "button", onClick: () => onPick(meta.kind), "data-slot": "email-builder-palette-item", "data-block-kind": meta.kind, className: "flex min-h-[44px] items-center gap-3 rounded-md border border-input bg-background p-2 text-left hover:bg-accent hover:text-accent-foreground", children: [_jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium text-sm", children: meta.label }), _jsx("div", { className: "truncate text-muted-foreground text-xs", children: meta.description })] })] }, meta.kind));
|
|
21
|
+
}) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Alignment, BlockKind, BlockPropsMap, ColumnLayout, EmailBlock } from './types.js';
|
|
2
|
+
export declare function getDefaultBlockProps(): {
|
|
3
|
+
[K in BlockKind]: BlockPropsMap[K];
|
|
4
|
+
};
|
|
5
|
+
export declare const COLUMN_WIDTHS: Record<ColumnLayout, number[]>;
|
|
6
|
+
export declare function getColumnWidths(layout: ColumnLayout): number[];
|
|
7
|
+
export declare function createBlock<K extends BlockKind>(kind: K, overrides?: Partial<BlockPropsMap[K]>): EmailBlock<K>;
|
|
8
|
+
export interface BlockMeta {
|
|
9
|
+
kind: BlockKind;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
category: 'content' | 'layout' | 'marketing';
|
|
13
|
+
}
|
|
14
|
+
export declare const BLOCK_REGISTRY: BlockMeta[];
|
|
15
|
+
export declare const ALIGNMENT_OPTIONS: Alignment[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const DEFAULT_PADDING = { top: 12, right: 24, bottom: 12, left: 24 };
|
|
2
|
+
let __idCounter = 0;
|
|
3
|
+
function genId(prefix = 'blk') {
|
|
4
|
+
__idCounter += 1;
|
|
5
|
+
// Prefix + epoch + counter is enough for stable in-memory uniqueness.
|
|
6
|
+
return `${prefix}_${Date.now().toString(36)}_${__idCounter.toString(36)}`;
|
|
7
|
+
}
|
|
8
|
+
export function getDefaultBlockProps() {
|
|
9
|
+
const heading = {
|
|
10
|
+
text: 'Your headline',
|
|
11
|
+
level: 2,
|
|
12
|
+
alignment: 'left',
|
|
13
|
+
color: '#111827',
|
|
14
|
+
fontFamily: '',
|
|
15
|
+
fontWeight: 600,
|
|
16
|
+
fontSize: 28,
|
|
17
|
+
padding: { ...DEFAULT_PADDING },
|
|
18
|
+
};
|
|
19
|
+
const text = {
|
|
20
|
+
content: 'Tell your readers what this email is about.',
|
|
21
|
+
alignment: 'left',
|
|
22
|
+
color: '#374151',
|
|
23
|
+
fontFamily: '',
|
|
24
|
+
fontWeight: 400,
|
|
25
|
+
fontSize: 16,
|
|
26
|
+
padding: { ...DEFAULT_PADDING },
|
|
27
|
+
};
|
|
28
|
+
const image = {
|
|
29
|
+
src: '',
|
|
30
|
+
alt: 'Image',
|
|
31
|
+
linkUrl: '',
|
|
32
|
+
width: 600,
|
|
33
|
+
height: 'auto',
|
|
34
|
+
alignment: 'center',
|
|
35
|
+
padding: { ...DEFAULT_PADDING },
|
|
36
|
+
};
|
|
37
|
+
const button = {
|
|
38
|
+
text: 'Click me',
|
|
39
|
+
url: 'https://',
|
|
40
|
+
backgroundColor: '#4F46E5',
|
|
41
|
+
textColor: '#ffffff',
|
|
42
|
+
borderRadius: 8,
|
|
43
|
+
size: 'md',
|
|
44
|
+
alignment: 'center',
|
|
45
|
+
fullWidth: false,
|
|
46
|
+
padding: { ...DEFAULT_PADDING },
|
|
47
|
+
};
|
|
48
|
+
const divider = {
|
|
49
|
+
color: '#e5e7eb',
|
|
50
|
+
thickness: 1,
|
|
51
|
+
width: 100,
|
|
52
|
+
style: 'solid',
|
|
53
|
+
padding: { ...DEFAULT_PADDING },
|
|
54
|
+
};
|
|
55
|
+
const spacer = { height: 24 };
|
|
56
|
+
const columns = {
|
|
57
|
+
layout: '50-50',
|
|
58
|
+
verticalAlignment: 'top',
|
|
59
|
+
backgroundColor: 'transparent',
|
|
60
|
+
gap: 16,
|
|
61
|
+
mobileStacking: true,
|
|
62
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
63
|
+
};
|
|
64
|
+
const social = {
|
|
65
|
+
links: [
|
|
66
|
+
{ platform: 'twitter', url: 'https://twitter.com/' },
|
|
67
|
+
{ platform: 'linkedin', url: 'https://linkedin.com/' },
|
|
68
|
+
],
|
|
69
|
+
iconSize: 24,
|
|
70
|
+
spacing: 8,
|
|
71
|
+
alignment: 'center',
|
|
72
|
+
padding: { ...DEFAULT_PADDING },
|
|
73
|
+
};
|
|
74
|
+
const footer = {
|
|
75
|
+
companyName: 'Your Company',
|
|
76
|
+
address: '123 Main St, City, Country',
|
|
77
|
+
customText: 'You received this email because you signed up at example.com.',
|
|
78
|
+
unsubscribeText: 'Unsubscribe',
|
|
79
|
+
textColor: '#6b7280',
|
|
80
|
+
fontSize: 12,
|
|
81
|
+
alignment: 'center',
|
|
82
|
+
padding: { top: 24, right: 24, bottom: 24, left: 24 },
|
|
83
|
+
};
|
|
84
|
+
return { heading, text, image, button, divider, spacer, columns, social, footer };
|
|
85
|
+
}
|
|
86
|
+
export const COLUMN_WIDTHS = {
|
|
87
|
+
'50-50': [50, 50],
|
|
88
|
+
'33-67': [33, 67],
|
|
89
|
+
'67-33': [67, 33],
|
|
90
|
+
'33-33-33': [33, 33, 34],
|
|
91
|
+
};
|
|
92
|
+
export function getColumnWidths(layout) {
|
|
93
|
+
return COLUMN_WIDTHS[layout] ?? [100];
|
|
94
|
+
}
|
|
95
|
+
export function createBlock(kind, overrides) {
|
|
96
|
+
const defaults = { ...getDefaultBlockProps()[kind] };
|
|
97
|
+
const props = overrides ? { ...defaults, ...overrides } : defaults;
|
|
98
|
+
const block = { id: genId(kind), kind, props };
|
|
99
|
+
if (kind === 'columns') {
|
|
100
|
+
const layout = props.layout;
|
|
101
|
+
block.children = Array.from({ length: getColumnWidths(layout).length }, () => []);
|
|
102
|
+
}
|
|
103
|
+
return block;
|
|
104
|
+
}
|
|
105
|
+
export const BLOCK_REGISTRY = [
|
|
106
|
+
{
|
|
107
|
+
kind: 'heading',
|
|
108
|
+
label: 'Heading',
|
|
109
|
+
description: 'Section headline.',
|
|
110
|
+
category: 'content',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
kind: 'text',
|
|
114
|
+
label: 'Text',
|
|
115
|
+
description: 'A paragraph of text.',
|
|
116
|
+
category: 'content',
|
|
117
|
+
},
|
|
118
|
+
{ kind: 'image', label: 'Image', description: 'An inline image.', category: 'content' },
|
|
119
|
+
{
|
|
120
|
+
kind: 'button',
|
|
121
|
+
label: 'Button',
|
|
122
|
+
description: 'Call-to-action button.',
|
|
123
|
+
category: 'content',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
kind: 'divider',
|
|
127
|
+
label: 'Divider',
|
|
128
|
+
description: 'A horizontal divider.',
|
|
129
|
+
category: 'layout',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
kind: 'spacer',
|
|
133
|
+
label: 'Spacer',
|
|
134
|
+
description: 'Vertical whitespace.',
|
|
135
|
+
category: 'layout',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
kind: 'columns',
|
|
139
|
+
label: 'Columns',
|
|
140
|
+
description: 'Multi-column layout.',
|
|
141
|
+
category: 'layout',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
kind: 'social',
|
|
145
|
+
label: 'Social',
|
|
146
|
+
description: 'Social media icons.',
|
|
147
|
+
category: 'marketing',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
kind: 'footer',
|
|
151
|
+
label: 'Footer',
|
|
152
|
+
description: 'Email footer with unsubscribe link.',
|
|
153
|
+
category: 'marketing',
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
export const ALIGNMENT_OPTIONS = ['left', 'center', 'right'];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { BlockErrorBoundary } from './block-error-boundary.js';
|
|
4
|
+
import { ButtonBlock } from './block-renderers/button.js';
|
|
5
|
+
import { ColumnsBlock } from './block-renderers/columns.js';
|
|
6
|
+
import { DividerBlock } from './block-renderers/divider.js';
|
|
7
|
+
import { FooterBlock } from './block-renderers/footer.js';
|
|
8
|
+
import { HeadingBlock } from './block-renderers/heading.js';
|
|
9
|
+
import { ImageBlock } from './block-renderers/image.js';
|
|
10
|
+
import { SocialBlock } from './block-renderers/social.js';
|
|
11
|
+
import { SpacerBlock } from './block-renderers/spacer.js';
|
|
12
|
+
import { TextBlock } from './block-renderers/text.js';
|
|
13
|
+
/** Pure renderer — turns an EmailBlock into preview React. No selection chrome. */
|
|
14
|
+
export function BlockRenderer({ block }) {
|
|
15
|
+
return (_jsx(BlockErrorBoundary, { blockId: block.id, children: _jsx(BlockBody, { block: block }) }));
|
|
16
|
+
}
|
|
17
|
+
function BlockBody({ block }) {
|
|
18
|
+
switch (block.kind) {
|
|
19
|
+
case 'heading':
|
|
20
|
+
return _jsx(HeadingBlock, { props: block.props });
|
|
21
|
+
case 'text':
|
|
22
|
+
return _jsx(TextBlock, { props: block.props });
|
|
23
|
+
case 'image':
|
|
24
|
+
return _jsx(ImageBlock, { props: block.props });
|
|
25
|
+
case 'button':
|
|
26
|
+
return _jsx(ButtonBlock, { props: block.props });
|
|
27
|
+
case 'divider':
|
|
28
|
+
return _jsx(DividerBlock, { props: block.props });
|
|
29
|
+
case 'spacer':
|
|
30
|
+
return _jsx(SpacerBlock, { props: block.props });
|
|
31
|
+
case 'columns':
|
|
32
|
+
return (_jsx(ColumnsBlock, { props: block.props, columnChildren: block.children }));
|
|
33
|
+
case 'social':
|
|
34
|
+
return _jsx(SocialBlock, { props: block.props });
|
|
35
|
+
case 'footer':
|
|
36
|
+
return _jsx(FooterBlock, { props: block.props });
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
const SIZES = {
|
|
4
|
+
sm: { paddingX: 16, paddingY: 8, fontSize: 14 },
|
|
5
|
+
md: { paddingX: 24, paddingY: 12, fontSize: 16 },
|
|
6
|
+
lg: { paddingX: 32, paddingY: 16, fontSize: 18 },
|
|
7
|
+
};
|
|
8
|
+
export function ButtonBlock({ props }) {
|
|
9
|
+
const s = SIZES[props.size];
|
|
10
|
+
const wrapperStyle = {
|
|
11
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
12
|
+
textAlign: props.alignment,
|
|
13
|
+
};
|
|
14
|
+
const linkStyle = {
|
|
15
|
+
display: 'inline-block',
|
|
16
|
+
backgroundColor: props.backgroundColor,
|
|
17
|
+
color: props.textColor,
|
|
18
|
+
borderRadius: props.borderRadius,
|
|
19
|
+
padding: `${s.paddingY}px ${s.paddingX}px`,
|
|
20
|
+
fontSize: s.fontSize,
|
|
21
|
+
fontWeight: 600,
|
|
22
|
+
textDecoration: 'none',
|
|
23
|
+
width: props.fullWidth ? '100%' : undefined,
|
|
24
|
+
};
|
|
25
|
+
return (_jsx("div", { "data-slot": "email-block-button", style: wrapperStyle, children: _jsx("a", { href: props.url, style: linkStyle, rel: "noopener noreferrer", children: props.text }) }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { getColumnWidths } from '../block-registry.js';
|
|
4
|
+
import { BlockRenderer } from '../block-renderer.js';
|
|
5
|
+
function getAlignItems(alignment) {
|
|
6
|
+
if (alignment === 'top')
|
|
7
|
+
return 'flex-start';
|
|
8
|
+
if (alignment === 'middle')
|
|
9
|
+
return 'center';
|
|
10
|
+
return 'flex-end';
|
|
11
|
+
}
|
|
12
|
+
export function ColumnsBlock({ props, columnChildren, }) {
|
|
13
|
+
const widths = getColumnWidths(props.layout);
|
|
14
|
+
const wrapperStyle = {
|
|
15
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
16
|
+
backgroundColor: props.backgroundColor !== 'transparent' ? props.backgroundColor : undefined,
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: props.mobileStacking ? undefined : 'row',
|
|
19
|
+
gap: props.gap,
|
|
20
|
+
alignItems: getAlignItems(props.verticalAlignment),
|
|
21
|
+
flexWrap: 'wrap',
|
|
22
|
+
};
|
|
23
|
+
return (_jsx("div", { "data-slot": "email-block-columns", style: wrapperStyle, children: widths.map((w, i) => (_jsx("div", { "data-slot": "email-block-columns-cell", style: {
|
|
24
|
+
flex: `0 0 calc(${w}% - ${(props.gap * (widths.length - 1)) / widths.length}px)`,
|
|
25
|
+
minWidth: 120,
|
|
26
|
+
}, children: (columnChildren?.[i] ?? []).map((b) => (_jsx(BlockRenderer, { block: b }, b.id))) }, i))) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
export function DividerBlock({ props }) {
|
|
4
|
+
const wrapperStyle = {
|
|
5
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
6
|
+
};
|
|
7
|
+
const ruleStyle = {
|
|
8
|
+
width: `${props.width}%`,
|
|
9
|
+
margin: '0 auto',
|
|
10
|
+
borderTop: `${props.thickness}px ${props.style} ${props.color}`,
|
|
11
|
+
height: 0,
|
|
12
|
+
};
|
|
13
|
+
return (_jsx("div", { "data-slot": "email-block-divider", style: wrapperStyle, children: _jsx("div", { style: ruleStyle }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function FooterBlock({ props }) {
|
|
4
|
+
const wrapperStyle = {
|
|
5
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
6
|
+
textAlign: props.alignment,
|
|
7
|
+
color: props.textColor,
|
|
8
|
+
fontSize: props.fontSize,
|
|
9
|
+
lineHeight: 1.5,
|
|
10
|
+
};
|
|
11
|
+
return (_jsxs("div", { "data-slot": "email-block-footer", style: wrapperStyle, children: [props.companyName ? _jsx("div", { children: props.companyName }) : null, props.address ? _jsx("div", { children: props.address }) : null, props.customText ? _jsx("div", { children: props.customText }) : null, props.unsubscribeText ? (_jsx("div", { style: { marginTop: 8 }, children: _jsx("a", { href: "#unsubscribe", style: { color: props.textColor, textDecoration: 'underline' }, children: props.unsubscribeText }) })) : null] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createElement } from 'react';
|
|
4
|
+
export function HeadingBlock({ props }) {
|
|
5
|
+
const tag = `h${props.level}`;
|
|
6
|
+
const style = {
|
|
7
|
+
margin: 0,
|
|
8
|
+
color: props.color,
|
|
9
|
+
fontSize: props.fontSize,
|
|
10
|
+
fontWeight: props.fontWeight,
|
|
11
|
+
fontFamily: props.fontFamily || undefined,
|
|
12
|
+
lineHeight: 1.3,
|
|
13
|
+
};
|
|
14
|
+
const wrapperStyle = {
|
|
15
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
16
|
+
textAlign: props.alignment,
|
|
17
|
+
};
|
|
18
|
+
return (_jsx("div", { "data-slot": "email-block-heading", style: wrapperStyle, children: createElement(tag, { style }, props.text) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ImageOff } from 'lucide-react';
|
|
4
|
+
export function ImageBlock({ props }) {
|
|
5
|
+
const wrapperStyle = {
|
|
6
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
7
|
+
textAlign: props.alignment,
|
|
8
|
+
};
|
|
9
|
+
if (!props.src) {
|
|
10
|
+
return (_jsx("div", { "data-slot": "email-block-image", style: wrapperStyle, children: _jsxs("div", { className: "mx-auto flex items-center justify-center rounded-md border border-border border-dashed bg-muted/50 text-muted-foreground", style: {
|
|
11
|
+
width: '100%',
|
|
12
|
+
maxWidth: props.width,
|
|
13
|
+
height: 120,
|
|
14
|
+
}, children: [_jsx(ImageOff, { className: "size-6", "aria-hidden": true }), _jsx("span", { className: "ml-2 text-sm", children: "No image selected" })] }) }));
|
|
15
|
+
}
|
|
16
|
+
const img = (_jsx("img", { src: props.src, alt: props.alt, width: props.width, height: props.height === 'auto' ? undefined : props.height, style: { display: 'block', width: '100%', maxWidth: props.width, height: 'auto' } }));
|
|
17
|
+
return (_jsx("div", { "data-slot": "email-block-image", style: wrapperStyle, children: props.linkUrl ? (_jsx("a", { href: props.linkUrl, rel: "noopener noreferrer", children: img })) : (img) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
export function SocialBlock({ props }) {
|
|
4
|
+
const visible = props.links.filter((l) => Boolean(l.url));
|
|
5
|
+
const wrapperStyle = {
|
|
6
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
7
|
+
textAlign: props.alignment,
|
|
8
|
+
};
|
|
9
|
+
return (_jsx("div", { "data-slot": "email-block-social", style: wrapperStyle, children: _jsx("div", { style: {
|
|
10
|
+
display: 'inline-flex',
|
|
11
|
+
gap: props.spacing,
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
}, children: visible.map((link, i) => (_jsx("a", { href: link.url, rel: "noopener noreferrer", "aria-label": link.platform, className: "inline-flex items-center justify-center rounded bg-muted px-3 py-1 text-foreground text-xs no-underline", style: {
|
|
15
|
+
width: link.iconUrl ? props.iconSize : undefined,
|
|
16
|
+
height: link.iconUrl ? props.iconSize : undefined,
|
|
17
|
+
padding: link.iconUrl ? 0 : undefined,
|
|
18
|
+
}, children: link.iconUrl ? (_jsx("img", { src: link.iconUrl, alt: link.platform, width: props.iconSize, height: props.iconSize, style: { display: 'block' } })) : (link.platform) }, `${link.platform}-${i}`))) }) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function TextBlock({ props }) {
|
|
4
|
+
const wrapperStyle = {
|
|
5
|
+
padding: `${props.padding.top}px ${props.padding.right}px ${props.padding.bottom}px ${props.padding.left}px`,
|
|
6
|
+
textAlign: props.alignment,
|
|
7
|
+
color: props.color,
|
|
8
|
+
fontSize: props.fontSize,
|
|
9
|
+
fontWeight: props.fontWeight,
|
|
10
|
+
fontFamily: props.fontFamily || undefined,
|
|
11
|
+
lineHeight: 1.55,
|
|
12
|
+
wordBreak: 'break-word',
|
|
13
|
+
};
|
|
14
|
+
// Render as paragraphs split on blank lines.
|
|
15
|
+
const paragraphs = props.content.split(/\n{2,}/);
|
|
16
|
+
return (_jsx("div", { "data-slot": "email-block-text", style: wrapperStyle, children: paragraphs.map((para, i) => (_jsx("p", { style: { margin: i > 0 ? '12px 0 0 0' : 0 }, children: para.split('\n').map((line, lineIdx, lines) => (_jsxs("span", { children: [line, lineIdx < lines.length - 1 ? _jsx("br", {}) : null] }, lineIdx))) }, i))) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DevicePreview, EmailBlock, EmailBuilderLabels } from './types.js';
|
|
3
|
+
interface BuilderCanvasProps {
|
|
4
|
+
blocks: EmailBlock[];
|
|
5
|
+
selectedBlockId: string | null;
|
|
6
|
+
onSelectBlock: (id: string | null) => void;
|
|
7
|
+
device: DevicePreview;
|
|
8
|
+
labels: EmailBuilderLabels;
|
|
9
|
+
/** Optional content to render above the empty state — typically the palette CTA. */
|
|
10
|
+
emptyStateAction?: ReactNode;
|
|
11
|
+
/** Per-block floating bar; rendered near the selected block. */
|
|
12
|
+
renderFloatingBar?: (block: EmailBlock) => ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function BuilderCanvas({ blocks, selectedBlockId, onSelectBlock, device, labels, emptyStateAction, renderFloatingBar, }: BuilderCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSortable } from '@dnd-kit/sortable';
|
|
4
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
import { BlockRenderer } from './block-renderer.js';
|
|
7
|
+
const DEVICE_WIDTHS = { desktop: 600, mobile: 375 };
|
|
8
|
+
export function BuilderCanvas({ blocks, selectedBlockId, onSelectBlock, device, labels, emptyStateAction, renderFloatingBar, }) {
|
|
9
|
+
return (_jsx("div", { "data-slot": "email-builder-canvas", className: "flex h-full w-full flex-col items-center overflow-y-auto bg-muted/40 p-6", children: _jsx("div", { "data-slot": "email-builder-canvas-frame", className: "rounded-md border border-border bg-background text-left shadow-sm transition-all", style: { width: '100%', maxWidth: DEVICE_WIDTHS[device] }, children: blocks.length === 0 ? (_jsx(EmptyState, { labels: labels, action: emptyStateAction })) : (blocks.map((block) => (_jsx(SortableBlock, { block: block, selected: block.id === selectedBlockId, onSelect: () => onSelectBlock(block.id), renderFloatingBar: renderFloatingBar }, block.id)))) }) }));
|
|
10
|
+
}
|
|
11
|
+
function SortableBlock({ block, selected, onSelect, renderFloatingBar }) {
|
|
12
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
13
|
+
id: block.id,
|
|
14
|
+
data: { kind: 'block', blockId: block.id },
|
|
15
|
+
});
|
|
16
|
+
// dnd-kit injects role="button" + tabIndex by default; strip those so we don't
|
|
17
|
+
// create nested interactive controls (the inner select <button> handles a11y).
|
|
18
|
+
const { role: _role, tabIndex: _tabIndex, ...sortableAttrs } = attributes;
|
|
19
|
+
return (_jsxs("div", { ref: setNodeRef, "data-slot": "email-builder-canvas-block", "data-block-id": block.id, "data-selected": selected ? 'true' : undefined, style: { transform: CSS.Transform.toString(transform), transition }, className: cn('group relative cursor-grab transition-shadow', selected && 'ring-2 ring-primary ring-inset', isDragging && 'z-10 opacity-60'), ...sortableAttrs, ...listeners, children: [_jsx(BlockRenderer, { block: block }), _jsx("button", { type: "button", "data-slot": "email-builder-canvas-block-select", "aria-label": `Select ${block.kind} block`, "aria-pressed": selected, className: cn('absolute inset-0 z-0 cursor-pointer bg-transparent outline-none', selected ? 'pointer-events-none' : 'pointer-events-auto'), onClick: (e) => {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
onSelect();
|
|
22
|
+
} }), selected && renderFloatingBar ? (_jsx(_Fragment, { children: _jsx("div", { "data-slot": "email-builder-canvas-block-actions", className: "-top-3 -translate-x-1/2 absolute left-1/2 z-10", onClick: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), children: renderFloatingBar(block) }) })) : null] }));
|
|
23
|
+
}
|
|
24
|
+
function EmptyState({ labels, action }) {
|
|
25
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center gap-3 px-6 py-16 text-center", children: [_jsx("div", { className: "font-semibold text-base text-foreground", children: labels.emptyTitle }), _jsx("p", { className: "max-w-sm text-muted-foreground text-sm", children: labels.emptyDescription }), action ? _jsx("div", { className: "pt-2", children: action }) : null] }));
|
|
26
|
+
}
|
|
@@ -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 {};
|