From 9ee98a2ff881645fccd6dbe881859e93fbc344f4 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 9 Jul 2025 23:21:27 -0700 Subject: [PATCH] refactor: modernize Edra editor components and enhance functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate all Edra components to Svelte 5 with runes syntax - Implement unified ContentInsertionPane for consistent content insertion - Standardize placeholder components with improved layouts - Enhance bubble menu with better color picker and text styling - Improve drag handle interactions and visual feedback - Update slash command system for better extensibility Key improvements: - ContentInsertionPane: New unified interface for all content types - Placeholder components: Consistent sizing and spacing - GeolocationPlaceholder: Simplified map integration - UrlEmbedPlaceholder: Better preview handling - ComposerMediaHandler: Converted to Svelte 5 class syntax - BubbleMenu components: Enhanced UI/UX with modern patterns All components now use: - Interface Props pattern for better type safety - $state and $derived for reactive state - Modern event handling syntax - Consistent styling with CSS variables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../admin/composer/BubbleColorPicker.svelte | 18 +- .../admin/composer/BubbleTextStyleMenu.svelte | 64 +- .../admin/composer/ComposerBubbleMenu.svelte | 22 +- .../composer/ComposerMediaHandler.svelte.ts | 54 +- src/lib/components/edra/drag-handle.svelte | 71 +- .../extensions/slash-command/slashcommand.ts | 55 +- .../components/AudioPlaceholder.svelte | 64 +- .../components/ContentInsertionPane.svelte | 638 ++++++++++++------ .../EnhancedImagePlaceholder.svelte | 21 +- .../components/GalleryPlaceholder.svelte | 23 +- .../components/GeolocationPlaceholder.svelte | 311 +++------ .../components/IFramePlaceholder.svelte | 64 +- .../components/ImagePlaceholder.svelte | 42 +- .../components/SlashCommandList.svelte | 4 +- .../components/UrlEmbedPlaceholder.svelte | 319 +++------ .../components/VideoPlaceholder.svelte | 64 +- 16 files changed, 1024 insertions(+), 810 deletions(-) diff --git a/src/lib/components/admin/composer/BubbleColorPicker.svelte b/src/lib/components/admin/composer/BubbleColorPicker.svelte index d62ca63..e5954f6 100644 --- a/src/lib/components/admin/composer/BubbleColorPicker.svelte +++ b/src/lib/components/admin/composer/BubbleColorPicker.svelte @@ -36,7 +36,7 @@ '#FFC107', // Amber '#FF9800', // Orange '#FF5722', // Deep Orange - '#795548' // Brown + '#795548' // Brown ] // Lighter, pastel colors for highlighting @@ -60,7 +60,7 @@ '#FFE0B2', // Light Orange '#FFCCBC', // Light Deep Orange '#D7CCC8', // Light Brown - '#F5F5F5' // Light Gray + '#F5F5F5' // Light Gray ] const presetColors = $derived(mode === 'text' ? textPresetColors : highlightPresetColors) @@ -123,9 +123,7 @@
{mode === 'text' ? 'Text Color' : 'Highlight Color'} - +
@@ -141,7 +139,7 @@
{#if !showPicker} - {:else} @@ -152,9 +150,7 @@ sliderDirection="horizontal" isAlpha={false} /> - +
{/if}
@@ -335,7 +331,7 @@ :global(.bubble-color-picker .input) { margin-top: 8px; - + input { width: 100%; padding: 6px 10px; @@ -354,4 +350,4 @@ } } } - \ No newline at end of file + diff --git a/src/lib/components/admin/composer/BubbleTextStyleMenu.svelte b/src/lib/components/admin/composer/BubbleTextStyleMenu.svelte index 4e2c341..1009f35 100644 --- a/src/lib/components/admin/composer/BubbleTextStyleMenu.svelte +++ b/src/lib/components/admin/composer/BubbleTextStyleMenu.svelte @@ -1,6 +1,6 @@ - + + {#if showPane} + paneManager.close()} + {deleteNode} + {albumId} + /> + {/if} \ No newline at end of file + + .location-form { + display: flex; + flex-direction: column; + gap: $unit-2x; + } + + .form-group { + display: flex; + flex-direction: column; + gap: $unit-half; + } + + .form-label { + font-size: $font-size-extra-small; + font-weight: 500; + color: $gray-30; + } + + .form-input, + .form-textarea { + padding: $unit $unit-2x; + border: 1px solid $gray-85; + border-radius: $corner-radius-sm; + font-size: $font-size-small; + background: $white; + font-family: inherit; + + &:focus { + outline: none; + border-color: $primary-color; + } + } + + .form-textarea { + resize: vertical; + min-height: 60px; + } + + .coordinates-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $unit-2x; + } + + .location-options { + display: flex; + gap: $unit-3x; + align-items: center; + } + + .option-label { + display: flex; + align-items: center; + gap: $unit; + font-size: $font-size-extra-small; + font-weight: 500; + color: $gray-30; + } + + .color-input { + width: 36px; + height: 24px; + padding: 0; + border: 1px solid $gray-85; + border-radius: $corner-radius-sm; + cursor: pointer; + } + + .zoom-input { + width: 100px; + } + + .submit-btn { + width: 100%; + padding: $unit-2x; + background: $primary-color; + color: $white; + border: none; + border-radius: $corner-radius-sm; + font-size: $font-size-small; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + + &:hover:not(:disabled) { + background: darken($primary-color, 10%); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + .required { + color: $red-60; + } + diff --git a/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte b/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte index 00799a4..9d4016f 100644 --- a/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte +++ b/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte @@ -4,6 +4,7 @@ import { NodeViewWrapper } from 'svelte-tiptap' import { getContext } from 'svelte' import ContentInsertionPane from './ContentInsertionPane.svelte' + import { paneManager } from '$lib/stores/pane-manager' const { editor, deleteNode, getPos }: NodeViewProps = $props() @@ -11,20 +12,29 @@ const editorContext = getContext('editorContext') || {} const albumId = $derived(editorContext.albumId) + // Generate unique pane ID based on node position + const paneId = $derived(`image-placeholder-${getPos?.() ?? Math.random()}`) + let showPane = $state(false) let panePosition = $state({ x: 0, y: 0 }) + // Subscribe to pane manager + const paneState = $derived($paneManager) + $effect(() => { + showPane = paneManager.isActive(paneId, paneState) + }) + function handleClick(e: MouseEvent) { if (!editor.isEditable) return e.preventDefault() - + // Get position for pane const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() panePosition = { x: rect.left, y: rect.bottom + 8 } - showPane = true + paneManager.open(paneId) } // Handle keyboard navigation @@ -34,7 +44,7 @@ handleClick(e as any) } else if (e.key === 'Escape') { if (showPane) { - showPane = false + paneManager.close() } else { deleteNode() } @@ -53,12 +63,13 @@ Insert an image - + {#if showPane} showPane = false} + contentType="image" + onClose={() => paneManager.close()} {deleteNode} {albumId} /> diff --git a/src/lib/components/edra/headless/components/GalleryPlaceholder.svelte b/src/lib/components/edra/headless/components/GalleryPlaceholder.svelte index bafd55e..4d3d6c5 100644 --- a/src/lib/components/edra/headless/components/GalleryPlaceholder.svelte +++ b/src/lib/components/edra/headless/components/GalleryPlaceholder.svelte @@ -4,27 +4,37 @@ import { NodeViewWrapper } from 'svelte-tiptap' import { getContext } from 'svelte' import ContentInsertionPane from './ContentInsertionPane.svelte' + import { paneManager } from '$lib/stores/pane-manager' - const { editor, deleteNode }: NodeViewProps = $props() + const { editor, deleteNode, getPos }: NodeViewProps = $props() // Get album context if available const editorContext = getContext('editorContext') || {} const albumId = $derived(editorContext.albumId) + // Generate unique pane ID based on node position + const paneId = $derived(`gallery-${getPos?.() ?? Math.random()}`) + let showPane = $state(false) let panePosition = $state({ x: 0, y: 0 }) + // Subscribe to pane manager + const paneState = $derived($paneManager) + $effect(() => { + showPane = paneManager.isActive(paneId, paneState) + }) + function handleClick(e: MouseEvent) { if (!editor.isEditable) return e.preventDefault() - + // Get position for pane const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() panePosition = { x: rect.left, y: rect.bottom + 8 } - showPane = true + paneManager.open(paneId) } // Handle keyboard navigation @@ -34,7 +44,7 @@ handleClick(e as any) } else if (e.key === 'Escape') { if (showPane) { - showPane = false + paneManager.close() } else { deleteNode() } @@ -53,12 +63,13 @@ Insert a gallery - + {#if showPane} showPane = false} + contentType="gallery" + onClose={() => paneManager.close()} {deleteNode} {albumId} /> diff --git a/src/lib/components/edra/headless/components/GeolocationPlaceholder.svelte b/src/lib/components/edra/headless/components/GeolocationPlaceholder.svelte index 1341795..0fa75b3 100644 --- a/src/lib/components/edra/headless/components/GeolocationPlaceholder.svelte +++ b/src/lib/components/edra/headless/components/GeolocationPlaceholder.svelte @@ -1,257 +1,118 @@ - -
-
- -
+ + - {#if !isConfigured} -
-

Add Location

-

Add a map with a location marker

- -
- {:else} -
-

Configure Location

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- {/if} -
+ {#if showPane} + paneManager.close()} + {deleteNode} + {albumId} + /> + {/if}
diff --git a/src/lib/components/edra/headless/components/IFramePlaceholder.svelte b/src/lib/components/edra/headless/components/IFramePlaceholder.svelte index a27310e..a2feaae 100644 --- a/src/lib/components/edra/headless/components/IFramePlaceholder.svelte +++ b/src/lib/components/edra/headless/components/IFramePlaceholder.svelte @@ -2,30 +2,78 @@ import type { NodeViewProps } from '@tiptap/core' import CodeXML from 'lucide-svelte/icons/code-xml' import { NodeViewWrapper } from 'svelte-tiptap' - const { editor }: NodeViewProps = $props() + import { getContext } from 'svelte' + import ContentInsertionPane from './ContentInsertionPane.svelte' + import { paneManager } from '$lib/stores/pane-manager' + + const { editor, deleteNode, getPos }: NodeViewProps = $props() + + // Get album context if available + const editorContext = getContext('editorContext') || {} + const albumId = $derived(editorContext.albumId) + + // Generate unique pane ID based on node position + const paneId = $derived(`iframe-${getPos?.() ?? Math.random()}`) + + let showPane = $state(false) + let panePosition = $state({ x: 0, y: 0 }) + + // Subscribe to pane manager + const paneState = $derived($paneManager) + $effect(() => { + showPane = paneManager.isActive(paneId, paneState) + }) function handleClick(e: MouseEvent) { if (!editor.isEditable) return e.preventDefault() - const iFrameURL = prompt('Enter the URL of an iFrame:') - if (!iFrameURL) { - return + + // Get position for pane + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + panePosition = { + x: rect.left, + y: rect.bottom + 8 + } + paneManager.open(paneId) + } + + // Handle keyboard navigation + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick(e as any) + } else if (e.key === 'Escape') { + if (showPane) { + paneManager.close() + } else { + deleteNode() + } } - editor.chain().focus().setIframe({ src: iFrameURL }).run() } - + + {#if showPane} + paneManager.close()} + {deleteNode} + {albumId} + /> + {/if} \ No newline at end of file + diff --git a/src/lib/components/edra/headless/components/UrlEmbedPlaceholder.svelte b/src/lib/components/edra/headless/components/UrlEmbedPlaceholder.svelte index 2fe1062..b964bfd 100644 --- a/src/lib/components/edra/headless/components/UrlEmbedPlaceholder.svelte +++ b/src/lib/components/edra/headless/components/UrlEmbedPlaceholder.svelte @@ -1,279 +1,118 @@ - - {#if showInput && !node.attrs.url} -
- - -
- {:else if loading} -
- - Loading preview... -
- {:else if error} -
- -
- {errorMessage} - -
-
- {:else} - - - - Embed a link - + + + + {#if showPane} + paneManager.close()} + {deleteNode} + {albumId} + /> {/if} diff --git a/src/lib/components/edra/headless/components/VideoPlaceholder.svelte b/src/lib/components/edra/headless/components/VideoPlaceholder.svelte index a12fb84..abebc6e 100644 --- a/src/lib/components/edra/headless/components/VideoPlaceholder.svelte +++ b/src/lib/components/edra/headless/components/VideoPlaceholder.svelte @@ -2,30 +2,78 @@ import type { NodeViewProps } from '@tiptap/core' import Video from 'lucide-svelte/icons/video' import { NodeViewWrapper } from 'svelte-tiptap' - const { editor }: NodeViewProps = $props() + import { getContext } from 'svelte' + import ContentInsertionPane from './ContentInsertionPane.svelte' + import { paneManager } from '$lib/stores/pane-manager' + + const { editor, deleteNode, getPos }: NodeViewProps = $props() + + // Get album context if available + const editorContext = getContext('editorContext') || {} + const albumId = $derived(editorContext.albumId) + + // Generate unique pane ID based on node position + const paneId = $derived(`video-${getPos?.() ?? Math.random()}`) + + let showPane = $state(false) + let panePosition = $state({ x: 0, y: 0 }) + + // Subscribe to pane manager + const paneState = $derived($paneManager) + $effect(() => { + showPane = paneManager.isActive(paneId, paneState) + }) function handleClick(e: MouseEvent) { if (!editor.isEditable) return e.preventDefault() - const videoUrl = prompt('Enter the URL of the video:') - if (!videoUrl) { - return + + // Get position for pane + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + panePosition = { + x: rect.left, + y: rect.bottom + 8 + } + paneManager.open(paneId) + } + + // Handle keyboard navigation + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick(e as any) + } else if (e.key === 'Escape') { + if (showPane) { + paneManager.close() + } else { + deleteNode() + } } - editor.chain().focus().setVideo(videoUrl).run() } - + + {#if showPane} + paneManager.close()} + {deleteNode} + {albumId} + /> + {/if}