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.
Files changed (34) hide show
  1. package/components/VlDropdown.vue +8 -7
  2. package/components/VlTutorialStepBuilder.vue +137 -586
  3. package/components/step-builder/SbContextMenu.vue +41 -0
  4. package/components/step-builder/SbGraphView.vue +93 -0
  5. package/components/step-builder/SbHeader.vue +16 -0
  6. package/components/step-builder/SbHistoryDrawer.vue +43 -0
  7. package/components/step-builder/SbPathPreview.vue +57 -0
  8. package/components/step-builder/SbStepList.vue +87 -0
  9. package/components/step-builder/SbTimeline.vue +66 -0
  10. package/components/step-builder/cards/SbActionsCard.vue +26 -0
  11. package/components/step-builder/cards/SbAdvanceCard.vue +43 -0
  12. package/components/step-builder/cards/SbConditionsCard.vue +20 -0
  13. package/components/step-builder/cards/SbContentCard.vue +45 -0
  14. package/components/step-builder/cards/SbCursorCard.vue +102 -0
  15. package/components/step-builder/cards/SbGlobalOptsCard.vue +40 -0
  16. package/components/step-builder/cards/SbHighlightCard.vue +40 -0
  17. package/components/step-builder/cards/SbHoverCard.vue +32 -0
  18. package/components/step-builder/cards/SbOptionsCard.vue +50 -0
  19. package/components/step-builder/cards/SbOutputCard.vue +17 -0
  20. package/components/step-builder/cards/SbPositionCard.vue +87 -0
  21. package/components/step-builder/cards/SbScrollCard.vue +17 -0
  22. package/docs/extending-tutorial-system.md +376 -0
  23. package/package.json +10 -9
  24. package/tutorials/TutorialConfig.js +4 -0
  25. package/tutorials/TutorialEngine.js +49 -3
  26. package/tutorials/extensions/HideIfExtension.js +12 -0
  27. package/tutorials/extensions/LinkNextExtension.js +117 -0
  28. package/tutorials/modules/Cursor.js +24 -4
  29. package/tutorials/modules/Highlight.js +7 -5
  30. package/tutorials/modules/HoverForcer.js +1 -0
  31. package/tutorials/modules/Overlay.js +2 -0
  32. package/tutorials/step-builder/StepBuilderState.js +265 -0
  33. package/tutorials/tutorial.scss +186 -0
  34. 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 || '/images/tutorial-cursor.svg';
46
+ cursorEl.src = cfg.image || configImage || FALLBACK_CURSOR_SVG;
37
47
  cursorEl.className = 'tutorial-cursor';
38
- cursorEl.style.width = DEFAULT_CURSOR_SIZE;
39
- cursorEl.style.height = DEFAULT_CURSOR_SIZE;
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
- ring.style.backgroundColor = `rgba(255, 68, 68, ${bgOpacity})`;
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: '#ffd700',
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 || 8;
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
+ }
@@ -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; }