html-overlay-node 0.1.0

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.
@@ -0,0 +1,269 @@
1
+ /**
2
+ * PropertyPanel - Node property editor panel
3
+ */
4
+ export class PropertyPanel {
5
+ constructor(container, { graph, hooks, registry, render }) {
6
+ this.container = container;
7
+ this.graph = graph;
8
+ this.hooks = hooks;
9
+ this.registry = registry;
10
+ this.render = render; // Store render callback
11
+
12
+ this.panel = null;
13
+ this.currentNode = null;
14
+ this.isVisible = false;
15
+
16
+ this._createPanel();
17
+ }
18
+
19
+ _createPanel() {
20
+ // Create panel element
21
+ this.panel = document.createElement('div');
22
+ this.panel.className = 'property-panel';
23
+ this.panel.style.display = 'none';
24
+
25
+ // Panel HTML structure
26
+ this.panel.innerHTML = `
27
+ <div class="panel-inner">
28
+ <div class="panel-header">
29
+ <div class="panel-title">
30
+ <span class="title-text">Node Properties</span>
31
+ </div>
32
+ <button class="panel-close" type="button">×</button>
33
+ </div>
34
+ <div class="panel-content">
35
+ <!-- Content will be dynamically generated -->
36
+ </div>
37
+ </div>
38
+ `;
39
+
40
+ this.container.appendChild(this.panel);
41
+
42
+ // Event listeners
43
+ this.panel.querySelector('.panel-close').addEventListener('click', () => {
44
+ this.close();
45
+ });
46
+
47
+ // Close on ESC key
48
+ document.addEventListener('keydown', (e) => {
49
+ if (e.key === 'Escape' && this.isVisible) {
50
+ this.close();
51
+ }
52
+ });
53
+ }
54
+
55
+ open(node) {
56
+ if (!node) return;
57
+
58
+ this.currentNode = node;
59
+ this.isVisible = true;
60
+
61
+ // Update content
62
+ this._renderContent();
63
+
64
+ // Show panel
65
+ this.panel.style.display = 'block';
66
+ this.panel.classList.add('panel-visible');
67
+ }
68
+
69
+ close() {
70
+ this.isVisible = false;
71
+ this.panel.classList.remove('panel-visible');
72
+
73
+ setTimeout(() => {
74
+ this.panel.style.display = 'none';
75
+ this.currentNode = null;
76
+ }, 200);
77
+ }
78
+
79
+ _renderContent() {
80
+ const node = this.currentNode;
81
+ if (!node) return;
82
+
83
+ const content = this.panel.querySelector('.panel-content');
84
+ const def = this.registry?.types?.get(node.type);
85
+
86
+ content.innerHTML = `
87
+ <div class="section">
88
+ <div class="section-title">Basic Info</div>
89
+ <div class="section-body">
90
+ <div class="field">
91
+ <label>Type</label>
92
+ <input type="text" value="${node.type}" readonly />
93
+ </div>
94
+ <div class="field">
95
+ <label>Title</label>
96
+ <input type="text" data-field="title" value="${node.title || ''}" />
97
+ </div>
98
+ <div class="field">
99
+ <label>ID</label>
100
+ <input type="text" value="${node.id}" readonly />
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="section">
106
+ <div class="section-title">Position & Size</div>
107
+ <div class="section-body">
108
+ <div class="field-row">
109
+ <div class="field">
110
+ <label>X</label>
111
+ <input type="number" data-field="x" value="${Math.round(node.computed.x)}" />
112
+ </div>
113
+ <div class="field">
114
+ <label>Y</label>
115
+ <input type="number" data-field="y" value="${Math.round(node.computed.y)}" />
116
+ </div>
117
+ </div>
118
+ <div class="field-row">
119
+ <div class="field">
120
+ <label>Width</label>
121
+ <input type="number" data-field="width" value="${node.computed.w}" />
122
+ </div>
123
+ <div class="field">
124
+ <label>Height</label>
125
+ <input type="number" data-field="height" value="${node.computed.h}" />
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ ${this._renderPorts(node)}
132
+ ${this._renderState(node)}
133
+
134
+ <div class="panel-actions">
135
+ <button class="btn-secondary panel-close-btn">Close</button>
136
+ </div>
137
+ `;
138
+
139
+ // Add event listeners for inputs
140
+ this._attachInputListeners();
141
+ }
142
+
143
+ _renderPorts(node) {
144
+ if (!node.inputs.length && !node.outputs.length) return '';
145
+
146
+ return `
147
+ <div class="section">
148
+ <div class="section-title">Ports</div>
149
+ <div class="section-body">
150
+ ${node.inputs.length ? `
151
+ <div class="port-group">
152
+ <div class="port-group-title">Inputs (${node.inputs.length})</div>
153
+ ${node.inputs.map(p => `
154
+ <div class="port-item">
155
+ <span class="port-icon ${p.portType || 'data'}"></span>
156
+ <span class="port-name">${p.name}</span>
157
+ ${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
158
+ </div>
159
+ `).join('')}
160
+ </div>
161
+ ` : ''}
162
+
163
+ ${node.outputs.length ? `
164
+ <div class="port-group">
165
+ <div class="port-group-title">Outputs (${node.outputs.length})</div>
166
+ ${node.outputs.map(p => `
167
+ <div class="port-item">
168
+ <span class="port-icon ${p.portType || 'data'}"></span>
169
+ <span class="port-name">${p.name}</span>
170
+ ${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
171
+ </div>
172
+ `).join('')}
173
+ </div>
174
+ ` : ''}
175
+ </div>
176
+ </div>
177
+ `;
178
+ }
179
+
180
+ _renderState(node) {
181
+ if (!node.state || Object.keys(node.state).length === 0) return '';
182
+
183
+ return `
184
+ <div class="section">
185
+ <div class="section-title">State</div>
186
+ <div class="section-body">
187
+ ${Object.entries(node.state).map(([key, value]) => `
188
+ <div class="field">
189
+ <label>${key}</label>
190
+ <input
191
+ type="${typeof value === 'number' ? 'number' : 'text'}"
192
+ data-field="state.${key}"
193
+ value="${value}"
194
+ />
195
+ </div>
196
+ `).join('')}
197
+ </div>
198
+ </div>
199
+ `;
200
+ }
201
+
202
+ _attachInputListeners() {
203
+ const inputs = this.panel.querySelectorAll('[data-field]');
204
+
205
+ inputs.forEach(input => {
206
+ input.addEventListener('change', () => {
207
+ this._handleFieldChange(input.dataset.field, input.value);
208
+ });
209
+ });
210
+
211
+ // Close button
212
+ this.panel.querySelector('.panel-close-btn').addEventListener('click', () => {
213
+ this.close();
214
+ });
215
+ }
216
+
217
+ _handleFieldChange(field, value) {
218
+ const node = this.currentNode;
219
+ if (!node) return;
220
+
221
+ switch (field) {
222
+ case 'title':
223
+ node.title = value;
224
+ break;
225
+
226
+ case 'x':
227
+ node.pos.x = parseFloat(value);
228
+ this.graph.updateWorldTransforms();
229
+ break;
230
+
231
+ case 'y':
232
+ node.pos.y = parseFloat(value);
233
+ this.graph.updateWorldTransforms();
234
+ break;
235
+
236
+ case 'width':
237
+ node.size.width = parseFloat(value);
238
+ break;
239
+
240
+ case 'height':
241
+ node.size.height = parseFloat(value);
242
+ break;
243
+
244
+ default:
245
+ // Handle state fields
246
+ if (field.startsWith('state.')) {
247
+ const key = field.substring(6);
248
+ if (node.state) {
249
+ const originalValue = node.state[key];
250
+ node.state[key] = typeof originalValue === 'number' ? parseFloat(value) : value;
251
+ }
252
+ }
253
+ }
254
+
255
+ // Emit update event
256
+ this.hooks?.emit('node:updated', node);
257
+
258
+ // Trigger render to update canvas immediately
259
+ if (this.render) {
260
+ this.render();
261
+ }
262
+ }
263
+
264
+ destroy() {
265
+ if (this.panel) {
266
+ this.panel.remove();
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,75 @@
1
+ export function randomUUID() {
2
+ // 1) 전역 객체 안전 획득
3
+ const g =
4
+ typeof globalThis !== "undefined" ? globalThis :
5
+ typeof self !== "undefined" ? self :
6
+ typeof window !== "undefined" ? window :
7
+ typeof global !== "undefined" ? global : {};
8
+
9
+ const c = g.crypto || g.msCrypto; // IE11 호환
10
+
11
+ // 2) 네이티브 지원 (브라우저/Deno 등)
12
+ if (c && typeof c.randomUUID === "function") {
13
+ return c.randomUUID();
14
+ }
15
+
16
+ // 3) Web Crypto만 있는 경우 (getRandomValues로 직접 생성)
17
+ if (c && typeof c.getRandomValues === "function") {
18
+ const bytes = new Uint8Array(16);
19
+ c.getRandomValues(bytes);
20
+ // RFC4122 버전/변형 비트 설정
21
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
22
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
23
+
24
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
25
+ return (
26
+ hex.slice(0, 4).join("") + "-" +
27
+ hex.slice(4, 6).join("") + "-" +
28
+ hex.slice(6, 8).join("") + "-" +
29
+ hex.slice(8, 10).join("") + "-" +
30
+ hex.slice(10, 16).join("")
31
+ );
32
+ }
33
+
34
+ // 4) Node.js 전용 대체 (require가 있을 때)
35
+ try {
36
+ // 번들러/ESM 충돌 피하려고 런타임에만 require 접근
37
+
38
+ const req = Function('return typeof require === "function" ? require : null')();
39
+ if (req) {
40
+ const nodeCrypto = req("crypto");
41
+ if (typeof nodeCrypto.randomUUID === "function") {
42
+ return nodeCrypto.randomUUID();
43
+ }
44
+ const bytes = nodeCrypto.randomBytes(16);
45
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
46
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
47
+
48
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
49
+ return (
50
+ hex.slice(0, 4).join("") + "-" +
51
+ hex.slice(4, 6).join("") + "-" +
52
+ hex.slice(6, 8).join("") + "-" +
53
+ hex.slice(8, 10).join("") + "-" +
54
+ hex.slice(10, 16).join("")
55
+ );
56
+ }
57
+ } catch {
58
+ // ignore
59
+ }
60
+
61
+ // 5) 최후의 비보안 대체 (CSPRNG 아님!)
62
+ const bytes = new Uint8Array(16);
63
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
64
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
65
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
66
+
67
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
68
+ return (
69
+ hex.slice(0, 4).join("") + "-" +
70
+ hex.slice(4, 6).join("") + "-" +
71
+ hex.slice(6, 8).join("") + "-" +
72
+ hex.slice(8, 10).join("") + "-" +
73
+ hex.slice(10, 16).join("")
74
+ );
75
+ }