@commonpub/layer 0.7.2 → 0.7.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.
Files changed (38) hide show
  1. package/components/editors/ArticleEditor.vue +11 -12
  2. package/components/editors/BlogEditor.vue +17 -18
  3. package/components/editors/ExplainerEditor.vue +13 -14
  4. package/components/editors/ProjectEditor.vue +17 -18
  5. package/composables/useMarkdownImport.ts +1 -1
  6. package/package.json +5 -5
  7. package/pages/docs/[siteSlug]/edit.vue +4 -4
  8. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  9. package/components/editors/BlockCanvas.vue +0 -487
  10. package/components/editors/BlockInsertZone.vue +0 -84
  11. package/components/editors/BlockPicker.vue +0 -285
  12. package/components/editors/BlockWrapper.vue +0 -192
  13. package/components/editors/EditorBlocks.vue +0 -248
  14. package/components/editors/EditorSection.vue +0 -81
  15. package/components/editors/EditorShell.vue +0 -196
  16. package/components/editors/EditorTagInput.vue +0 -114
  17. package/components/editors/EditorVisibility.vue +0 -110
  18. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  19. package/components/editors/blocks/CalloutBlock.vue +0 -122
  20. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  21. package/components/editors/blocks/CodeBlock.vue +0 -177
  22. package/components/editors/blocks/DividerBlock.vue +0 -22
  23. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  24. package/components/editors/blocks/EmbedBlock.vue +0 -20
  25. package/components/editors/blocks/GalleryBlock.vue +0 -236
  26. package/components/editors/blocks/HeadingBlock.vue +0 -96
  27. package/components/editors/blocks/ImageBlock.vue +0 -271
  28. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  29. package/components/editors/blocks/MathBlock.vue +0 -37
  30. package/components/editors/blocks/PartsListBlock.vue +0 -358
  31. package/components/editors/blocks/QuizBlock.vue +0 -47
  32. package/components/editors/blocks/QuoteBlock.vue +0 -101
  33. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  34. package/components/editors/blocks/SliderBlock.vue +0 -318
  35. package/components/editors/blocks/TextBlock.vue +0 -201
  36. package/components/editors/blocks/ToolListBlock.vue +0 -70
  37. package/components/editors/blocks/VideoBlock.vue +0 -22
  38. package/composables/useBlockEditor.ts +0 -187
