@commonpub/editor 0.7.6 → 0.7.8
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/extensions/buildStep.d.ts +2 -3
- package/dist/extensions/buildStep.d.ts.map +1 -1
- package/dist/extensions/buildStep.js +8 -6
- package/dist/extensions/buildStep.js.map +1 -1
- package/package.json +3 -3
- package/vue/components/BlockCanvas.vue +6 -11
- package/vue/components/BlockInsertZone.vue +17 -30
- package/vue/components/blocks/BuildStepBlock.vue +308 -19
- package/vue/composables/useBlockEditor.ts +1 -1
|
@@ -4,10 +4,9 @@ declare module '@tiptap/core' {
|
|
|
4
4
|
buildStep: {
|
|
5
5
|
setBuildStep: (attributes: {
|
|
6
6
|
stepNumber: number;
|
|
7
|
-
|
|
8
|
-
image?: string;
|
|
7
|
+
title?: string;
|
|
9
8
|
time?: string;
|
|
10
|
-
|
|
9
|
+
children?: Array<[string, Record<string, unknown>]>;
|
|
11
10
|
}) => ReturnType;
|
|
12
11
|
};
|
|
13
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildStep.d.ts","sourceRoot":"","sources":["../../src/extensions/buildStep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,SAAS,EAAE;YACT,YAAY,EAAE,CAAC,UAAU,EAAE;gBAAE,UAAU,EAAE,MAAM,CAAC;gBAAC,
|
|
1
|
+
{"version":3,"file":"buildStep.d.ts","sourceRoot":"","sources":["../../src/extensions/buildStep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,SAAS,EAAE;YACT,YAAY,EAAE,CAAC,UAAU,EAAE;gBAAE,UAAU,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;aAAE,KAAK,UAAU,CAAC;SACtJ,CAAC;KACH;CACF;AAED,eAAO,MAAM,kBAAkB,gBAiD7B,CAAC"}
|
|
@@ -6,23 +6,25 @@ export const CommonPubBuildStep = Node.create({
|
|
|
6
6
|
addAttributes() {
|
|
7
7
|
return {
|
|
8
8
|
stepNumber: { default: 1 },
|
|
9
|
-
|
|
10
|
-
instructions: { default: '' },
|
|
9
|
+
title: { default: '' },
|
|
11
10
|
time: { default: null },
|
|
12
|
-
|
|
11
|
+
children: {
|
|
13
12
|
default: [],
|
|
14
13
|
parseHTML: (element) => {
|
|
15
14
|
try {
|
|
16
|
-
return JSON.parse(element.getAttribute('data-
|
|
15
|
+
return JSON.parse(element.getAttribute('data-children') || '[]');
|
|
17
16
|
}
|
|
18
17
|
catch {
|
|
19
18
|
return [];
|
|
20
19
|
}
|
|
21
20
|
},
|
|
22
21
|
renderHTML: (attributes) => ({
|
|
23
|
-
'data-
|
|
22
|
+
'data-children': JSON.stringify(attributes.children),
|
|
24
23
|
}),
|
|
25
24
|
},
|
|
25
|
+
// Legacy attributes — kept for migration from old format
|
|
26
|
+
instructions: { default: null },
|
|
27
|
+
image: { default: null },
|
|
26
28
|
};
|
|
27
29
|
},
|
|
28
30
|
parseHTML() {
|
|
@@ -31,7 +33,7 @@ export const CommonPubBuildStep = Node.create({
|
|
|
31
33
|
renderHTML({ node, HTMLAttributes }) {
|
|
32
34
|
return ['div', mergeAttributes(HTMLAttributes, { class: 'cpub-build-step' }),
|
|
33
35
|
['span', { class: 'cpub-build-step-number' }, `Step ${node.attrs.stepNumber}`],
|
|
34
|
-
['
|
|
36
|
+
['span', { class: 'cpub-build-step-title' }, node.attrs.title || ''],
|
|
35
37
|
];
|
|
36
38
|
},
|
|
37
39
|
addCommands() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildStep.js","sourceRoot":"","sources":["../../src/extensions/buildStep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAUrD,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5C,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,IAAI;IAEV,aAAa;QACX,OAAO;YACL,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE;YAC1B,KAAK,EAAE,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"buildStep.js","sourceRoot":"","sources":["../../src/extensions/buildStep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAUrD,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5C,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,IAAI;IAEV,aAAa;QACX,OAAO;YACL,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE;YAC1B,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YACtB,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;YACvB,QAAQ,EAAE;gBACR,OAAO,EAAE,EAAE;gBACX,SAAS,EAAE,CAAC,OAAoB,EAAE,EAAE;oBAClC,IAAI,CAAC;wBACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC;oBACnE,CAAC;oBAAC,MAAM,CAAC;wBACP,OAAO,EAAE,CAAC;oBACZ,CAAC;gBACH,CAAC;gBACD,UAAU,EAAE,CAAC,UAAmC,EAAE,EAAE,CAAC,CAAC;oBACpD,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC;iBACrD,CAAC;aACH;YACD,yDAAyD;YACzD,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;YAC/B,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;SACzB,CAAC;IACJ,CAAC;IAED,SAAS;QACP,OAAO,CAAC,EAAE,GAAG,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE;QACjC,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;YAC1E,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,EAAE,QAAQ,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;YAC9E,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;SACrE,CAAC;IACJ,CAAC;IAED,WAAW;QACT,OAAO;YACL,YAAY,EACV,CAAC,UAAU,EAAE,EAAE,CACf,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;gBACf,OAAO,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;YACxE,CAAC;SACJ,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/editor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TipTap block editor with 18+ maker-focused extensions for CommonPub",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"remark-rehype": "^11.1.2",
|
|
51
51
|
"unified": "^11.0.5",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@commonpub/
|
|
54
|
-
"@commonpub/
|
|
53
|
+
"@commonpub/config": "0.9.0",
|
|
54
|
+
"@commonpub/schema": "0.9.5"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vue": "^3.4.0",
|
|
@@ -456,8 +456,9 @@ defineExpose({
|
|
|
456
456
|
<BlockPicker :groups="blockTypes" :visible="true" @select="onPickerSelect" @close="closePicker" />
|
|
457
457
|
</div>
|
|
458
458
|
|
|
459
|
-
<!-- Insert zone after each block -->
|
|
459
|
+
<!-- Insert zone after each block (always visible on last block) -->
|
|
460
460
|
<BlockInsertZone
|
|
461
|
+
:always-visible="index === blockEditor.blocks.value.length - 1"
|
|
461
462
|
@insert="openPicker(index + 1)"
|
|
462
463
|
@drop="onDrop(index + 1, $event)"
|
|
463
464
|
/>
|
|
@@ -522,29 +523,23 @@ defineExpose({
|
|
|
522
523
|
|
|
523
524
|
<style scoped>
|
|
524
525
|
.cpub-block-canvas {
|
|
525
|
-
padding:
|
|
526
|
+
padding: 16px 0 52px;
|
|
526
527
|
min-height: 300px;
|
|
527
528
|
position: relative;
|
|
528
529
|
display: flex;
|
|
529
530
|
flex-direction: column;
|
|
530
|
-
align-items:
|
|
531
|
+
align-items: stretch;
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
.cpub-canvas-page {
|
|
534
535
|
width: 100%;
|
|
535
|
-
|
|
536
|
-
background: var(--surface);
|
|
537
|
-
border: var(--border-width-default) solid var(--border);
|
|
538
|
-
box-shadow: var(--shadow-md);
|
|
539
|
-
padding: 44px 56px;
|
|
536
|
+
padding: 0 24px;
|
|
540
537
|
position: relative;
|
|
541
538
|
}
|
|
542
539
|
|
|
543
540
|
@media (max-width: 768px) {
|
|
544
541
|
.cpub-canvas-page {
|
|
545
|
-
|
|
546
|
-
box-shadow: none;
|
|
547
|
-
padding: 16px;
|
|
542
|
+
padding: 0 12px;
|
|
548
543
|
}
|
|
549
544
|
.cpub-block-canvas {
|
|
550
545
|
padding: 8px 0 48px;
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Insert zone between blocks — shows "+ Insert block" button.
|
|
4
4
|
* Appears between every block and at the top/bottom of the canvas.
|
|
5
|
+
* Button shows on hover unless `alwaysVisible` is set.
|
|
5
6
|
*/
|
|
6
7
|
import { ref } from 'vue';
|
|
7
8
|
|
|
9
|
+
defineProps<{
|
|
10
|
+
alwaysVisible?: boolean;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
8
13
|
defineEmits<{
|
|
9
14
|
insert: [];
|
|
10
15
|
}>();
|
|
@@ -25,7 +30,10 @@ function onDragLeave(): void {
|
|
|
25
30
|
<template>
|
|
26
31
|
<div
|
|
27
32
|
class="cpub-insert-zone"
|
|
28
|
-
:class="{
|
|
33
|
+
:class="{
|
|
34
|
+
'cpub-insert-zone--dragover': isDragOver,
|
|
35
|
+
'cpub-insert-zone--hover-only': !alwaysVisible,
|
|
36
|
+
}"
|
|
29
37
|
@dragover="onDragOver"
|
|
30
38
|
@dragleave="onDragLeave"
|
|
31
39
|
@drop="isDragOver = false"
|
|
@@ -42,31 +50,17 @@ function onDragLeave(): void {
|
|
|
42
50
|
display: flex;
|
|
43
51
|
align-items: center;
|
|
44
52
|
justify-content: center;
|
|
45
|
-
height:
|
|
53
|
+
height: 28px;
|
|
46
54
|
position: relative;
|
|
47
|
-
transition: height 0.15s;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* Always-visible thin line */
|
|
51
|
-
.cpub-insert-zone::before {
|
|
52
|
-
content: '';
|
|
53
|
-
position: absolute;
|
|
54
|
-
left: 0;
|
|
55
|
-
right: 0;
|
|
56
|
-
top: 50%;
|
|
57
|
-
height: 1px;
|
|
58
|
-
background: var(--border, rgba(0, 0, 0, 0.1));
|
|
59
|
-
pointer-events: none;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
.cpub-insert-zone
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
.cpub-insert-zone--hover-only .cpub-insert-btn {
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition: opacity 0.12s;
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
.cpub-insert-zone:hover
|
|
68
|
-
|
|
69
|
-
height: 36px;
|
|
62
|
+
.cpub-insert-zone--hover-only:hover .cpub-insert-btn {
|
|
63
|
+
opacity: 1;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
.cpub-insert-btn {
|
|
@@ -78,17 +72,10 @@ function onDragLeave(): void {
|
|
|
78
72
|
letter-spacing: 0.04em;
|
|
79
73
|
color: var(--text-faint);
|
|
80
74
|
background: transparent;
|
|
81
|
-
border: 2px dashed
|
|
75
|
+
border: 2px dashed var(--border2, rgba(0, 0, 0, 0.08));
|
|
82
76
|
padding: 4px 14px;
|
|
83
77
|
cursor: pointer;
|
|
84
|
-
|
|
85
|
-
transition: opacity 0.15s, background 0.1s, border-color 0.1s, color 0.1s;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.cpub-insert-zone:hover .cpub-insert-btn,
|
|
89
|
-
.cpub-insert-zone--dragover .cpub-insert-btn {
|
|
90
|
-
opacity: 1;
|
|
91
|
-
border-color: var(--border2);
|
|
78
|
+
transition: background 0.1s, border-color 0.1s, color 0.1s;
|
|
92
79
|
}
|
|
93
80
|
|
|
94
81
|
.cpub-insert-btn:hover {
|
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Build step block — numbered step with title and
|
|
3
|
+
* Build step block — numbered step with title, time, and nested child blocks.
|
|
4
|
+
* Uses its own useBlockEditor instance for children (text, image, code, callout, divider).
|
|
5
|
+
* Migrates old flat format (instructions + image) to children on load.
|
|
4
6
|
*/
|
|
5
|
-
import { computed } from 'vue';
|
|
7
|
+
import { ref, computed, watch, inject, type Component } from 'vue';
|
|
8
|
+
import type { BlockTuple } from '@commonpub/editor';
|
|
9
|
+
import { useBlockEditor } from '../../composables/useBlockEditor.js';
|
|
10
|
+
import { UPLOAD_HANDLER_KEY } from '../../provide.js';
|
|
11
|
+
|
|
12
|
+
import TextBlock from './TextBlock.vue';
|
|
13
|
+
import ImageBlock from './ImageBlock.vue';
|
|
14
|
+
import CodeBlock from './CodeBlock.vue';
|
|
15
|
+
import CalloutBlock from './CalloutBlock.vue';
|
|
16
|
+
import DividerBlock from './DividerBlock.vue';
|
|
17
|
+
|
|
18
|
+
const CHILD_COMPONENTS: Record<string, Component> = {
|
|
19
|
+
paragraph: TextBlock,
|
|
20
|
+
text: TextBlock,
|
|
21
|
+
image: ImageBlock,
|
|
22
|
+
code: CodeBlock,
|
|
23
|
+
code_block: CodeBlock,
|
|
24
|
+
codeBlock: CodeBlock,
|
|
25
|
+
callout: CalloutBlock,
|
|
26
|
+
divider: DividerBlock,
|
|
27
|
+
horizontalRule: DividerBlock,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const INSERTABLE_TYPES = [
|
|
31
|
+
{ type: 'paragraph', label: 'Text', icon: 'fa-solid fa-paragraph' },
|
|
32
|
+
{ type: 'image', label: 'Image', icon: 'fa-regular fa-image' },
|
|
33
|
+
{ type: 'code_block', label: 'Code', icon: 'fa-solid fa-code' },
|
|
34
|
+
{ type: 'callout', label: 'Callout', icon: 'fa-solid fa-circle-info' },
|
|
35
|
+
{ type: 'divider', label: 'Divider', icon: 'fa-solid fa-minus' },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
6
38
|
const props = defineProps<{
|
|
7
39
|
content: Record<string, unknown>;
|
|
8
40
|
}>();
|
|
@@ -11,13 +43,122 @@ const emit = defineEmits<{
|
|
|
11
43
|
update: [content: Record<string, unknown>];
|
|
12
44
|
}>();
|
|
13
45
|
|
|
46
|
+
const uploadHandler = inject(UPLOAD_HANDLER_KEY, undefined);
|
|
47
|
+
|
|
14
48
|
const stepNumber = computed(() => (props.content.stepNumber as number) ?? 1);
|
|
15
49
|
const title = computed(() => (props.content.title as string) ?? '');
|
|
16
|
-
const instructions = computed(() => (props.content.instructions as string) ?? '');
|
|
17
50
|
const time = computed(() => (props.content.time as string) ?? '');
|
|
18
51
|
|
|
52
|
+
// --- Migration: old flat format → children BlockTuple[] ---
|
|
53
|
+
function migrateToChildren(content: Record<string, unknown>): BlockTuple[] {
|
|
54
|
+
if (content.children && Array.isArray(content.children) && content.children.length > 0) {
|
|
55
|
+
return content.children as BlockTuple[];
|
|
56
|
+
}
|
|
57
|
+
const children: BlockTuple[] = [];
|
|
58
|
+
const instructions = content.instructions as string | undefined;
|
|
59
|
+
if (instructions && instructions.trim()) {
|
|
60
|
+
// Wrap plain text in <p> tags if not already HTML
|
|
61
|
+
const html = instructions.startsWith('<') ? instructions : `<p>${instructions}</p>`;
|
|
62
|
+
children.push(['paragraph', { html }]);
|
|
63
|
+
}
|
|
64
|
+
const image = content.image as string | undefined;
|
|
65
|
+
if (image && image.trim()) {
|
|
66
|
+
children.push(['image', { src: image, alt: '', caption: '' }]);
|
|
67
|
+
}
|
|
68
|
+
if (children.length === 0) {
|
|
69
|
+
children.push(['paragraph', { html: '' }]);
|
|
70
|
+
}
|
|
71
|
+
return children;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Child block editor ---
|
|
75
|
+
const initialChildren = migrateToChildren(props.content);
|
|
76
|
+
const childEditor = useBlockEditor(initialChildren);
|
|
77
|
+
|
|
78
|
+
/** Track what we last emitted so we can distinguish our own updates from external (undo) */
|
|
79
|
+
let lastEmittedJson = JSON.stringify(initialChildren);
|
|
80
|
+
|
|
81
|
+
function emitFullUpdate(): void {
|
|
82
|
+
const children = childEditor.toBlockTuples();
|
|
83
|
+
lastEmittedJson = JSON.stringify(children);
|
|
84
|
+
emit('update', {
|
|
85
|
+
stepNumber: stepNumber.value,
|
|
86
|
+
title: title.value,
|
|
87
|
+
time: time.value,
|
|
88
|
+
children,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
19
92
|
function updateField(field: string, value: unknown): void {
|
|
20
|
-
|
|
93
|
+
const children = childEditor.toBlockTuples();
|
|
94
|
+
lastEmittedJson = JSON.stringify(children);
|
|
95
|
+
emit('update', {
|
|
96
|
+
stepNumber: stepNumber.value,
|
|
97
|
+
title: title.value,
|
|
98
|
+
time: time.value,
|
|
99
|
+
children,
|
|
100
|
+
[field]: value,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onChildUpdate(blockId: string, content: Record<string, unknown>): void {
|
|
105
|
+
childEditor.updateBlock(blockId, content);
|
|
106
|
+
emitFullUpdate();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function addChild(type: string): void {
|
|
110
|
+
childEditor.addBlock(type);
|
|
111
|
+
emitFullUpdate();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function removeChild(blockId: string): void {
|
|
115
|
+
if (childEditor.blocks.value.length <= 1) return;
|
|
116
|
+
childEditor.removeBlock(blockId);
|
|
117
|
+
emitFullUpdate();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function moveChildUp(blockId: string): void {
|
|
121
|
+
childEditor.moveBlockUp(blockId);
|
|
122
|
+
emitFullUpdate();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function moveChildDown(blockId: string): void {
|
|
126
|
+
childEditor.moveBlockDown(blockId);
|
|
127
|
+
emitFullUpdate();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getChildComponent(type: string): Component {
|
|
131
|
+
return CHILD_COMPONENTS[type] ?? TextBlock;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function needsUpload(type: string): boolean {
|
|
135
|
+
return type === 'image';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Sync from external changes (undo/redo) ---
|
|
139
|
+
watch(
|
|
140
|
+
() => props.content.children,
|
|
141
|
+
(newChildren) => {
|
|
142
|
+
if (!newChildren || !Array.isArray(newChildren)) return;
|
|
143
|
+
const newJson = JSON.stringify(newChildren);
|
|
144
|
+
if (newJson === lastEmittedJson) return;
|
|
145
|
+
// External change — re-initialize child editor
|
|
146
|
+
lastEmittedJson = newJson;
|
|
147
|
+
childEditor.fromBlockTuples(newChildren as BlockTuple[]);
|
|
148
|
+
},
|
|
149
|
+
{ deep: true },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// --- Add menu ---
|
|
153
|
+
const showAddMenu = ref(false);
|
|
154
|
+
|
|
155
|
+
function toggleAddMenu(): void {
|
|
156
|
+
showAddMenu.value = !showAddMenu.value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onAddType(type: string): void {
|
|
160
|
+
showAddMenu.value = false;
|
|
161
|
+
addChild(type);
|
|
21
162
|
}
|
|
22
163
|
</script>
|
|
23
164
|
|
|
@@ -40,14 +181,63 @@ function updateField(field: string, value: unknown): void {
|
|
|
40
181
|
@input="updateField('time', ($event.target as HTMLInputElement).value)"
|
|
41
182
|
/>
|
|
42
183
|
</div>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
184
|
+
|
|
185
|
+
<div class="cpub-step-children">
|
|
186
|
+
<div
|
|
187
|
+
v-for="(block, idx) in childEditor.blocks.value"
|
|
188
|
+
:key="block.id"
|
|
189
|
+
class="cpub-step-child"
|
|
190
|
+
>
|
|
191
|
+
<component
|
|
192
|
+
:is="getChildComponent(block.type)"
|
|
193
|
+
:content="block.content"
|
|
194
|
+
v-bind="needsUpload(block.type) && uploadHandler ? { onUpload: uploadHandler } : {}"
|
|
195
|
+
@update="(c: Record<string, unknown>) => onChildUpdate(block.id, c)"
|
|
196
|
+
/>
|
|
197
|
+
<div class="cpub-step-child-actions">
|
|
198
|
+
<button
|
|
199
|
+
v-if="idx > 0"
|
|
200
|
+
class="cpub-step-child-btn"
|
|
201
|
+
title="Move up"
|
|
202
|
+
@click="moveChildUp(block.id)"
|
|
203
|
+
>
|
|
204
|
+
<i class="fa-solid fa-chevron-up"></i>
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
v-if="idx < childEditor.blocks.value.length - 1"
|
|
208
|
+
class="cpub-step-child-btn"
|
|
209
|
+
title="Move down"
|
|
210
|
+
@click="moveChildDown(block.id)"
|
|
211
|
+
>
|
|
212
|
+
<i class="fa-solid fa-chevron-down"></i>
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
v-if="childEditor.blocks.value.length > 1"
|
|
216
|
+
class="cpub-step-child-btn cpub-step-child-btn--danger"
|
|
217
|
+
title="Remove block"
|
|
218
|
+
@click="removeChild(block.id)"
|
|
219
|
+
>
|
|
220
|
+
<i class="fa-solid fa-xmark"></i>
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Add block button -->
|
|
226
|
+
<div class="cpub-step-add-row">
|
|
227
|
+
<button class="cpub-step-add-btn" @click="toggleAddMenu">
|
|
228
|
+
<i class="fa-solid fa-plus"></i> Add block
|
|
229
|
+
</button>
|
|
230
|
+
<div v-if="showAddMenu" class="cpub-step-add-menu">
|
|
231
|
+
<button
|
|
232
|
+
v-for="t in INSERTABLE_TYPES"
|
|
233
|
+
:key="t.type"
|
|
234
|
+
class="cpub-step-add-option"
|
|
235
|
+
@click="onAddType(t.type)"
|
|
236
|
+
>
|
|
237
|
+
<i :class="t.icon"></i> {{ t.label }}
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
51
241
|
</div>
|
|
52
242
|
</div>
|
|
53
243
|
</template>
|
|
@@ -91,13 +281,112 @@ function updateField(field: string, value: unknown): void {
|
|
|
91
281
|
.cpub-step-time:focus { border-color: var(--accent); }
|
|
92
282
|
.cpub-step-time::placeholder { color: var(--text-faint); }
|
|
93
283
|
|
|
94
|
-
|
|
284
|
+
/* --- Children --- */
|
|
285
|
+
.cpub-step-children {
|
|
286
|
+
padding: 12px 16px;
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
gap: 8px;
|
|
290
|
+
}
|
|
95
291
|
|
|
96
|
-
.cpub-step-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
292
|
+
.cpub-step-child {
|
|
293
|
+
position: relative;
|
|
294
|
+
border: var(--border-width-default) solid transparent;
|
|
295
|
+
padding: 4px 0;
|
|
296
|
+
transition: border-color 0.1s;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.cpub-step-child:hover {
|
|
300
|
+
border-color: var(--border2);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.cpub-step-child-actions {
|
|
304
|
+
position: absolute;
|
|
305
|
+
top: 2px;
|
|
306
|
+
right: 2px;
|
|
307
|
+
display: flex;
|
|
308
|
+
gap: 2px;
|
|
309
|
+
opacity: 0;
|
|
310
|
+
transition: opacity 0.1s;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.cpub-step-child:hover .cpub-step-child-actions {
|
|
314
|
+
opacity: 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.cpub-step-child-btn {
|
|
318
|
+
width: 22px; height: 22px;
|
|
319
|
+
background: var(--surface2);
|
|
320
|
+
border: var(--border-width-default) solid var(--border2);
|
|
321
|
+
color: var(--text-faint);
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
display: flex; align-items: center; justify-content: center;
|
|
324
|
+
font-size: 9px;
|
|
325
|
+
padding: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.cpub-step-child-btn:hover {
|
|
329
|
+
background: var(--surface3);
|
|
330
|
+
color: var(--text);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.cpub-step-child-btn--danger:hover {
|
|
334
|
+
background: var(--red-bg);
|
|
335
|
+
color: var(--red);
|
|
336
|
+
border-color: var(--red-border);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* --- Add block --- */
|
|
340
|
+
.cpub-step-add-row {
|
|
341
|
+
position: relative;
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
padding-top: 4px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cpub-step-add-btn {
|
|
348
|
+
display: flex; align-items: center; gap: 6px;
|
|
349
|
+
padding: 4px 10px;
|
|
350
|
+
font-size: 11px; font-family: var(--font-mono);
|
|
351
|
+
background: transparent;
|
|
352
|
+
border: var(--border-width-default) dashed var(--border2);
|
|
353
|
+
color: var(--text-faint);
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: all 0.1s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.cpub-step-add-btn:hover {
|
|
359
|
+
border-color: var(--accent);
|
|
360
|
+
color: var(--accent);
|
|
361
|
+
background: var(--accent-bg);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.cpub-step-add-menu {
|
|
365
|
+
position: absolute;
|
|
366
|
+
bottom: 100%;
|
|
367
|
+
left: 0;
|
|
368
|
+
margin-bottom: 4px;
|
|
369
|
+
display: flex;
|
|
370
|
+
gap: 0;
|
|
371
|
+
background: var(--surface);
|
|
372
|
+
border: var(--border-width-default) solid var(--border);
|
|
373
|
+
box-shadow: var(--shadow-md);
|
|
374
|
+
z-index: 10;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.cpub-step-add-option {
|
|
378
|
+
display: flex; align-items: center; gap: 6px;
|
|
379
|
+
padding: 6px 12px;
|
|
380
|
+
font-size: 11px;
|
|
381
|
+
background: transparent;
|
|
382
|
+
border: none;
|
|
383
|
+
color: var(--text-dim);
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.cpub-step-add-option:hover {
|
|
389
|
+
background: var(--accent-bg);
|
|
390
|
+
color: var(--accent);
|
|
101
391
|
}
|
|
102
|
-
.cpub-step-instructions::placeholder { color: var(--text-faint); }
|
|
103
392
|
</style>
|
|
@@ -21,7 +21,7 @@ const BLOCK_DEFAULTS: Record<string, () => Record<string, unknown>> = {
|
|
|
21
21
|
embed: () => ({ url: '', type: 'generic', html: '' }),
|
|
22
22
|
horizontal_rule: () => ({}),
|
|
23
23
|
partsList: () => ({ parts: [] }),
|
|
24
|
-
buildStep: () => ({ stepNumber: 1,
|
|
24
|
+
buildStep: () => ({ stepNumber: 1, title: '', time: '', children: [['paragraph', { html: '' }]] }),
|
|
25
25
|
toolList: () => ({ tools: [] }),
|
|
26
26
|
downloads: () => ({ files: [] }),
|
|
27
27
|
quiz: () => ({ question: '', options: [], feedback: '' }),
|