@refrakt-md/editor 0.8.2 → 0.8.3
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/app/dist/assets/{index-Bn8ajfVl.js → index-3MvwKRVQ.js} +1 -1
- package/app/dist/assets/{index-DNtuldOx.js → index-B7e694w6.js} +1 -1
- package/app/dist/assets/{index-xo7v6nRB.js → index-BBljOYQu.js} +1 -1
- package/app/dist/assets/{index-DQUOY-pF.js → index-BEGy_i8o.js} +1 -1
- package/app/dist/assets/{index-DNJBunzP.js → index-BGy7ixjW.js} +1 -1
- package/app/dist/assets/{index-dGztG-54.js → index-BaLgiiKk.js} +1 -1
- package/app/dist/assets/{index-D5ucdUTo.js → index-BjlNcvOf.js} +1 -1
- package/app/dist/assets/{index-Cgbvx23V.js → index-CKfKYVw7.js} +1 -1
- package/app/dist/assets/{index-BDj1XPol.js → index-COFbngzR.js} +1 -1
- package/app/dist/assets/{index-aPeHMqUX.js → index-CPEo_rvd.js} +1 -1
- package/app/dist/assets/{index-BXe1fKaT.js → index-CQDCT-XT.js} +1 -1
- package/app/dist/assets/{index-CXeK-dZx.js → index-CUmEjEeR.js} +1 -1
- package/app/dist/assets/{index-80NtMar1.js → index-CeV-Af4N.js} +1 -1
- package/app/dist/assets/{index-CaRBCHaX.js → index-ChbH55h5.js} +1 -1
- package/app/dist/assets/index-CzvG5PZT.css +1 -0
- package/app/dist/assets/{index-BfxTGrHB.js → index-D9-aYc3I.js} +1 -1
- package/app/dist/assets/{index-DGYxLhpR.js → index-DezxtfNV.js} +1 -1
- package/app/dist/assets/{index-DskvyNKT.js → index-DrI4IfXE.js} +1 -1
- package/app/dist/assets/{index-CCkzIGTi.js → index-DwfxgjnU.js} +1 -1
- package/app/dist/assets/index-ogrpJNou.js +555 -0
- package/app/dist/index.html +2 -2
- package/app/src/lib/components/BlockEditor.svelte +381 -47
- package/app/src/lib/components/InlineEditor.svelte +15 -5
- package/app/src/lib/components/ProseBlockCard.svelte +446 -0
- package/app/src/lib/components/ProseEditPanel.svelte +470 -0
- package/app/src/lib/editor/block-parser.ts +59 -2
- package/package.json +6 -6
- package/app/dist/assets/index-B6H6LF1M.css +0 -1
- package/app/dist/assets/index-Cd12jZId.js +0 -479
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ProseBlock, ParsedBlock } from '../editor/block-parser.js';
|
|
3
|
+
import type { ThemeConfig, RendererNode } from '@refrakt-md/transform';
|
|
4
|
+
import { renderBlockPreview } from '../preview/block-renderer.js';
|
|
5
|
+
import { initRuneBehaviors } from '@refrakt-md/behaviors';
|
|
6
|
+
import { stripInlineMarkdown } from '../editor/inline-markdown.js';
|
|
7
|
+
|
|
8
|
+
export interface ProseElementClickInfo {
|
|
9
|
+
/** Type of the clicked element: heading, paragraph, fence, list, quote, hr, image */
|
|
10
|
+
elementType: string;
|
|
11
|
+
/** Plain text content of the element */
|
|
12
|
+
text: string;
|
|
13
|
+
/** Bounding rect for popover positioning */
|
|
14
|
+
rect: DOMRect;
|
|
15
|
+
/** Index of the child block within the prose block's children array */
|
|
16
|
+
childIndex: number;
|
|
17
|
+
/** For headings: the level (1-6) */
|
|
18
|
+
headingLevel?: number;
|
|
19
|
+
/** For fences: the language string */
|
|
20
|
+
fenceLanguage?: string;
|
|
21
|
+
/** For fences: the code content */
|
|
22
|
+
fenceCode?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
block: ProseBlock;
|
|
27
|
+
themeConfig: ThemeConfig | null;
|
|
28
|
+
themeCss: string;
|
|
29
|
+
highlightCss?: string;
|
|
30
|
+
highlightTransform?: ((tree: RendererNode) => RendererNode) | null;
|
|
31
|
+
communityTags?: Record<string, unknown> | null;
|
|
32
|
+
communityPostTransforms?: Record<string, Function> | null;
|
|
33
|
+
communityStyles?: Record<string, Record<string, unknown>> | null;
|
|
34
|
+
aggregated?: Record<string, unknown>;
|
|
35
|
+
dragHandle?: boolean;
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
onsectionclick?: (info: ProseElementClickInfo) => void;
|
|
38
|
+
onblockclick?: (info: { x: number; y: number }) => void;
|
|
39
|
+
ondragstart: (e: DragEvent) => void;
|
|
40
|
+
ondragover: (e: DragEvent) => void;
|
|
41
|
+
ondrop: (e: DragEvent) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
block,
|
|
46
|
+
themeConfig,
|
|
47
|
+
themeCss,
|
|
48
|
+
highlightCss: hlCssProp = '',
|
|
49
|
+
highlightTransform: hlTransformProp = null,
|
|
50
|
+
communityTags = null,
|
|
51
|
+
communityPostTransforms = null,
|
|
52
|
+
communityStyles = null,
|
|
53
|
+
aggregated = {},
|
|
54
|
+
dragHandle = true,
|
|
55
|
+
readOnly = false,
|
|
56
|
+
onsectionclick,
|
|
57
|
+
onblockclick,
|
|
58
|
+
ondragstart,
|
|
59
|
+
ondragover,
|
|
60
|
+
ondrop,
|
|
61
|
+
}: Props = $props();
|
|
62
|
+
|
|
63
|
+
// ── Shadow DOM preview ─────────────────────────────────────
|
|
64
|
+
let previewContainer: HTMLDivElement | undefined = $state();
|
|
65
|
+
let shadowRoot: ShadowRoot | null = null;
|
|
66
|
+
let previewDebounce: ReturnType<typeof setTimeout>;
|
|
67
|
+
let behaviorCleanup: (() => void) | null = null;
|
|
68
|
+
|
|
69
|
+
/** Block-level element tag names we consider hoverable prose targets */
|
|
70
|
+
const PROSE_BLOCK_TAGS = new Set([
|
|
71
|
+
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
72
|
+
'P', 'PRE', 'UL', 'OL', 'BLOCKQUOTE', 'HR', 'TABLE',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** Human-readable label for a DOM element tag */
|
|
76
|
+
function tagLabel(tag: string): string {
|
|
77
|
+
if (tag.startsWith('H') && tag.length === 2) return `h${tag[1]}`;
|
|
78
|
+
const labels: Record<string, string> = {
|
|
79
|
+
P: 'paragraph',
|
|
80
|
+
PRE: 'code',
|
|
81
|
+
UL: 'list',
|
|
82
|
+
OL: 'list',
|
|
83
|
+
BLOCKQUOTE: 'quote',
|
|
84
|
+
HR: 'divider',
|
|
85
|
+
TABLE: 'table',
|
|
86
|
+
};
|
|
87
|
+
return labels[tag] ?? tag.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Find the nearest block-level prose target from the event target */
|
|
91
|
+
function findProseTarget(start: HTMLElement, root: HTMLElement): HTMLElement | null {
|
|
92
|
+
let el: HTMLElement | null = start;
|
|
93
|
+
while (el && el !== root) {
|
|
94
|
+
if (PROSE_BLOCK_TAGS.has(el.tagName)) return el;
|
|
95
|
+
el = el.parentElement;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Map a DOM element back to its child index in the prose block.
|
|
101
|
+
* Uses text-matching: compare the element's text content against each child's stripped text. */
|
|
102
|
+
function findChildIndex(el: HTMLElement): number {
|
|
103
|
+
const elText = (el.textContent ?? '').trim();
|
|
104
|
+
const tag = el.tagName;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < block.children.length; i++) {
|
|
107
|
+
const child = block.children[i];
|
|
108
|
+
|
|
109
|
+
// Match by type + text content
|
|
110
|
+
if (tag.startsWith('H') && tag.length === 2 && child.type === 'heading') {
|
|
111
|
+
const childText = stripInlineMarkdown((child as any).text ?? '').trim();
|
|
112
|
+
if (elText === childText) return i;
|
|
113
|
+
} else if (tag === 'P' && child.type === 'paragraph') {
|
|
114
|
+
const childText = stripInlineMarkdown(child.source).trim();
|
|
115
|
+
if (elText === childText) return i;
|
|
116
|
+
} else if (tag === 'PRE' && child.type === 'fence') {
|
|
117
|
+
// Code fences render as <pre><code>...</code></pre>
|
|
118
|
+
const codeEl = el.querySelector('code');
|
|
119
|
+
const codeText = (codeEl ?? el).textContent?.trim() ?? '';
|
|
120
|
+
const childCode = ((child as any).code ?? '').trim();
|
|
121
|
+
if (codeText === childCode) return i;
|
|
122
|
+
} else if ((tag === 'UL' || tag === 'OL') && child.type === 'list') {
|
|
123
|
+
const childText = child.source.replace(/^[-*+\d.]+\s*/gm, '').trim();
|
|
124
|
+
if (elText.includes(childText.slice(0, 30)) || childText.includes(elText.slice(0, 30))) return i;
|
|
125
|
+
} else if (tag === 'BLOCKQUOTE' && child.type === 'quote') {
|
|
126
|
+
const childText = child.source.replace(/^>\s*/gm, '').trim();
|
|
127
|
+
if (elText === childText) return i;
|
|
128
|
+
} else if (tag === 'HR' && child.type === 'hr') {
|
|
129
|
+
return i;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Attach hover + click handlers to the shadow DOM wrapper */
|
|
137
|
+
function attachProseHandlers(wrapper: HTMLElement) {
|
|
138
|
+
let hoveredEl: HTMLElement | null = null;
|
|
139
|
+
let removeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
140
|
+
const HOVER_DEBOUNCE = 75;
|
|
141
|
+
|
|
142
|
+
// Tooltip element
|
|
143
|
+
const tooltip = document.createElement('div');
|
|
144
|
+
tooltip.className = 'rf-edit-tooltip';
|
|
145
|
+
wrapper.appendChild(tooltip);
|
|
146
|
+
|
|
147
|
+
function showTooltip(label: string, me: MouseEvent) {
|
|
148
|
+
tooltip.textContent = label;
|
|
149
|
+
tooltip.style.left = `${me.clientX + 12}px`;
|
|
150
|
+
tooltip.style.top = `${me.clientY + 12}px`;
|
|
151
|
+
tooltip.classList.add('rf-tooltip-visible');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hideTooltip() {
|
|
155
|
+
tooltip.classList.remove('rf-tooltip-visible');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
wrapper.addEventListener('mouseover', (e) => {
|
|
159
|
+
const target = findProseTarget(e.target as HTMLElement, wrapper);
|
|
160
|
+
|
|
161
|
+
if (removeTimer !== null) {
|
|
162
|
+
clearTimeout(removeTimer);
|
|
163
|
+
removeTimer = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (target === hoveredEl) {
|
|
167
|
+
// Same target — already highlighted
|
|
168
|
+
} else {
|
|
169
|
+
hoveredEl?.classList.remove('rf-editable-hover');
|
|
170
|
+
hoveredEl = target;
|
|
171
|
+
hoveredEl?.classList.add('rf-editable-hover');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (target) {
|
|
175
|
+
wrapper.classList.remove('rf-block-hover');
|
|
176
|
+
showTooltip(tagLabel(target.tagName), e as MouseEvent);
|
|
177
|
+
} else if (onblockclick) {
|
|
178
|
+
wrapper.classList.add('rf-block-hover');
|
|
179
|
+
showTooltip('prose', e as MouseEvent);
|
|
180
|
+
} else {
|
|
181
|
+
hideTooltip();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
wrapper.addEventListener('mousemove', (e) => {
|
|
186
|
+
if (tooltip.classList.contains('rf-tooltip-visible')) {
|
|
187
|
+
tooltip.style.left = `${e.clientX + 12}px`;
|
|
188
|
+
tooltip.style.top = `${e.clientY + 12}px`;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
wrapper.addEventListener('mouseout', (e) => {
|
|
193
|
+
const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
|
194
|
+
wrapper.classList.remove('rf-block-hover');
|
|
195
|
+
if (hoveredEl && (!related || !hoveredEl.contains(related))) {
|
|
196
|
+
if (removeTimer !== null) clearTimeout(removeTimer);
|
|
197
|
+
const el = hoveredEl;
|
|
198
|
+
removeTimer = setTimeout(() => {
|
|
199
|
+
el.classList.remove('rf-editable-hover');
|
|
200
|
+
if (hoveredEl === el) hoveredEl = null;
|
|
201
|
+
removeTimer = null;
|
|
202
|
+
}, HOVER_DEBOUNCE);
|
|
203
|
+
hideTooltip();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
wrapper.addEventListener('click', (e) => {
|
|
208
|
+
hideTooltip();
|
|
209
|
+
const target = findProseTarget(e.target as HTMLElement, wrapper);
|
|
210
|
+
if (!target) {
|
|
211
|
+
// Dead space — open prose edit panel
|
|
212
|
+
onblockclick?.({ x: (e as MouseEvent).clientX, y: (e as MouseEvent).clientY });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopPropagation();
|
|
218
|
+
|
|
219
|
+
const childIndex = findChildIndex(target);
|
|
220
|
+
if (childIndex === -1) return;
|
|
221
|
+
|
|
222
|
+
const child = block.children[childIndex];
|
|
223
|
+
const rect = target.getBoundingClientRect();
|
|
224
|
+
const tag = target.tagName;
|
|
225
|
+
|
|
226
|
+
const info: ProseElementClickInfo = {
|
|
227
|
+
elementType: child.type,
|
|
228
|
+
text: (target.textContent ?? '').trim(),
|
|
229
|
+
rect,
|
|
230
|
+
childIndex,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (child.type === 'heading') {
|
|
234
|
+
info.headingLevel = (child as any).level;
|
|
235
|
+
} else if (child.type === 'fence') {
|
|
236
|
+
info.fenceLanguage = (child as any).language ?? '';
|
|
237
|
+
info.fenceCode = (child as any).code ?? '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
onsectionclick?.(info);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Hover CSS injected into shadow DOM ───────────────────────
|
|
245
|
+
const hoverCss = `
|
|
246
|
+
/* Wrapper: inset border on dead-space hover */
|
|
247
|
+
.rf-preview-wrapper {
|
|
248
|
+
transition: box-shadow 150ms ease;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
}
|
|
251
|
+
.rf-preview-wrapper.rf-block-hover {
|
|
252
|
+
box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.5);
|
|
253
|
+
border-radius: 4px;
|
|
254
|
+
}
|
|
255
|
+
/* Block-level elements: invisible outline + transition */
|
|
256
|
+
h1, h2, h3, h4, h5, h6, p, pre, ul, ol, blockquote, hr, table {
|
|
257
|
+
outline: 2px dashed transparent;
|
|
258
|
+
outline-offset: 4px;
|
|
259
|
+
border-radius: 4px;
|
|
260
|
+
transition: outline-color 150ms ease;
|
|
261
|
+
}
|
|
262
|
+
/* Hover affordance */
|
|
263
|
+
.rf-editable-hover {
|
|
264
|
+
outline-color: rgba(59, 130, 246, 0.5);
|
|
265
|
+
}
|
|
266
|
+
.rf-editable-hover, .rf-editable-hover * {
|
|
267
|
+
cursor: text !important;
|
|
268
|
+
}
|
|
269
|
+
/* Hover tooltip */
|
|
270
|
+
.rf-edit-tooltip {
|
|
271
|
+
position: fixed;
|
|
272
|
+
padding: 2px 8px;
|
|
273
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
274
|
+
font-size: 11px;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
line-height: 1.4;
|
|
277
|
+
color: #fff;
|
|
278
|
+
background: rgba(30, 41, 59, 0.9);
|
|
279
|
+
border-radius: 4px;
|
|
280
|
+
pointer-events: none;
|
|
281
|
+
z-index: 10000;
|
|
282
|
+
white-space: nowrap;
|
|
283
|
+
opacity: 0;
|
|
284
|
+
transition: opacity 120ms ease;
|
|
285
|
+
}
|
|
286
|
+
.rf-edit-tooltip.rf-tooltip-visible {
|
|
287
|
+
opacity: 1;
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
// ── Preview rendering ───────────────────────────────────────
|
|
292
|
+
$effect(() => {
|
|
293
|
+
if (!previewContainer || !themeConfig) return;
|
|
294
|
+
|
|
295
|
+
if (!shadowRoot || shadowRoot.host !== previewContainer) {
|
|
296
|
+
shadowRoot = previewContainer.attachShadow({ mode: 'open' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const source = block.source;
|
|
300
|
+
const config = themeConfig;
|
|
301
|
+
const css = themeCss;
|
|
302
|
+
const hlCss = hlCssProp || '';
|
|
303
|
+
const hlTransform = hlTransformProp;
|
|
304
|
+
|
|
305
|
+
clearTimeout(previewDebounce);
|
|
306
|
+
previewDebounce = setTimeout(() => {
|
|
307
|
+
if (!shadowRoot) return;
|
|
308
|
+
|
|
309
|
+
if (behaviorCleanup) {
|
|
310
|
+
behaviorCleanup();
|
|
311
|
+
behaviorCleanup = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const { html } = renderBlockPreview(
|
|
316
|
+
source,
|
|
317
|
+
config,
|
|
318
|
+
hlTransform,
|
|
319
|
+
communityTags ?? undefined,
|
|
320
|
+
communityPostTransforms ?? undefined,
|
|
321
|
+
aggregated,
|
|
322
|
+
communityStyles ?? undefined,
|
|
323
|
+
);
|
|
324
|
+
const scopedCss = css.replace(/:root/g, ':host');
|
|
325
|
+
shadowRoot.innerHTML = `<style>${scopedCss}
|
|
326
|
+
${hlCss}
|
|
327
|
+
:host { display: block; }
|
|
328
|
+
.rf-preview-wrapper {
|
|
329
|
+
font-family: var(--rf-font-sans, system-ui, -apple-system, sans-serif);
|
|
330
|
+
color: var(--rf-color-text, #1a1a2e);
|
|
331
|
+
line-height: 1.6;
|
|
332
|
+
--rf-content-max: calc(100% - 6rem);
|
|
333
|
+
--rf-content-gutter: 1.5rem;
|
|
334
|
+
}
|
|
335
|
+
.rf-preview-wrapper > article {
|
|
336
|
+
display: grid;
|
|
337
|
+
grid-template-columns:
|
|
338
|
+
[full-start] 1fr
|
|
339
|
+
[wide-start] minmax(0, var(--rf-wide-inset, 8rem))
|
|
340
|
+
[content-start] min(var(--rf-content-max), 100% - var(--rf-content-gutter) * 2)
|
|
341
|
+
[content-end] minmax(0, var(--rf-wide-inset, 8rem))
|
|
342
|
+
[wide-end] 1fr
|
|
343
|
+
[full-end];
|
|
344
|
+
}
|
|
345
|
+
.rf-preview-wrapper > article > * { grid-column: content; }
|
|
346
|
+
${!readOnly && onsectionclick ? hoverCss : ''}
|
|
347
|
+
</style>
|
|
348
|
+
<div class="rf-preview-wrapper">${html}</div>`;
|
|
349
|
+
|
|
350
|
+
const wrapper = shadowRoot.querySelector('.rf-preview-wrapper') as HTMLElement | null;
|
|
351
|
+
if (wrapper) {
|
|
352
|
+
behaviorCleanup = initRuneBehaviors(wrapper);
|
|
353
|
+
wrapper.addEventListener('click', (e) => {
|
|
354
|
+
const anchor = (e.target as HTMLElement).closest('a');
|
|
355
|
+
if (anchor) e.preventDefault();
|
|
356
|
+
}, true);
|
|
357
|
+
|
|
358
|
+
// Attach inline-edit hover + click handlers for prose elements
|
|
359
|
+
if (!readOnly && onsectionclick) {
|
|
360
|
+
attachProseHandlers(wrapper);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
if (shadowRoot) {
|
|
365
|
+
shadowRoot.innerHTML = `<style>:host { display: block; padding: 0.75rem 1.5rem; color: #999; font-family: system-ui; font-size: 12px; }</style><em>Preview unavailable</em>`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}, 50);
|
|
369
|
+
|
|
370
|
+
return () => {
|
|
371
|
+
clearTimeout(previewDebounce);
|
|
372
|
+
if (behaviorCleanup) {
|
|
373
|
+
behaviorCleanup();
|
|
374
|
+
behaviorCleanup = null;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
</script>
|
|
380
|
+
|
|
381
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
382
|
+
<div
|
|
383
|
+
class="prose-card"
|
|
384
|
+
draggable={dragHandle ? 'true' : 'false'}
|
|
385
|
+
ondragstart={ondragstart}
|
|
386
|
+
ondragover={ondragover}
|
|
387
|
+
ondrop={ondrop}
|
|
388
|
+
>
|
|
389
|
+
{#if dragHandle}
|
|
390
|
+
<span class="prose-card__drag" title="Drag to reorder">
|
|
391
|
+
<svg width="8" height="14" viewBox="0 0 8 14" fill="currentColor">
|
|
392
|
+
<circle cx="2" cy="2" r="1.2" />
|
|
393
|
+
<circle cx="6" cy="2" r="1.2" />
|
|
394
|
+
<circle cx="2" cy="7" r="1.2" />
|
|
395
|
+
<circle cx="6" cy="7" r="1.2" />
|
|
396
|
+
<circle cx="2" cy="12" r="1.2" />
|
|
397
|
+
<circle cx="6" cy="12" r="1.2" />
|
|
398
|
+
</svg>
|
|
399
|
+
</span>
|
|
400
|
+
{/if}
|
|
401
|
+
|
|
402
|
+
{#if themeConfig}
|
|
403
|
+
<div class="prose-card__preview" bind:this={previewContainer}></div>
|
|
404
|
+
{/if}
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<style>
|
|
408
|
+
.prose-card {
|
|
409
|
+
position: relative;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Drag handle */
|
|
413
|
+
.prose-card__drag {
|
|
414
|
+
position: absolute;
|
|
415
|
+
left: 6px;
|
|
416
|
+
top: 6px;
|
|
417
|
+
cursor: grab;
|
|
418
|
+
color: var(--ed-text-muted);
|
|
419
|
+
padding: 0.2rem;
|
|
420
|
+
user-select: none;
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
opacity: 0;
|
|
424
|
+
transition: opacity var(--ed-transition-fast);
|
|
425
|
+
z-index: 1;
|
|
426
|
+
border-radius: var(--ed-radius-sm);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.prose-card:hover .prose-card__drag {
|
|
430
|
+
opacity: 0.35;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.prose-card:hover .prose-card__drag:hover {
|
|
434
|
+
opacity: 1;
|
|
435
|
+
background: var(--ed-surface-2);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.prose-card__drag:active {
|
|
439
|
+
cursor: grabbing;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* Preview */
|
|
443
|
+
.prose-card__preview {
|
|
444
|
+
overflow: hidden;
|
|
445
|
+
}
|
|
446
|
+
</style>
|