html-overlay-node 0.1.9 → 0.1.11

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.
@@ -39,7 +39,7 @@ export class HtmlOverlay {
39
39
  const header = document.createElement("div");
40
40
  header.className = "node-header";
41
41
  Object.assign(header.style, {
42
- height: "24px",
42
+ height: "26px",
43
43
  flexShrink: "0",
44
44
  display: "flex",
45
45
  alignItems: "center",
@@ -111,7 +111,7 @@ export class HtmlOverlay {
111
111
  const def = this.registry.types.get(node.type);
112
112
 
113
113
  // render 함수가 있거나, html 설정 객체가 있으면 처리
114
- const hasHtml = !!(def?.html);
114
+ const hasHtml = !!def?.html;
115
115
  if (!hasHtml) continue;
116
116
 
117
117
  const el = this._ensureNodeElement(node, def, graph);
@@ -130,13 +130,16 @@ export class HtmlOverlay {
130
130
  def.html.update(node, el, {
131
131
  selected: selection.has(node.id),
132
132
  header: parts.header,
133
- body: parts.body
133
+ body: parts.body,
134
134
  });
135
135
  }
136
136
 
137
137
  seen.add(node.id);
138
138
  }
139
139
 
140
+ // ── Interactive Stepping Play Button ─────────────────────
141
+ this._drawStepOverlay(graph);
142
+
140
143
  // 없어진 노드 제거
141
144
  for (const [id, el] of this.nodes) {
142
145
  if (!seen.has(id)) {
@@ -146,6 +149,65 @@ export class HtmlOverlay {
146
149
  }
147
150
  }
148
151
 
152
+ _drawStepOverlay(graph) {
153
+ const runner = graph.runner;
154
+ if (!runner || runner.executionMode !== "step" || !runner.activePlan) {
155
+ if (this._stepBtn) {
156
+ this._stepBtn.style.display = "none";
157
+ }
158
+ return;
159
+ }
160
+
161
+ const nextStep = runner.activePlan[runner.activeStepIndex];
162
+ if (!nextStep) {
163
+ if (this._stepBtn) this._stepBtn.style.display = "none";
164
+ return;
165
+ }
166
+
167
+ const node = graph.nodes.get(nextStep.nodeId);
168
+ if (!node) return;
169
+
170
+ if (!this._stepBtn) {
171
+ this._stepBtn = document.createElement("button");
172
+ this._stepBtn.className = "step-play-button";
173
+ this._stepBtn.innerHTML = "▶";
174
+ Object.assign(this._stepBtn.style, {
175
+ position: "absolute",
176
+ zIndex: "100",
177
+ width: "20px",
178
+ height: "20px",
179
+ borderRadius: "4px",
180
+ border: "none",
181
+ background: "transparent",
182
+ color: "white",
183
+ fontSize: "12px",
184
+ cursor: "pointer",
185
+ display: "flex",
186
+ alignItems: "center",
187
+ justifyContent: "center",
188
+ // boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
189
+ pointerEvents: "auto",
190
+ transition: "transform 0.1s, background 0.2s",
191
+ });
192
+ this._stepBtn.addEventListener("mouseover", () => {
193
+ this._stepBtn.style.transform = "scale(1)";
194
+ });
195
+ this._stepBtn.addEventListener("mouseout", () => {
196
+ this._stepBtn.style.transform = "scale(1)";
197
+ });
198
+ this._stepBtn.addEventListener("click", (e) => {
199
+ e.stopPropagation();
200
+ runner.executeNextStep();
201
+ });
202
+ this.container.appendChild(this._stepBtn);
203
+ }
204
+
205
+ // Position the button in the top-right corner of the node (header area)
206
+ this._stepBtn.style.display = "flex";
207
+ this._stepBtn.style.left = `${node.computed.x + node.computed.w - 26}px`;
208
+ this._stepBtn.style.top = `${node.computed.y + 2}px`;
209
+ }
210
+
149
211
  /**
150
212
  * Sync container transform with renderer state (lightweight update)
151
213
  * Called when zoom/pan occurs without needing full redraw
@@ -28,8 +28,11 @@ export function portRect(node, port, idx, dir) {
28
28
  };
29
29
 
30
30
  // Fixed spacing
31
- const headerHeight = 28;
32
- const y = ny + headerHeight + 10 + idx * 24;
31
+ // Sync with CanvasRenderer (headerH = 26)
32
+ const headerHeight = 26;
33
+ const padding = 8;
34
+ const portSpacing = 20;
35
+ const y = ny + headerHeight + padding + (idx * portSpacing) + portSpacing / 2;
33
36
 
34
37
  // Ports centered on node edges (half inside, half outside)
35
38
  const portWidth = 12;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * HelpOverlay - Modular help keyboard shortcuts overlay
3
+ */
4
+ export class HelpOverlay {
5
+ constructor(container, options = {}) {
6
+ this.container = container;
7
+ this.options = {
8
+ shortcuts: options.shortcuts || this._getDefaultShortcuts(),
9
+ onToggle: options.onToggle || null,
10
+ };
11
+
12
+ this.isVisible = false;
13
+ this.overlay = null;
14
+ this.toggleBtn = null;
15
+
16
+ this._createElements();
17
+ this._bindEvents();
18
+ }
19
+
20
+ _getDefaultShortcuts() {
21
+ return [
22
+ {
23
+ group: "Selection",
24
+ items: [
25
+ { label: "Select node", key: "Click" },
26
+ { label: "Multi-select", key: "Shift+Click" },
27
+ { label: "Box select", key: "Ctrl+Drag" },
28
+ ],
29
+ },
30
+ {
31
+ group: "Edit",
32
+ items: [
33
+ { label: "Delete", key: "Del" },
34
+ { label: "Undo", key: "Ctrl+Z" },
35
+ { label: "Redo", key: "Ctrl+Y" },
36
+ ],
37
+ },
38
+ {
39
+ group: "Group & Align",
40
+ items: [
41
+ { label: "Create group", key: "Ctrl+G" },
42
+ { label: "Align horizontal", key: "A" },
43
+ { label: "Align vertical", key: "Shift+A" },
44
+ ],
45
+ },
46
+ {
47
+ group: "View",
48
+ items: [
49
+ { label: "Toggle snap", key: "G" },
50
+ { label: "Pan", key: "Mid+Drag" },
51
+ { label: "Zoom", key: "Scroll" },
52
+ { label: "Context menu", key: "RClick" },
53
+ ],
54
+ },
55
+ ];
56
+ }
57
+
58
+ _createElements() {
59
+ // Create Toggle Button
60
+ this.toggleBtn = document.createElement("div");
61
+ this.toggleBtn.id = "helpToggle";
62
+ this.toggleBtn.title = "단축키 (?)";
63
+ this.toggleBtn.textContent = "?";
64
+ this.container.appendChild(this.toggleBtn);
65
+
66
+ // Create Overlay
67
+ this.overlay = document.createElement("div");
68
+ this.overlay.id = "helpOverlay";
69
+
70
+ const sectionsHtml = this.options.shortcuts
71
+ .map(
72
+ (group) => `
73
+ <h4>${group.group}</h4>
74
+ ${group.items
75
+ .map(
76
+ (item) => `
77
+ <div class="shortcut-item">
78
+ <span>${item.label}</span>
79
+ <span class="shortcut-key">${item.key}</span>
80
+ </div>
81
+ `
82
+ )
83
+ .join("")}
84
+ `
85
+ )
86
+ .join("");
87
+
88
+ this.overlay.innerHTML = `
89
+ <h3>
90
+ <span>Keyboard Shortcuts</span>
91
+ <button class="close-btn" id="helpClose" title="Close">×</button>
92
+ </h3>
93
+ ${sectionsHtml}
94
+ `;
95
+
96
+ this.container.appendChild(this.overlay);
97
+ }
98
+
99
+ _bindEvents() {
100
+ this.toggleBtn.addEventListener("click", () => this.toggle());
101
+
102
+ const closeBtn = this.overlay.querySelector("#helpClose");
103
+ if (closeBtn) {
104
+ closeBtn.addEventListener("click", (e) => {
105
+ e.stopPropagation();
106
+ this.close();
107
+ });
108
+ }
109
+
110
+ // Close when clicking outside
111
+ document.addEventListener("mousedown", (e) => {
112
+ if (this.isVisible) {
113
+ if (!this.overlay.contains(e.target) && !this.toggleBtn.contains(e.target)) {
114
+ this.close();
115
+ }
116
+ }
117
+ });
118
+
119
+ // Keyboard shortcuts
120
+ window.addEventListener("keydown", (e) => {
121
+ if (e.key === "?" || (e.shiftKey && e.key === "/")) {
122
+ // Only toggle if not typing in an input
123
+ if (!["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
124
+ e.preventDefault();
125
+ this.toggle();
126
+ }
127
+ }
128
+
129
+ if (e.key === "Escape" && this.isVisible) {
130
+ this.close();
131
+ }
132
+ });
133
+ }
134
+
135
+ toggle() {
136
+ if (this.isVisible) this.close();
137
+ else this.open();
138
+ }
139
+
140
+ open() {
141
+ this.isVisible = true;
142
+ this.overlay.classList.add("visible");
143
+ this.toggleBtn.classList.add("active");
144
+ if (this.options.onToggle) this.options.onToggle(true);
145
+ }
146
+
147
+ close() {
148
+ this.isVisible = false;
149
+ this.overlay.classList.remove("visible");
150
+ this.toggleBtn.classList.remove("active");
151
+ if (this.options.onToggle) this.options.onToggle(false);
152
+ }
153
+
154
+ destroy() {
155
+ this.toggleBtn?.remove();
156
+ this.overlay?.remove();
157
+ }
158
+ }
@@ -10,7 +10,7 @@
10
10
  background: radial-gradient(circle at top right, #3c3c3c 0, #1f1f1f 55%);
11
11
  background-color: #262626;
12
12
  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
13
- border-left: 1px solid rgba(255, 255, 255, 0.06);
13
+ border-left: 1px solid var(--color-border);
14
14
 
15
15
  opacity: 0;
16
16
  transform: translateX(20px);
@@ -37,7 +37,7 @@
37
37
  align-items: center;
38
38
  justify-content: space-between;
39
39
  padding-bottom: 16px;
40
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
40
+ border-bottom: 1px solid var(--color-border);
41
41
  margin-bottom: 20px;
42
42
  flex-shrink: 0;
43
43
  }
@@ -51,7 +51,7 @@
51
51
  .title-text {
52
52
  font-size: 18px;
53
53
  font-weight: 600;
54
- color: #f5f5f5;
54
+ color: var(--color-text-main);
55
55
  letter-spacing: 0.01em;
56
56
  }
57
57
 
@@ -71,8 +71,8 @@
71
71
  }
72
72
 
73
73
  .panel-close:hover {
74
- background: rgba(255, 255, 255, 0.08);
75
- color: #fff;
74
+ background: var(--color-bg-surface);
75
+ color: var(--color-text-main);
76
76
  transform: translateY(-1px);
77
77
  }
78
78
 
@@ -114,7 +114,7 @@
114
114
  .section-title {
115
115
  font-size: 13px;
116
116
  font-weight: 600;
117
- color: #d0d0d0;
117
+ color: var(--color-text-dim);
118
118
  margin-bottom: 12px;
119
119
  text-transform: uppercase;
120
120
  letter-spacing: 0.08em;
@@ -135,39 +135,70 @@
135
135
 
136
136
  .field label {
137
137
  font-size: 12px;
138
- color: #9e9e9e;
138
+ color: var(--color-text-muted);
139
139
  font-weight: 500;
140
140
  }
141
141
 
142
142
  .field input {
143
143
  width: 100%;
144
144
  padding: 8px 10px;
145
- border-radius: 6px;
146
- border: 1px solid rgba(255, 255, 255, 0.12);
147
- background: rgba(0, 0, 0, 0.25);
148
- color: #f5f5f5;
145
+ border-radius: var(--radius-md);
146
+ border: 1px solid var(--color-border);
147
+ background: rgba(0, 0, 0, 0.3);
148
+ color: var(--color-text-main);
149
149
  font-size: 13px;
150
150
  outline: none;
151
151
  transition: all 0.2s ease;
152
- font-family:
153
- system-ui,
154
- -apple-system,
155
- sans-serif;
152
+ font-family: inherit;
156
153
  box-sizing: border-box;
157
154
  line-height: 1.4;
158
155
  }
159
156
 
157
+ .field input:hover {
158
+ border-color: var(--color-border-focus);
159
+ background: rgba(0, 0, 0, 0.35);
160
+ }
161
+
160
162
  .field input:focus {
161
- border-color: #6366f1;
163
+ border-color: var(--color-primary);
162
164
  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
163
- background: rgba(0, 0, 0, 0.35);
165
+ background: rgba(0, 0, 0, 0.45);
164
166
  }
165
167
 
166
168
  .field input[readonly] {
167
- background: rgba(0, 0, 0, 0.4);
168
- color: #888;
169
+ background: rgba(0, 0, 0, 0.15);
170
+ color: #707080;
171
+ border-color: rgba(255, 255, 255, 0.04);
169
172
  cursor: not-allowed;
170
- opacity: 0.7;
173
+ opacity: 0.8;
174
+ }
175
+
176
+ .field select {
177
+ width: 100%;
178
+ padding: 8px 10px;
179
+ border-radius: var(--radius-md);
180
+ border: 1px solid var(--color-border);
181
+ background: rgba(0, 0, 0, 0.3);
182
+ color: var(--color-text-main);
183
+ font-size: 13px;
184
+ outline: none;
185
+ transition: all 0.2s ease;
186
+ font-family: inherit;
187
+ cursor: pointer;
188
+ appearance: none;
189
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238888a8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
190
+ background-repeat: no-repeat;
191
+ background-position: right 10px center;
192
+ }
193
+
194
+ .field select:hover {
195
+ border-color: var(--color-border-focus);
196
+ background: rgba(0, 0, 0, 0.35);
197
+ }
198
+
199
+ .field select:focus {
200
+ border-color: var(--color-primary);
201
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
171
202
  }
172
203
 
173
204
  .field-row {
@@ -256,22 +287,22 @@
256
287
  }
257
288
 
258
289
  .btn-primary {
259
- background: #6366f1;
260
- color: #fff;
290
+ background: var(--color-primary);
291
+ color: var(--color-text-main);
261
292
  }
262
293
 
263
294
  .btn-primary:hover {
264
- background: #818cf8;
295
+ background: var(--color-primary-hover);
265
296
  transform: translateY(-1px);
266
297
  box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
267
298
  }
268
299
 
269
300
  .btn-secondary {
270
- background: rgba(255, 255, 255, 0.08);
271
- color: #d0d0d0;
301
+ background: var(--color-bg-surface);
302
+ color: var(--color-text-dim);
272
303
  }
273
304
 
274
305
  .btn-secondary:hover {
275
- background: rgba(255, 255, 255, 0.12);
276
- color: #fff;
306
+ background: var(--color-bg-surface);
307
+ color: var(--color-text-main);
277
308
  }