@operato/property-panel 10.0.0-beta.2 → 10.0.0-beta.21

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 (27) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/src/ox-property-panel.js +2 -15
  3. package/dist/src/ox-property-panel.js.map +1 -1
  4. package/dist/src/property-panel/data-binding/data-binding-mapper.js +11 -3
  5. package/dist/src/property-panel/data-binding/data-binding-mapper.js.map +1 -1
  6. package/dist/src/property-panel/data-binding/data-binding-popup.d.ts +62 -0
  7. package/dist/src/property-panel/data-binding/data-binding-popup.js +1411 -0
  8. package/dist/src/property-panel/data-binding/data-binding-popup.js.map +1 -0
  9. package/dist/src/property-panel/data-binding/data-binding.d.ts +1 -0
  10. package/dist/src/property-panel/data-binding/data-binding.js +33 -2
  11. package/dist/src/property-panel/data-binding/data-binding.js.map +1 -1
  12. package/dist/src/property-panel/specifics/specifics.js +1 -1
  13. package/dist/src/property-panel/specifics/specifics.js.map +1 -1
  14. package/dist/src/property-panel/styles/styles.js +2 -0
  15. package/dist/src/property-panel/styles/styles.js.map +1 -1
  16. package/dist/src/property-panel/threed/property-scene3d.d.ts +7 -1
  17. package/dist/src/property-panel/threed/property-scene3d.js +176 -60
  18. package/dist/src/property-panel/threed/property-scene3d.js.map +1 -1
  19. package/dist/src/property-panel/threed/threed.js +8 -0
  20. package/dist/src/property-panel/threed/threed.js.map +1 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +6 -5
  23. package/translations/en.json +2 -0
  24. package/translations/ja.json +2 -0
  25. package/translations/ko.json +2 -0
  26. package/translations/ms.json +3 -1
  27. package/translations/zh.json +2 -0
