diff --git a/src/lib/actions/tooltip.ts b/src/lib/actions/tooltip.ts new file mode 100644 index 0000000..5d061e4 --- /dev/null +++ b/src/lib/actions/tooltip.ts @@ -0,0 +1,52 @@ +import tippy, { type Props as TippyProps, type Instance } from 'tippy.js' + +export interface TooltipOptions extends Partial { + content: string + enabled?: boolean +} + +export function tooltip(element: HTMLElement, options: TooltipOptions | string) { + let instance: Instance | undefined + + function createTooltip(opts: TooltipOptions | string) { + // Normalize options + const config: TooltipOptions = typeof opts === 'string' ? { content: opts } : opts + + // Skip if disabled + if (config.enabled === false) return + + // Create tippy instance with sensible defaults + instance = tippy(element, { + content: config.content, + placement: config.placement || 'top', + arrow: config.arrow !== false, + animation: config.animation || 'scale', + theme: config.theme || 'link-tooltip', + delay: config.delay || [200, 0], + duration: config.duration || [200, 150], + offset: config.offset || [0, 10], + ...config + }) + } + + // Initialize tooltip + createTooltip(options) + + return { + update(newOptions: TooltipOptions | string) { + // Destroy existing instance + if (instance) { + instance.destroy() + instance = undefined + } + + // Create new instance with updated options + createTooltip(newOptions) + }, + destroy() { + if (instance) { + instance.destroy() + } + } + } +} \ No newline at end of file diff --git a/src/lib/components/edra/editor.css b/src/lib/components/edra/editor.css index b249457..175c4f0 100644 --- a/src/lib/components/edra/editor.css +++ b/src/lib/components/edra/editor.css @@ -1,5 +1,8 @@ /* Base TipTap Editor Styles with Light/Dark Theme Support */ +/* Import Tippy.js animation styles */ +@import 'tippy.js/animations/scale.css'; + .tiptap :first-child { margin-top: 0; } @@ -80,6 +83,7 @@ font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono'; } + /* List Styling */ .tiptap ul li p, .tiptap ol li p { @@ -180,16 +184,34 @@ input[type='checkbox'] { width: 1.5rem; height: 1.5rem; display: flex; - padding-right: 0.5rem; - flex-direction: column; - justify-content: center; + flex-direction: row; align-items: center; - cursor: grab; + justify-content: center; opacity: 100; - transition-property: opacity; + transition-property: opacity, background-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0.4, 0, 1, 1); color: var(--border-color-hover); + cursor: grab; + padding: 0.25rem; + border-radius: 6px; /* $corner-radius-sm */ +} + +/* Invisible hover zone to bridge the gap */ +.drag-handle::after { + content: ''; + position: absolute; + left: 100%; + top: 50%; + transform: translateY(-50%); + width: 12px; /* Slightly larger than the 8px gap */ + height: 100%; + pointer-events: auto; +} + +.drag-handle:hover { + opacity: 100 !important; + background-color: #e8e8e8; /* $grey-80 */ } .drag-handle:active { @@ -201,6 +223,11 @@ input[type='checkbox'] { pointer-events: none; } +.drag-handle.hide.menu-open { + opacity: 100; + pointer-events: auto; +} + @media screen and (max-width: 600px) { .drag-handle { display: none; diff --git a/src/lib/components/edra/headless/menus/link-menu.svelte b/src/lib/components/edra/headless/menus/link-menu.svelte index b12d527..45240af 100644 --- a/src/lib/components/edra/headless/menus/link-menu.svelte +++ b/src/lib/components/edra/headless/menus/link-menu.svelte @@ -2,10 +2,11 @@ import { type Editor } from '@tiptap/core' import { BubbleMenu } from 'svelte-tiptap' import type { ShouldShowProps } from '../../utils.js' - import Copy from 'lucide-svelte/icons/copy' import Trash from 'lucide-svelte/icons/trash' import Edit from 'lucide-svelte/icons/pen' import Check from 'lucide-svelte/icons/check' + import ExternalLink from 'lucide-svelte/icons/external-link' + import { tooltip } from '$lib/actions/tooltip' interface Props { editor: Editor @@ -13,9 +14,17 @@ let { editor }: Props = $props() - const link = $derived.by(() => editor.getAttributes('link').href) - + let link = $state('') let isEditing = $state(false) + let isCopied = $state(false) + + // Update link when editor selection changes + $effect(() => { + if (editor && editor.isActive('link')) { + const attrs = editor.getAttributes('link') + link = attrs.href || '' + } + }) function setLink(url: string) { if (url.trim() === '') { @@ -52,6 +61,9 @@ shouldShow={(props: ShouldShowProps) => { if (!props.editor.isEditable) return false if (props.editor.isActive('link')) { + // Update link state when bubble menu is shown + const attrs = props.editor.getAttributes('link') + link = attrs.href || '' return true } else { isEditing = false @@ -60,61 +72,237 @@ return false } }} - class="bubble-menu-wrapper" + class="bubble-menu-wrapper link-bubble-menu" + tippyOptions={{ + animation: 'scale', + duration: [200, 150], + inertia: true + }} > {#if isEditing} - + {:else} - {link} - {/if} - - {#if !isEditing} - - - {:else} - + {/if} + + diff --git a/src/lib/components/edra/tooltip.scss b/src/lib/components/edra/tooltip.scss new file mode 100644 index 0000000..12877d3 --- /dev/null +++ b/src/lib/components/edra/tooltip.scss @@ -0,0 +1,28 @@ +// Import Tippy.js base styles +@import 'tippy.js/dist/tippy.css'; +@import 'tippy.js/animations/scale.css'; + +// Link tooltip styles - only apply to tooltips with the link-tooltip theme +:global(.tippy-box[data-theme~='link-tooltip']) { + background-color: $grey-00; + color: $grey-100; + font-size: $font-size-extra-small; + font-family: $font-stack; + font-weight: $font-weight; + letter-spacing: $letter-spacing; + line-height: 1.4; + padding: $unit-half $unit; + border-radius: $corner-radius-sm; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 9999; +} + +:global(.tippy-box[data-theme~='link-tooltip'] .tippy-arrow) { + color: $grey-00; +} + +// Animation adjustments for link tooltips +:global(.tippy-box[data-theme~='link-tooltip'][data-animation='scale'][data-state='hidden']) { + opacity: 0; + transform: scale(0.5); +} \ No newline at end of file