js-kompo-utils 1.0.13 → 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/VlDropdown.vue +8 -7
- 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
|
@@ -9,6 +9,14 @@ gsap.registerPlugin(MotionPathPlugin);
|
|
|
9
9
|
const DEFAULT_CURSOR_SIZE = 'clamp(32px, 8vw, 48px)';
|
|
10
10
|
const DEFAULT_SVG_PATH = 'M 0,0 C 0.3,0.1 0.7,0.9 1,1';
|
|
11
11
|
|
|
12
|
+
// Inline SVG cursor as data URI fallback when no image is configured
|
|
13
|
+
const FALLBACK_CURSOR_SVG = 'data:image/svg+xml,' + encodeURIComponent(
|
|
14
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">'
|
|
15
|
+
+ '<path d="M5 2 L5 26 L11.5 19.5 L17 29 L21 27 L15.5 17.5 L24 17.5 Z" '
|
|
16
|
+
+ 'fill="white" stroke="#333" stroke-width="1.5" stroke-linejoin="round"/>'
|
|
17
|
+
+ '</svg>'
|
|
18
|
+
);
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Cursor module: animated cursor element using GSAP MotionPathPlugin.
|
|
14
22
|
*/
|
|
@@ -27,16 +35,18 @@ export class Cursor {
|
|
|
27
35
|
*/
|
|
28
36
|
create(cfg = {}, isMobile = false) {
|
|
29
37
|
let cursorEl;
|
|
38
|
+
const configImage = this.engine?.config?.get('cursorImage');
|
|
39
|
+
const configSize = this.engine?.config?.get('cursorSize') || DEFAULT_CURSOR_SIZE;
|
|
30
40
|
|
|
31
41
|
if (isMobile) {
|
|
32
42
|
cursorEl = document.createElement('div');
|
|
33
43
|
cursorEl.className = 'tutorial-cursor tutorial-cursor--mobile';
|
|
34
44
|
} else {
|
|
35
45
|
cursorEl = document.createElement('img');
|
|
36
|
-
cursorEl.src = cfg.image ||
|
|
46
|
+
cursorEl.src = cfg.image || configImage || FALLBACK_CURSOR_SVG;
|
|
37
47
|
cursorEl.className = 'tutorial-cursor';
|
|
38
|
-
cursorEl.style.width =
|
|
39
|
-
cursorEl.style.height =
|
|
48
|
+
cursorEl.style.width = configSize;
|
|
49
|
+
cursorEl.style.height = configSize;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
document.body.appendChild(cursorEl);
|
|
@@ -269,6 +279,13 @@ export class Cursor {
|
|
|
269
279
|
};
|
|
270
280
|
}
|
|
271
281
|
|
|
282
|
+
_hexToRgba(hex, alpha) {
|
|
283
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
284
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
285
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
286
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
287
|
+
}
|
|
288
|
+
|
|
272
289
|
/**
|
|
273
290
|
* Create a click-pulse visual effect at the given coordinates.
|
|
274
291
|
* @param {number} x
|
|
@@ -287,7 +304,10 @@ export class Cursor {
|
|
|
287
304
|
ring.className = 'tutorial-pulse-ring';
|
|
288
305
|
ring.style.left = x + 'px';
|
|
289
306
|
ring.style.top = y + 'px';
|
|
290
|
-
|
|
307
|
+
const configColor = this.engine?.config?.get('pulseColor');
|
|
308
|
+
const devMode = this.engine?.config?.get('devMode');
|
|
309
|
+
const baseColor = configColor || (devMode ? '#4a6cf7' : '#ff4444');
|
|
310
|
+
ring.style.backgroundColor = this._hexToRgba(baseColor, bgOpacity);
|
|
291
311
|
ring.style.setProperty('--pulse-scale', String(scale));
|
|
292
312
|
ring.style.setProperty('--pulse-duration', duration + 's');
|
|
293
313
|
ring.style.setProperty('--pulse-delay', delay + 's');
|
|
@@ -69,7 +69,7 @@ function buildHighlightPathData(groups, padding, radius) {
|
|
|
69
69
|
* @param {{ x, y, w, h, r }} cr
|
|
70
70
|
* @returns {SVGRectElement}
|
|
71
71
|
*/
|
|
72
|
-
function createBorderRect(cr) {
|
|
72
|
+
function createBorderRect(cr, color) {
|
|
73
73
|
return createSvgEl('rect', {
|
|
74
74
|
x: cr.x,
|
|
75
75
|
y: cr.y,
|
|
@@ -77,7 +77,7 @@ function createBorderRect(cr) {
|
|
|
77
77
|
height: cr.h,
|
|
78
78
|
rx: cr.r,
|
|
79
79
|
fill: 'none',
|
|
80
|
-
stroke:
|
|
80
|
+
stroke: color,
|
|
81
81
|
'stroke-width': '2',
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -186,7 +186,7 @@ export class Highlight {
|
|
|
186
186
|
this._generation++;
|
|
187
187
|
const gen = this._generation;
|
|
188
188
|
const padding = config.padding || 8;
|
|
189
|
-
const radius = config.borderRadius
|
|
189
|
+
const radius = config.borderRadius ?? this.engine.config.get('highlightBorderRadius') ?? 8;
|
|
190
190
|
const blockOutside = config.blockOutside || false;
|
|
191
191
|
|
|
192
192
|
this._cancelStability = waitForStableHighlight(config, () => {
|
|
@@ -230,8 +230,10 @@ export class Highlight {
|
|
|
230
230
|
});
|
|
231
231
|
this.svgEl.appendChild(this.pathEl);
|
|
232
232
|
|
|
233
|
+
const highlightColor = this.engine.config.get('highlightColor') || '#ffd700';
|
|
234
|
+
this._highlightColor = highlightColor;
|
|
233
235
|
for (const cr of result.rects) {
|
|
234
|
-
this.svgEl.appendChild(createBorderRect(cr));
|
|
236
|
+
this.svgEl.appendChild(createBorderRect(cr, highlightColor));
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
document.body.appendChild(this.svgEl);
|
|
@@ -264,7 +266,7 @@ export class Highlight {
|
|
|
264
266
|
border.remove();
|
|
265
267
|
}
|
|
266
268
|
for (const cr of result.rects) {
|
|
267
|
-
this.svgEl.appendChild(createBorderRect(cr));
|
|
269
|
+
this.svgEl.appendChild(createBorderRect(cr, this._highlightColor || '#ffd700'));
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
|
|
@@ -236,6 +236,7 @@ export class HoverForcer {
|
|
|
236
236
|
}
|
|
237
237
|
el.classList.remove(FORCE_HOVER_CLASS);
|
|
238
238
|
el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
|
239
|
+
el.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
|
239
240
|
}
|
|
240
241
|
this.forcedElements.clear();
|
|
241
242
|
|
|
@@ -41,6 +41,8 @@ export class Overlay {
|
|
|
41
41
|
this.el.setAttribute('aria-modal', 'true');
|
|
42
42
|
this.el.setAttribute('aria-label', 'Tutorial');
|
|
43
43
|
this.el.style.backgroundColor = 'rgba(0,0,0,0.5)'; // Default dark, setMode() toggles
|
|
44
|
+
this.el.style.overscrollBehavior = 'contain';
|
|
45
|
+
this.el.style.touchAction = 'none';
|
|
44
46
|
|
|
45
47
|
document.body.appendChild(this.el);
|
|
46
48
|
document.body.style.overflow = 'hidden';
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// StepBuilderState.js — Framework-agnostic state management for Step Builder
|
|
2
|
+
// Handles steps CRUD, undo/redo, screenshots, and change detection.
|
|
3
|
+
|
|
4
|
+
var DEFAULT_STEP = { html: '', overlay: true, position: 'left', align: 'center' };
|
|
5
|
+
|
|
6
|
+
export class StepBuilderState {
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.steps = [JSON.parse(JSON.stringify(DEFAULT_STEP))];
|
|
10
|
+
this.idx = 0;
|
|
11
|
+
this.undoStack = [];
|
|
12
|
+
this.redoStack = [];
|
|
13
|
+
this.maxUndoSize = 50;
|
|
14
|
+
this.screenshots = {};
|
|
15
|
+
this.originalSteps = JSON.parse(JSON.stringify(this.steps));
|
|
16
|
+
this._listeners = {};
|
|
17
|
+
this._version = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --------------- Event Bus ---------------
|
|
21
|
+
|
|
22
|
+
on(event, cb) {
|
|
23
|
+
if (!this._listeners[event]) this._listeners[event] = [];
|
|
24
|
+
this._listeners[event].push(cb);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
off(event, cb) {
|
|
28
|
+
if (!this._listeners[event]) return;
|
|
29
|
+
this._listeners[event] = this._listeners[event].filter(function(fn) { return fn !== cb; });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
emit(event, data) {
|
|
33
|
+
if (!this._listeners[event]) return;
|
|
34
|
+
this._listeners[event].forEach(function(cb) {
|
|
35
|
+
try { cb(data); } catch (e) { console.warn('StepBuilderState event error:', event, e); }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --------------- Init ---------------
|
|
40
|
+
|
|
41
|
+
init(steps) {
|
|
42
|
+
var cloned = JSON.parse(JSON.stringify(steps));
|
|
43
|
+
// Replace contents in-place so external references stay valid
|
|
44
|
+
this.steps.length = 0;
|
|
45
|
+
for (var i = 0; i < cloned.length; i++) this.steps.push(cloned[i]);
|
|
46
|
+
this.originalSteps = JSON.parse(JSON.stringify(this.steps));
|
|
47
|
+
this.idx = 0;
|
|
48
|
+
this.undoStack = [];
|
|
49
|
+
this.redoStack = [];
|
|
50
|
+
this.screenshots = {};
|
|
51
|
+
this._version++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --------------- Getters ---------------
|
|
55
|
+
|
|
56
|
+
get step() {
|
|
57
|
+
return this.steps[this.idx] || {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get canUndo() {
|
|
61
|
+
return this.undoStack.length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get canRedo() {
|
|
65
|
+
return this.redoStack.length > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get undoLabels() {
|
|
69
|
+
return this.undoStack.map(function(e) { return { label: e.label, time: e.time }; });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get redoLabels() {
|
|
73
|
+
return this.redoStack.map(function(e) { return { label: e.label, time: e.time }; });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --------------- Undo / Redo ---------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Call before field edits to record undo state.
|
|
80
|
+
* Debounced: rapid edits (typing) only snapshot once per second.
|
|
81
|
+
*/
|
|
82
|
+
beginEdit(label) {
|
|
83
|
+
if (this._editActive) return;
|
|
84
|
+
this._pushUndo(label);
|
|
85
|
+
this._editActive = true;
|
|
86
|
+
clearTimeout(this._editTimer);
|
|
87
|
+
this._editTimer = setTimeout(() => { this._editActive = false; }, 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_pushUndo(label) {
|
|
91
|
+
this.undoStack.push({
|
|
92
|
+
snapshot: JSON.parse(JSON.stringify(this.steps)),
|
|
93
|
+
label: label || 'Change',
|
|
94
|
+
time: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
if (this.undoStack.length > this.maxUndoSize) this.undoStack.shift();
|
|
97
|
+
this.redoStack = [];
|
|
98
|
+
this.emit('undo-changed', { undoSize: this.undoStack.length, redoSize: 0 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
undo() {
|
|
102
|
+
if (!this.undoStack.length) return;
|
|
103
|
+
|
|
104
|
+
// Push current state to redo
|
|
105
|
+
this.redoStack.push({
|
|
106
|
+
snapshot: JSON.parse(JSON.stringify(this.steps)),
|
|
107
|
+
label: this.undoStack[this.undoStack.length - 1].label,
|
|
108
|
+
time: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
var entry = this.undoStack.pop();
|
|
112
|
+
|
|
113
|
+
// Replace steps in-place
|
|
114
|
+
this.steps.length = 0;
|
|
115
|
+
for (var i = 0; i < entry.snapshot.length; i++) this.steps.push(entry.snapshot[i]);
|
|
116
|
+
|
|
117
|
+
// Clamp idx
|
|
118
|
+
this.idx = Math.min(this.idx, this.steps.length - 1);
|
|
119
|
+
this._version++;
|
|
120
|
+
this.emit('state-restored');
|
|
121
|
+
this.emit('undo-changed', { undoSize: this.undoStack.length, redoSize: this.redoStack.length });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
redo() {
|
|
125
|
+
if (!this.redoStack.length) return;
|
|
126
|
+
|
|
127
|
+
// Push current state to undo
|
|
128
|
+
this.undoStack.push({
|
|
129
|
+
snapshot: JSON.parse(JSON.stringify(this.steps)),
|
|
130
|
+
label: this.redoStack[this.redoStack.length - 1].label,
|
|
131
|
+
time: Date.now(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
var entry = this.redoStack.pop();
|
|
135
|
+
|
|
136
|
+
// Replace steps in-place
|
|
137
|
+
this.steps.length = 0;
|
|
138
|
+
for (var i = 0; i < entry.snapshot.length; i++) this.steps.push(entry.snapshot[i]);
|
|
139
|
+
|
|
140
|
+
// Clamp idx
|
|
141
|
+
this.idx = Math.min(this.idx, this.steps.length - 1);
|
|
142
|
+
this._version++;
|
|
143
|
+
this.emit('state-restored');
|
|
144
|
+
this.emit('undo-changed', { undoSize: this.undoStack.length, redoSize: this.redoStack.length });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --------------- Step CRUD ---------------
|
|
148
|
+
|
|
149
|
+
selectStep(i) {
|
|
150
|
+
this.idx = Math.max(0, Math.min(i, this.steps.length - 1));
|
|
151
|
+
this._version++;
|
|
152
|
+
this.emit('step-selected', { index: this.idx });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addStep(afterIndex, branch) {
|
|
156
|
+
this._pushUndo('Add step after #' + afterIndex);
|
|
157
|
+
var newStep = JSON.parse(JSON.stringify(DEFAULT_STEP));
|
|
158
|
+
if (branch) newStep._branch = branch;
|
|
159
|
+
this.steps.splice(afterIndex + 1, 0, newStep);
|
|
160
|
+
this.idx = afterIndex + 1;
|
|
161
|
+
this._version++;
|
|
162
|
+
this.emit('step-added', { index: this.idx });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
deleteStep(index) {
|
|
166
|
+
if (this.steps.length <= 1) return;
|
|
167
|
+
this._pushUndo('Delete step #' + index);
|
|
168
|
+
|
|
169
|
+
// Remap screenshots — shift indices above deleted down by 1
|
|
170
|
+
var newScreenshots = {};
|
|
171
|
+
var keys = Object.keys(this.screenshots);
|
|
172
|
+
for (var k = 0; k < keys.length; k++) {
|
|
173
|
+
var i = parseInt(keys[k], 10);
|
|
174
|
+
if (i < index) newScreenshots[i] = this.screenshots[i];
|
|
175
|
+
else if (i > index) newScreenshots[i - 1] = this.screenshots[i];
|
|
176
|
+
// i === index is dropped
|
|
177
|
+
}
|
|
178
|
+
this.screenshots = newScreenshots;
|
|
179
|
+
|
|
180
|
+
this.steps.splice(index, 1);
|
|
181
|
+
this.idx = Math.min(index, this.steps.length - 1);
|
|
182
|
+
this._version++;
|
|
183
|
+
this.emit('step-removed', { index: index });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
duplicateStep(index) {
|
|
187
|
+
this._pushUndo('Duplicate step #' + index);
|
|
188
|
+
var clone = JSON.parse(JSON.stringify(this.steps[index]));
|
|
189
|
+
this.steps.splice(index + 1, 0, clone);
|
|
190
|
+
this.idx = index + 1;
|
|
191
|
+
this._version++;
|
|
192
|
+
this.emit('step-added', { index: this.idx });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
moveStep(fromIndex, toIndex) {
|
|
196
|
+
if (fromIndex === toIndex) return;
|
|
197
|
+
this._pushUndo('Move step #' + fromIndex + ' → #' + toIndex);
|
|
198
|
+
|
|
199
|
+
var step = this.steps.splice(fromIndex, 1)[0];
|
|
200
|
+
this.steps.splice(toIndex, 0, step);
|
|
201
|
+
|
|
202
|
+
// Remap screenshots
|
|
203
|
+
var newScreenshots = {};
|
|
204
|
+
var keys = Object.keys(this.screenshots);
|
|
205
|
+
for (var k = 0; k < keys.length; k++) {
|
|
206
|
+
var i = parseInt(keys[k], 10);
|
|
207
|
+
var newIdx = i;
|
|
208
|
+
if (i === fromIndex) {
|
|
209
|
+
newIdx = toIndex;
|
|
210
|
+
} else if (fromIndex < toIndex && i > fromIndex && i <= toIndex) {
|
|
211
|
+
newIdx = i - 1;
|
|
212
|
+
} else if (fromIndex > toIndex && i >= toIndex && i < fromIndex) {
|
|
213
|
+
newIdx = i + 1;
|
|
214
|
+
}
|
|
215
|
+
newScreenshots[newIdx] = this.screenshots[i];
|
|
216
|
+
}
|
|
217
|
+
this.screenshots = newScreenshots;
|
|
218
|
+
|
|
219
|
+
this.idx = toIndex;
|
|
220
|
+
this._version++;
|
|
221
|
+
this.emit('step-moved', { from: fromIndex, to: toIndex });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --------------- Screenshots ---------------
|
|
225
|
+
|
|
226
|
+
setScreenshot(index, dataUrl) {
|
|
227
|
+
this.screenshots[index] = dataUrl;
|
|
228
|
+
this.emit('screenshot-updated', { index: index });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getScreenshot(index) {
|
|
232
|
+
return this.screenshots[index] || null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --------------- Change Detection ---------------
|
|
236
|
+
|
|
237
|
+
hasStepChanged(index) {
|
|
238
|
+
var orig = this.originalSteps[index];
|
|
239
|
+
var curr = this.steps[index];
|
|
240
|
+
if (!orig || !curr) return true;
|
|
241
|
+
|
|
242
|
+
var allKeys = {};
|
|
243
|
+
Object.keys(orig).forEach(function(k) { allKeys[k] = true; });
|
|
244
|
+
Object.keys(curr).forEach(function(k) { allKeys[k] = true; });
|
|
245
|
+
|
|
246
|
+
var keys = Object.keys(allKeys);
|
|
247
|
+
for (var i = 0; i < keys.length; i++) {
|
|
248
|
+
var k = keys[i];
|
|
249
|
+
// Skip internal builder properties except _branch
|
|
250
|
+
if (k.charAt(0) === '_' && k !== '_branch') continue;
|
|
251
|
+
|
|
252
|
+
var a = orig[k];
|
|
253
|
+
var b = curr[k];
|
|
254
|
+
|
|
255
|
+
// Treat undefined, null, and empty arrays as equivalent "empty" values
|
|
256
|
+
var aEmpty = a === undefined || a === null || (Array.isArray(a) && a.length === 0);
|
|
257
|
+
var bEmpty = b === undefined || b === null || (Array.isArray(b) && b.length === 0);
|
|
258
|
+
if (aEmpty && bEmpty) continue;
|
|
259
|
+
|
|
260
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/tutorials/tutorial.scss
CHANGED
|
@@ -840,3 +840,189 @@
|
|
|
840
840
|
font-size: 12px;
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
|
+
|
|
844
|
+
// ─── Step Builder ───
|
|
845
|
+
.sb-wrapper {
|
|
846
|
+
position: fixed;
|
|
847
|
+
top: 0;
|
|
848
|
+
right: 0;
|
|
849
|
+
height: 100vh;
|
|
850
|
+
z-index: 99997;
|
|
851
|
+
display: flex;
|
|
852
|
+
flex-direction: row-reverse;
|
|
853
|
+
}
|
|
854
|
+
.sb {
|
|
855
|
+
width: 420px;
|
|
856
|
+
height: 100vh;
|
|
857
|
+
background: linear-gradient(170deg, #1e1e2e 0%, #161625 100%);
|
|
858
|
+
color: #e0e4ec;
|
|
859
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
860
|
+
font-size: 13px;
|
|
861
|
+
line-height: 1.5;
|
|
862
|
+
display: flex;
|
|
863
|
+
flex-direction: column;
|
|
864
|
+
border-left: 1px solid rgba(255,255,255,0.06);
|
|
865
|
+
box-shadow: -4px 0 24px rgba(0,0,0,0.4);
|
|
866
|
+
outline: none;
|
|
867
|
+
flex-shrink: 0;
|
|
868
|
+
}
|
|
869
|
+
.sb * { box-sizing: border-box; }
|
|
870
|
+
|
|
871
|
+
// Resize handle
|
|
872
|
+
.sb-resize { position: absolute; width: 6px; top: 0; bottom: 0; left: 0; cursor: ew-resize; z-index: 2; }
|
|
873
|
+
.sb-resize:hover { background: rgba(108,138,255,0.2); }
|
|
874
|
+
|
|
875
|
+
// Header
|
|
876
|
+
.sb-header { height: 44px; min-height: 44px; padding: 0 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(255,255,255,0.02); }
|
|
877
|
+
.sb-header-title { font-size: 14px; font-weight: 700; flex: 1; }
|
|
878
|
+
.sb-hbtn { background: none; border: 1px solid rgba(255,255,255,0.1); color: #aaa; border-radius: 6px; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: all 0.15s; }
|
|
879
|
+
.sb-hbtn:hover { background: rgba(255,255,255,0.08); color: #fff; }
|
|
880
|
+
.sb-hbtn--danger:hover { color: #e74c3c; border-color: #e74c3c; }
|
|
881
|
+
|
|
882
|
+
// Steps
|
|
883
|
+
.sb-steps { border-bottom: 1px solid rgba(255,255,255,0.06); padding: 8px 14px; flex-shrink: 0; }
|
|
884
|
+
.sb-steps-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
|
885
|
+
.sb-steps-count { font-size: 11px; font-weight: 600; color: #7a7f8e; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
886
|
+
.sb-steps-list { max-height: 200px; overflow-y: auto; }
|
|
887
|
+
.sb-step { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 6px; cursor: pointer; transition: background 0.15s; border: 1px solid transparent; }
|
|
888
|
+
.sb-step:hover { background: rgba(108,138,255,0.1); }
|
|
889
|
+
.sb-step--active { background: rgba(108,138,255,0.2); border-color: rgba(108,138,255,0.3); }
|
|
890
|
+
.sb-step-num { width: 22px; height: 22px; border-radius: 50%; background: rgba(255,255,255,0.08); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
|
|
891
|
+
.sb-step--active .sb-step-num { background: #6c8aff; color: #fff; }
|
|
892
|
+
.sb-step-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #b0b4c0; font-size: 12px; }
|
|
893
|
+
.sb-step-icons { display: flex; gap: 3px; font-size: 11px; color: #7a7f8e; flex-shrink: 0; }
|
|
894
|
+
.sb-step-actions { display: flex; gap: 4px; margin-top: 6px; }
|
|
895
|
+
|
|
896
|
+
// Detail zone
|
|
897
|
+
.sb-detail { flex: 1; overflow-y: auto; padding: 8px 14px; }
|
|
898
|
+
.sb-detail::-webkit-scrollbar { width: 5px; }
|
|
899
|
+
.sb-detail::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
|
|
900
|
+
|
|
901
|
+
// Cards
|
|
902
|
+
.sb-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; margin-bottom: 6px; overflow: hidden; }
|
|
903
|
+
.sb-card--open { background: rgba(255,255,255,0.04); }
|
|
904
|
+
.sb-card-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; cursor: pointer; user-select: none; font-size: 11px; font-weight: 600; color: #6c8aff; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
905
|
+
.sb-card-head:hover { background: rgba(255,255,255,0.03); }
|
|
906
|
+
.sb-chevron { font-size: 10px; color: #555; }
|
|
907
|
+
.sb-card-body { padding: 8px 12px 12px; }
|
|
908
|
+
|
|
909
|
+
// Inputs
|
|
910
|
+
.sb-input { background: rgba(255,255,255,0.06); color: #e0e4ec; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 7px 10px; font-size: 13px; font-family: inherit; outline: none; width: 100%; transition: border-color 0.2s; }
|
|
911
|
+
.sb-input:focus { border-color: #6c8aff; box-shadow: 0 0 0 2px rgba(108,138,255,0.15); }
|
|
912
|
+
.sb-input--num { width: 72px; }
|
|
913
|
+
.sb-input--sm { width: auto; min-width: 80px; }
|
|
914
|
+
.sb-textarea { resize: vertical; min-height: 60px; font-family: "Fira Code", monospace; font-size: 12px; }
|
|
915
|
+
select.sb-input { cursor: pointer; appearance: none; padding-right: 24px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; }
|
|
916
|
+
|
|
917
|
+
// Labels, rows
|
|
918
|
+
.sb-lbl { font-size: 11px; color: #7a7f8e; min-width: 50px; flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 4px; display: block; }
|
|
919
|
+
.sb-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
|
|
920
|
+
.sb-row--pick .sb-input { flex: 1; }
|
|
921
|
+
.sb-check { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #b0b4c0; cursor: pointer; margin-right: 12px; }
|
|
922
|
+
.sb-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: #6c8aff; cursor: pointer; }
|
|
923
|
+
|
|
924
|
+
// Pick button
|
|
925
|
+
.sb-pick { background: rgba(108,138,255,0.15); color: #6c8aff; border: 1px solid rgba(108,138,255,0.3); border-radius: 6px; padding: 6px 10px; font-size: 11px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: all 0.15s; }
|
|
926
|
+
.sb-pick:hover { background: rgba(108,138,255,0.25); }
|
|
927
|
+
.sb-pick--danger { background: rgba(231,76,60,0.15); color: #e74c3c; border-color: rgba(231,76,60,0.3); }
|
|
928
|
+
.sb-pick--danger:hover { background: rgba(231,76,60,0.25); }
|
|
929
|
+
|
|
930
|
+
// Buttons
|
|
931
|
+
.sb-btn { border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; padding: 6px 12px; transition: all 0.15s; font-family: inherit; white-space: nowrap; }
|
|
932
|
+
.sb-btn:hover { filter: brightness(1.15); }
|
|
933
|
+
.sb-btn-sm { padding: 4px 8px; font-size: 11px; }
|
|
934
|
+
.sb-btn-green { background: #2ecc71; color: #fff; }
|
|
935
|
+
.sb-btn-ghost { background: rgba(255,255,255,0.06); color: #ccc; border: 1px solid rgba(255,255,255,0.1); }
|
|
936
|
+
.sb-btn-ghost:hover { background: rgba(255,255,255,0.12); }
|
|
937
|
+
|
|
938
|
+
// Options row
|
|
939
|
+
.sb-opt-row { display: flex; gap: 6px; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
|
|
940
|
+
.sb-opt-row .sb-input { flex: 1; }
|
|
941
|
+
|
|
942
|
+
// Code output
|
|
943
|
+
.sb-code { background: rgba(0,0,0,0.3); color: #8bc78b; padding: 10px 12px; border-radius: 6px; font-family: "Fira Code", monospace; font-size: 10px; line-height: 1.6; overflow: auto; max-height: 200px; white-space: pre-wrap; border: 1px solid rgba(255,255,255,0.04); }
|
|
944
|
+
|
|
945
|
+
// Undo/Redo buttons
|
|
946
|
+
.sb-hbtn:disabled { opacity: 0.3; cursor: default; }
|
|
947
|
+
.sb-hbtn:disabled:hover { background: none; color: #aaa; }
|
|
948
|
+
|
|
949
|
+
// History drawer
|
|
950
|
+
.sb-history-drawer { width: 0; height: 100vh; background: linear-gradient(170deg, #1e1e2e, #161625); border-right: 1px solid rgba(255,255,255,0.08); transition: width 0.25s ease; overflow: hidden; flex-shrink: 0; }
|
|
951
|
+
.sb-history-drawer--open { width: 240px; overflow-y: auto; }
|
|
952
|
+
.sb-history-header { padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,0.06); font-size: 12px; font-weight: 600; color: #6c8aff; }
|
|
953
|
+
.sb-history-list { padding: 6px; }
|
|
954
|
+
.sb-history-item { display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 11px; color: #b0b4c0; transition: background 0.15s; }
|
|
955
|
+
.sb-history-item:hover { background: rgba(108,138,255,0.1); }
|
|
956
|
+
.sb-history-item--current { color: #6c8aff; font-weight: 600; cursor: default; }
|
|
957
|
+
.sb-history-item--current::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #6c8aff; flex-shrink: 0; }
|
|
958
|
+
.sb-history-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
959
|
+
.sb-history-time { font-size: 9px; color: #7a7f8e; flex-shrink: 0; }
|
|
960
|
+
|
|
961
|
+
// Drag & Drop
|
|
962
|
+
.sb-step--drag-over { border-color: #6c8aff; background: rgba(108,138,255,0.15); }
|
|
963
|
+
|
|
964
|
+
// Context menu
|
|
965
|
+
.sb-context-menu { position: fixed; z-index: 100010; background: linear-gradient(170deg, #2a2a3d, #1e1e2e); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; padding: 4px; min-width: 160px; box-shadow: 0 8px 30px rgba(0,0,0,0.5); font-size: 12px; }
|
|
966
|
+
.sb-ctx-item { padding: 6px 12px; border-radius: 6px; cursor: pointer; color: #b0b4c0; transition: background 0.1s; }
|
|
967
|
+
.sb-ctx-item:hover { background: rgba(255,255,255,0.06); }
|
|
968
|
+
.sb-ctx-item--green { color: #2ecc71; }
|
|
969
|
+
.sb-ctx-item--red { color: #e74c3c; }
|
|
970
|
+
.sb-ctx-item--disabled { opacity: 0.3; pointer-events: none; }
|
|
971
|
+
.sb-ctx-sep { height: 1px; background: rgba(255,255,255,0.06); margin: 3px 8px; }
|
|
972
|
+
|
|
973
|
+
// Branch tabs
|
|
974
|
+
.sb-branch-tabs { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; }
|
|
975
|
+
.sb-branch-tab { font-size: 10px; padding: 2px 8px; border-radius: 4px; cursor: pointer; color: #7a7f8e; border-bottom: 2px solid transparent; transition: all 0.15s; }
|
|
976
|
+
.sb-branch-tab:hover { color: #e0e4ec; }
|
|
977
|
+
.sb-branch-tab--active { color: #e0e4ec; background: rgba(255,255,255,0.06); }
|
|
978
|
+
|
|
979
|
+
// Branch bar on step items
|
|
980
|
+
.sb-step-branch-bar { width: 3px; height: 18px; border-radius: 2px; flex-shrink: 0; }
|
|
981
|
+
|
|
982
|
+
// Modified dot
|
|
983
|
+
.sb-step-modified { color: #f1c40f; font-size: 8px; }
|
|
984
|
+
|
|
985
|
+
// Waypoint rows
|
|
986
|
+
.sb-wp-row { padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
|
|
987
|
+
|
|
988
|
+
// Anchor badge
|
|
989
|
+
.sb-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-family: "Fira Code", monospace; background: rgba(108,138,255,0.15); color: #6c8aff; }
|
|
990
|
+
|
|
991
|
+
// Color input
|
|
992
|
+
.sb-color-input { width: 36px; height: 36px; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 2px; cursor: pointer; background: transparent; }
|
|
993
|
+
|
|
994
|
+
@media (max-width: 600px) {
|
|
995
|
+
.sb { width: 100vw; }
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ─── Timeline ───
|
|
999
|
+
.sb-timeline { border-top: 1px solid rgba(255,255,255,0.06); flex-shrink: 0; padding: 6px 14px; }
|
|
1000
|
+
.sb-timeline-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
|
1001
|
+
.sb-timeline-strip { display: flex; gap: 6px; overflow-x: auto; padding: 4px 0; }
|
|
1002
|
+
.sb-timeline-strip::-webkit-scrollbar { height: 4px; }
|
|
1003
|
+
.sb-timeline-strip::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
|
|
1004
|
+
.sb-timeline-thumb { position: relative; width: 70px; height: 52px; border-radius: 6px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); cursor: pointer; overflow: hidden; flex-shrink: 0; }
|
|
1005
|
+
.sb-timeline-thumb.sb-active { border-color: #6c8aff; }
|
|
1006
|
+
.sb-timeline-thumb-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.5; }
|
|
1007
|
+
.sb-timeline-thumb-num { position: absolute; top: 2px; left: 4px; font-size: 9px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
|
|
1008
|
+
.sb-timeline-capture { position: absolute; bottom: 2px; right: 2px; font-size: 10px; background: none; border: none; cursor: pointer; opacity: 0; transition: opacity 0.15s; padding: 2px; color: #ccc; }
|
|
1009
|
+
.sb-timeline-thumb:hover .sb-timeline-capture { opacity: 0.7; }
|
|
1010
|
+
.sb-timeline-thumb-icons { position: absolute; bottom: 2px; left: 4px; display: flex; gap: 2px; font-size: 9px; color: #7a7f8e; }
|
|
1011
|
+
.sb-timeline-modified { position: absolute; top: 3px; right: 3px; width: 6px; height: 6px; border-radius: 50%; background: #f1c40f; }
|
|
1012
|
+
|
|
1013
|
+
// ─── Path Preview ───
|
|
1014
|
+
.sb-path-preview { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 99996; }
|
|
1015
|
+
|
|
1016
|
+
// ─── Graph View ───
|
|
1017
|
+
.sb-graph-modal { position: fixed; inset: 0; z-index: 100000; background: rgba(10,10,20,0.92); backdrop-filter: blur(6px); display: flex; flex-direction: column; font-family: system-ui, -apple-system, sans-serif; outline: none; }
|
|
1018
|
+
.sb-graph-header { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid rgba(255,255,255,0.08); flex-shrink: 0; }
|
|
1019
|
+
.sb-graph-title { font-size: 16px; font-weight: 700; color: #e0e4ec; flex: 1; }
|
|
1020
|
+
.sb-graph-container { flex: 1; overflow: auto; position: relative; }
|
|
1021
|
+
.sb-graph-arrows { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1022
|
+
.sb-graph-node { position: absolute; width: 160px; height: 72px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); cursor: pointer; display: flex; flex-direction: column; justify-content: center; padding: 8px 10px 8px 16px; transition: all 0.15s; overflow: hidden; }
|
|
1023
|
+
.sb-graph-node:hover { background: rgba(108,138,255,0.1); border-color: rgba(108,138,255,0.3); }
|
|
1024
|
+
.sb-graph-node--active { background: rgba(108,138,255,0.2); border-color: #6c8aff; }
|
|
1025
|
+
.sb-graph-node-branch { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; }
|
|
1026
|
+
.sb-graph-node-num { font-size: 10px; font-weight: 700; color: #6c8aff; }
|
|
1027
|
+
.sb-graph-node-text { font-size: 11px; color: #b0b4c0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1028
|
+
.sb-graph-node-icons { font-size: 10px; color: #7a7f8e; display: flex; gap: 3px; }
|