@jhits/plugin-newsletter 0.0.4 → 0.0.6
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/api/handler.d.ts +51 -0
- package/dist/api/handler.d.ts.map +1 -0
- package/dist/api/handler.js +526 -0
- package/dist/api/router.d.ts +11 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +82 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +222 -0
- package/dist/index.server.d.ts +10 -0
- package/dist/index.server.d.ts.map +1 -0
- package/dist/index.server.js +8 -0
- package/dist/init.d.ts +49 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +42 -0
- package/dist/lib/blocks/BlockRenderer.d.ts +43 -0
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -0
- package/dist/lib/blocks/BlockRenderer.js +48 -0
- package/dist/lib/email/EmailRenderer.d.ts +47 -0
- package/dist/lib/email/EmailRenderer.d.ts.map +1 -0
- package/dist/lib/email/EmailRenderer.js +359 -0
- package/dist/lib/email/index.d.ts +6 -0
- package/dist/lib/email/index.d.ts.map +1 -0
- package/dist/lib/email/index.js +4 -0
- package/dist/lib/mappers/apiMapper.d.ts +30 -0
- package/dist/lib/mappers/apiMapper.d.ts.map +1 -0
- package/dist/lib/mappers/apiMapper.js +36 -0
- package/dist/lib/utils/blockHelpers.d.ts +23 -0
- package/dist/lib/utils/blockHelpers.d.ts.map +1 -0
- package/dist/lib/utils/blockHelpers.js +65 -0
- package/dist/lib/utils/slugify.d.ts +14 -0
- package/dist/lib/utils/slugify.d.ts.map +1 -0
- package/dist/lib/utils/slugify.js +37 -0
- package/dist/registry/BlockRegistry.d.ts +31 -0
- package/dist/registry/BlockRegistry.d.ts.map +1 -0
- package/dist/registry/BlockRegistry.js +34 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +4 -0
- package/dist/state/EditorContext.d.ts +44 -0
- package/dist/state/EditorContext.d.ts.map +1 -0
- package/dist/state/EditorContext.js +212 -0
- package/dist/state/index.d.ts +10 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +6 -0
- package/dist/state/reducer.d.ts +11 -0
- package/dist/state/reducer.d.ts.map +1 -0
- package/dist/state/reducer.js +488 -0
- package/dist/state/types.d.ts +157 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +26 -0
- package/dist/types/block.d.ts +230 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +8 -0
- package/dist/types/newsletter.d.ts +129 -0
- package/dist/types/newsletter.d.ts.map +1 -0
- package/dist/types/newsletter.js +4 -0
- package/dist/types/registry.d.ts +13 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/registry.js +4 -0
- package/dist/views/CanvasEditor/BlockWrapper.d.ts +23 -0
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -0
- package/dist/views/CanvasEditor/BlockWrapper.js +44 -0
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts +14 -0
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -0
- package/dist/views/CanvasEditor/CanvasEditorView.js +139 -0
- package/dist/views/CanvasEditor/EditorBody.d.ts +24 -0
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -0
- package/dist/views/CanvasEditor/EditorBody.js +21 -0
- package/dist/views/CanvasEditor/EditorHeader.d.ts +12 -0
- package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -0
- package/dist/views/CanvasEditor/EditorHeader.js +47 -0
- package/dist/views/CanvasEditor/components/CustomBlockItem.d.ts +10 -0
- package/dist/views/CanvasEditor/components/CustomBlockItem.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/CustomBlockItem.js +36 -0
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +25 -0
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/EditorCanvas.js +397 -0
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +7 -0
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/EditorLibrary.js +25 -0
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +9 -0
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/EditorSidebar.js +16 -0
- package/dist/views/CanvasEditor/components/ErrorBanner.d.ts +6 -0
- package/dist/views/CanvasEditor/components/ErrorBanner.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/ErrorBanner.js +8 -0
- package/dist/views/CanvasEditor/components/LibraryItem.d.ts +10 -0
- package/dist/views/CanvasEditor/components/LibraryItem.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/LibraryItem.js +35 -0
- package/dist/views/CanvasEditor/components/SlashCommandDetector.d.ts +18 -0
- package/dist/views/CanvasEditor/components/SlashCommandDetector.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/SlashCommandDetector.js +164 -0
- package/dist/views/CanvasEditor/components/SlashCommandMenu.d.ts +22 -0
- package/dist/views/CanvasEditor/components/SlashCommandMenu.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/SlashCommandMenu.js +57 -0
- package/dist/views/CanvasEditor/components/index.d.ts +16 -0
- package/dist/views/CanvasEditor/components/index.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/index.js +9 -0
- package/dist/views/CanvasEditor/hooks/index.d.ts +7 -0
- package/dist/views/CanvasEditor/hooks/index.d.ts.map +1 -0
- package/dist/views/CanvasEditor/hooks/index.js +6 -0
- package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +3 -0
- package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +1 -0
- package/dist/views/CanvasEditor/hooks/useKeyboardShortcuts.js +114 -0
- package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts +5 -0
- package/dist/views/CanvasEditor/hooks/useNewsletterLoader.d.ts.map +1 -0
- package/dist/views/CanvasEditor/hooks/useNewsletterLoader.js +28 -0
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +2 -0
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -0
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +46 -0
- package/dist/views/CanvasEditor/hooks/useSlashCommand.d.ts +31 -0
- package/dist/views/CanvasEditor/hooks/useSlashCommand.d.ts.map +1 -0
- package/dist/views/CanvasEditor/hooks/useSlashCommand.js +87 -0
- package/dist/views/CanvasEditor/index.d.ts +12 -0
- package/dist/views/CanvasEditor/index.d.ts.map +1 -0
- package/dist/views/CanvasEditor/index.js +7 -0
- package/dist/views/NewsletterEditor.d.ts +16 -0
- package/dist/views/NewsletterEditor.d.ts.map +1 -0
- package/dist/views/NewsletterEditor.js +10 -0
- package/dist/views/NewsletterManager.d.ts +10 -0
- package/dist/views/NewsletterManager.d.ts.map +1 -0
- package/dist/views/NewsletterManager.js +95 -0
- package/dist/views/SettingsView.d.ts +10 -0
- package/dist/views/SettingsView.d.ts.map +1 -0
- package/dist/views/SettingsView.js +103 -0
- package/dist/views/SubscribersView.d.ts +10 -0
- package/dist/views/SubscribersView.d.ts.map +1 -0
- package/dist/views/SubscribersView.js +94 -0
- package/package.json +24 -23
- package/src/api/handler.ts +340 -1
- package/src/api/router.ts +35 -0
- package/src/index.tsx +284 -4
- package/src/index.tsx.patch +98 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +125 -0
- package/src/lib/email/EmailRenderer.tsx +425 -0
- package/src/lib/email/index.ts +6 -0
- package/src/lib/mappers/apiMapper.ts +57 -0
- package/src/lib/utils/blockHelpers.ts +71 -0
- package/src/lib/utils/slugify.ts +43 -0
- package/src/registry/BlockRegistry.ts +53 -0
- package/src/registry/index.ts +5 -0
- package/src/state/EditorContext.tsx +279 -0
- package/src/state/index.ts +10 -0
- package/src/state/reducer.ts +561 -0
- package/src/state/types.ts +154 -0
- package/src/types/block.ts +275 -0
- package/src/types/newsletter.ts +114 -1
- package/src/types/registry.ts +14 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +143 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +249 -0
- package/src/views/CanvasEditor/EditorBody.tsx +95 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +139 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +83 -0
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +674 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +120 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +156 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +71 -0
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +196 -0
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +131 -0
- package/src/views/CanvasEditor/components/index.ts +16 -0
- package/src/views/CanvasEditor/hooks/index.ts +7 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +136 -0
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +34 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +54 -0
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +106 -0
- package/src/views/CanvasEditor/index.ts +12 -0
- package/src/views/NewsletterEditor.tsx +38 -0
- package/src/views/NewsletterManager.tsx +240 -0
- package/src/views/SettingsView.tsx +14 -14
- package/src/views/SubscribersView.tsx +20 -20
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Library, Image as ImageIcon, LayoutTemplate, Type, Box } from 'lucide-react';
|
|
4
|
+
import { LibraryItem, CustomBlockItem } from './index';
|
|
5
|
+
export function EditorLibrary({ registeredBlocks, onAddBlock }) {
|
|
6
|
+
// Get all registered blocks (excluding Hero block for newsletters)
|
|
7
|
+
const allBlocks = registeredBlocks.filter(block => block.type !== 'hero');
|
|
8
|
+
const textBlocks = allBlocks.filter(block => block.category === 'text');
|
|
9
|
+
const customBlocks = allBlocks.filter(block => block.category === 'custom');
|
|
10
|
+
const mediaBlocks = allBlocks.filter(block => block.category === 'media');
|
|
11
|
+
const layoutBlocks = allBlocks.filter(block => block.category === 'layout');
|
|
12
|
+
return (_jsxs("div", { className: "p-6 w-72 min-w-0 max-w-full", children: [textBlocks.length > 0 && (_jsxs("div", { className: "mb-10", children: [_jsx("h3", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6", children: "Text" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: textBlocks.map((block) => {
|
|
13
|
+
const IconComponent = block.icon || block.components.Icon || Type;
|
|
14
|
+
return (_jsx(LibraryItem, { icon: _jsx(IconComponent, { size: 16 }), label: block.name, blockType: block.type, description: block.description, onAddBlock: onAddBlock }, block.type));
|
|
15
|
+
}) })] })), mediaBlocks.length > 0 && (_jsxs("div", { className: "mb-10", children: [_jsx("h3", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6", children: "Media" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: mediaBlocks.map((block) => {
|
|
16
|
+
const IconComponent = block.icon || block.components.Icon || ImageIcon;
|
|
17
|
+
return (_jsx(LibraryItem, { icon: _jsx(IconComponent, { size: 16 }), label: block.name, blockType: block.type, description: block.description, onAddBlock: onAddBlock }, block.type));
|
|
18
|
+
}) })] })), layoutBlocks.length > 0 && (_jsxs("div", { className: "mb-10", children: [_jsx("h3", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6", children: "Layout" }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: layoutBlocks.map((block) => {
|
|
19
|
+
const IconComponent = block.icon || block.components.Icon || LayoutTemplate;
|
|
20
|
+
return (_jsx(LibraryItem, { icon: _jsx(IconComponent, { size: 16 }), label: block.name, blockType: block.type, description: block.description, onAddBlock: onAddBlock }, block.type));
|
|
21
|
+
}) })] })), customBlocks.length > 0 && (_jsxs("div", { children: [_jsx("h3", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6", children: "Custom Blocks" }), _jsx("div", { className: "space-y-3", children: customBlocks.map((block) => {
|
|
22
|
+
const IconComponent = block.icon || block.components.Icon || Box;
|
|
23
|
+
return (_jsx(CustomBlockItem, { blockType: block.type, name: block.name, description: block.description, icon: _jsx(IconComponent, { size: 16 }), onAddBlock: onAddBlock }, block.type));
|
|
24
|
+
}) })] })), allBlocks.length === 0 && (_jsxs("div", { className: "text-center py-12 text-neutral-400 dark:text-neutral-500", children: [_jsx(Library, { size: 32, className: "mx-auto mb-4 opacity-50" }), _jsx("p", { className: "text-sm", children: "No blocks available" }), _jsx("p", { className: "text-xs mt-2", children: "Register blocks in your app configuration" })] }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NewsletterMetadata } from '../../../types/newsletter';
|
|
2
|
+
export interface EditorSidebarProps {
|
|
3
|
+
slug: string;
|
|
4
|
+
metadata: NewsletterMetadata;
|
|
5
|
+
status: string;
|
|
6
|
+
onMetadataUpdate: (metadata: Partial<NewsletterMetadata>) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function EditorSidebar({ slug, metadata, status, onMetadataUpdate, }: EditorSidebarProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=EditorSidebar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorSidebar.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/EditorSidebar.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,IAAI,CAAC;CACrE;AAED,wBAAgB,aAAa,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,gBAAgB,GACnB,EAAE,kBAAkB,2CAyIpB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Mail, Users } from 'lucide-react';
|
|
4
|
+
export function EditorSidebar({ slug, metadata, status, onMetadataUpdate, }) {
|
|
5
|
+
return (_jsxs("div", { className: "p-8 w-80 min-w-0 max-w-full space-y-12 overflow-y-auto max-h-full", children: [_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-3 mb-6", children: [_jsx(Mail, { size: 14, className: "text-neutral-500 dark:text-neutral-400" }), _jsx("label", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black", children: "Newsletter Settings" })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Email Subject" }), _jsx("input", { type: "text", value: metadata.subject || '', onChange: (e) => onMetadataUpdate({ subject: e.target.value }), placeholder: "Enter email subject", className: "w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text" }), _jsxs("p", { className: "text-[9px] text-neutral-400 dark:text-neutral-500 mt-1", children: [metadata.subject?.length || 0, " / 100 characters"] })] }), _jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Preview Text" }), _jsx("textarea", { value: metadata.previewText || '', onChange: (e) => onMetadataUpdate({ previewText: e.target.value }), placeholder: "Preview text shown in email clients", rows: 3, className: "w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text resize-none" }), _jsxs("p", { className: "text-[9px] text-neutral-400 dark:text-neutral-500 mt-1", children: [metadata.previewText?.length || 0, " / 150 characters"] })] }), _jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Language" }), _jsxs("select", { value: metadata.lang || 'en', onChange: (e) => onMetadataUpdate({ lang: e.target.value }), className: "w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text", children: [_jsx("option", { value: "en", children: "English" }), _jsx("option", { value: "nl", children: "Dutch" }), _jsx("option", { value: "sv", children: "Swedish" })] })] })] })] }), _jsxs("section", { className: "pt-8 border-t border-neutral-200 dark:border-neutral-800", children: [_jsxs("div", { className: "flex items-center gap-3 mb-6", children: [_jsx(Users, { size: 14, className: "text-neutral-500 dark:text-neutral-400" }), _jsx("label", { className: "text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black", children: "Recipients" })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Filter Type" }), _jsxs("select", { value: metadata.recipientFilter?.type || 'all', onChange: (e) => onMetadataUpdate({
|
|
6
|
+
recipientFilter: {
|
|
7
|
+
type: e.target.value,
|
|
8
|
+
value: metadata.recipientFilter?.value,
|
|
9
|
+
}
|
|
10
|
+
}), className: "w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text", children: [_jsx("option", { value: "all", children: "All Subscribers" }), _jsx("option", { value: "language", children: "By Language" }), _jsx("option", { value: "custom", children: "Custom Filter" })] })] }), metadata.recipientFilter?.type === 'language' && (_jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Language" }), _jsxs("select", { value: metadata.recipientFilter?.value || 'en', onChange: (e) => onMetadataUpdate({
|
|
11
|
+
recipientFilter: {
|
|
12
|
+
type: 'language',
|
|
13
|
+
value: e.target.value,
|
|
14
|
+
}
|
|
15
|
+
}), className: "w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text", children: [_jsx("option", { value: "en", children: "English" }), _jsx("option", { value: "nl", children: "Dutch" }), _jsx("option", { value: "sv", children: "Swedish" })] })] }))] })] }), _jsx("section", { className: "pt-8 border-t border-neutral-200 dark:border-neutral-800", children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Slug" }), _jsx("input", { type: "text", value: slug, readOnly: true, className: "w-full px-3 py-2 text-xs bg-dashboard-bg border border-dashboard-border rounded-lg text-dashboard-text opacity-60 cursor-not-allowed" })] }), _jsxs("div", { children: [_jsx("label", { className: "text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2", children: "Status" }), _jsx("div", { className: "px-3 py-2 text-xs bg-dashboard-bg border border-dashboard-border rounded-lg", children: _jsx("span", { className: "uppercase font-bold", children: status }) })] })] }) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorBanner.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/ErrorBanner.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,gBAAgB,kDAoBjE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlertTriangle, X } from 'lucide-react';
|
|
4
|
+
export function ErrorBanner({ error, onDismiss }) {
|
|
5
|
+
if (!error)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs("div", { className: "bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 px-6 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3 flex-1", children: [_jsx(AlertTriangle, { className: "text-red-600 dark:text-red-400 flex-shrink-0", size: 20 }), _jsx("p", { className: "text-red-800 dark:text-red-300 text-sm font-medium", children: error })] }), _jsx("button", { onClick: onDismiss, className: "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200 transition-colors", "aria-label": "Dismiss error", children: _jsx(X, { size: 18 }) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface LibraryItemProps {
|
|
3
|
+
icon: React.ReactNode;
|
|
4
|
+
label: string;
|
|
5
|
+
blockType: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
onAddBlock?: (blockType: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function LibraryItem({ icon, label, blockType, description, onAddBlock }: LibraryItemProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=LibraryItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LibraryItem.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/LibraryItem.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA2B,MAAM,OAAO,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,WAAW,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,SAAS,EACT,WAAW,EACX,UAAU,EACb,EAAE,gBAAgB,2CAoDlB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useRef } from 'react';
|
|
4
|
+
export function LibraryItem({ icon, label, blockType, description, onAddBlock }) {
|
|
5
|
+
const [hasDragged, setHasDragged] = useState(false);
|
|
6
|
+
const mouseDownRef = useRef(null);
|
|
7
|
+
const handleDragStart = (e) => {
|
|
8
|
+
e.dataTransfer.setData('block-type', blockType);
|
|
9
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
10
|
+
setHasDragged(true);
|
|
11
|
+
};
|
|
12
|
+
const handleMouseDown = (e) => {
|
|
13
|
+
mouseDownRef.current = { x: e.clientX, y: e.clientY };
|
|
14
|
+
setHasDragged(false);
|
|
15
|
+
};
|
|
16
|
+
const handleMouseMove = (e) => {
|
|
17
|
+
if (mouseDownRef.current) {
|
|
18
|
+
const dx = Math.abs(e.clientX - mouseDownRef.current.x);
|
|
19
|
+
const dy = Math.abs(e.clientY - mouseDownRef.current.y);
|
|
20
|
+
if (dx > 5 || dy > 5) {
|
|
21
|
+
setHasDragged(true);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const handleClick = (e) => {
|
|
26
|
+
if (!hasDragged && onAddBlock) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
onAddBlock(blockType);
|
|
30
|
+
}
|
|
31
|
+
mouseDownRef.current = null;
|
|
32
|
+
setTimeout(() => setHasDragged(false), 100);
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { draggable: true, onDragStart: handleDragStart, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onClick: handleClick, className: "flex flex-col items-center justify-center p-5 rounded-2xl border border-dashboard-border bg-dashboard-card hover:border-primary hover:shadow-xl hover:shadow-primary/5 transition-all cursor-pointer group", children: [_jsx("div", { className: "text-neutral-400 dark:text-neutral-500 group-hover:text-primary dark:group-hover:text-primary mb-3 transition-colors duration-300", children: React.cloneElement(icon, { strokeWidth: 1.5 }) }), _jsx("span", { className: "text-[9px] font-black uppercase tracking-[0.15em] text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-950 dark:group-hover:text-white transition-colors", children: label })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { useSlashCommand } from '../hooks/useSlashCommand';
|
|
3
|
+
export interface SlashCommandDetectorProps {
|
|
4
|
+
children: React.ReactElement;
|
|
5
|
+
blockId: string;
|
|
6
|
+
blockIndex: number;
|
|
7
|
+
blockType: string;
|
|
8
|
+
content: string;
|
|
9
|
+
onContentChange: (content: string) => void;
|
|
10
|
+
slashCommand?: ReturnType<typeof useSlashCommand>;
|
|
11
|
+
onAddBlockBelow?: (blockType: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detects "/" input and shows slash command menu
|
|
15
|
+
* Wraps block edit components to add slash command functionality
|
|
16
|
+
*/
|
|
17
|
+
export declare function SlashCommandDetector({ children, blockId, blockIndex, blockType, content, onContentChange, slashCommand, onAddBlockBelow, }: SlashCommandDetectorProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=SlashCommandDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlashCommandDetector.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/SlashCommandDetector.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,yBAAyB;IACtC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,YAAY,CAAC,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAClD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,QAAQ,EACR,OAAO,EACP,UAAU,EACV,SAAS,EACT,OAAO,EACP,eAAe,EACf,YAAY,EACZ,eAAe,GAClB,EAAE,yBAAyB,2CAsK3B"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Detects "/" input and shows slash command menu
|
|
6
|
+
* Wraps block edit components to add slash command functionality
|
|
7
|
+
*/
|
|
8
|
+
export function SlashCommandDetector({ children, blockId, blockIndex, blockType, content, onContentChange, slashCommand, onAddBlockBelow, }) {
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const lastContentRef = useRef('');
|
|
11
|
+
const slashIndexRef = useRef(-1);
|
|
12
|
+
// Listen for input events on contentEditable elements
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!slashCommand || blockType !== 'paragraph')
|
|
15
|
+
return;
|
|
16
|
+
const container = containerRef.current;
|
|
17
|
+
if (!container)
|
|
18
|
+
return;
|
|
19
|
+
const handleInput = (e) => {
|
|
20
|
+
const target = e.target;
|
|
21
|
+
if (!target.isContentEditable)
|
|
22
|
+
return;
|
|
23
|
+
const textContent = target.textContent || '';
|
|
24
|
+
const plainText = textContent.replace(/\u00A0/g, ' '); // Replace non-breaking spaces
|
|
25
|
+
const previousText = (lastContentRef.current || '').replace(/\u00A0/g, ' ');
|
|
26
|
+
// Only allow "/" when paragraph is completely empty (no content before "/")
|
|
27
|
+
const wasEmpty = previousText.trim().length === 0;
|
|
28
|
+
const isNowEmpty = plainText.trim().length === 0;
|
|
29
|
+
if (plainText.length > previousText.length) {
|
|
30
|
+
// Character was added
|
|
31
|
+
const lastChar = plainText[plainText.length - 1];
|
|
32
|
+
// Check if "/" was typed and paragraph was empty before
|
|
33
|
+
if (lastChar === '/' && wasEmpty && plainText === '/') {
|
|
34
|
+
slashIndexRef.current = 0;
|
|
35
|
+
// Get cursor position
|
|
36
|
+
const selection = window.getSelection();
|
|
37
|
+
if (selection && selection.rangeCount > 0) {
|
|
38
|
+
const range = selection.getRangeAt(0);
|
|
39
|
+
const rect = range.getBoundingClientRect();
|
|
40
|
+
slashCommand.openMenu({
|
|
41
|
+
top: rect.bottom + 5,
|
|
42
|
+
left: rect.left,
|
|
43
|
+
}, blockId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (slashCommand.isOpen && slashIndexRef.current >= 0) {
|
|
47
|
+
// Update query if menu is open (only if still starts with "/")
|
|
48
|
+
if (plainText.startsWith('/')) {
|
|
49
|
+
const query = plainText.slice(1);
|
|
50
|
+
slashCommand.updateQuery(query);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// If user typed something other than "/", close the menu
|
|
54
|
+
slashCommand.closeMenu();
|
|
55
|
+
slashIndexRef.current = -1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (!wasEmpty || (plainText.trim().length > 0 && plainText !== '/')) {
|
|
59
|
+
// If paragraph had content before or now has content that's not just "/", ensure menu is closed
|
|
60
|
+
if (slashCommand.isOpen) {
|
|
61
|
+
slashCommand.closeMenu();
|
|
62
|
+
slashIndexRef.current = -1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (plainText.length < previousText.length) {
|
|
67
|
+
// Content was deleted
|
|
68
|
+
if (slashCommand.isOpen) {
|
|
69
|
+
if (plainText === '/') {
|
|
70
|
+
// Still just "/", keep menu open
|
|
71
|
+
slashIndexRef.current = 0;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// No longer just "/", close menu
|
|
75
|
+
slashCommand.closeMenu();
|
|
76
|
+
slashIndexRef.current = -1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
lastContentRef.current = textContent; // Store original with non-breaking spaces
|
|
81
|
+
};
|
|
82
|
+
// Listen for input events on contentEditable elements within container
|
|
83
|
+
container.addEventListener('input', handleInput);
|
|
84
|
+
return () => {
|
|
85
|
+
container.removeEventListener('input', handleInput);
|
|
86
|
+
};
|
|
87
|
+
}, [blockType, slashCommand, blockId]);
|
|
88
|
+
// Handle keyboard events for slash command and Enter key
|
|
89
|
+
const handleKeyDown = useCallback((e) => {
|
|
90
|
+
// Handle Enter key to create new paragraph when not in slash command mode
|
|
91
|
+
if (!slashCommand?.isOpen && e.key === 'Enter' && !e.shiftKey) {
|
|
92
|
+
const container = containerRef.current;
|
|
93
|
+
if (container) {
|
|
94
|
+
const contentEditable = container.querySelector('[contenteditable="true"]');
|
|
95
|
+
if (contentEditable && document.activeElement === contentEditable) {
|
|
96
|
+
const selection = window.getSelection();
|
|
97
|
+
if (selection && selection.rangeCount > 0) {
|
|
98
|
+
const range = selection.getRangeAt(0);
|
|
99
|
+
const textContent = contentEditable.textContent || '';
|
|
100
|
+
const plainText = textContent.replace(/\u00A0/g, ' ');
|
|
101
|
+
// Check if cursor is at the end of the content
|
|
102
|
+
if (range.startOffset === plainText.length && range.endOffset === plainText.length) {
|
|
103
|
+
// Create new paragraph below when Enter is pressed at end
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
if (onAddBlockBelow) {
|
|
107
|
+
onAddBlockBelow('paragraph');
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!slashCommand?.isOpen)
|
|
116
|
+
return;
|
|
117
|
+
if (e.key === 'ArrowDown') {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
slashCommand.moveSelection('down');
|
|
120
|
+
}
|
|
121
|
+
else if (e.key === 'ArrowUp') {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
slashCommand.moveSelection('up');
|
|
124
|
+
}
|
|
125
|
+
else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
const selectedBlock = slashCommand.filteredBlocks[slashCommand.selectedIndex];
|
|
129
|
+
if (selectedBlock) {
|
|
130
|
+
// Don't clear content here - the block replacement will handle it
|
|
131
|
+
// Just replace the block with selected type (this will replace the current paragraph)
|
|
132
|
+
slashCommand.selectCurrent(blockId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (e.key === 'Escape') {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
// Clear the "/" from content - only clear if menu was open
|
|
139
|
+
if (slashCommand.isOpen) {
|
|
140
|
+
const container = containerRef.current;
|
|
141
|
+
if (container) {
|
|
142
|
+
const contentEditable = container.querySelector('[contenteditable="true"]');
|
|
143
|
+
if (contentEditable) {
|
|
144
|
+
const textContent = contentEditable.textContent || '';
|
|
145
|
+
const plainText = textContent.replace(/\u00A0/g, ' ');
|
|
146
|
+
// Only clear if it's just "/"
|
|
147
|
+
if (plainText === '/') {
|
|
148
|
+
contentEditable.textContent = '';
|
|
149
|
+
onContentChange('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
slashCommand.closeMenu();
|
|
155
|
+
slashIndexRef.current = -1;
|
|
156
|
+
}
|
|
157
|
+
}, [slashCommand, content, onContentChange, blockId, onAddBlockBelow]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
// Always listen for keydown events (for both slash command and Enter key handling)
|
|
160
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
161
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
162
|
+
}, [handleKeyDown]);
|
|
163
|
+
return (_jsx("div", { ref: containerRef, children: children }));
|
|
164
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BlockTypeDefinition } from '../../../types/block';
|
|
2
|
+
export interface SlashCommandMenuProps {
|
|
3
|
+
/** Registered blocks to show in menu */
|
|
4
|
+
blocks: BlockTypeDefinition[];
|
|
5
|
+
/** Current search query */
|
|
6
|
+
query: string;
|
|
7
|
+
/** Selected index */
|
|
8
|
+
selectedIndex: number;
|
|
9
|
+
/** Callback when a block is selected - receives replaceBlockId if replacing */
|
|
10
|
+
onSelect: (replaceBlockId?: string) => void;
|
|
11
|
+
/** Position for the menu */
|
|
12
|
+
position: {
|
|
13
|
+
top: number;
|
|
14
|
+
left: number;
|
|
15
|
+
} | null;
|
|
16
|
+
/** Callback to close the menu */
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
/** Block ID to replace (if replacing existing block) */
|
|
19
|
+
replaceBlockId?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function SlashCommandMenu({ blocks, query, selectedIndex, onSelect, position, onClose, replaceBlockId, }: SlashCommandMenuProps): import("react/jsx-runtime").JSX.Element | null;
|
|
22
|
+
//# sourceMappingURL=SlashCommandMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlashCommandMenu.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/SlashCommandMenu.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IAClC,wCAAwC;IACxC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,4BAA4B;IAC5B,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,iCAAiC;IACjC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,EAC7B,MAAM,EACN,KAAK,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,cAAc,GACjB,EAAE,qBAAqB,kDAmGvB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { Search } from 'lucide-react';
|
|
5
|
+
export function SlashCommandMenu({ blocks, query, selectedIndex, onSelect, position, onClose, replaceBlockId, }) {
|
|
6
|
+
const menuRef = useRef(null);
|
|
7
|
+
// Handle keyboard navigation
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handleKeyDown = (e) => {
|
|
10
|
+
if (e.key === 'ArrowDown') {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
// Selection is handled by parent
|
|
13
|
+
}
|
|
14
|
+
else if (e.key === 'ArrowUp') {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
// Selection is handled by parent
|
|
17
|
+
}
|
|
18
|
+
else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
if (blocks[selectedIndex]) {
|
|
21
|
+
onSelect(replaceBlockId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (e.key === 'Escape') {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
30
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
31
|
+
}, [blocks, selectedIndex, onSelect, onClose, replaceBlockId]);
|
|
32
|
+
// Scroll selected item into view
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (menuRef.current && selectedIndex >= 0) {
|
|
35
|
+
const selectedElement = menuRef.current.children[selectedIndex];
|
|
36
|
+
if (selectedElement) {
|
|
37
|
+
selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, [selectedIndex]);
|
|
41
|
+
if (!position || blocks.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs("div", { ref: menuRef, className: "fixed z-50 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-2xl overflow-hidden", style: {
|
|
45
|
+
top: `${position.top}px`,
|
|
46
|
+
left: `${position.left}px`,
|
|
47
|
+
width: '280px',
|
|
48
|
+
maxHeight: '320px',
|
|
49
|
+
overflowY: 'auto',
|
|
50
|
+
}, children: [_jsxs("div", { className: "p-2 border-b border-neutral-200 dark:border-neutral-700 flex items-center gap-2", children: [_jsx(Search, { size: 14, className: "text-neutral-400" }), _jsx("input", { type: "text", value: query, readOnly: true, placeholder: "Search blocks...", className: "flex-1 bg-transparent border-none outline-none text-sm text-neutral-600 dark:text-neutral-300 placeholder:text-neutral-400" })] }), _jsx("div", { className: "py-1", children: blocks.map((block, index) => {
|
|
51
|
+
const IconComponent = block.icon || block.components.Icon;
|
|
52
|
+
const isSelected = index === selectedIndex;
|
|
53
|
+
return (_jsxs("button", { onClick: () => onSelect(replaceBlockId), className: `w-full px-3 py-2 flex items-center gap-3 text-left transition-colors ${isSelected
|
|
54
|
+
? 'bg-primary/10 text-primary dark:bg-primary/20'
|
|
55
|
+
: 'hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-300'}`, children: [IconComponent && (_jsx("div", { className: `flex-shrink-0 ${isSelected ? 'text-primary' : 'text-neutral-400'}`, children: _jsx(IconComponent, {}) })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: `text-sm font-medium ${isSelected ? 'text-primary' : 'text-neutral-900 dark:text-neutral-100'}`, children: block.name }), block.description && (_jsx("div", { className: "text-xs text-neutral-500 dark:text-neutral-400 truncate", children: block.description }))] })] }, block.type));
|
|
56
|
+
}) })] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Editor Components Exports
|
|
3
|
+
*/
|
|
4
|
+
export { ErrorBanner } from './ErrorBanner';
|
|
5
|
+
export type { ErrorBannerProps } from './ErrorBanner';
|
|
6
|
+
export { LibraryItem } from './LibraryItem';
|
|
7
|
+
export type { LibraryItemProps } from './LibraryItem';
|
|
8
|
+
export { CustomBlockItem } from './CustomBlockItem';
|
|
9
|
+
export type { CustomBlockItemProps } from './CustomBlockItem';
|
|
10
|
+
export { EditorLibrary } from './EditorLibrary';
|
|
11
|
+
export type { EditorLibraryProps } from './EditorLibrary';
|
|
12
|
+
export { EditorCanvas } from './EditorCanvas';
|
|
13
|
+
export type { EditorCanvasProps } from './EditorCanvas';
|
|
14
|
+
export { EditorSidebar } from './EditorSidebar';
|
|
15
|
+
export type { EditorSidebarProps } from './EditorSidebar';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/components/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Editor Components Exports
|
|
3
|
+
*/
|
|
4
|
+
export { ErrorBanner } from './ErrorBanner';
|
|
5
|
+
export { LibraryItem } from './LibraryItem';
|
|
6
|
+
export { CustomBlockItem } from './CustomBlockItem';
|
|
7
|
+
export { EditorLibrary } from './EditorLibrary';
|
|
8
|
+
export { EditorCanvas } from './EditorCanvas';
|
|
9
|
+
export { EditorSidebar } from './EditorSidebar';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/hooks/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EditorState } from '../../../state/types';
|
|
2
|
+
export declare function useKeyboardShortcuts(state: EditorState, dispatch: (action: any) => void, canUndo: boolean, canRedo: boolean, undo: () => void, redo: () => void): void;
|
|
3
|
+
//# sourceMappingURL=useKeyboardShortcuts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useKeyboardShortcuts.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAUxD,wBAAgB,oBAAoB,CAChC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,EAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,IAAI,EAChB,IAAI,EAAE,MAAM,IAAI,QAqHnB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
// Generate a unique block ID
|
|
3
|
+
function generateBlockId() {
|
|
4
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
8
|
+
}
|
|
9
|
+
export function useKeyboardShortcuts(state, dispatch, canUndo, canRedo, undo, redo) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleKeyDown = (e) => {
|
|
12
|
+
// Don't handle shortcuts if user is typing in an input/textarea/contentEditable
|
|
13
|
+
const target = e.target;
|
|
14
|
+
const isEditableElement = target.tagName === 'INPUT' ||
|
|
15
|
+
target.tagName === 'TEXTAREA' ||
|
|
16
|
+
target.isContentEditable ||
|
|
17
|
+
target.closest('input, textarea, [contenteditable="true"]');
|
|
18
|
+
// Check for Ctrl+Z / Cmd+Z (Undo)
|
|
19
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
20
|
+
if (!isEditableElement) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
if (canUndo) {
|
|
24
|
+
undo();
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Check for Ctrl+Shift+Z / Cmd+Shift+Z or Ctrl+Y / Cmd+Y (Redo)
|
|
30
|
+
if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') || ((e.ctrlKey || e.metaKey) && e.key === 'y')) {
|
|
31
|
+
if (!isEditableElement) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
if (canRedo) {
|
|
35
|
+
redo();
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Check for Ctrl+V (Windows/Linux) or Cmd+V (Mac)
|
|
41
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
|
42
|
+
// Don't paste if user is typing in an input/textarea/contentEditable
|
|
43
|
+
if (isEditableElement) {
|
|
44
|
+
return; // Let the browser handle paste in editable elements
|
|
45
|
+
}
|
|
46
|
+
// Check if there's a copied block
|
|
47
|
+
if (typeof window !== 'undefined') {
|
|
48
|
+
const copiedBlockJson = localStorage.getItem('__NEWSLETTER_EDITOR_COPIED_BLOCK__');
|
|
49
|
+
if (copiedBlockJson) {
|
|
50
|
+
try {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
const copiedBlock = JSON.parse(copiedBlockJson);
|
|
54
|
+
// Clone a block with new IDs (recursive for nested blocks)
|
|
55
|
+
const cloneBlock = (blockToClone) => {
|
|
56
|
+
const cloned = {
|
|
57
|
+
...blockToClone,
|
|
58
|
+
id: generateBlockId(),
|
|
59
|
+
data: { ...blockToClone.data },
|
|
60
|
+
meta: blockToClone.meta ? { ...blockToClone.meta } : undefined,
|
|
61
|
+
};
|
|
62
|
+
// Handle children if they exist
|
|
63
|
+
if (blockToClone.children) {
|
|
64
|
+
if (Array.isArray(blockToClone.children) && blockToClone.children.length > 0) {
|
|
65
|
+
if (typeof blockToClone.children[0] === 'object') {
|
|
66
|
+
cloned.children = blockToClone.children.map(cloneBlock);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// If children are IDs, find and clone the actual blocks
|
|
70
|
+
const allBlocks = state.blocks;
|
|
71
|
+
const childIds = blockToClone.children;
|
|
72
|
+
const childBlocks = childIds
|
|
73
|
+
.map((childId) => allBlocks.find(b => b.id === childId))
|
|
74
|
+
.filter((b) => b !== undefined);
|
|
75
|
+
cloned.children = childBlocks.map(cloneBlock);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return cloned;
|
|
80
|
+
};
|
|
81
|
+
const pastedBlock = cloneBlock(copiedBlock);
|
|
82
|
+
// Find where to paste - use hovered block or selected block, or paste at end
|
|
83
|
+
const hoveredBlockId = window.__NEWSLETTER_EDITOR_HOVERED_BLOCK_ID__;
|
|
84
|
+
const targetBlockId = hoveredBlockId || state.selectedBlockId;
|
|
85
|
+
let pasteIndex;
|
|
86
|
+
if (targetBlockId) {
|
|
87
|
+
const targetIndex = state.blocks.findIndex(b => b.id === targetBlockId);
|
|
88
|
+
if (targetIndex !== -1) {
|
|
89
|
+
pasteIndex = targetIndex + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Dispatch ADD_BLOCK with the full block structure
|
|
93
|
+
dispatch({
|
|
94
|
+
type: 'ADD_BLOCK',
|
|
95
|
+
payload: {
|
|
96
|
+
block: pastedBlock,
|
|
97
|
+
index: pasteIndex,
|
|
98
|
+
containerId: undefined
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error('Failed to paste block:', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
110
|
+
return () => {
|
|
111
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
112
|
+
};
|
|
113
|
+
}, [state.blocks, state.selectedBlockId, dispatch, canUndo, canRedo, undo, redo]);
|
|
114
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Newsletter } from '../../../types/newsletter';
|
|
2
|
+
export declare function useNewsletterLoader(newsletterSlug: string | undefined, currentNewsletterId: string | null, loadNewsletter: (newsletter: Newsletter) => void): {
|
|
3
|
+
isLoadingNewsletter: boolean;
|
|
4
|
+
};
|
|
5
|
+
//# sourceMappingURL=useNewsletterLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNewsletterLoader.d.ts","sourceRoot":"","sources":["../../../../src/views/CanvasEditor/hooks/useNewsletterLoader.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvD,wBAAgB,mBAAmB,CAC/B,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,mBAAmB,EAAE,MAAM,GAAG,IAAI,EAClC,cAAc,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI;;EA2BnD"}
|