@@ -0,0 +1,1411 @@
1
+ /**
2
+ * @license Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { __decorate } from "tslib";
5
+ import '@material/web/icon/icon.js';
6
+ import '@operato/input/ox-buttons-radio.js';
7
+ import '@operato/input/ox-input-code.js';
8
+ import '@operato/i18n/ox-i18n.js';
9
+ import '@operato/help/ox-help-icon.js';
10
+ import './data-binding-value-map.js';
11
+ import './data-binding-value-range.js';
12
+ import { css, html, LitElement, nothing } from 'lit';
13
+ import { customElement, property, state } from 'lit/decorators.js';
14
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements';
15
+ import { DataBindingMapper } from './data-binding-mapper.js';
16
+ function buildDataTree(data, prefix = '') {
17
+ if (data == null || typeof data !== 'object')
18
+ return [];
19
+ return Object.keys(data).map(key => {
20
+ const value = data[key];
21
+ const path = prefix ? `${prefix}.${key}` : key;
22
+ const type = Array.isArray(value) ? 'array' : typeof value;
23
+ return {
24
+ key,
25
+ path,
26
+ value,
27
+ type,
28
+ children: type === 'object' || type === 'array' ? buildDataTree(value, path) : undefined
29
+ };
30
+ });
31
+ }
32
+ function typeIcon(type) {
33
+ switch (type) {
34
+ case 'string':
35
+ return 'abc';
36
+ case 'number':
37
+ return 'tag';
38
+ case 'boolean':
39
+ return 'toggle_on';
40
+ case 'object':
41
+ return 'data_object';
42
+ case 'array':
43
+ return 'data_array';
44
+ default:
45
+ return 'help_outline';
46
+ }
47
+ }
48
+ function truncate(val, max = 24) {
49
+ if (val == null)
50
+ return 'null';
51
+ const s = typeof val === 'object' ? JSON.stringify(val) : String(val);
52
+ return s.length > max ? s.slice(0, max) + '…' : s;
53
+ }
54
+ let DataBindingPopup = class DataBindingPopup extends ScopedElementsMixin(LitElement) {
55
+ constructor() {
56
+ super(...arguments);
57
+ this.mappings = [];
58
+ this.properties = [];
59
+ this._viewMode = 'mappings';
60
+ this._selectedIndex = 0;
61
+ this._inspectorMode = 'source-select';
62
+ this._sourceTree = [];
63
+ this._expandedPaths = new Set();
64
+ this._previewInput = undefined;
65
+ this._previewOutput = undefined;
66
+ }
67
+ static get scopedElements() {
68
+ return {
69
+ 'data-binding-mapper': DataBindingMapper
70
+ };
71
+ }
72
+ get _selected() {
73
+ return this.selected;
74
+ }
75
+ get _root() {
76
+ var _a;
77
+ return (_a = this._selected) === null || _a === void 0 ? void 0 : _a.root;
78
+ }
79
+ get _currentMapping() {
80
+ return this._selectedIndex < this.mappings.length ? this.mappings[this._selectedIndex] : null;
81
+ }
82
+ connectedCallback() {
83
+ super.connectedCallback();
84
+ this._refreshPreview();
85
+ }
86
+ render() {
87
+ const mapping = this._currentMapping;
88
+ return html `
89
+ ${this._renderLeftPanel()}
90
+
91
+ ${this._viewMode === 'mappings'
92
+ ? html `
93
+ <div flow-editor>
94
+ ${mapping ? this._renderFlowCards(mapping) : html `<div class="empty-state">Select or add a mapping</div>`}
95
+ ${mapping ? this._renderRuleConfig(mapping) : nothing}
96
+ </div>
97
+ ${this._renderInspector(mapping)}
98
+ `
99
+ : html `
100
+ <div flow-view>
101
+ ${this._renderFlowOverview()}
102
+ </div>
103
+ `}
104
+ `;
105
+ }
106
+ /* ── Left Panel: Tab Header + Mapping List or Flow ── */
107
+ _renderLeftPanel() {
108
+ return html `
109
+ <div mapping-list>
110
+ <div tab-header>
111
+ <div ?active=${this._viewMode === 'mappings'} @click=${() => (this._viewMode = 'mappings')}>Mappings</div>
112
+ <div ?active=${this._viewMode === 'flow'} @click=${() => (this._viewMode = 'flow')}>Flow</div>
113
+ </div>
114
+ ${this._viewMode === 'mappings' ? this._renderMappingItems() : nothing}
115
+ </div>
116
+ `;
117
+ }
118
+ _renderMappingItems() {
119
+ return html `
120
+ <header>
121
+ <span>${this.mappings.length} mappings</span>
122
+ <md-icon @click=${() => this._addMapping()} title="Add mapping">add</md-icon>
123
+ </header>
124
+ <div mapping-items>
125
+ ${this.mappings.map((m, i) => html `
126
+ <div mapping-item ?selected=${i === this._selectedIndex} @click=${() => this._selectMapping(i)}>
127
+ <span class="status-dot ${this._mappingStatus(m)}"></span>
128
+ <span class="summary">${this._mappingSummary(m, i)}</span>
129
+ <md-icon
130
+ class="delete-btn"
131
+ @click=${(e) => {
132
+ e.stopPropagation();
133
+ this._deleteMapping(i);
134
+ }}
135
+ title="Delete"
136
+ >close</md-icon
137
+ >
138
+ </div>
139
+ `)}
140
+ </div>
141
+ `;
142
+ }
143
+ /* ── Column 2: Flow Cards ── */
144
+ _renderFlowCards(mapping) {
145
+ return html `
146
+ <div flow-cards>
147
+ <!-- Source Card -->
148
+ <div class="flow-card">
149
+ <div class="flow-card-header"><md-icon>input</md-icon> SOURCE</div>
150
+ <input
151
+ type="text"
152
+ placeholder="(self)"
153
+ .value=${mapping.source || ''}
154
+ @focus=${() => {
155
+ this._inspectorMode = 'source-select';
156
+ }}
157
+ @change=${(e) => this._updateMapping({ source: e.target.value })}
158
+ />
159
+ <input
160
+ id="accessor-input"
161
+ type="text"
162
+ placeholder="accessor"
163
+ .value=${mapping.accessor || ''}
164
+ @focus=${() => {
165
+ this._inspectorMode = 'source-data';
166
+ this._refreshSourceTree();
167
+ }}
168
+ @change=${(e) => this._updateMapping({ accessor: e.target.value })}
169
+ />
170
+ </div>
171
+
172
+
173
+ <!-- Arrow -->
174
+ <div class="flow-arrow">→</div>
175
+
176
+ <!-- Rule Card -->
177
+ <div class="flow-card" @click=${() => (this._inspectorMode = 'rule-help')}>
178
+ <div class="flow-card-header"><md-icon>tune</md-icon> RULE</div>
179
+ <div style="display:flex;flex-wrap:wrap;gap:4px">
180
+ ${['value', 'map', 'range', 'eval'].map(r => html `
181
+ <span
182
+ class="rule-badge"
183
+ ?active=${mapping.rule === r}
184
+ @click=${(e) => { e.stopPropagation(); this._changeRule(r); }}
185
+ >${r}</span
186
+ >
187
+ `)}
188
+ </div>
189
+ ${mapping.rule !== 'value' && mapping.param
190
+ ? html `<div style="font-size:10px;color:var(--md-sys-color-on-surface-variant,#49454f);margin-top:2px">${this._ruleSummary(mapping)}</div>`
191
+ : nothing}
192
+ </div>
193
+
194
+ <!-- Arrow -->
195
+ <div class="flow-arrow">→</div>
196
+
197
+ <!-- Target Card -->
198
+ <div class="flow-card">
199
+ <div class="flow-card-header"><md-icon>output</md-icon> TARGET</div>
200
+ <input
201
+ type="text"
202
+ placeholder="(self)"
203
+ .value=${mapping.target || ''}
204
+ @focus=${() => (this._inspectorMode = 'target-select')}
205
+ @change=${(e) => this._updateMapping({ target: e.target.value })}
206
+ />
207
+ <input
208
+ id="property-input"
209
+ type="text"
210
+ placeholder="property"
211
+ .value=${mapping.property || ''}
212
+ @focus=${() => (this._inspectorMode = 'target-prop')}
213
+ @change=${(e) => this._updateMapping({ property: e.target.value })}
214
+ />
215
+ </div>
216
+
217
+ </div>
218
+ `;
219
+ }
220
+ /* ── Rule Config ── */
221
+ _renderRuleConfig(mapping) {
222
+ return html `
223
+ <div rule-config @focusin=${() => (this._inspectorMode = 'rule-help')}>
224
+ ${mapping.rule === 'map'
225
+ ? html `
226
+ <data-binding-value-map
227
+ .value=${mapping.param || {}}
228
+ .valuetype=${'string'}
229
+ active
230
+ @change=${(e) => this._updateMapping({ param: e.target.value })}
231
+ ></data-binding-value-map>
232
+ `
233
+ : nothing}
234
+ ${mapping.rule === 'range'
235
+ ? html `
236
+ <data-binding-value-range
237
+ .value=${mapping.param || {}}
238
+ .valuetype=${'string'}
239
+ active
240
+ @change=${(e) => this._updateMapping({ param: e.target.value })}
241
+ ></data-binding-value-range>
242
+ `
243
+ : nothing}
244
+ ${mapping.rule === 'eval'
245
+ ? html `
246
+ <ox-input-code
247
+ .value=${mapping.param || ''}
248
+ language="javascript"
249
+ active
250
+ @change=${(e) => this._updateMapping({ param: e.target.value })}
251
+ ></ox-input-code>
252
+ `
253
+ : nothing}
254
+
255
+ <div class="options-row">
256
+ <label>
257
+ <input type="checkbox" .checked=${mapping.partial === true} @change=${(e) => this._updateMapping({ partial: e.target.checked })} />
258
+ Partial
259
+ </label>
260
+ <label>
261
+ <input type="checkbox" .checked=${mapping.ndnsp === true} @change=${(e) => this._updateMapping({ ndnsp: e.target.checked })} />
262
+ No Data No Spreading
263
+ </label>
264
+ </div>
265
+ </div>
266
+ `;
267
+ }
268
+ /* ── Column 3: Inspector ── */
269
+ _renderInspector(mapping) {
270
+ const modeLabel = {
271
+ 'source-select': 'Source Components',
272
+ 'source-data': 'Source Data',
273
+ 'target-select': 'Target Selector',
274
+ 'target-prop': 'Target Properties',
275
+ 'rule-help': 'Rule Guide'
276
+ };
277
+ const modeIcon = {
278
+ 'source-select': 'list',
279
+ 'source-data': 'input',
280
+ 'target-select': 'list',
281
+ 'target-prop': 'output',
282
+ 'rule-help': 'help_outline'
283
+ };
284
+ return html `
285
+ <div inspector>
286
+ <header>
287
+ <md-icon>${modeIcon[this._inspectorMode]}</md-icon>
288
+ ${modeLabel[this._inspectorMode]}
289
+ </header>
290
+
291
+ <div tree-content>
292
+ ${this._inspectorMode === 'source-select' ? this._renderComponentList('source') : nothing}
293
+ ${this._inspectorMode === 'source-data' ? this._renderSourceTree() : nothing}
294
+ ${this._inspectorMode === 'target-select' ? this._renderComponentList('target') : nothing}
295
+ ${this._inspectorMode === 'target-prop' ? this._renderTargetTree() : nothing}
296
+ ${this._inspectorMode === 'rule-help' ? this._renderRuleHelp() : nothing}
297
+ </div>
298
+
299
+ ${this._renderPreviewBar(mapping)}
300
+ </div>
301
+ `;
302
+ }
303
+ _renderComponentList(mode) {
304
+ var _a;
305
+ // root의 indexMap(Map)에서 직접 컴포넌트 ID 목록을 가져온다
306
+ let ids = [];
307
+ try {
308
+ const root = this._root;
309
+ if (root === null || root === void 0 ? void 0 : root.indexMap) {
310
+ for (const [id, components] of root.indexMap.entries()) {
311
+ const comp = Array.isArray(components) ? components[0] : components;
312
+ ids.push({ value: `#${id}`, description: ((_a = comp === null || comp === void 0 ? void 0 : comp.model) === null || _a === void 0 ? void 0 : _a.type) || '' });
313
+ }
314
+ ids.sort((a, b) => a.value.localeCompare(b.value));
315
+ }
316
+ }
317
+ catch (_b) {
318
+ /* ignore */
319
+ }
320
+ const selectHandler = (value) => {
321
+ if (mode === 'source') {
322
+ this._updateMapping({ source: value === '(self)' ? '' : value });
323
+ this._inspectorMode = 'source-data';
324
+ this._refreshSourceTree();
325
+ // accessor input에 자동 포커스
326
+ this.updateComplete.then(() => {
327
+ var _a;
328
+ (_a = this.renderRoot.querySelector('#accessor-input')) === null || _a === void 0 ? void 0 : _a.focus();
329
+ });
330
+ }
331
+ else {
332
+ this._updateMapping({ target: value });
333
+ this._inspectorMode = 'target-prop';
334
+ // property input에 자동 포커스
335
+ this.updateComplete.then(() => {
336
+ var _a;
337
+ (_a = this.renderRoot.querySelector('#property-input')) === null || _a === void 0 ? void 0 : _a.focus();
338
+ });
339
+ }
340
+ };
341
+ // target 전용 특수 셀렉터
342
+ const targetSpecials = [
343
+ { value: '(self)', icon: 'person', label: '(self)', desc: '자기 자신' },
344
+ { value: '(children)', icon: 'account_tree', label: '(children)', desc: '자식 컴포넌트' },
345
+ { value: '(key)', icon: 'key', label: '(key)', desc: '키 기반 매핑' },
346
+ { value: '[propkey]', icon: 'dynamic_feed', label: '[propkey]', desc: '속성-키 매핑' }
347
+ ];
348
+ const sourceSpecials = [{ value: '(self)', icon: 'person', label: '(self)', desc: '자기 자신' }];
349
+ const specials = mode === 'target' ? targetSpecials : sourceSpecials;
350
+ return html `
351
+ ${specials.map(s => html `
352
+ <div class="tree-node">
353
+ <div class="tree-row" @click=${() => selectHandler(s.value)}>
354
+ <md-icon style="--md-icon-size:14px">${s.icon}</md-icon>
355
+ <span class="tree-key">${s.label}</span>
356
+ <span class="tree-value">${s.desc}</span>
357
+ </div>
358
+ </div>
359
+ `)}
360
+ ${ids.length
361
+ ? html `
362
+ <div style="padding:6px 8px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--md-sys-color-on-surface-variant,#49454f);border-top:1px solid var(--md-sys-color-outline-variant,rgba(0,0,0,0.1));margin-top:4px">
363
+ Components
364
+ </div>
365
+ ${ids.map(({ value, description }) => html `
366
+ <div class="tree-node">
367
+ <div class="tree-row" @click=${() => selectHandler(value)}>
368
+ <md-icon style="--md-icon-size:14px">widgets</md-icon>
369
+ <span class="tree-key">${value}</span>
370
+ <span class="tree-value">${description}</span>
371
+ </div>
372
+ </div>
373
+ `)}
374
+ `
375
+ : nothing}
376
+ `;
377
+ }
378
+ _renderSourceTree() {
379
+ return html `
380
+ ${this._sourceTree.length > 0
381
+ ? this._renderTreeNodes(this._sourceTree, 'accessor')
382
+ : html `<div class="empty-state" style="font-size:12px;height:auto;padding:12px">No data available</div>`}
383
+ ${this._renderJsonataHelp()}
384
+ `;
385
+ }
386
+ _renderJsonataHelp() {
387
+ const examples = [
388
+ { syntax: 'name', desc: '단일 속성' },
389
+ { syntax: 'address.city', desc: '중첩 속성' },
390
+ { syntax: '$[0]', desc: '루트 배열 첫 번째' },
391
+ { syntax: '$[0].name', desc: '루트 배열 요소의 속성' },
392
+ { syntax: 'orders[0]', desc: '배열 첫 번째' },
393
+ { syntax: 'orders[-1]', desc: '배열 마지막' },
394
+ { syntax: 'orders[price>100]', desc: '필터' },
395
+ { syntax: 'orders.price', desc: '배열 내 속성 추출' },
396
+ { syntax: '$sum(orders.price)', desc: '합계' },
397
+ { syntax: '$count(items)', desc: '개수' },
398
+ { syntax: '$string(value)', desc: '문자열 변환' },
399
+ { syntax: '$number(text)', desc: '숫자 변환' },
400
+ { syntax: '$now()', desc: '현재 시각 (ISO)' },
401
+ { syntax: 'a ? a : b', desc: '조건식' }
402
+ ];
403
+ return html `
404
+ <div style="border-top:1px solid var(--md-sys-color-outline-variant,rgba(0,0,0,0.1));margin-top:8px;padding:8px">
405
+ <div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--md-sys-color-on-surface-variant,#49454f);margin-bottom:4px">
406
+ JSONata Syntax
407
+ <a href="https://docs.jsonata.org/overview" target="_blank" rel="noopener"
408
+ style="font-size:10px;font-weight:400;text-decoration:none;color:var(--md-sys-color-primary,#6750a4);margin-left:6px"
409
+ >docs ↗</a>
410
+ </div>
411
+ ${examples.map(ex => html `
412
+ <div style="display:flex;gap:6px;padding:2px 0;font-size:11px;cursor:pointer;border-radius:3px"
413
+ @click=${() => this._updateMapping({ accessor: ex.syntax })}>
414
+ <code style="color:var(--md-sys-color-primary,#6750a4);min-width:110px;font-size:11px">${ex.syntax}</code>
415
+ <span style="color:var(--md-sys-color-on-surface-variant,#49454f)">${ex.desc}</span>
416
+ </div>
417
+ `)}
418
+ </div>
419
+ `;
420
+ }
421
+ _renderTargetTree() {
422
+ var _a, _b, _c, _d, _e;
423
+ const nodes = this.properties.map(p => ({
424
+ key: p.name || p.label,
425
+ path: p.name || p.label,
426
+ value: null,
427
+ type: 'property'
428
+ }));
429
+ // GLTF 컴포넌트: nodes, animations 항상 표시
430
+ const isGltf = ((_b = (_a = this._selected) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.type) === 'gltf-object';
431
+ if (isGltf) {
432
+ // nodes (Mesh만 표시)
433
+ const allNodeNames = ((_c = this._selected) === null || _c === void 0 ? void 0 : _c.nodeNames) || [];
434
+ const realObject = (_d = this._selected) === null || _d === void 0 ? void 0 : _d.realObject;
435
+ const meshNames = realObject
436
+ ? allNodeNames.filter(name => { var _a, _b; return (_b = (_a = realObject.getNode) === null || _a === void 0 ? void 0 : _a.call(realObject, name)) === null || _b === void 0 ? void 0 : _b.isMesh; })
437
+ : allNodeNames;
438
+ const nodeChildren = meshNames.map(name => ({
439
+ key: name,
440
+ path: `nodes.${name}`,
441
+ value: null,
442
+ type: 'object',
443
+ children: ['color', 'visible', 'opacity', 'emissive', 'emissiveIntensity', 'scale'].map(prop => ({
444
+ key: prop,
445
+ path: `nodes.${name}.${prop}`,
446
+ value: null,
447
+ type: 'property'
448
+ }))
449
+ }));
450
+ nodes.push({
451
+ key: 'nodes',
452
+ path: 'nodes',
453
+ value: null,
454
+ type: 'object',
455
+ children: nodeChildren
456
+ });
457
+ // animations (애니메이션 제어)
458
+ const animNames = ((_e = this._selected) === null || _e === void 0 ? void 0 : _e.animationNames) || [];
459
+ const animChildren = animNames.map(name => ({
460
+ key: name,
461
+ path: `animations.${name}`,
462
+ value: null,
463
+ type: 'object',
464
+ children: ['play', 'speed', 'weight', 'loop'].map(prop => ({
465
+ key: prop,
466
+ path: `animations.${name}.${prop}`,
467
+ value: null,
468
+ type: 'property'
469
+ }))
470
+ }));
471
+ nodes.push({
472
+ key: 'animations',
473
+ path: 'animations',
474
+ value: null,
475
+ type: 'object',
476
+ children: animChildren
477
+ });
478
+ }
479
+ return html `${this._renderTreeNodes(nodes, 'property')}`;
480
+ }
481
+ _renderTreeNodes(nodes, targetField) {
482
+ var _a, _b;
483
+ const currentPath = targetField === 'accessor'
484
+ ? (_a = this._currentMapping) === null || _a === void 0 ? void 0 : _a.accessor
485
+ : (_b = this._currentMapping) === null || _b === void 0 ? void 0 : _b.property;
486
+ return nodes.map(node => html `
487
+ <div class="tree-node">
488
+ <div
489
+ class="tree-row"
490
+ ?selected=${currentPath === node.path}
491
+ @click=${() => {
492
+ if (node.children) {
493
+ const newSet = new Set(this._expandedPaths);
494
+ newSet.has(node.path) ? newSet.delete(node.path) : newSet.add(node.path);
495
+ this._expandedPaths = newSet;
496
+ }
497
+ else {
498
+ this._selectTreePath(node.path, targetField);
499
+ }
500
+ }}
501
+ >
502
+ ${node.children
503
+ ? html `<md-icon>${this._expandedPaths.has(node.path) ? 'expand_more' : 'chevron_right'}</md-icon>`
504
+ : html `<md-icon style="--md-icon-size:14px">${typeIcon(node.type)}</md-icon>`}
505
+ <span class="tree-key">${node.key}</span>
506
+ ${node.value != null && !node.children ? html `<span class="tree-value">${truncate(node.value)}</span>` : nothing}
507
+ </div>
508
+ ${node.children && this._expandedPaths.has(node.path)
509
+ ? html `<div class="tree-children">
510
+ ${node.children.length > 0
511
+ ? this._renderTreeNodes(node.children, targetField)
512
+ : html `<div style="padding:4px 8px;font-size:11px;color:var(--md-sys-color-on-surface-variant,#49454f)">(empty)</div>`}
513
+ </div>`
514
+ : nothing}
515
+ </div>
516
+ `);
517
+ }
518
+ _renderPreviewBar(mapping) {
519
+ if (!mapping)
520
+ return nothing;
521
+ this._refreshPreview();
522
+ return html `
523
+ <div preview-section>
524
+ <div class="preview-label">Preview</div>
525
+ ${this._previewInput !== undefined
526
+ ? html `
527
+ <div class="preview-row">
528
+ <md-icon style="--md-icon-size:12px;color:var(--md-sys-color-tertiary,#7d5260)">input</md-icon>
529
+ <span class="preview-input">${truncate(this._previewInput, 32)}</span>
530
+ <span class="flow-arrow" style="font-size:12px;padding:0 4px">→</span>
531
+ <span class="preview-output">${truncate(this._previewOutput, 32)}</span>
532
+ </div>
533
+ `
534
+ : html `<div style="font-size:11px;color:var(--md-sys-color-on-surface-variant,#49454f)">No data</div>`}
535
+ </div>
536
+ `;
537
+ }
538
+ /* ── Actions ── */
539
+ _selectMapping(index) {
540
+ this._selectedIndex = index;
541
+ this._inspectorMode = 'source-select';
542
+ }
543
+ _addMapping() {
544
+ this.mappings = [...this.mappings, { rule: 'value' }];
545
+ this._selectedIndex = this.mappings.length - 1;
546
+ this._dispatchChange();
547
+ }
548
+ _deleteMapping(index) {
549
+ this.mappings = this.mappings.filter((_, i) => i !== index);
550
+ if (this._selectedIndex >= this.mappings.length) {
551
+ this._selectedIndex = Math.max(0, this.mappings.length - 1);
552
+ }
553
+ this._dispatchChange();
554
+ }
555
+ _updateMapping(patch) {
556
+ const mapping = this._currentMapping;
557
+ if (!mapping)
558
+ return;
559
+ const updated = { ...mapping, ...patch };
560
+ const mappings = [...this.mappings];
561
+ mappings[this._selectedIndex] = updated;
562
+ this.mappings = mappings;
563
+ this._dispatchChange();
564
+ this._refreshPreview();
565
+ }
566
+ _changeRule(rule) {
567
+ const mapping = this._currentMapping;
568
+ if (!mapping)
569
+ return;
570
+ // 기존 param 보존
571
+ this._updateMapping({ rule, param: rule === mapping.rule ? mapping.param : undefined });
572
+ }
573
+ _selectTreePath(path, targetField) {
574
+ if (targetField === 'accessor') {
575
+ this._updateMapping({ accessor: path });
576
+ }
577
+ else {
578
+ this._updateMapping({ property: path });
579
+ }
580
+ }
581
+ _findComponent(idOrSelector) {
582
+ var _a;
583
+ if (!idOrSelector || idOrSelector === '(self)')
584
+ return this._selected;
585
+ const id = idOrSelector.startsWith('#') ? idOrSelector.substring(1) : idOrSelector;
586
+ return (_a = this._root) === null || _a === void 0 ? void 0 : _a.findById(id);
587
+ }
588
+ _refreshSourceTree() {
589
+ const mapping = this._currentMapping;
590
+ if (!mapping) {
591
+ this._sourceTree = [];
592
+ return;
593
+ }
594
+ const sourceComponent = this._findComponent(mapping.source || '');
595
+ const data = sourceComponent === null || sourceComponent === void 0 ? void 0 : sourceComponent.data;
596
+ this._sourceTree = data ? buildDataTree(data) : [];
597
+ }
598
+ _refreshPreview() {
599
+ var _a, _b;
600
+ const mapping = this._currentMapping;
601
+ if (!mapping) {
602
+ this._previewInput = undefined;
603
+ this._previewOutput = undefined;
604
+ return;
605
+ }
606
+ try {
607
+ const sourceComponent = this._findComponent(mapping.source || '');
608
+ const data = sourceComponent === null || sourceComponent === void 0 ? void 0 : sourceComponent.data;
609
+ if (data == null) {
610
+ this._previewInput = undefined;
611
+ this._previewOutput = undefined;
612
+ return;
613
+ }
614
+ // Simple accessor extraction
615
+ let input = data;
616
+ if (mapping.accessor) {
617
+ const keys = mapping.accessor.split('.');
618
+ for (const k of keys) {
619
+ if (input == null)
620
+ break;
621
+ input = input[k];
622
+ }
623
+ }
624
+ this._previewInput = input;
625
+ // Simple rule preview (no full evaluator import for now)
626
+ if (mapping.rule === 'value') {
627
+ this._previewOutput = input;
628
+ }
629
+ else if (mapping.rule === 'map' && mapping.param) {
630
+ this._previewOutput = (_b = (_a = mapping.param[String(input)]) !== null && _a !== void 0 ? _a : mapping.param['default']) !== null && _b !== void 0 ? _b : input;
631
+ }
632
+ else if (mapping.rule === 'range' && mapping.param) {
633
+ this._previewOutput = undefined;
634
+ for (const [range, val] of Object.entries(mapping.param)) {
635
+ if (range === 'default')
636
+ continue;
637
+ const [from, to] = range.split('~').map(Number);
638
+ const num = Number(input);
639
+ if (!isNaN(num) && num >= from && (isNaN(to) || to === 0 || num < to)) {
640
+ this._previewOutput = val;
641
+ break;
642
+ }
643
+ }
644
+ if (this._previewOutput === undefined) {
645
+ this._previewOutput = mapping.param['default'];
646
+ }
647
+ }
648
+ else {
649
+ this._previewOutput = '(eval)';
650
+ }
651
+ }
652
+ catch (_c) {
653
+ this._previewInput = undefined;
654
+ this._previewOutput = '(error)';
655
+ }
656
+ }
657
+ _renderRuleHelp() {
658
+ var _a;
659
+ const rule = ((_a = this._currentMapping) === null || _a === void 0 ? void 0 : _a.rule) || 'value';
660
+ const helpContent = {
661
+ value: {
662
+ title: 'Value (Pass-through)',
663
+ desc: '소스 데이터를 그대로 타겟 속성에 전달합니다. 타입 변환 없이 원본 값이 설정됩니다.',
664
+ examples: [
665
+ { code: 'source.data.name → target.text', desc: '텍스트 전달' },
666
+ { code: 'source.data → target.data', desc: '객체 전체 전달' },
667
+ { code: 'source.data.active → target.hidden', desc: '불리언 전달' }
668
+ ]
669
+ },
670
+ map: {
671
+ title: 'Map (Key-Value Mapping)',
672
+ desc: '입력 값을 키로 사용하여 미리 정의된 값으로 변환합니다. 일치하는 키가 없으면 default 값을 사용합니다.',
673
+ examples: [
674
+ { code: 'active → "green"', desc: '상태값 → 색상' },
675
+ { code: 'true → "운전중"', desc: '불리언 → 텍스트' },
676
+ { code: 'default → "gray"', desc: '미매칭 시 기본값' }
677
+ ]
678
+ },
679
+ range: {
680
+ title: 'Range (범위 매핑)',
681
+ desc: '숫자 입력값이 어떤 범위에 속하는지에 따라 값을 변환합니다. from~to 형식으로 범위를 지정합니다 (from 이상, to 미만).',
682
+ examples: [
683
+ { code: '0~50 → "blue"', desc: '0 이상 50 미만' },
684
+ { code: '50~80 → "green"', desc: '50 이상 80 미만' },
685
+ { code: '80~ → "red"', desc: '80 이상 (상한 없음)' },
686
+ { code: 'default → "gray"', desc: '범위 밖 기본값' }
687
+ ]
688
+ },
689
+ eval: {
690
+ title: 'Eval (JavaScript)',
691
+ desc: 'JavaScript 코드로 값을 변환합니다. value 변수로 입력값에 접근하고, return으로 결과를 반환합니다. targets[0]으로 타겟 컴포넌트에 접근 가능합니다.',
692
+ examples: [
693
+ { code: 'return value * 100', desc: '숫자 변환' },
694
+ { code: 'return value > 80 ? "red" : "green"', desc: '조건 분기' },
695
+ { code: 'return `${value}%`', desc: '문자열 포맷팅' },
696
+ { code: 'return Math.round(value * 10) / 10', desc: '소수점 반올림' },
697
+ { code: 'return targets[0].get("text")', desc: '타겟 속성 참조' }
698
+ ]
699
+ }
700
+ };
701
+ const help = helpContent[rule];
702
+ return html `
703
+ <div style="padding:8px">
704
+ <div style="font-size:13px;font-weight:600;color:var(--md-sys-color-primary,#6750a4);margin-bottom:6px">
705
+ ${help.title}
706
+ </div>
707
+ <div style="font-size:12px;color:var(--md-sys-color-on-surface,#1c1b1f);line-height:1.5;margin-bottom:10px">
708
+ ${help.desc}
709
+ </div>
710
+ <div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--md-sys-color-on-surface-variant,#49454f);margin-bottom:4px">
711
+ Examples
712
+ </div>
713
+ ${help.examples.map(ex => html `
714
+ <div style="padding:3px 0;font-size:11px;display:flex;gap:6px">
715
+ <code style="color:var(--md-sys-color-primary,#6750a4);min-width:50%;font-size:11px">${ex.code}</code>
716
+ <span style="color:var(--md-sys-color-on-surface-variant,#49454f)">${ex.desc}</span>
717
+ </div>
718
+ `)}
719
+ </div>
720
+ `;
721
+ }
722
+ /* ── Flow Overview ── */
723
+ _renderFlowOverview() {
724
+ const root = this._root;
725
+ if (!root)
726
+ return html `<div class="empty-state">No scene</div>`;
727
+ const bindings = [];
728
+ const allIds = new Set();
729
+ const sourcesUsed = new Set();
730
+ // 모든 컴포넌트 순회
731
+ const traverse = (component) => {
732
+ var _a, _b, _c;
733
+ const id = (_a = component.model) === null || _a === void 0 ? void 0 : _a.id;
734
+ if (id)
735
+ allIds.add(id);
736
+ const mappings = (_b = component.model) === null || _b === void 0 ? void 0 : _b.mappings;
737
+ if (mappings && Array.isArray(mappings)) {
738
+ for (const m of mappings) {
739
+ if (!m || !m.target || !m.property)
740
+ continue;
741
+ const source = m.source || '(self)';
742
+ const sourceKey = source === '(self)' ? (id || '(self)') : source.replace(/^#/, '');
743
+ sourcesUsed.add(sourceKey);
744
+ bindings.push({
745
+ ownerID: id || '(anonymous)',
746
+ ownerType: ((_c = component.model) === null || _c === void 0 ? void 0 : _c.type) || '',
747
+ source: sourceKey,
748
+ accessor: m.accessor || '',
749
+ target: m.target,
750
+ property: m.property,
751
+ rule: m.rule || 'value'
752
+ });
753
+ }
754
+ }
755
+ if (component.components) {
756
+ for (const child of component.components) {
757
+ traverse(child);
758
+ }
759
+ }
760
+ };
761
+ traverse(root);
762
+ // 소스별 그룹핑
763
+ const bySource = new Map();
764
+ for (const b of bindings) {
765
+ const key = b.source;
766
+ if (!bySource.has(key))
767
+ bySource.set(key, []);
768
+ bySource.get(key).push(b);
769
+ }
770
+ // 소스별 기대 스키마 추출
771
+ const schemaBySource = new Map();
772
+ for (const [source, bs] of bySource.entries()) {
773
+ const fields = new Map();
774
+ for (const b of bs) {
775
+ if (!b.accessor)
776
+ continue;
777
+ const usages = fields.get(b.accessor) || [];
778
+ usages.push(`${b.target}.${b.property}`);
779
+ fields.set(b.accessor, usages);
780
+ }
781
+ if (fields.size > 0)
782
+ schemaBySource.set(source, fields);
783
+ }
784
+ // 바인딩 없는 ID 컴포넌트
785
+ const unboundIds = [...allIds].filter(id => {
786
+ const isSource = sourcesUsed.has(id);
787
+ const isTarget = bindings.some(b => b.target === `#${id}` || b.ownerID === id);
788
+ return !isSource && !isTarget;
789
+ });
790
+ return html `
791
+ ${bySource.size === 0
792
+ ? html `<div class="empty-state" style="height:auto;padding:20px">No data bindings in this board</div>`
793
+ : nothing}
794
+
795
+ ${[...bySource.entries()].map(([source, bs]) => html `
796
+ <div class="flow-source-group">
797
+ <div class="flow-source-header">
798
+ <md-icon>sensors</md-icon>
799
+ ${source}
800
+ </div>
801
+
802
+ ${bs.map(b => html `
803
+ <div class="flow-binding-row">
804
+ <span class="flow-accessor">${b.accessor || '(all)'}</span>
805
+ <span style="color:var(--md-sys-color-outline,#79747e)">→</span>
806
+ <span class="flow-target">${b.target}</span>
807
+ <span class="flow-property">.${b.property}</span>
808
+ <span class="flow-rule-tag">${b.rule}</span>
809
+ </div>
810
+ `)}
811
+
812
+ ${schemaBySource.has(source)
813
+ ? html `
814
+ <div style="padding:4px 12px 2px 32px;font-size:10px;font-weight:600;color:var(--md-sys-color-on-surface-variant,#49454f)">
815
+ Expected Fields
816
+ </div>
817
+ ${[...schemaBySource.get(source).entries()].map(([field, usages]) => html `
818
+ <div class="flow-schema-field">
819
+ <code>${field}</code>
820
+ <span class="usages">(${usages.length} usage${usages.length > 1 ? 's' : ''})</span>
821
+ </div>
822
+ `)}
823
+ `
824
+ : nothing}
825
+ </div>
826
+ `)}
827
+
828
+ ${unboundIds.length > 0
829
+ ? html `
830
+ <div class="flow-section-label">Unbound Components</div>
831
+ ${unboundIds.map(id => html `
832
+ <div class="flow-issue">
833
+ <md-icon>info</md-icon>
834
+ <span>#${id} — no bindings</span>
835
+ </div>
836
+ `)}
837
+ `
838
+ : nothing}
839
+ `;
840
+ }
841
+ _ruleSummary(mapping) {
842
+ if (mapping.rule === 'map' && mapping.param) {
843
+ const keys = Object.keys(mapping.param);
844
+ return `${keys.length} entries`;
845
+ }
846
+ if (mapping.rule === 'range' && mapping.param) {
847
+ const keys = Object.keys(mapping.param).filter(k => k !== 'default');
848
+ return `${keys.length} ranges`;
849
+ }
850
+ if (mapping.rule === 'eval' && mapping.param) {
851
+ return `${String(mapping.param).length} chars`;
852
+ }
853
+ return '';
854
+ }
855
+ _mappingStatus(m) {
856
+ if (!m || (!m.target && !m.property))
857
+ return 'empty';
858
+ if (m.target && m.property && m.rule)
859
+ return 'complete';
860
+ return 'partial';
861
+ }
862
+ _mappingSummary(m, i) {
863
+ if (!m || (!m.target && !m.property))
864
+ return `#${i + 1} (empty)`;
865
+ const parts = [];
866
+ if (m.accessor)
867
+ parts.push(m.accessor);
868
+ parts.push('→');
869
+ if (m.property)
870
+ parts.push(m.property);
871
+ return parts.join(' ');
872
+ }
873
+ _dispatchChange() {
874
+ this.dispatchEvent(new CustomEvent('mappings-change', {
875
+ bubbles: true,
876
+ composed: true,
877
+ detail: { mappings: this.mappings.filter(m => m && m.property) }
878
+ }));
879
+ }
880
+ };
881
+ DataBindingPopup.styles = [
882
+ css `
883
+ :host {
884
+ display: flex;
885
+ width: 1000px;
886
+ height: 600px;
887
+ overflow: hidden;
888
+ font-family: 'Roboto', Arial, sans-serif;
889
+ font-size: 13px;
890
+ color: var(--md-sys-color-on-surface, #1c1b1f);
891
+ }
892
+
893
+ /* ── Column 1: Mapping List ── */
894
+ [mapping-list] {
895
+ width: 170px;
896
+ min-width: 170px;
897
+ border-right: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
898
+ display: flex;
899
+ flex-direction: column;
900
+ background: var(--md-sys-color-surface-container-low, #f7f2fa);
901
+ }
902
+
903
+ [mapping-list] header {
904
+ display: flex;
905
+ align-items: center;
906
+ justify-content: space-between;
907
+ padding: 10px 12px;
908
+ font-weight: 600;
909
+ font-size: 12px;
910
+ text-transform: uppercase;
911
+ letter-spacing: 0.5px;
912
+ color: var(--md-sys-color-on-surface-variant, #49454f);
913
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
914
+ }
915
+
916
+ [mapping-list] header md-icon {
917
+ cursor: pointer;
918
+ --md-icon-size: 18px;
919
+ border-radius: 50%;
920
+ padding: 2px;
921
+ }
922
+
923
+ [mapping-list] header md-icon:hover {
924
+ background: var(--md-sys-color-primary-container, rgba(0, 100, 200, 0.12));
925
+ }
926
+
927
+ [mapping-items] {
928
+ flex: 1;
929
+ overflow-y: auto;
930
+ }
931
+
932
+ [mapping-item] {
933
+ padding: 8px 12px;
934
+ cursor: pointer;
935
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.06));
936
+ font-size: 11px;
937
+ display: flex;
938
+ align-items: center;
939
+ gap: 6px;
940
+ transition: background 0.15s;
941
+ }
942
+
943
+ [mapping-item]:hover {
944
+ background: var(--md-sys-color-surface-container-highest, rgba(0, 0, 0, 0.08));
945
+ }
946
+
947
+ [mapping-item][selected] {
948
+ background: var(--md-sys-color-secondary-container, rgba(0, 100, 200, 0.15));
949
+ color: var(--md-sys-color-on-secondary-container, #1d192b);
950
+ }
951
+
952
+ .status-dot {
953
+ width: 6px;
954
+ height: 6px;
955
+ border-radius: 50%;
956
+ flex-shrink: 0;
957
+ }
958
+
959
+ .status-dot.complete {
960
+ background: var(--md-sys-color-primary, #6750a4);
961
+ }
962
+
963
+ .status-dot.partial {
964
+ background: var(--md-sys-color-tertiary, #7d5260);
965
+ }
966
+
967
+ .status-dot.empty {
968
+ background: var(--md-sys-color-outline, #79747e);
969
+ }
970
+
971
+ .summary {
972
+ overflow: hidden;
973
+ text-overflow: ellipsis;
974
+ white-space: nowrap;
975
+ flex: 1;
976
+ }
977
+
978
+ [mapping-item] md-icon.delete-btn {
979
+ --md-icon-size: 14px;
980
+ opacity: 0;
981
+ transition: opacity 0.15s;
982
+ cursor: pointer;
983
+ }
984
+
985
+ [mapping-item]:hover md-icon.delete-btn {
986
+ opacity: 0.6;
987
+ }
988
+
989
+ [mapping-item] md-icon.delete-btn:hover {
990
+ opacity: 1;
991
+ }
992
+
993
+ /* ── Column 2: Flow Editor ── */
994
+ [flow-editor] {
995
+ flex: 1;
996
+ display: flex;
997
+ flex-direction: column;
998
+ overflow: hidden;
999
+ }
1000
+
1001
+ [flow-cards] {
1002
+ display: flex;
1003
+ align-items: stretch;
1004
+ gap: 0;
1005
+ padding: 16px;
1006
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
1007
+ }
1008
+
1009
+ .flow-card {
1010
+ flex: 1;
1011
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.15));
1012
+ border-radius: 8px;
1013
+ padding: 10px;
1014
+ background: var(--md-sys-color-surface-container, #f3edf7);
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ gap: 6px;
1018
+ }
1019
+
1020
+ .flow-card-header {
1021
+ font-size: 10px;
1022
+ font-weight: 600;
1023
+ text-transform: uppercase;
1024
+ letter-spacing: 0.8px;
1025
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1026
+ display: flex;
1027
+ align-items: center;
1028
+ gap: 4px;
1029
+ }
1030
+
1031
+ .flow-card-header md-icon {
1032
+ --md-icon-size: 14px;
1033
+ }
1034
+
1035
+ .flow-arrow {
1036
+ display: flex;
1037
+ align-items: center;
1038
+ padding: 0 6px;
1039
+ color: var(--md-sys-color-primary, #6750a4);
1040
+ font-size: 18px;
1041
+ flex-shrink: 0;
1042
+ }
1043
+
1044
+ .flow-card input,
1045
+ .flow-card select {
1046
+ width: 100%;
1047
+ box-sizing: border-box;
1048
+ padding: 4px 6px;
1049
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.2));
1050
+ border-radius: 4px;
1051
+ font-size: 12px;
1052
+ background: var(--md-sys-color-surface, #fffbfe);
1053
+ color: var(--md-sys-color-on-surface, #1c1b1f);
1054
+ outline: none;
1055
+ }
1056
+
1057
+ .flow-card input:focus {
1058
+ border-color: var(--md-sys-color-primary, #6750a4);
1059
+ box-shadow: 0 0 0 1px var(--md-sys-color-primary, #6750a4);
1060
+ }
1061
+
1062
+ .rule-badge {
1063
+ display: inline-flex;
1064
+ align-items: center;
1065
+ gap: 3px;
1066
+ padding: 2px 8px;
1067
+ border-radius: 12px;
1068
+ font-size: 11px;
1069
+ font-weight: 500;
1070
+ cursor: pointer;
1071
+ transition: background 0.15s;
1072
+ }
1073
+
1074
+ .rule-badge[active] {
1075
+ background: var(--md-sys-color-primary-container, #eaddff);
1076
+ color: var(--md-sys-color-on-primary-container, #21005d);
1077
+ }
1078
+
1079
+ .rule-badge:not([active]) {
1080
+ background: var(--md-sys-color-surface-container-highest, #e6e0e9);
1081
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1082
+ }
1083
+
1084
+ .rule-badge:hover {
1085
+ filter: brightness(0.95);
1086
+ }
1087
+
1088
+ /* ── Rule Config Area ── */
1089
+ [rule-config] {
1090
+ flex: 1;
1091
+ overflow-y: auto;
1092
+ padding: 12px 16px;
1093
+ display: flex;
1094
+ flex-direction: column;
1095
+ }
1096
+
1097
+ [rule-config] ox-input-code {
1098
+ flex: 1;
1099
+ min-height: 100px;
1100
+ }
1101
+
1102
+ [rule-config] .options-row {
1103
+ display: flex;
1104
+ gap: 16px;
1105
+ margin-top: 8px;
1106
+ font-size: 12px;
1107
+ align-items: center;
1108
+ }
1109
+
1110
+ [rule-config] .options-row label {
1111
+ display: flex;
1112
+ align-items: center;
1113
+ gap: 4px;
1114
+ cursor: pointer;
1115
+ }
1116
+
1117
+ /* ── Column 3: Inspector ── */
1118
+ [inspector] {
1119
+ width: 260px;
1120
+ min-width: 260px;
1121
+ border-left: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
1122
+ display: flex;
1123
+ flex-direction: column;
1124
+ background: var(--md-sys-color-surface-container-low, #f7f2fa);
1125
+ }
1126
+
1127
+ [inspector] header {
1128
+ display: flex;
1129
+ align-items: center;
1130
+ gap: 6px;
1131
+ padding: 10px 12px;
1132
+ font-weight: 600;
1133
+ font-size: 12px;
1134
+ text-transform: uppercase;
1135
+ letter-spacing: 0.5px;
1136
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1137
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
1138
+ }
1139
+
1140
+ [inspector] header md-icon {
1141
+ --md-icon-size: 16px;
1142
+ }
1143
+
1144
+ [tree-content] {
1145
+ flex: 1;
1146
+ overflow-y: auto;
1147
+ padding: 4px 0;
1148
+ }
1149
+
1150
+ .tree-node {
1151
+ cursor: pointer;
1152
+ user-select: none;
1153
+ }
1154
+
1155
+ .tree-row {
1156
+ display: flex;
1157
+ align-items: center;
1158
+ gap: 2px;
1159
+ padding: 3px 8px;
1160
+ transition: background 0.1s;
1161
+ }
1162
+
1163
+ .tree-row:hover {
1164
+ background: var(--md-sys-color-surface-container-highest, rgba(0, 0, 0, 0.06));
1165
+ }
1166
+
1167
+ .tree-row[selected] {
1168
+ background: var(--md-sys-color-secondary-container, rgba(0, 100, 200, 0.15));
1169
+ font-weight: 600;
1170
+ }
1171
+
1172
+ .tree-row md-icon {
1173
+ --md-icon-size: 14px;
1174
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1175
+ }
1176
+
1177
+ .tree-key {
1178
+ font-weight: 500;
1179
+ font-size: 12px;
1180
+ color: var(--md-sys-color-on-surface, #1c1b1f);
1181
+ }
1182
+
1183
+ .tree-value {
1184
+ font-size: 11px;
1185
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1186
+ margin-left: 4px;
1187
+ overflow: hidden;
1188
+ text-overflow: ellipsis;
1189
+ white-space: nowrap;
1190
+ }
1191
+
1192
+ .tree-children {
1193
+ padding-left: 16px;
1194
+ }
1195
+
1196
+ /* ── Preview ── */
1197
+ [preview-section] {
1198
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
1199
+ padding: 10px 12px;
1200
+ }
1201
+
1202
+ .preview-label {
1203
+ font-size: 10px;
1204
+ font-weight: 600;
1205
+ text-transform: uppercase;
1206
+ letter-spacing: 0.5px;
1207
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1208
+ margin-bottom: 6px;
1209
+ }
1210
+
1211
+ .preview-row {
1212
+ display: flex;
1213
+ align-items: center;
1214
+ gap: 6px;
1215
+ font-size: 12px;
1216
+ padding: 2px 0;
1217
+ }
1218
+
1219
+ .preview-input {
1220
+ color: var(--md-sys-color-tertiary, #7d5260);
1221
+ font-family: monospace;
1222
+ }
1223
+
1224
+ .preview-output {
1225
+ color: var(--md-sys-color-primary, #6750a4);
1226
+ font-family: monospace;
1227
+ font-weight: 600;
1228
+ }
1229
+
1230
+ /* ── Tab Header ── */
1231
+ [tab-header] {
1232
+ display: flex;
1233
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
1234
+ }
1235
+
1236
+ [tab-header] > div {
1237
+ flex: 1;
1238
+ text-align: center;
1239
+ padding: 8px 0;
1240
+ font-size: 11px;
1241
+ font-weight: 600;
1242
+ text-transform: uppercase;
1243
+ letter-spacing: 0.5px;
1244
+ cursor: pointer;
1245
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1246
+ transition: all 0.15s;
1247
+ }
1248
+
1249
+ [tab-header] > div[active] {
1250
+ color: var(--md-sys-color-primary, #6750a4);
1251
+ border-bottom: 2px solid var(--md-sys-color-primary, #6750a4);
1252
+ }
1253
+
1254
+ [tab-header] > div:not([active]):hover {
1255
+ background: var(--md-sys-color-surface-container-highest, rgba(0, 0, 0, 0.05));
1256
+ }
1257
+
1258
+ /* ── Flow View ── */
1259
+ [flow-view] {
1260
+ flex: 1;
1261
+ overflow-y: auto;
1262
+ padding: 8px 0;
1263
+ }
1264
+
1265
+ .flow-source-group {
1266
+ margin-bottom: 12px;
1267
+ }
1268
+
1269
+ .flow-source-header {
1270
+ display: flex;
1271
+ align-items: center;
1272
+ gap: 6px;
1273
+ padding: 6px 12px;
1274
+ font-size: 12px;
1275
+ font-weight: 600;
1276
+ color: var(--md-sys-color-primary, #6750a4);
1277
+ }
1278
+
1279
+ .flow-source-header md-icon {
1280
+ --md-icon-size: 16px;
1281
+ }
1282
+
1283
+ .flow-binding-row {
1284
+ display: flex;
1285
+ align-items: center;
1286
+ gap: 4px;
1287
+ padding: 3px 12px 3px 32px;
1288
+ font-size: 11px;
1289
+ cursor: pointer;
1290
+ transition: background 0.1s;
1291
+ }
1292
+
1293
+ .flow-binding-row:hover {
1294
+ background: var(--md-sys-color-surface-container-highest, rgba(0, 0, 0, 0.06));
1295
+ }
1296
+
1297
+ .flow-accessor {
1298
+ color: var(--md-sys-color-tertiary, #7d5260);
1299
+ font-family: monospace;
1300
+ min-width: 80px;
1301
+ }
1302
+
1303
+ .flow-target {
1304
+ color: var(--md-sys-color-on-surface, #1c1b1f);
1305
+ font-weight: 500;
1306
+ }
1307
+
1308
+ .flow-property {
1309
+ color: var(--md-sys-color-primary, #6750a4);
1310
+ font-family: monospace;
1311
+ }
1312
+
1313
+ .flow-rule-tag {
1314
+ font-size: 9px;
1315
+ padding: 1px 5px;
1316
+ border-radius: 8px;
1317
+ background: var(--md-sys-color-surface-container-highest, #e6e0e9);
1318
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1319
+ font-weight: 500;
1320
+ text-transform: uppercase;
1321
+ }
1322
+
1323
+ .flow-section-label {
1324
+ padding: 8px 12px 4px;
1325
+ font-size: 10px;
1326
+ font-weight: 600;
1327
+ text-transform: uppercase;
1328
+ letter-spacing: 0.5px;
1329
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1330
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.1));
1331
+ margin-top: 4px;
1332
+ }
1333
+
1334
+ .flow-issue {
1335
+ display: flex;
1336
+ align-items: center;
1337
+ gap: 6px;
1338
+ padding: 3px 12px;
1339
+ font-size: 11px;
1340
+ color: var(--md-sys-color-error, #b3261e);
1341
+ }
1342
+
1343
+ .flow-issue md-icon {
1344
+ --md-icon-size: 14px;
1345
+ color: var(--md-sys-color-error, #b3261e);
1346
+ }
1347
+
1348
+ .flow-schema-field {
1349
+ padding: 2px 12px 2px 32px;
1350
+ font-size: 11px;
1351
+ display: flex;
1352
+ align-items: center;
1353
+ gap: 4px;
1354
+ }
1355
+
1356
+ .flow-schema-field code {
1357
+ color: var(--md-sys-color-tertiary, #7d5260);
1358
+ font-size: 11px;
1359
+ }
1360
+
1361
+ .flow-schema-field .usages {
1362
+ color: var(--md-sys-color-on-surface-variant, #49454f);
1363
+ font-size: 10px;
1364
+ }
1365
+
1366
+ /* ── Empty state ── */
1367
+ .empty-state {
1368
+ display: flex;
1369
+ align-items: center;
1370
+ justify-content: center;
1371
+ height: 100%;
1372
+ color: var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, 0.4));
1373
+ font-size: 13px;
1374
+ }
1375
+ `
1376
+ ];
1377
+ __decorate([
1378
+ property({ type: Array })
1379
+ ], DataBindingPopup.prototype, "mappings", void 0);
1380
+ __decorate([
1381
+ property({ type: Array })
1382
+ ], DataBindingPopup.prototype, "properties", void 0);
1383
+ __decorate([
1384
+ property({ type: Object })
1385
+ ], DataBindingPopup.prototype, "selected", void 0);
1386
+ __decorate([
1387
+ state()
1388
+ ], DataBindingPopup.prototype, "_viewMode", void 0);
1389
+ __decorate([
1390
+ state()
1391
+ ], DataBindingPopup.prototype, "_selectedIndex", void 0);
1392
+ __decorate([
1393
+ state()
1394
+ ], DataBindingPopup.prototype, "_inspectorMode", void 0);
1395
+ __decorate([
1396
+ state()
1397
+ ], DataBindingPopup.prototype, "_sourceTree", void 0);
1398
+ __decorate([
1399
+ state()
1400
+ ], DataBindingPopup.prototype, "_expandedPaths", void 0);
1401
+ __decorate([
1402
+ state()
1403
+ ], DataBindingPopup.prototype, "_previewInput", void 0);
1404
+ __decorate([
1405
+ state()
1406
+ ], DataBindingPopup.prototype, "_previewOutput", void 0);
1407
+ DataBindingPopup = __decorate([
1408
+ customElement('data-binding-popup')
1409
+ ], DataBindingPopup);
1410
+ export { DataBindingPopup };
1411
+ //# sourceMappingURL=data-binding-popup.js.map