js-kompo-utils 1.0.14 → 1.0.15
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/components/VlTutorialStepBuilder.vue +137 -586
- package/components/step-builder/SbContextMenu.vue +41 -0
- package/components/step-builder/SbGraphView.vue +93 -0
- package/components/step-builder/SbHeader.vue +16 -0
- package/components/step-builder/SbHistoryDrawer.vue +43 -0
- package/components/step-builder/SbPathPreview.vue +57 -0
- package/components/step-builder/SbStepList.vue +87 -0
- package/components/step-builder/SbTimeline.vue +66 -0
- package/components/step-builder/cards/SbActionsCard.vue +26 -0
- package/components/step-builder/cards/SbAdvanceCard.vue +43 -0
- package/components/step-builder/cards/SbConditionsCard.vue +20 -0
- package/components/step-builder/cards/SbContentCard.vue +45 -0
- package/components/step-builder/cards/SbCursorCard.vue +102 -0
- package/components/step-builder/cards/SbGlobalOptsCard.vue +40 -0
- package/components/step-builder/cards/SbHighlightCard.vue +40 -0
- package/components/step-builder/cards/SbHoverCard.vue +32 -0
- package/components/step-builder/cards/SbOptionsCard.vue +50 -0
- package/components/step-builder/cards/SbOutputCard.vue +17 -0
- package/components/step-builder/cards/SbPositionCard.vue +87 -0
- package/components/step-builder/cards/SbScrollCard.vue +17 -0
- package/docs/extending-tutorial-system.md +376 -0
- package/package.json +10 -9
- package/tutorials/TutorialConfig.js +4 -0
- package/tutorials/TutorialEngine.js +49 -3
- package/tutorials/extensions/HideIfExtension.js +12 -0
- package/tutorials/extensions/LinkNextExtension.js +117 -0
- package/tutorials/modules/Cursor.js +24 -4
- package/tutorials/modules/Highlight.js +7 -5
- package/tutorials/modules/HoverForcer.js +1 -0
- package/tutorials/modules/Overlay.js +2 -0
- package/tutorials/step-builder/StepBuilderState.js +265 -0
- package/tutorials/tutorial.scss +186 -0
- package/tutorials/step-builder/step-builder-bridge.js +0 -436
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="sb.contextMenu" class="sb-context-menu"
|
|
3
|
+
:style="{ left: sb.contextMenu.x + 'px', top: sb.contextMenu.y + 'px' }">
|
|
4
|
+
<div class="sb-ctx-item sb-ctx-item--green" @click="action('insertAfter')">Insert After</div>
|
|
5
|
+
<div class="sb-ctx-item" @click="action('duplicate')">Duplicate</div>
|
|
6
|
+
<div class="sb-ctx-sep"></div>
|
|
7
|
+
<div class="sb-ctx-item" @click="action('copyStep')">Copy Step Code</div>
|
|
8
|
+
<div class="sb-ctx-sep"></div>
|
|
9
|
+
<div class="sb-ctx-item" :class="{ 'sb-ctx-item--disabled': sb.contextMenu.idx === 0 }" @click="action('moveUp')">Move Up</div>
|
|
10
|
+
<div class="sb-ctx-item" :class="{ 'sb-ctx-item--disabled': sb.contextMenu.idx >= sb.steps.length - 1 }" @click="action('moveDown')">Move Down</div>
|
|
11
|
+
<div class="sb-ctx-sep"></div>
|
|
12
|
+
<div class="sb-ctx-item sb-ctx-item--red" :class="{ 'sb-ctx-item--disabled': sb.steps.length <= 1 }" @click="action('delete')">Delete</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
<script>
|
|
16
|
+
export default {
|
|
17
|
+
inject: ['sb'],
|
|
18
|
+
watch: {
|
|
19
|
+
'sb.contextMenu'(val) {
|
|
20
|
+
if (val) {
|
|
21
|
+
const close = () => { this.sb.contextMenu = null; document.removeEventListener('click', close); };
|
|
22
|
+
this.$nextTick(() => document.addEventListener('click', close));
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
methods: {
|
|
27
|
+
action(type) {
|
|
28
|
+
const i = this.sb.contextMenu.idx;
|
|
29
|
+
this.sb.contextMenu = null;
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'insertAfter': this.sb.state.addStep(i); this.sb.previewNow(); break;
|
|
32
|
+
case 'duplicate': this.sb.state.duplicateStep(i); this.sb.previewNow(); break;
|
|
33
|
+
case 'copyStep': this.sb._copy(JSON.stringify(this.sb.steps[i], null, 2)); break;
|
|
34
|
+
case 'moveUp': if (i > 0) { this.sb.state.moveStep(i, i - 1); this.sb.previewNow(); } break;
|
|
35
|
+
case 'moveDown': if (i < this.sb.steps.length - 1) { this.sb.state.moveStep(i, i + 1); this.sb.previewNow(); } break;
|
|
36
|
+
case 'delete': this.sb.state.deleteStep(i); this.sb.previewNow(); break;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="sb.graphViewOpen" class="sb-graph-modal" @keydown.esc="sb.graphViewOpen = false" tabindex="0" ref="modal">
|
|
3
|
+
<div class="sb-graph-header">
|
|
4
|
+
<span class="sb-graph-title">Step Graph ({{ sb.steps.length }} steps)</span>
|
|
5
|
+
<button class="sb-hbtn" @click="sb.graphViewOpen = false">×</button>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="sb-graph-container" ref="container">
|
|
8
|
+
<svg :width="svgWidth" :height="svgHeight" class="sb-graph-arrows">
|
|
9
|
+
<defs>
|
|
10
|
+
<marker id="sb-arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
|
11
|
+
<polygon points="0 0, 8 3, 0 6" fill="#555" />
|
|
12
|
+
</marker>
|
|
13
|
+
</defs>
|
|
14
|
+
<path v-for="(arrow, ai) in arrows" :key="'a' + ai" :d="arrow"
|
|
15
|
+
stroke="#555" stroke-width="2" fill="none" marker-end="url(#sb-arrowhead)" />
|
|
16
|
+
</svg>
|
|
17
|
+
<div v-for="(node, ni) in nodes" :key="'n' + ni"
|
|
18
|
+
class="sb-graph-node" :class="{ 'sb-graph-node--active': node.index === sb.idx }"
|
|
19
|
+
:style="{ left: node.x + 'px', top: node.y + 'px' }"
|
|
20
|
+
@click="sb.selectStep(node.index); sb.graphViewOpen = false">
|
|
21
|
+
<div class="sb-graph-node-branch" :style="{ backgroundColor: node.branchColor }"></div>
|
|
22
|
+
<span class="sb-graph-node-num">{{ node.index }}</span>
|
|
23
|
+
<span class="sb-graph-node-text">{{ node.preview }}</span>
|
|
24
|
+
<span class="sb-graph-node-icons">
|
|
25
|
+
<span v-if="sb.steps[node.index].cursor">⌖</span>
|
|
26
|
+
<span v-if="sb.steps[node.index].highlight">☆</span>
|
|
27
|
+
<span v-if="sb.steps[node.index].options">…</span>
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
<script>
|
|
34
|
+
const NODE_W = 160, NODE_H = 72, GAP_X = 24, GAP_Y = 20, PAD = 32, COLS = 6;
|
|
35
|
+
const BRANCH_COLORS = ['#6c8aff', '#2ecc71', '#e74c3c', '#f1c40f', '#9b59b6', '#e67e22', '#1abc9c', '#e84393'];
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
inject: ['sb'],
|
|
39
|
+
watch: {
|
|
40
|
+
'sb.graphViewOpen'(val) {
|
|
41
|
+
if (val) this.$nextTick(() => this.$refs.modal?.focus());
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
computed: {
|
|
45
|
+
branchColorMap() {
|
|
46
|
+
const map = {}; let ci = 0;
|
|
47
|
+
for (const s of this.sb.steps) {
|
|
48
|
+
const b = s._branch || '_main';
|
|
49
|
+
if (!map[b]) map[b] = BRANCH_COLORS[ci++ % BRANCH_COLORS.length];
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
},
|
|
53
|
+
nodes() {
|
|
54
|
+
return this.sb.steps.map((s, i) => {
|
|
55
|
+
const row = Math.floor(i / COLS);
|
|
56
|
+
const colInRow = i % COLS;
|
|
57
|
+
const col = row % 2 === 0 ? colInRow : (COLS - 1 - colInRow);
|
|
58
|
+
return {
|
|
59
|
+
index: i,
|
|
60
|
+
x: PAD + col * (NODE_W + GAP_X),
|
|
61
|
+
y: PAD + row * (NODE_H + GAP_Y),
|
|
62
|
+
preview: (s.html || s.silentClick || '').substring(0, 20) || '(empty)',
|
|
63
|
+
branchColor: this.branchColorMap[s._branch || '_main'],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
arrows() {
|
|
68
|
+
const paths = [];
|
|
69
|
+
for (let i = 0; i < this.nodes.length - 1; i++) {
|
|
70
|
+
const a = this.nodes[i], b = this.nodes[i + 1];
|
|
71
|
+
const ax = a.x + NODE_W / 2, ay = a.y + NODE_H / 2;
|
|
72
|
+
const bx = b.x + NODE_W / 2, by = b.y + NODE_H / 2;
|
|
73
|
+
if (Math.abs(ay - by) < 5) {
|
|
74
|
+
// Same row: horizontal
|
|
75
|
+
const fromX = a.x + (bx > ax ? NODE_W : 0);
|
|
76
|
+
const toX = b.x + (bx > ax ? 0 : NODE_W);
|
|
77
|
+
paths.push(`M ${fromX},${ay} L ${toX},${by}`);
|
|
78
|
+
} else {
|
|
79
|
+
// Row change: U-turn
|
|
80
|
+
const midY = ay + (by - ay) / 2;
|
|
81
|
+
paths.push(`M ${ax},${a.y + NODE_H} L ${ax},${midY} L ${bx},${midY} L ${bx},${b.y}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
},
|
|
86
|
+
svgWidth() { return PAD * 2 + COLS * (NODE_W + GAP_X); },
|
|
87
|
+
svgHeight() {
|
|
88
|
+
const rows = Math.ceil(this.sb.steps.length / COLS);
|
|
89
|
+
return PAD * 2 + rows * (NODE_H + GAP_Y);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-header">
|
|
3
|
+
<span class="sb-header-title">Step Builder</span>
|
|
4
|
+
<button class="sb-hbtn" title="Undo (Ctrl+Z)" :disabled="!sb.state.canUndo"
|
|
5
|
+
@click="sb.state.undo(); sb.previewNow()">↶</button>
|
|
6
|
+
<button class="sb-hbtn" title="Redo (Ctrl+Shift+Z)" :disabled="!sb.state.canRedo"
|
|
7
|
+
@click="sb.state.redo(); sb.previewNow()">↷</button>
|
|
8
|
+
<button class="sb-hbtn" title="History" @click="sb.historyOpen = !sb.historyOpen">🕑</button>
|
|
9
|
+
<button class="sb-hbtn" title="Spectator" @click="sb.spectatorMode()">▶</button>
|
|
10
|
+
<button class="sb-hbtn" title="Copy All" @click="sb.copyAll()">📋</button>
|
|
11
|
+
<button class="sb-hbtn" title="Close" @click="sb.visible = false">×</button>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
<script>
|
|
15
|
+
export default { inject: ['sb'] };
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-history-drawer" :class="{ 'sb-history-drawer--open': sb.historyOpen }">
|
|
3
|
+
<div class="sb-history-header">
|
|
4
|
+
<span>History ({{ sb.state.undoStack.length }})</span>
|
|
5
|
+
<button class="sb-hbtn" @click="sb.historyOpen = false">×</button>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="sb-history-list">
|
|
8
|
+
<div class="sb-history-item sb-history-item--current">Current State</div>
|
|
9
|
+
<div v-for="(entry, i) in sb.state.undoLabels.slice().reverse()" :key="'u' + i"
|
|
10
|
+
class="sb-history-item" @click="undoToIndex(sb.state.undoLabels.length - 1 - i)">
|
|
11
|
+
<span class="sb-history-icon" :style="{ color: historyColor(entry.label) }">●</span>
|
|
12
|
+
<span class="sb-history-label">{{ entry.label }}</span>
|
|
13
|
+
<span class="sb-history-time">{{ timeAgo(entry.time) }}</span>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
<script>
|
|
19
|
+
export default {
|
|
20
|
+
inject: ['sb'],
|
|
21
|
+
methods: {
|
|
22
|
+
undoToIndex(targetIdx) {
|
|
23
|
+
const count = this.sb.state.undoStack.length - targetIdx;
|
|
24
|
+
for (let i = 0; i < count; i++) this.sb.state.undo();
|
|
25
|
+
this.sb.previewNow();
|
|
26
|
+
},
|
|
27
|
+
timeAgo(ts) {
|
|
28
|
+
const diff = Math.round((Date.now() - ts) / 1000);
|
|
29
|
+
if (diff < 5) return 'just now';
|
|
30
|
+
if (diff < 60) return diff + 's ago';
|
|
31
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
32
|
+
return Math.floor(diff / 3600) + 'h ago';
|
|
33
|
+
},
|
|
34
|
+
historyColor(label) {
|
|
35
|
+
if (label.startsWith('Add')) return '#2ecc71';
|
|
36
|
+
if (label.startsWith('Delete')) return '#e74c3c';
|
|
37
|
+
if (label.startsWith('Duplicate')) return '#6c8aff';
|
|
38
|
+
if (label.startsWith('Move')) return '#f1c40f';
|
|
39
|
+
return '#b0b4c0';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg v-if="visible" class="sb-path-preview" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path v-if="pathD" :d="pathD" fill="none" stroke="#f1c40f" stroke-width="2" stroke-dasharray="8,4" />
|
|
4
|
+
<circle v-if="fromPos" :cx="fromPos.x" :cy="fromPos.y" r="8" fill="#2ecc71" opacity="0.8" />
|
|
5
|
+
<text v-if="fromPos" :x="fromPos.x + 12" :y="fromPos.y + 4" fill="#2ecc71" font-size="10" font-family="system-ui">FROM</text>
|
|
6
|
+
<circle v-if="toPos" :cx="toPos.x" :cy="toPos.y" r="8" fill="#e74c3c" opacity="0.8" />
|
|
7
|
+
<text v-if="toPos" :x="toPos.x + 12" :y="toPos.y + 4" fill="#e74c3c" font-size="10" font-family="system-ui">TO</text>
|
|
8
|
+
<template v-for="(wp, wi) in waypointPositions">
|
|
9
|
+
<circle :key="'wc' + wi" :cx="wp.x" :cy="wp.y" r="6" fill="#00bcd4" opacity="0.8" />
|
|
10
|
+
<text :key="'wt' + wi" :x="wp.x + 10" :y="wp.y + 4" fill="#00bcd4" font-size="9" font-family="system-ui">WP{{ wi }}</text>
|
|
11
|
+
</template>
|
|
12
|
+
</svg>
|
|
13
|
+
</template>
|
|
14
|
+
<script>
|
|
15
|
+
export default {
|
|
16
|
+
inject: ['sb'],
|
|
17
|
+
computed: {
|
|
18
|
+
visible() {
|
|
19
|
+
return !!(this.sb.step.cursor && (this.sb.step.cursor.from || this.sb.step.cursor.to));
|
|
20
|
+
},
|
|
21
|
+
fromPos() { return this._resolve(this.sb.step.cursor?.from); },
|
|
22
|
+
toPos() { return this._resolve(this.sb.step.cursor?.to); },
|
|
23
|
+
pathD() {
|
|
24
|
+
if (!this.fromPos || !this.toPos) return null;
|
|
25
|
+
const svgPath = this.sb.step.cursor?.svgPath || 'M 0,0 C 0.3,0.1 0.7,0.9 1,1';
|
|
26
|
+
return this._denormalize(svgPath, this.fromPos, this.toPos);
|
|
27
|
+
},
|
|
28
|
+
waypointPositions() {
|
|
29
|
+
const wps = this.sb.step.cursor?.waypoints || [];
|
|
30
|
+
return wps.map(wp => this._resolve(wp.target)).filter(Boolean);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
methods: {
|
|
34
|
+
_resolve(selector) {
|
|
35
|
+
if (!selector) return null;
|
|
36
|
+
if (typeof selector === 'string' && selector.startsWith('screen:')) {
|
|
37
|
+
const parts = selector.replace('screen:', '').split(',');
|
|
38
|
+
return { x: parseFloat(parts[0]), y: parseFloat(parts[1]) };
|
|
39
|
+
}
|
|
40
|
+
const el = document.querySelector(selector);
|
|
41
|
+
if (!el) return null;
|
|
42
|
+
const rect = el.getBoundingClientRect();
|
|
43
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
44
|
+
},
|
|
45
|
+
_denormalize(pathStr, from, to) {
|
|
46
|
+
const match = pathStr.match(/C\s*([\d.]+),([\d.]+)\s+([\d.]+),([\d.]+)\s+([\d.]+),([\d.]+)/);
|
|
47
|
+
if (!match) return `M ${from.x},${from.y} L ${to.x},${to.y}`;
|
|
48
|
+
const dx = to.x - from.x, dy = to.y - from.y;
|
|
49
|
+
const cp1x = from.x + parseFloat(match[1]) * dx;
|
|
50
|
+
const cp1y = from.y + parseFloat(match[2]) * dy;
|
|
51
|
+
const cp2x = from.x + parseFloat(match[3]) * dx;
|
|
52
|
+
const cp2y = from.y + parseFloat(match[4]) * dy;
|
|
53
|
+
return `M ${from.x},${from.y} C ${cp1x},${cp1y} ${cp2x},${cp2y} ${to.x},${to.y}`;
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-steps">
|
|
3
|
+
<div class="sb-steps-bar">
|
|
4
|
+
<span class="sb-steps-count">STEPS ({{ sb.steps.length }})</span>
|
|
5
|
+
<button class="sb-btn sb-btn-sm sb-btn-green" @click="sb.addStep()">+</button>
|
|
6
|
+
</div>
|
|
7
|
+
<div v-if="branchInfo.order.length > 1" class="sb-branch-tabs">
|
|
8
|
+
<span class="sb-branch-tab" :class="{ 'sb-branch-tab--active': !activeBranchTab }"
|
|
9
|
+
@click="activeBranchTab = null">All</span>
|
|
10
|
+
<span v-for="b in branchInfo.order" :key="b"
|
|
11
|
+
class="sb-branch-tab" :class="{ 'sb-branch-tab--active': activeBranchTab === b }"
|
|
12
|
+
:style="{ borderBottomColor: branchInfo.colors[b] }"
|
|
13
|
+
@click="activeBranchTab = b">
|
|
14
|
+
{{ b === '_main' ? 'Main' : b }}
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="sb-steps-list">
|
|
18
|
+
<div v-for="(s, i) in sb.steps" :key="i"
|
|
19
|
+
v-show="!activeBranchTab || (s._branch || '_main') === activeBranchTab"
|
|
20
|
+
class="sb-step" :class="{ 'sb-step--active': i === sb.idx, 'sb-step--drag-over': dragOverIdx === i }"
|
|
21
|
+
draggable="true"
|
|
22
|
+
@click="sb.selectStep(i)"
|
|
23
|
+
@contextmenu.prevent="sb.contextMenu = { x: $event.clientX, y: $event.clientY, idx: i }"
|
|
24
|
+
@dragstart="onDragStart($event, i)"
|
|
25
|
+
@dragover.prevent="dragOverIdx = i"
|
|
26
|
+
@dragleave="dragOverIdx = null"
|
|
27
|
+
@drop="onDrop($event, i)"
|
|
28
|
+
@dragend="dragFromIdx = null; dragOverIdx = null">
|
|
29
|
+
<span class="sb-step-branch-bar" :style="{ backgroundColor: getBranchColor(s) }"></span>
|
|
30
|
+
<span class="sb-step-num">{{ i }}</span>
|
|
31
|
+
<span class="sb-step-text">{{ (s.html || s.silentClick || '').substring(0, 30) || '(empty)' }}</span>
|
|
32
|
+
<span class="sb-step-icons">
|
|
33
|
+
<span v-if="s.cursor" title="cursor">⌖</span>
|
|
34
|
+
<span v-if="s.highlight" title="highlight">☆</span>
|
|
35
|
+
<span v-if="s.options" title="options">…</span>
|
|
36
|
+
<span v-if="sb.state.hasStepChanged(i)" class="sb-step-modified" title="modified">●</span>
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="sb-step-actions">
|
|
41
|
+
<button class="sb-hbtn" @click="sb.moveStepUp()" title="Move up">↑</button>
|
|
42
|
+
<button class="sb-hbtn" @click="sb.moveStepDown()" title="Move down">↓</button>
|
|
43
|
+
<button class="sb-hbtn" @click="sb.duplicateStep()" title="Duplicate">⋔</button>
|
|
44
|
+
<button class="sb-hbtn sb-hbtn--danger" @click="sb.deleteStep()" title="Delete">✕</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
<script>
|
|
49
|
+
const BRANCH_COLORS = ['#6c8aff', '#2ecc71', '#e74c3c', '#f1c40f', '#9b59b6', '#e67e22', '#1abc9c', '#e84393'];
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
inject: ['sb'],
|
|
53
|
+
data() {
|
|
54
|
+
return { activeBranchTab: null, dragFromIdx: null, dragOverIdx: null };
|
|
55
|
+
},
|
|
56
|
+
computed: {
|
|
57
|
+
branchInfo() {
|
|
58
|
+
const colors = {}, order = [];
|
|
59
|
+
let ci = 0;
|
|
60
|
+
for (const s of this.sb.steps) {
|
|
61
|
+
const b = s._branch || '_main';
|
|
62
|
+
if (!colors[b]) { colors[b] = BRANCH_COLORS[ci++ % BRANCH_COLORS.length]; order.push(b); }
|
|
63
|
+
}
|
|
64
|
+
return { order, colors };
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
methods: {
|
|
68
|
+
getBranchColor(step) {
|
|
69
|
+
return this.branchInfo.colors[step._branch || '_main'] || BRANCH_COLORS[0];
|
|
70
|
+
},
|
|
71
|
+
onDragStart(e, idx) {
|
|
72
|
+
this.dragFromIdx = idx;
|
|
73
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
74
|
+
e.dataTransfer.setData('text/plain', String(idx));
|
|
75
|
+
},
|
|
76
|
+
onDrop(e, toIdx) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
this.dragOverIdx = null;
|
|
79
|
+
if (this.dragFromIdx !== null && this.dragFromIdx !== toIdx) {
|
|
80
|
+
this.sb.state.moveStep(this.dragFromIdx, toIdx);
|
|
81
|
+
this.sb.previewNow();
|
|
82
|
+
}
|
|
83
|
+
this.dragFromIdx = null;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-timeline">
|
|
3
|
+
<div class="sb-timeline-bar">
|
|
4
|
+
<span class="sb-steps-count">TIMELINE</span>
|
|
5
|
+
<button class="sb-hbtn" title="Graph View" @click="sb.graphViewOpen = true">◉</button>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="sb-timeline-strip">
|
|
8
|
+
<div v-for="(s, i) in sb.steps" :key="'t' + i"
|
|
9
|
+
class="sb-timeline-thumb" :class="{ 'sb-active': i === sb.idx }"
|
|
10
|
+
draggable="true"
|
|
11
|
+
@click="sb.selectStep(i)"
|
|
12
|
+
@dragstart="onDragStart($event, i)"
|
|
13
|
+
@dragover.prevent="dragOverIdx = i"
|
|
14
|
+
@dragleave="dragOverIdx = null"
|
|
15
|
+
@drop="onDrop($event, i)"
|
|
16
|
+
@dragend="dragFromIdx = null; dragOverIdx = null">
|
|
17
|
+
<img v-if="sb.state.getScreenshot(i)" :src="sb.state.getScreenshot(i)" class="sb-timeline-thumb-img">
|
|
18
|
+
<span class="sb-timeline-thumb-num">{{ i }}</span>
|
|
19
|
+
<button class="sb-timeline-capture" @click.stop="capture(i)" title="Capture">📷</button>
|
|
20
|
+
<span class="sb-timeline-thumb-icons">
|
|
21
|
+
<span v-if="s.cursor">⌖</span>
|
|
22
|
+
<span v-if="s.highlight">☆</span>
|
|
23
|
+
</span>
|
|
24
|
+
<span v-if="sb.state.hasStepChanged(i)" class="sb-timeline-modified"></span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
<script>
|
|
30
|
+
export default {
|
|
31
|
+
inject: ['sb'],
|
|
32
|
+
data() { return { dragFromIdx: null, dragOverIdx: null }; },
|
|
33
|
+
methods: {
|
|
34
|
+
async capture(index) {
|
|
35
|
+
if (!window.html2canvas) {
|
|
36
|
+
try {
|
|
37
|
+
const mod = await import('html2canvas');
|
|
38
|
+
window.html2canvas = mod.default || mod;
|
|
39
|
+
} catch { console.warn('html2canvas not available'); return; }
|
|
40
|
+
}
|
|
41
|
+
const wasVisible = this.sb.visible;
|
|
42
|
+
this.sb.visible = false;
|
|
43
|
+
await this.$nextTick();
|
|
44
|
+
try {
|
|
45
|
+
const canvas = await window.html2canvas(document.body, { scale: 0.3, logging: false });
|
|
46
|
+
this.sb.state.setScreenshot(index, canvas.toDataURL('image/jpeg', 0.5));
|
|
47
|
+
} catch (e) { console.warn('Screenshot failed:', e); }
|
|
48
|
+
this.sb.visible = wasVisible;
|
|
49
|
+
},
|
|
50
|
+
onDragStart(e, idx) {
|
|
51
|
+
this.dragFromIdx = idx;
|
|
52
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
53
|
+
e.dataTransfer.setData('text/plain', String(idx));
|
|
54
|
+
},
|
|
55
|
+
onDrop(e, toIdx) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
this.dragOverIdx = null;
|
|
58
|
+
if (this.dragFromIdx !== null && this.dragFromIdx !== toIdx) {
|
|
59
|
+
this.sb.state.moveStep(this.dragFromIdx, toIdx);
|
|
60
|
+
this.sb.previewNow();
|
|
61
|
+
}
|
|
62
|
+
this.dragFromIdx = null;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-card" :class="{ 'sb-card--open': sb.cards.actions }">
|
|
3
|
+
<div class="sb-card-head" @click="sb.toggle('actions')">
|
|
4
|
+
<span>ACTIONS</span><span class="sb-chevron">{{ sb.cards.actions ? '▼' : '▶' }}</span>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-show="sb.cards.actions" class="sb-card-body">
|
|
7
|
+
<div class="sb-row sb-row--pick">
|
|
8
|
+
<label class="sb-lbl">Silent Click</label>
|
|
9
|
+
<input :value="sb.step.silentClick || ''" @input="sb.setStepProp('silentClick', $event.target.value)" class="sb-input">
|
|
10
|
+
<button class="sb-pick" @click="sb.pickField('silentClick')">Pick</button>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="sb-row sb-row--pick">
|
|
13
|
+
<label class="sb-lbl">Link Next</label>
|
|
14
|
+
<input :value="sb.step.linkNext || ''" @input="sb.setStepProp('linkNext', $event.target.value)" class="sb-input">
|
|
15
|
+
<button class="sb-pick" @click="sb.pickField('linkNext')">Pick</button>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="sb-row">
|
|
18
|
+
<label class="sb-lbl">Redirect</label>
|
|
19
|
+
<input :value="sb.step.redirect || ''" @input="sb.setStepProp('redirect', $event.target.value)" class="sb-input" placeholder="/url">
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
<script>
|
|
25
|
+
export default { inject: ['sb'] };
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-card" :class="{ 'sb-card--open': sb.cards.advance }">
|
|
3
|
+
<div class="sb-card-head" @click="sb.toggle('advance')">
|
|
4
|
+
<span>ADVANCE</span><span class="sb-chevron">{{ sb.cards.advance ? '▼' : '▶' }}</span>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-show="sb.cards.advance" class="sb-card-body">
|
|
7
|
+
<select :value="advanceMode" @change="setAdvanceMode($event.target.value)" class="sb-input">
|
|
8
|
+
<option value="click">Click</option>
|
|
9
|
+
<option value="auto">Auto Next</option>
|
|
10
|
+
<option value="afterAnimation">After Animation</option>
|
|
11
|
+
</select>
|
|
12
|
+
<div v-if="advanceMode === 'auto'" class="sb-row" style="margin-top:8px">
|
|
13
|
+
<label class="sb-lbl">Delay (s)</label>
|
|
14
|
+
<input type="number" :value="sb.step.autoNext || 3" @input="setDelay(parseInt($event.target.value) || 3)" class="sb-input sb-input--num" step="1" min="1">
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
<script>
|
|
20
|
+
export default {
|
|
21
|
+
inject: ['sb'],
|
|
22
|
+
computed: {
|
|
23
|
+
advanceMode() {
|
|
24
|
+
if (this.sb.step.autoNext) return 'auto';
|
|
25
|
+
if (this.sb.step.afterAnimation) return 'afterAnimation';
|
|
26
|
+
return 'click';
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
methods: {
|
|
30
|
+
setAdvanceMode(mode) {
|
|
31
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 advance');
|
|
32
|
+
this.$delete(this.sb.step, 'autoNext');
|
|
33
|
+
this.$delete(this.sb.step, 'afterAnimation');
|
|
34
|
+
if (mode === 'auto') this.$set(this.sb.step, 'autoNext', 3);
|
|
35
|
+
else if (mode === 'afterAnimation') this.$set(this.sb.step, 'afterAnimation', true);
|
|
36
|
+
},
|
|
37
|
+
setDelay(val) {
|
|
38
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 autoNext delay');
|
|
39
|
+
this.$set(this.sb.step, 'autoNext', val);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-card" :class="{ 'sb-card--open': sb.cards.conditions }">
|
|
3
|
+
<div class="sb-card-head" @click="sb.toggle('conditions')">
|
|
4
|
+
<span>CONDITIONS</span><span class="sb-chevron">{{ sb.cards.conditions ? '▼' : '▶' }}</span>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-show="sb.cards.conditions" class="sb-card-body">
|
|
7
|
+
<div class="sb-row">
|
|
8
|
+
<label class="sb-lbl">Show If</label>
|
|
9
|
+
<input :value="sb.step.showIf || ''" @input="sb.setStepProp('showIf', $event.target.value)" class="sb-input" placeholder="permission:X, role:X, selector:.css">
|
|
10
|
+
</div>
|
|
11
|
+
<div class="sb-row">
|
|
12
|
+
<label class="sb-lbl">Hide If</label>
|
|
13
|
+
<input :value="sb.step.hideIf || ''" @input="sb.setStepProp('hideIf', $event.target.value)" class="sb-input" placeholder="selector:.css — skip if found">
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
<script>
|
|
19
|
+
export default { inject: ['sb'] };
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sb-card" :class="{ 'sb-card--open': sb.cards.content }">
|
|
3
|
+
<div class="sb-card-head" @click="sb.toggle('content')">
|
|
4
|
+
<span>CONTENT</span>
|
|
5
|
+
<span class="sb-chevron">{{ sb.cards.content ? '▼' : '▶' }}</span>
|
|
6
|
+
</div>
|
|
7
|
+
<div v-show="sb.cards.content" class="sb-card-body">
|
|
8
|
+
<label class="sb-lbl">HTML / Key</label>
|
|
9
|
+
<textarea :value="sb.step.html || ''" @input="setHtml($event.target.value)" class="sb-input sb-textarea" rows="3"></textarea>
|
|
10
|
+
<div class="sb-row">
|
|
11
|
+
<label class="sb-check"><input type="checkbox" :checked="!!sb.step.overlay" @change="toggleOverlay($event.target.checked)"> overlay</label>
|
|
12
|
+
<label class="sb-check"><input type="checkbox" :checked="!!sb.step.showBack" @change="toggleShowBack"> showBack</label>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="sb-row">
|
|
15
|
+
<label class="sb-lbl">Branch</label>
|
|
16
|
+
<input :value="sb.step._branch || ''" @input="setBranch($event.target.value)" class="sb-input" placeholder="(main)">
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
<script>
|
|
22
|
+
export default {
|
|
23
|
+
inject: ['sb'],
|
|
24
|
+
methods: {
|
|
25
|
+
setHtml(val) {
|
|
26
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 html');
|
|
27
|
+
this.$set(this.sb.step, 'html', val);
|
|
28
|
+
},
|
|
29
|
+
toggleOverlay(checked) {
|
|
30
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 overlay');
|
|
31
|
+
this.$set(this.sb.step, 'overlay', checked);
|
|
32
|
+
},
|
|
33
|
+
toggleShowBack(e) {
|
|
34
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 showBack');
|
|
35
|
+
if (e.target.checked) this.$set(this.sb.step, 'showBack', true);
|
|
36
|
+
else this.$delete(this.sb.step, 'showBack');
|
|
37
|
+
},
|
|
38
|
+
setBranch(val) {
|
|
39
|
+
this.sb.state.beginEdit('Edit step #' + this.sb.idx + ' \u2192 branch');
|
|
40
|
+
if (val) this.$set(this.sb.step, '_branch', val);
|
|
41
|
+
else this.$delete(this.sb.step, '_branch');
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
</script>
|