@@ -1,487 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * BlockCanvas — the main editor canvas that renders a block array
4
- * with wrappers, insert zones, and a block picker.
5
- *
6
- * Supports:
7
- * - Insert zones between blocks (click to open picker)
8
- * - Slash command (/ in empty text block) opens picker inline
9
- * - Drag-and-drop reordering via BlockWrapper
10
- * - Floating text toolbar on selection (delegated to FloatingToolbar)
11
- */
12
- import type { Component } from 'vue';
13
- import type { BlockEditor, EditorBlock } from '../../composables/useBlockEditor';
14
- import type { BlockTypeGroup } from './BlockPicker.vue';
15
-
16
- // Direct imports — Nuxt auto-imports are compile-time only and don't work with <component :is>
17
- import TextBlock from './blocks/TextBlock.vue';
18
- import HeadingBlock from './blocks/HeadingBlock.vue';
19
- import CodeBlock from './blocks/CodeBlock.vue';
20
- import ImageBlock from './blocks/ImageBlock.vue';
21
- import QuoteBlock from './blocks/QuoteBlock.vue';
22
- import CalloutBlock from './blocks/CalloutBlock.vue';
23
- import DividerBlock from './blocks/DividerBlock.vue';
24
- import VideoBlock from './blocks/VideoBlock.vue';
25
- import EmbedBlock from './blocks/EmbedBlock.vue';
26
- import GalleryBlock from './blocks/GalleryBlock.vue';
27
- import PartsListBlock from './blocks/PartsListBlock.vue';
28
- import BuildStepBlock from './blocks/BuildStepBlock.vue';
29
- import ToolListBlock from './blocks/ToolListBlock.vue';
30
- import DownloadsBlock from './blocks/DownloadsBlock.vue';
31
- import QuizBlock from './blocks/QuizBlock.vue';
32
- import SliderBlock from './blocks/SliderBlock.vue';
33
- import CheckpointBlock from './blocks/CheckpointBlock.vue';
34
- import MathBlock from './blocks/MathBlock.vue';
35
- import SectionHeaderBlock from './blocks/SectionHeaderBlock.vue';
36
- import MarkdownBlock from './blocks/MarkdownBlock.vue';
37
-
38
- const BLOCK_COMPONENTS: Record<string, Component> = {
39
- paragraph: TextBlock,
40
- text: TextBlock,
41
- heading: HeadingBlock,
42
- code: CodeBlock,
43
- code_block: CodeBlock,
44
- codeBlock: CodeBlock,
45
- image: ImageBlock,
46
- gallery: GalleryBlock,
47
- quote: QuoteBlock,
48
- blockquote: QuoteBlock,
49
- callout: CalloutBlock,
50
- divider: DividerBlock,
51
- horizontal_rule: DividerBlock,
52
- horizontalRule: DividerBlock,
53
- video: VideoBlock,
54
- embed: EmbedBlock,
55
- partsList: PartsListBlock,
56
- buildStep: BuildStepBlock,
57
- toolList: ToolListBlock,
58
- downloads: DownloadsBlock,
59
- quiz: QuizBlock,
60
- interactiveSlider: SliderBlock,
61
- slider: SliderBlock,
62
- checkpoint: CheckpointBlock,
63
- mathNotation: MathBlock,
64
- math: MathBlock,
65
- bulletList: TextBlock,
66
- orderedList: TextBlock,
67
- sectionHeader: SectionHeaderBlock,
68
- markdown: MarkdownBlock,
69
- };
70
-
71
- const props = defineProps<{
72
- blockEditor: BlockEditor;
73
- blockTypes: BlockTypeGroup[];
74
- }>();
75
-
76
- // --- Block picker state ---
77
- const pickerVisible = ref(false);
78
- const pickerInsertIndex = ref(0);
79
- /** When non-null, slash command is replacing this block instead of inserting */
80
- const slashCommandBlockId = ref<string | null>(null);
81
-
82
- function openPicker(atIndex: number): void {
83
- slashCommandBlockId.value = null;
84
- pickerInsertIndex.value = atIndex;
85
- pickerVisible.value = true;
86
- }
87
-
88
- function openSlashPicker(block: EditorBlock): void {
89
- const idx = props.blockEditor.getBlockIndex(block.id);
90
- if (idx === -1) return;
91
- slashCommandBlockId.value = block.id;
92
- pickerInsertIndex.value = idx;
93
- pickerVisible.value = true;
94
- }
95
-
96
- function closePicker(): void {
97
- pickerVisible.value = false;
98
- slashCommandBlockId.value = null;
99
- }
100
-
101
- function onPickerSelect(type: string, attrs?: Record<string, unknown>): void {
102
- if (slashCommandBlockId.value) {
103
- // Slash command: replace the text block with the chosen type
104
- props.blockEditor.replaceBlock(slashCommandBlockId.value, type, attrs);
105
- } else {
106
- // Normal insert
107
- props.blockEditor.addBlock(type, attrs, pickerInsertIndex.value);
108
- }
109
- closePicker();
110
- }
111
-
112
- // --- Floating toolbar state ---
113
- const floatingToolbar = ref<{
114
- visible: boolean;
115
- top: number;
116
- left: number;
117
- blockId: string;
118
- }>({ visible: false, top: 0, left: 0, blockId: '' });
119
-
120
- function onSelectionChange(block: EditorBlock, hasSelection: boolean, rect: DOMRect | null): void {
121
- if (hasSelection && rect) {
122
- const toolbarWidth = 180; // approximate toolbar width
123
- const toolbarHeight = 44;
124
- const rawTop = rect.top - toolbarHeight;
125
- const rawLeft = rect.left + rect.width / 2;
126
- floatingToolbar.value = {
127
- visible: true,
128
- top: Math.max(4, rawTop),
129
- left: Math.max(toolbarWidth / 2 + 4, Math.min(rawLeft, window.innerWidth - toolbarWidth / 2 - 4)),
130
- blockId: block.id,
131
- };
132
- } else {
133
- floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '' };
134
- }
135
- }
136
-
137
- // --- Floating toolbar commands ---
138
- const blockRefs = ref<Map<string, { getEditor?: () => unknown }>>(new Map());
139
-
140
- function setBlockRef(blockId: string, el: unknown): void {
141
- if (el && typeof el === 'object' && 'getEditor' in el) {
142
- blockRefs.value.set(blockId, el as { getEditor: () => unknown });
143
- }
144
- }
145
-
146
- function getActiveEditor(): unknown {
147
- const ref = blockRefs.value.get(floatingToolbar.value.blockId);
148
- return ref?.getEditor?.() ?? null;
149
- }
150
-
151
- /** TipTap editor chain interface for toolbar commands */
152
- interface TipTapChainable {
153
- focus: () => TipTapChainable;
154
- toggleMark: (mark: string) => TipTapChainable;
155
- unsetLink: () => TipTapChainable;
156
- extendMarkRange: (type: string) => TipTapChainable;
157
- setLink: (attrs: { href: string }) => TipTapChainable;
158
- run: () => void;
159
- }
160
-
161
- interface TipTapEditor {
162
- chain: () => TipTapChainable;
163
- isActive: (name: string) => boolean;
164
- }
165
-
166
- function toggleMark(mark: string): void {
167
- const editor = getActiveEditor() as TipTapEditor | null;
168
- if (!editor) return;
169
- editor.chain().focus().toggleMark(mark).run();
170
- }
171
-
172
- function toggleLink(): void {
173
- const editor = getActiveEditor() as TipTapEditor | null;
174
- if (!editor) return;
175
- if (editor.isActive('link')) {
176
- editor.chain().focus().unsetLink().run();
177
- return;
178
- }
179
- const url = window.prompt('Enter URL:');
180
- if (url) {
181
- editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
182
- }
183
- }
184
-
185
- // --- Empty state ---
186
- function addFirstBlock(): void {
187
- props.blockEditor.addBlock('paragraph');
188
- }
189
-
190
- // --- Block actions ---
191
- function onSelect(block: EditorBlock): void {
192
- props.blockEditor.selectBlock(block.id);
193
- }
194
-
195
- function onDelete(block: EditorBlock): void {
196
- props.blockEditor.removeBlock(block.id);
197
- }
198
-
199
- function onDuplicate(block: EditorBlock): void {
200
- props.blockEditor.duplicateBlock(block.id);
201
- }
202
-
203
- function onMoveUp(block: EditorBlock): void {
204
- props.blockEditor.moveBlockUp(block.id);
205
- }
206
-
207
- function onMoveDown(block: EditorBlock): void {
208
- props.blockEditor.moveBlockDown(block.id);
209
- }
210
-
211
- function onBlockUpdate(block: EditorBlock, content: Record<string, unknown>): void {
212
- props.blockEditor.updateBlock(block.id, content);
213
- }
214
-
215
- /** Enter at end of a text block → create new paragraph below */
216
- function onEnterAtEnd(block: EditorBlock): void {
217
- const idx = props.blockEditor.getBlockIndex(block.id);
218
- if (idx === -1) return;
219
- props.blockEditor.addBlock('paragraph', undefined, idx + 1);
220
- }
221
-
222
- /** Backspace in empty text block → delete it and focus previous */
223
- function onBackspaceEmpty(block: EditorBlock): void {
224
- const idx = props.blockEditor.getBlockIndex(block.id);
225
- if (idx === -1) return;
226
- // Don't delete the last block
227
- if (props.blockEditor.blocks.value.length <= 1) return;
228
- props.blockEditor.removeBlock(block.id);
229
- }
230
-
231
- // --- Drag and drop ---
232
- const draggedBlockId = ref<string | null>(null);
233
-
234
- function onDragStart(block: EditorBlock): void {
235
- draggedBlockId.value = block.id;
236
- }
237
-
238
- function onDragEnd(): void {
239
- draggedBlockId.value = null;
240
- }
241
-
242
- function onDrop(atIndex: number, event: DragEvent): void {
243
- event.preventDefault();
244
- if (!draggedBlockId.value) return;
245
-
246
- const fromIndex = props.blockEditor.getBlockIndex(draggedBlockId.value);
247
- if (fromIndex === -1) return;
248
-
249
- const toIndex = atIndex > fromIndex ? atIndex - 1 : atIndex;
250
- props.blockEditor.moveBlock(fromIndex, toIndex);
251
- draggedBlockId.value = null;
252
- }
253
-
254
- // --- Click outside to deselect ---
255
- function onCanvasClick(): void {
256
- props.blockEditor.selectBlock(null);
257
- floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '' };
258
- }
259
-
260
- // --- Resolve block component ---
261
- function getBlockComponent(type: string): Component {
262
- return BLOCK_COMPONENTS[type] ?? TextBlock;
263
- }
264
-
265
- /** Compute auto-numbered content for buildStep blocks */
266
- function getBlockContent(block: EditorBlock, index: number): Record<string, unknown> {
267
- if (block.type === 'buildStep') {
268
- // Count how many buildStep blocks precede this one
269
- let stepNum = 1;
270
- for (let i = 0; i < index; i++) {
271
- if (props.blockEditor.blocks.value[i].type === 'buildStep') stepNum++;
272
- }
273
- return { ...block.content, stepNumber: stepNum };
274
- }
275
- return block.content;
276
- }
277
-
278
- /** Check if a block type uses the TextBlock component (supports slash commands) */
279
- function isTextBlock(type: string): boolean {
280
- return type === 'paragraph' || type === 'bulletList' || type === 'orderedList';
281
- }
282
- </script>
283
-
284
- <template>
285
- <div class="cpub-block-canvas" @click.self="onCanvasClick">
286
- <!-- Page card wrapper — mimics document editing feel -->
287
- <div class="cpub-canvas-page">
288
-
289
- <!-- Empty state — click to create first paragraph -->
290
- <div v-if="blockEditor.isEmpty.value" class="cpub-canvas-empty" @click="addFirstBlock">
291
- <div class="cpub-canvas-empty-icon"><i class="fa-solid fa-pen-nib"></i></div>
292
- <p class="cpub-canvas-empty-title">Start writing</p>
293
- <p class="cpub-canvas-empty-desc">Click here to begin, or use the sidebar to add blocks</p>
294
- </div>
295
-
296
- <!-- Insert zone at top -->
297
- <EditorsBlockInsertZone @insert="openPicker(0)" />
298
- <!-- Picker at top position -->
299
- <div v-if="pickerVisible && !slashCommandBlockId && pickerInsertIndex === 0" class="cpub-canvas-picker-anchor">
300
- <EditorsBlockPicker :groups="blockTypes" :visible="true" @select="onPickerSelect" @close="closePicker" />
301
- </div>
302
-
303
- <!-- Block list -->
304
- <template v-for="(block, index) in blockEditor.blocks.value" :key="block.id">
305
- <EditorsBlockWrapper
306
- :block="block"
307
- :selected="blockEditor.selectedBlockId.value === block.id"
308
- @select="onSelect(block)"
309
- @delete="onDelete(block)"
310
- @duplicate="onDuplicate(block)"
311
- @move-up="onMoveUp(block)"
312
- @move-down="onMoveDown(block)"
313
- @drag-start="onDragStart(block)"
314
- @drag-end="onDragEnd"
315
- >
316
- <component
317
- :is="getBlockComponent(block.type)"
318
- :ref="(el: unknown) => isTextBlock(block.type) && setBlockRef(block.id, el)"
319
- :content="getBlockContent(block, index)"
320
- @update="(c: Record<string, unknown>) => onBlockUpdate(block, c)"
321
- @slash-command="openSlashPicker(block)"
322
- @selection-change="(has: boolean, rect: DOMRect | null) => onSelectionChange(block, has, rect)"
323
- @enter-at-end="onEnterAtEnd(block)"
324
- @backspace-empty="onBackspaceEmpty(block)"
325
- />
326
- </EditorsBlockWrapper>
327
-
328
- <!-- Picker: slash command replaces this block -->
329
- <div v-if="pickerVisible && slashCommandBlockId === block.id" class="cpub-canvas-picker-anchor">
330
- <EditorsBlockPicker :groups="blockTypes" :visible="true" @select="onPickerSelect" @close="closePicker" />
331
- </div>
332
-
333
- <!-- Insert zone after each block -->
334
- <EditorsBlockInsertZone
335
- @insert="openPicker(index + 1)"
336
- @drop="onDrop(index + 1, $event)"
337
- />
338
-
339
- <!-- Picker: insert zone triggered at this position -->
340
- <div v-if="pickerVisible && !slashCommandBlockId && pickerInsertIndex === index + 1" class="cpub-canvas-picker-anchor">
341
- <EditorsBlockPicker :groups="blockTypes" :visible="true" @select="onPickerSelect" @close="closePicker" />
342
- </div>
343
- </template>
344
-
345
- </div><!-- /.cpub-canvas-page -->
346
-
347
- <!-- Floating text toolbar -->
348
- <Teleport to="body">
349
- <div
350
- v-if="floatingToolbar.visible"
351
- class="cpub-floating-toolbar"
352
- :style="{ top: floatingToolbar.top + 'px', left: floatingToolbar.left + 'px' }"
353
- >
354
- <button class="cpub-ft-btn" title="Bold" @mousedown.prevent="toggleMark('bold')">
355
- <i class="fa-solid fa-bold"></i>
356
- </button>
357
- <button class="cpub-ft-btn" title="Italic" @mousedown.prevent="toggleMark('italic')">
358
- <i class="fa-solid fa-italic"></i>
359
- </button>
360
- <button class="cpub-ft-btn" title="Strikethrough" @mousedown.prevent="toggleMark('strike')">
361
- <i class="fa-solid fa-strikethrough"></i>
362
- </button>
363
- <button class="cpub-ft-btn" title="Inline code" @mousedown.prevent="toggleMark('code')">
364
- <i class="fa-solid fa-code"></i>
365
- </button>
366
- <div class="cpub-ft-divider" />
367
- <button class="cpub-ft-btn" title="Link" @mousedown.prevent="toggleLink">
368
- <i class="fa-solid fa-link"></i>
369
- </button>
370
- </div>
371
- </Teleport>
372
- </div>
373
- </template>
374
-
375
- <style scoped>
376
- .cpub-block-canvas {
377
- padding: 36px 0 52px;
378
- min-height: 300px;
379
- position: relative;
380
- display: flex;
381
- flex-direction: column;
382
- align-items: center;
383
- }
384
-
385
- .cpub-canvas-page {
386
- width: 100%;
387
- max-width: 680px;
388
- background: var(--surface);
389
- border: var(--border-width-default) solid var(--border);
390
- box-shadow: var(--shadow-md);
391
- padding: 44px 56px;
392
- position: relative;
393
- }
394
-
395
- @media (max-width: 768px) {
396
- .cpub-canvas-page {
397
- border: none;
398
- box-shadow: none;
399
- padding: 16px;
400
- }
401
- .cpub-block-canvas {
402
- padding: 8px 0 48px;
403
- }
404
- }
405
-
406
- .cpub-canvas-empty {
407
- text-align: center;
408
- padding: 48px 24px 32px;
409
- cursor: pointer;
410
- border: 2px dashed transparent;
411
- transition: border-color 0.15s, background 0.15s;
412
- }
413
-
414
- .cpub-canvas-empty:hover {
415
- border-color: var(--accent-border);
416
- background: var(--accent-bg);
417
- }
418
-
419
- .cpub-canvas-empty-icon {
420
- font-size: 32px;
421
- color: var(--text-faint);
422
- margin-bottom: 12px;
423
- }
424
-
425
- .cpub-canvas-empty-title {
426
- font-size: 16px;
427
- font-weight: 600;
428
- color: var(--text-dim);
429
- margin-bottom: 6px;
430
- }
431
-
432
- .cpub-canvas-empty-desc {
433
- font-size: 12px;
434
- color: var(--text-faint);
435
- }
436
-
437
- .cpub-canvas-picker-anchor {
438
- position: relative;
439
- display: flex;
440
- justify-content: center;
441
- }
442
- </style>
443
-
444
- <!-- Floating toolbar styles (global since it's teleported) -->
445
- <style>
446
- .cpub-floating-toolbar {
447
- --ft-surface: rgba(255, 255, 255, 0.15);
448
- position: fixed;
449
- z-index: 200;
450
- display: flex;
451
- align-items: center;
452
- gap: 0;
453
- background: var(--text, #1a1a1a);
454
- border: var(--border-width-default) solid var(--border, #1a1a1a);
455
- box-shadow: var(--shadow-md);
456
- padding: 3px;
457
- transform: translateX(-50%);
458
- pointer-events: auto;
459
- }
460
-
461
- .cpub-ft-btn {
462
- width: 30px;
463
- height: 28px;
464
- display: flex;
465
- align-items: center;
466
- justify-content: center;
467
- background: transparent;
468
- border: none;
469
- color: var(--surface3, #eaeae7);
470
- cursor: pointer;
471
- font-size: 11px;
472
- padding: 0;
473
- transition: background 0.08s, color 0.08s;
474
- }
475
-
476
- .cpub-ft-btn:hover {
477
- background: var(--ft-surface);
478
- color: var(--surface, #fff);
479
- }
480
-
481
- .cpub-ft-divider {
482
- width: 2px;
483
- height: 18px;
484
- background: var(--ft-surface);
485
- margin: 0 2px;
486
- }
487
- </style>
@@ -1,84 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Insert zone between blocks — shows "+ Insert block" button.
4
- * Appears between every block and at the top/bottom of the canvas.
5
- */
6
- defineEmits<{
7
- insert: [];
8
- }>();
9
-
10
- const isDragOver = ref(false);
11
-
12
- function onDragOver(event: DragEvent): void {
13
- event.preventDefault();
14
- event.dataTransfer!.dropEffect = 'move';
15
- isDragOver.value = true;
16
- }
17
-
18
- function onDragLeave(): void {
19
- isDragOver.value = false;
20
- }
21
- </script>
22
-
23
- <template>
24
- <div
25
- class="cpub-insert-zone"
26
- :class="{ 'cpub-insert-zone--dragover': isDragOver }"
27
- @dragover="onDragOver"
28
- @dragleave="onDragLeave"
29
- @drop="isDragOver = false"
30
- >
31
- <button class="cpub-insert-btn" @click="$emit('insert')">
32
- <i class="fa-solid fa-plus"></i>
33
- <span>Insert block</span>
34
- </button>
35
- </div>
36
- </template>
37
-
38
- <style scoped>
39
- .cpub-insert-zone {
40
- display: flex;
41
- align-items: center;
42
- justify-content: center;
43
- height: 8px;
44
- position: relative;
45
- transition: height 0.15s;
46
- }
47
-
48
- .cpub-insert-zone:hover,
49
- .cpub-insert-zone--dragover {
50
- height: 36px;
51
- }
52
-
53
- .cpub-insert-btn {
54
- display: flex;
55
- align-items: center;
56
- gap: 6px;
57
- font-family: var(--font-mono);
58
- font-size: 10px;
59
- letter-spacing: 0.04em;
60
- color: var(--text-faint);
61
- background: transparent;
62
- border: 2px dashed transparent;
63
- padding: 4px 14px;
64
- cursor: pointer;
65
- opacity: 0;
66
- transition: opacity 0.15s, background 0.1s, border-color 0.1s, color 0.1s;
67
- }
68
-
69
- .cpub-insert-zone:hover .cpub-insert-btn,
70
- .cpub-insert-zone--dragover .cpub-insert-btn {
71
- opacity: 1;
72
- border-color: var(--border2);
73
- }
74
-
75
- .cpub-insert-btn:hover {
76
- border-color: var(--accent);
77
- color: var(--accent);
78
- background: var(--accent-bg);
79
- }
80
-
81
- .cpub-insert-btn i {
82
- font-size: 9px;
83
- }
84
- </style>