@operato/property-panel 10.0.0-beta.7 → 10.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +486 -0
  2. package/dist/src/graphql/data-subscription.js +5 -1
  3. package/dist/src/graphql/data-subscription.js.map +1 -1
  4. package/dist/src/index.d.ts +4 -0
  5. package/dist/src/index.js +6 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/ox-property-panel.d.ts +17 -0
  8. package/dist/src/ox-property-panel.js +105 -39
  9. package/dist/src/ox-property-panel.js.map +1 -1
  10. package/dist/src/property-panel/abstract-property.js +11 -2
  11. package/dist/src/property-panel/abstract-property.js.map +1 -1
  12. package/dist/src/property-panel/data-binding/data-binding-mapper.js +11 -3
  13. package/dist/src/property-panel/data-binding/data-binding-mapper.js.map +1 -1
  14. package/dist/src/property-panel/data-binding/data-binding-popup.d.ts +63 -0
  15. package/dist/src/property-panel/data-binding/data-binding-popup.js +1414 -0
  16. package/dist/src/property-panel/data-binding/data-binding-popup.js.map +1 -0
  17. package/dist/src/property-panel/data-binding/data-binding-value-map.d.ts +1 -1
  18. package/dist/src/property-panel/data-binding/data-binding-value-range.d.ts +1 -1
  19. package/dist/src/property-panel/data-binding/data-binding.d.ts +1 -0
  20. package/dist/src/property-panel/data-binding/data-binding.js +37 -5
  21. package/dist/src/property-panel/data-binding/data-binding.js.map +1 -1
  22. package/dist/src/property-panel/effects/effects.js +1 -1
  23. package/dist/src/property-panel/effects/effects.js.map +1 -1
  24. package/dist/src/property-panel/effects/property-event-tap.js +21 -1
  25. package/dist/src/property-panel/effects/property-event-tap.js.map +1 -1
  26. package/dist/src/property-panel/effects/property-event.d.ts +15 -7
  27. package/dist/src/property-panel/effects/property-event.js +17 -38
  28. package/dist/src/property-panel/effects/property-event.js.map +1 -1
  29. package/dist/src/property-panel/effects/property-shadow.js +14 -5
  30. package/dist/src/property-panel/effects/property-shadow.js.map +1 -1
  31. package/dist/src/property-panel/event-handlers/event-handlers-help.d.ts +23 -0
  32. package/dist/src/property-panel/event-handlers/event-handlers-help.js +356 -0
  33. package/dist/src/property-panel/event-handlers/event-handlers-help.js.map +1 -0
  34. package/dist/src/property-panel/event-handlers/event-handlers-mapper.d.ts +31 -0
  35. package/dist/src/property-panel/event-handlers/event-handlers-mapper.js +238 -0
  36. package/dist/src/property-panel/event-handlers/event-handlers-mapper.js.map +1 -0
  37. package/dist/src/property-panel/event-handlers/event-handlers-popup.d.ts +42 -0
  38. package/dist/src/property-panel/event-handlers/event-handlers-popup.js +375 -0
  39. package/dist/src/property-panel/event-handlers/event-handlers-popup.js.map +1 -0
  40. package/dist/src/property-panel/event-handlers/event-handlers.d.ts +54 -0
  41. package/dist/src/property-panel/event-handlers/event-handlers.js +410 -0
  42. package/dist/src/property-panel/event-handlers/event-handlers.js.map +1 -0
  43. package/dist/src/property-panel/inspector/inspector-controller.d.ts +66 -0
  44. package/dist/src/property-panel/inspector/inspector-controller.js +288 -0
  45. package/dist/src/property-panel/inspector/inspector-controller.js.map +1 -0
  46. package/dist/src/property-panel/inspector/inspector-group-view.d.ts +15 -0
  47. package/dist/src/property-panel/inspector/inspector-group-view.js +59 -0
  48. package/dist/src/property-panel/inspector/inspector-group-view.js.map +1 -0
  49. package/dist/src/property-panel/inspector/inspector-highlight.d.ts +7 -0
  50. package/dist/src/property-panel/inspector/inspector-highlight.js +25 -0
  51. package/dist/src/property-panel/inspector/inspector-highlight.js.map +1 -0
  52. package/dist/src/property-panel/inspector/inspector-model.d.ts +44 -0
  53. package/dist/src/property-panel/inspector/inspector-model.js +72 -0
  54. package/dist/src/property-panel/inspector/inspector-model.js.map +1 -0
  55. package/dist/src/property-panel/inspector/inspector-tree-view.d.ts +18 -0
  56. package/dist/src/property-panel/inspector/inspector-tree-view.js +61 -0
  57. package/dist/src/property-panel/inspector/inspector-tree-view.js.map +1 -0
  58. package/dist/src/property-panel/inspector/inspector.d.ts +17 -12
  59. package/dist/src/property-panel/inspector/inspector.js +281 -179
  60. package/dist/src/property-panel/inspector/inspector.js.map +1 -1
  61. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.d.ts +20 -0
  62. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js +125 -0
  63. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js.map +1 -0
  64. package/dist/src/property-panel/shapes/placeholder-convert.d.ts +34 -0
  65. package/dist/src/property-panel/shapes/placeholder-convert.js +117 -0
  66. package/dist/src/property-panel/shapes/placeholder-convert.js.map +1 -0
  67. package/dist/src/property-panel/shapes/shapes.js +28 -10
  68. package/dist/src/property-panel/shapes/shapes.js.map +1 -1
  69. package/dist/src/property-panel/specifics/specifics.d.ts +1 -0
  70. package/dist/src/property-panel/specifics/specifics.js +2 -1
  71. package/dist/src/property-panel/specifics/specifics.js.map +1 -1
  72. package/dist/src/property-panel/threed/property-material3d.js +8 -1
  73. package/dist/src/property-panel/threed/property-material3d.js.map +1 -1
  74. package/dist/src/property-panel/threed/property-scene3d.d.ts +34 -6
  75. package/dist/src/property-panel/threed/property-scene3d.js +332 -201
  76. package/dist/src/property-panel/threed/property-scene3d.js.map +1 -1
  77. package/dist/src/property-panel/threed/threed.js +8 -0
  78. package/dist/src/property-panel/threed/threed.js.map +1 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +8 -7
  81. package/themes/grist-theme.css +1 -1
  82. package/translations/en.json +34 -1
  83. package/translations/ja.json +34 -1
  84. package/translations/ko.json +34 -1
  85. package/translations/ms.json +34 -1
  86. package/translations/zh.json +34 -1
@@ -0,0 +1,61 @@
1
+ import { html, nothing } from 'lit';
2
+ import { matchesQuery } from './inspector-model.js';
3
+ import { highlight } from './inspector-highlight.js';
4
+ /** 컴포넌트 자신 또는 자손 중 검색 매치가 있으면 표시 대상. */
5
+ function shouldBeShown(component, terms) {
6
+ if (matchesQuery(component, terms))
7
+ return true;
8
+ const children = component.isContainer() ? component.components : [];
9
+ return children.some(child => shouldBeShown(child, terms));
10
+ }
11
+ /** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */
12
+ export function countMatches(component, terms) {
13
+ let n = matchesQuery(component, terms) ? 1 : 0;
14
+ const children = component.isContainer() ? component.components : [];
15
+ for (const child of children)
16
+ n += countMatches(child, terms);
17
+ return n;
18
+ }
19
+ function renderNode(component, depth, ctx) {
20
+ if (!component || !shouldBeShown(component, ctx.terms)) {
21
+ return html ``;
22
+ }
23
+ const children = component.isContainer() ? component.components : [];
24
+ const extended = ctx.isExtended(component) ? children.filter(child => shouldBeShown(child, ctx.terms)) : [];
25
+ const { type, id, tag, class: clazz } = component.state;
26
+ const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '');
27
+ return html `
28
+ <div
29
+ class="component"
30
+ ?selected=${ctx.selected.indexOf(component) > -1}
31
+ .component=${component}
32
+ ?sortable=${component.isContainer()}
33
+ >
34
+ <span>
35
+ ${depth > 0
36
+ ? html `
37
+ <md-icon class="eye">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>
38
+ <md-icon class="lock" ?locked=${!!component.get('locked')}
39
+ >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon
40
+ >
41
+ <pre>${' '.repeat(Math.max(0, depth - 1))}</pre>
42
+ `
43
+ : html ` <pre>${' '.repeat(depth + 1)}</pre> `}
44
+
45
+ <span class=${ctx.nodeHandleClass(component)}> </span>
46
+
47
+ <span class="type">${depth == 0 ? html `ROOT(count: ${ctx.count})` : highlight(type, ctx.terms)}</span>
48
+ ${label ? html ` <span class="name">${highlight(label, ctx.terms)}</span> ` : nothing}
49
+ </span>
50
+
51
+ ${extended.map(child => renderNode(child, depth + 1, ctx))}
52
+ </div>
53
+ `;
54
+ }
55
+ /** scene.root 부터 트리를 렌더한다. */
56
+ export function renderInspectorTree(root, ctx) {
57
+ if (!root)
58
+ return html ``;
59
+ return renderNode(root, 0, ctx);
60
+ }
61
+ //# sourceMappingURL=inspector-tree-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector-tree-view.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-tree-view.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 트리 뷰 렌더러.\n *\n * 계층(Container/Group) 구조를 재귀로 그린다. SceneInspector 의 shadow root 에 직접 렌더되며\n * (클릭 위임이 `.component` 프로퍼티/classList 에 의존하므로 별도 커스텀엘리먼트로 분리하지 않는다),\n * 순수 렌더 함수로 컨트롤러/상태를 주입받는다.\n */\nimport { Component, Container } from '@hatiolab/things-scene'\nimport { html, nothing, TemplateResult } from 'lit'\n\nimport { matchesQuery, SearchTerm } from './inspector-model.js'\nimport { highlight } from './inspector-highlight.js'\n\n/** 트리 렌더에 필요한 컨텍스트(호스트/컨트롤러가 주입). */\nexport interface TreeViewContext {\n /** 현재 선택 컴포넌트 목록. */\n selected: Component[]\n /** 파싱된 검색어(AND). */\n terms: SearchTerm[]\n /** ROOT 노드에 표시할 매치 개수. */\n count: number\n isExtended: (c: Component) => boolean\n nodeHandleClass: (c: Component) => string\n}\n\n/** 컴포넌트 자신 또는 자손 중 검색 매치가 있으면 표시 대상. */\nfunction shouldBeShown(component: Component, terms: SearchTerm[]): boolean {\n if (matchesQuery(component, terms)) return true\n const children = component.isContainer() ? (component as Container).components : []\n return children.some(child => shouldBeShown(child, terms))\n}\n\n/** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */\nexport function countMatches(component: Component, terms: SearchTerm[]): number {\n let n = matchesQuery(component, terms) ? 1 : 0\n const children = component.isContainer() ? (component as Container).components : []\n for (const child of children) n += countMatches(child, terms)\n return n\n}\n\nfunction renderNode(component: Component, depth: number, ctx: TreeViewContext): TemplateResult {\n if (!component || !shouldBeShown(component, ctx.terms)) {\n return html``\n }\n\n const children = component.isContainer() ? (component as Container).components : []\n const extended = ctx.isExtended(component) ? children.filter(child => shouldBeShown(child, ctx.terms)) : []\n const { type, id, tag, class: clazz } = component.state\n const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '')\n\n return html`\n <div\n class=\"component\"\n ?selected=${ctx.selected.indexOf(component) > -1}\n .component=${component}\n ?sortable=${component.isContainer()}\n >\n <span>\n ${depth > 0\n ? html`\n <md-icon class=\"eye\">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>\n <md-icon class=\"lock\" ?locked=${!!component.get('locked')}\n >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon\n >\n <pre>${' '.repeat(Math.max(0, depth - 1))}</pre>\n `\n : html` <pre>${' '.repeat(depth + 1)}</pre> `}\n\n <span class=${ctx.nodeHandleClass(component)}> </span>\n\n <span class=\"type\">${depth == 0 ? html`ROOT(count: ${ctx.count})` : highlight(type, ctx.terms)}</span>\n ${label ? html` <span class=\"name\">${highlight(label, ctx.terms)}</span> ` : nothing}\n </span>\n\n ${extended.map(child => renderNode(child, depth + 1, ctx))}\n </div>\n `\n}\n\n/** scene.root 부터 트리를 렌더한다. */\nexport function renderInspectorTree(root: Component | undefined, ctx: TreeViewContext): TemplateResult {\n if (!root) return html``\n return renderNode(root, 0, ctx)\n}\n"]}
@@ -1,27 +1,32 @@
1
1
  import '@operato/input/ox-input-search.js';
2
2
  import { LitElement, PropertyValues, TemplateResult } from 'lit';
3
- import { Component, Scene } from '@hatiolab/things-scene';
3
+ import { Scene } from '@hatiolab/things-scene';
4
4
  export declare class SceneInspector extends LitElement {
5
5
  static styles: import("lit").CSSResult[];
6
6
  scene?: Scene;
7
7
  private searchText;
8
- private _extendedMap;
8
+ private viewMode;
9
+ private controller;
9
10
  private show;
10
- private count;
11
- disconnectScene(scene?: Scene): void;
11
+ /** reveal(스크롤) 판정용 직전 선택. */
12
+ private _lastSelected;
12
13
  disconnectedCallback(): void;
13
14
  render(): TemplateResult<1>;
15
+ private _renderResult;
16
+ private _setViewMode;
17
+ private _collapseGroups;
18
+ private _expandGroups;
14
19
  firstUpdated(): void;
20
+ /** 외부에서 강제 재렌더용(기존 공개 메쏘드 유지). */
15
21
  refresh(): void;
16
22
  updated(change: PropertyValues<this>): void;
23
+ /**
24
+ * 선택이 바뀌면(캔버스 등) 해당 항목을 리스트에 노출한다 — 그룹 뷰면 그룹을 펼친 뒤 스크롤.
25
+ * scrollIntoView({block:'nearest'}) 라 이미 보이는 경우(인스펙터 클릭 등)엔 사실상 no-op.
26
+ */
27
+ private _maybeReveal;
28
+ /** 이벤트 타겟에서 상위로 올라가며 `.component` 를 가진 요소를 찾는다. */
29
+ private _resolveComponent;
17
30
  _onclick(e: MouseEvent): void;
18
31
  _ondblclick(e: MouseEvent): void;
19
- get extendedMap(): any;
20
- getNodeHandleClass(component: Component): "extended" | "collapsed" | "collapsespace";
21
- isExtended(component: Component): boolean;
22
- toggleExtended(component: Component): void;
23
- toggleHidden(component: Component): void;
24
- selectComponent(component: Component, append?: boolean): void;
25
- shouldBeShown(component: Component, counting?: boolean): boolean;
26
- renderComponent(component: Component, depth: number): TemplateResult;
27
32
  }
@@ -1,33 +1,65 @@
1
1
  import { __decorate } from "tslib";
2
+ /*
3
+ * Copyright © HatioLab Inc. All rights reserved.
4
+ *
5
+ * SceneInspector — 속성패널 inspector 탭 호스트.
6
+ *
7
+ * 얇은 호스트: 검색 상태 + 클릭 위임만 담당하고, scene 상호작용은 InspectorController,
8
+ * 렌더는 뷰 렌더러(현재 트리)로 위임한다. (아키텍처 리팩터 P0 — 동작은 기존과 동일)
9
+ */
2
10
  import '@operato/input/ox-input-search.js';
3
- import { css, html, LitElement } from 'lit';
11
+ import { css, html, LitElement, nothing } from 'lit';
4
12
  import { property, state } from 'lit/decorators.js';
5
- // TODO test Sortable
6
- // import Sortable from 'sortablejs'
7
13
  import { i18next } from '@operato/i18n';
8
14
  import { InspectorStyles } from '@operato/styles/inspector-styles.js';
15
+ import { parseQuery } from './inspector-model.js';
16
+ import { InspectorController } from './inspector-controller.js';
17
+ import { countMatches, renderInspectorTree } from './inspector-tree-view.js';
18
+ import { renderInspectorGroupView } from './inspector-group-view.js';
19
+ /** 뷰 모드 선호를 세션 간 유지(localStorage). 전역 사용자 선호. */
20
+ const VIEW_MODE_STORAGE_KEY = 'scene-inspector.view-mode';
21
+ function readStoredViewMode() {
22
+ try {
23
+ const v = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
24
+ return v === 'group' || v === 'tree' ? v : 'tree';
25
+ }
26
+ catch (_a) {
27
+ return 'tree';
28
+ }
29
+ }
30
+ function writeStoredViewMode(mode) {
31
+ try {
32
+ localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode);
33
+ }
34
+ catch (_a) {
35
+ /* localStorage 미가용(SSR/프라이빗 등) — 무시 */
36
+ }
37
+ }
9
38
  export class SceneInspector extends LitElement {
10
39
  constructor() {
11
40
  super(...arguments);
12
41
  this.searchText = '';
42
+ this.viewMode = readStoredViewMode();
43
+ this.controller = new InspectorController();
13
44
  this.show = false;
14
- this.count = -1;
15
- }
16
- disconnectScene(scene) {
17
- if (scene) {
18
- scene.off('selected', undefined, this);
19
- scene.off('execute', undefined, this);
20
- scene.off('undo', undefined, this);
21
- scene.off('redo', undefined, this);
22
- }
45
+ /** reveal(스크롤) 판정용 직전 선택. */
46
+ this._lastSelected = [];
47
+ this._collapseGroups = () => {
48
+ this.controller.collapseAllGroups();
49
+ this.requestUpdate();
50
+ };
51
+ this._expandGroups = () => {
52
+ const index = this.controller.index;
53
+ if (index)
54
+ this.controller.expandAllGroups([...index.byType.keys()]);
55
+ this.requestUpdate();
56
+ };
23
57
  }
24
58
  disconnectedCallback() {
25
59
  super.disconnectedCallback();
26
- this.disconnectScene(this.scene);
27
- delete this._extendedMap;
60
+ this.controller.unbind();
28
61
  }
29
62
  render() {
30
- this.count = 0;
31
63
  return html `
32
64
  <ox-input-search
33
65
  .placeholder=${i18next.t('text.search with type, id or tag')}
@@ -35,73 +67,93 @@ export class SceneInspector extends LitElement {
35
67
  @change=${(e) => (this.searchText = (e.target.value || '').toLowerCase())}
36
68
  ></ox-input-search>
37
69
 
38
- <div result>${!this.scene ? html `` : this.renderComponent(this.scene.root, 0)}</div>
70
+ <div toolbar>
71
+ <md-icon class="view-btn" ?selected=${this.viewMode === 'tree'} title="Tree view" @click=${() => this._setViewMode('tree')}>account_tree</md-icon>
72
+ <md-icon class="view-btn" ?selected=${this.viewMode === 'group'} title="Group by type" @click=${() => this._setViewMode('group')}>category</md-icon>
73
+ <span class="spacer"></span>
74
+ ${this.viewMode === 'group'
75
+ ? html `
76
+ <md-icon title="Expand all groups" @click=${this._expandGroups}>unfold_more</md-icon>
77
+ <md-icon title="Collapse to group level" @click=${this._collapseGroups}>unfold_less</md-icon>
78
+ `
79
+ : nothing}
80
+ </div>
81
+
82
+ <div result>${this._renderResult()}</div>
39
83
  `;
40
84
  }
85
+ _renderResult() {
86
+ if (!this.scene)
87
+ return html ``;
88
+ const terms = parseQuery(this.searchText);
89
+ if (this.viewMode === 'group') {
90
+ return renderInspectorGroupView(this.controller.index, {
91
+ selected: this.controller.selected,
92
+ terms,
93
+ isGroupExpanded: (type) => this.controller.isGroupExpanded(type),
94
+ isTypeDimmed: (type) => this.controller.isTypeDimmed(type),
95
+ isTypeHidden: (type) => this.controller.isTypeHidden(type)
96
+ });
97
+ }
98
+ return renderInspectorTree(this.scene.root, {
99
+ selected: this.controller.selected,
100
+ terms,
101
+ count: countMatches(this.scene.root, terms),
102
+ isExtended: (c) => this.controller.isExtended(c),
103
+ nodeHandleClass: (c) => this.controller.nodeHandleClass(c)
104
+ });
105
+ }
106
+ _setViewMode(mode) {
107
+ if (this.viewMode === mode)
108
+ return;
109
+ this.viewMode = mode;
110
+ writeStoredViewMode(mode); // 세션 간 유지
111
+ }
41
112
  firstUpdated() {
42
113
  dispatchEvent(new Event('resize'));
43
114
  this.renderRoot.addEventListener('click', this._onclick.bind(this));
44
115
  this.renderRoot.addEventListener('dblclick', this._ondblclick.bind(this));
45
116
  }
117
+ /** 외부에서 강제 재렌더용(기존 공개 메쏘드 유지). */
46
118
  refresh() {
47
- var _a;
48
- let selected = ((_a = this.scene) === null || _a === void 0 ? void 0 : _a.selected) || [];
49
- selected.forEach((component) => {
50
- let parent = component.parent;
51
- while (parent && !this.extendedMap.get(parent)) {
52
- this.extendedMap.set(parent, true);
53
- parent = parent.parent;
54
- }
55
- });
56
119
  this.requestUpdate();
57
120
  }
58
121
  updated(change) {
59
122
  if (change.has('scene')) {
60
- let oldScene = change.get('scene');
61
- if (oldScene) {
62
- this.disconnectScene(oldScene);
63
- delete this._extendedMap;
64
- }
65
- if (this.scene && this.scene.root) {
66
- this.scene.on('selected', this.refresh, this);
67
- this.scene.on('execute', this.refresh, this);
68
- this.scene.on('undo', this.refresh, this);
69
- this.scene.on('redo', this.refresh, this);
70
- this.refresh();
123
+ this.controller.bind(this.scene, () => this.requestUpdate());
124
+ }
125
+ this._maybeReveal();
126
+ }
127
+ /**
128
+ * 선택이 바뀌면(캔버스 ) 해당 항목을 리스트에 노출한다 — 그룹 뷰면 그룹을 펼친 뒤 스크롤.
129
+ * scrollIntoView({block:'nearest'}) 이미 보이는 경우(인스펙터 클릭 등)엔 사실상 no-op.
130
+ */
131
+ _maybeReveal() {
132
+ const selected = this.controller.selected;
133
+ const changed = selected.length !== this._lastSelected.length || selected.some((c, i) => c !== this._lastSelected[i]);
134
+ if (!changed)
135
+ return;
136
+ if (selected.length === 0) {
137
+ this._lastSelected = selected;
138
+ return;
139
+ }
140
+ if (this.viewMode === 'group') {
141
+ const types = [...new Set(selected.map(c => c.state.type))];
142
+ if (types.some(t => !this.controller.isGroupExpanded(t))) {
143
+ this.controller.expandAllGroups(types);
144
+ return; // _lastSelected 미갱신 → 다음 updated 에서 changed 유지 → 그때 스크롤
71
145
  }
72
146
  }
73
- // TODO test Sortable
74
- // this.updateComplete.then(() => {
75
- // this.renderRoot.querySelectorAll('[sortable]').forEach(sortable => {
76
- // new Sortable(sortable as HTMLElement, this.sortableConfig)
77
- // })
78
- // })
147
+ this._lastSelected = selected;
148
+ this.updateComplete.then(() => {
149
+ const el = this.renderRoot.querySelector('.component[selected]');
150
+ el === null || el === void 0 ? void 0 : el.scrollIntoView({ block: 'nearest' });
151
+ });
79
152
  }
80
- // TODO test Sortable
81
- // sortableConfig: Sortable.Options = {
82
- // group: 'inspector',
83
- // animation: 150,
84
- // draggable: '.component',
85
- // swapThreshold: 1,
86
- // onSort: this.onSort.bind(this)
87
- // }
88
- // TODO test Sortable
89
- // onSort(e: Sortable.SortableEvent) {
90
- // if (!this.scene) return
91
- // var component = (e.item as HTMLElement & { component: Component }).component
92
- // var to_container = (e.to as HTMLElement & { component: Component }).component as Container
93
- // var to_index = e.newIndex! - 1
94
- // this.scene.move(component, to_container, to_index)
95
- // this.show = false
96
- // this.updateComplete.then(() => {
97
- // this.show = true
98
- // })
99
- // }
100
- _onclick(e) {
101
- e.stopPropagation();
102
- var targetElement = e.target;
103
- var classList = targetElement.classList;
104
- var component;
153
+ /** 이벤트 타겟에서 상위로 올라가며 `.component` 를 가진 요소를 찾는다. */
154
+ _resolveComponent(e) {
155
+ let targetElement = e.target;
156
+ let component;
105
157
  while (!component && targetElement) {
106
158
  component = targetElement.component;
107
159
  if (component)
@@ -110,14 +162,56 @@ export class SceneInspector extends LitElement {
110
162
  if (!targetElement || targetElement === this.renderRoot)
111
163
  break;
112
164
  }
165
+ return component;
166
+ }
167
+ _onclick(e) {
168
+ var _a, _b, _c, _d, _e;
169
+ e.stopPropagation();
170
+ const target = e.target;
171
+ // 그룹 hidden 아이콘 클릭 → 그룹(타입) 전원 hidden 토글 (헤더 펼침보다 먼저 처리)
172
+ if (target.classList.contains('group-hidden')) {
173
+ const type = (_b = (_a = target.closest) === null || _a === void 0 ? void 0 : _a.call(target, '[data-group-type]')) === null || _b === void 0 ? void 0 : _b.getAttribute('data-group-type');
174
+ if (type != null)
175
+ this.controller.toggleTypeHidden(type);
176
+ this.requestUpdate();
177
+ return;
178
+ }
179
+ // 그룹 dim-out 아이콘 클릭 → 그룹(타입) 포커스 dim 토글 (헤더 펼침보다 먼저 처리)
180
+ if (target.classList.contains('group-dim')) {
181
+ const type = (_d = (_c = target.closest) === null || _c === void 0 ? void 0 : _c.call(target, '[data-group-type]')) === null || _d === void 0 ? void 0 : _d.getAttribute('data-group-type');
182
+ if (type != null)
183
+ this.controller.toggleTypeDim(type);
184
+ this.requestUpdate();
185
+ return;
186
+ }
187
+ // 타입 그룹 헤더 클릭 → 그룹 펼침/접힘 (멤버 행은 헤더의 자손이 아니므로 여기 안 걸림)
188
+ const header = (_e = target.closest) === null || _e === void 0 ? void 0 : _e.call(target, '[data-group-type]');
189
+ if (header) {
190
+ const type = header.getAttribute('data-group-type');
191
+ if (type != null)
192
+ this.controller.toggleGroup(type);
193
+ this.requestUpdate();
194
+ return;
195
+ }
196
+ const classList = e.target.classList;
197
+ const component = this._resolveComponent(e);
113
198
  if (component) {
114
199
  if (classList.contains('eye')) {
115
- this.toggleHidden(component);
200
+ this.controller.toggleHidden(component);
201
+ }
202
+ else if (classList.contains('lock')) {
203
+ this.controller.toggleLocked(component);
116
204
  }
117
205
  else if (classList.contains('extended') || classList.contains('collapsed')) {
118
- this.toggleExtended(component);
206
+ this.controller.toggleExtended(component);
207
+ }
208
+ // shift = 범위(볼륨) 선택/블록 해제, ctrl/cmd = 개별 토글, 그 외 = 단일 선택
209
+ if (e.shiftKey) {
210
+ this.controller.selectRange(component, parseQuery(this.searchText));
211
+ }
212
+ else {
213
+ this.controller.select(component, e.metaKey || e.ctrlKey);
119
214
  }
120
- this.selectComponent(component, e.shiftKey);
121
215
  }
122
216
  else {
123
217
  if (classList.contains('inspector')) {
@@ -129,117 +223,16 @@ export class SceneInspector extends LitElement {
129
223
  }
130
224
  _ondblclick(e) {
131
225
  e.stopPropagation();
132
- var targetElement = e.target;
133
- var component;
134
- while (!component && targetElement) {
135
- component = targetElement.component;
136
- if (component)
137
- break;
138
- targetElement = targetElement.parentNode;
139
- if (!targetElement || targetElement === this.renderRoot)
140
- break;
141
- }
142
- if (component && component.isContainer()) {
143
- this.toggleExtended(component);
144
- }
145
- this.requestUpdate();
146
- }
147
- get extendedMap() {
148
- if (!this._extendedMap) {
149
- this._extendedMap = new WeakMap();
150
- }
151
- return this._extendedMap;
152
- }
153
- getNodeHandleClass(component) {
154
- if (component.isContainer() && component.components.length > 0) {
155
- return !!this.extendedMap.get(component) ? 'extended' : 'collapsed';
156
- }
157
- else {
158
- return 'collapsespace';
159
- }
160
- }
161
- isExtended(component) {
162
- return !!this.extendedMap.get(component);
163
- }
164
- toggleExtended(component) {
165
- var extended = this.isExtended(component);
166
- if (extended) {
167
- this.extendedMap.delete(component);
168
- }
169
- else {
170
- this.extendedMap.set(component, !extended);
171
- }
172
- this.requestUpdate();
173
- }
174
- toggleHidden(component) {
175
- component.set('hidden', !component.hidden);
176
- this.requestUpdate();
177
- }
178
- selectComponent(component, append = false) {
179
- const selected = this.scene.selected;
180
- if (append) {
181
- const idx = selected.findIndex((s) => s === component);
182
- if (idx != -1) {
183
- selected.splice(idx, 1);
184
- this.scene.selected = [...selected];
185
- }
186
- else {
187
- this.scene.selected = [...selected, component];
226
+ const component = this._resolveComponent(e);
227
+ if (component) {
228
+ if (component.isContainer()) {
229
+ this.controller.toggleExtended(component);
188
230
  }
231
+ // 캔버스를 해당 컴포넌트로 센터 이동(줌).
232
+ this.controller.centerTo(component);
189
233
  }
190
- else {
191
- this.scene.selected = [component];
192
- }
193
- component.trigger('reactionreset');
194
- component.trigger('reaction');
195
234
  this.requestUpdate();
196
235
  }
197
- shouldBeShown(component, counting = false) {
198
- const { type, name, id, tag } = component.state;
199
- const found = !this.searchText || `${type} ${name || ''} ${id || ''} ${tag || ''}`.toLowerCase().search(this.searchText) > -1;
200
- if (counting && found) {
201
- this.count++;
202
- }
203
- const foundChildren = (component.isContainer() ? component.components : []).filter((child) => this.shouldBeShown(child, counting)).length;
204
- return !!(found || foundChildren > 0);
205
- }
206
- renderComponent(component, depth) {
207
- var _a;
208
- if (!component) {
209
- return html ``;
210
- }
211
- if (!this.shouldBeShown(component, depth == 0)) {
212
- return html ``;
213
- }
214
- const children = component.isContainer() ? component.components : [];
215
- const extended = this.isExtended(component) ? children.filter((child) => this.shouldBeShown(child)) : [];
216
- const { type, id, tag, class: clazz } = component.state;
217
- const name = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '');
218
- return html `
219
- <div
220
- class="component"
221
- ?selected=${(((_a = this.scene) === null || _a === void 0 ? void 0 : _a.selected) || []).indexOf(component) > -1}
222
- .component=${component}
223
- ?sortable=${component.isContainer()}
224
- >
225
- <span>
226
- ${depth > 0
227
- ? html `
228
- <md-icon class="eye">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>
229
- <pre>${' '.repeat(depth)}</pre>
230
- `
231
- : html ` <pre>${' '.repeat(depth + 2)}</pre> `}
232
-
233
- <span class=${this.getNodeHandleClass(component)}> </span>
234
-
235
- <span class="type">${depth == 0 ? html `ROOT(count: ${this.count})` : type}</span>
236
- ${name ? html ` <span class="name">${name}</span> ` : html ``}
237
- </span>
238
-
239
- ${extended.map((child) => this.renderComponent(child, depth + 1))}
240
- </div>
241
- `;
242
- }
243
236
  }
244
237
  SceneInspector.styles = [
245
238
  InspectorStyles,
@@ -306,15 +299,121 @@ SceneInspector.styles = [
306
299
  }
307
300
 
308
301
  .eye {
309
- margin: 0 0 0 5px;
302
+ margin: 0 0 0 4px;
310
303
  vertical-align: middle;
311
304
  opacity: 0.7;
312
- font-size: 1.1em;
305
+ font-size: 1em;
306
+ width: 1em;
313
307
  color: var(--scene-inspector-eye-icon-color);
308
+ cursor: pointer;
309
+ }
310
+
311
+ .lock {
312
+ margin: 0;
313
+ vertical-align: middle;
314
+ opacity: 0.3;
315
+ font-size: 1em;
316
+ width: 1em;
317
+ cursor: pointer;
318
+ }
319
+ .lock[locked] {
320
+ opacity: 0.9;
321
+ color: var(--md-sys-color-error, #d9534f);
314
322
  }
315
323
 
316
324
  pre {
317
325
  display: inline;
326
+ /* 트리 인덴트(수직라인까지의 간격) 최소화 — depth 당 좁은 폭 */
327
+ font-size: 0.6em;
328
+ margin: 0;
329
+ }
330
+
331
+ /* 뷰 모드 툴바 */
332
+ div[toolbar] {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 4px;
336
+ padding: 2px 8px 4px;
337
+ }
338
+ div[toolbar] .spacer {
339
+ flex: 1;
340
+ }
341
+ div[toolbar] md-icon {
342
+ cursor: pointer;
343
+ opacity: 0.5;
344
+ font-size: 18px;
345
+ border-radius: 4px;
346
+ padding: 2px;
347
+ }
348
+ div[toolbar] md-icon:hover {
349
+ opacity: 0.9;
350
+ }
351
+ div[toolbar] md-icon[selected] {
352
+ opacity: 1;
353
+ color: var(--md-sys-color-primary, #4a9eff);
354
+ background: rgba(0, 0, 0, 0.05);
355
+ }
356
+
357
+ /* 타입 그룹 뷰 */
358
+ .group-header {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 4px;
362
+ font-size: 14px;
363
+ font-weight: 600;
364
+ cursor: pointer;
365
+ padding: 2px 0;
366
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
367
+ }
368
+ .group-header:hover {
369
+ background: rgba(0, 0, 0, 0.03);
370
+ }
371
+ .group-count {
372
+ margin-left: auto;
373
+ font-size: 0.72em;
374
+ font-weight: normal;
375
+ background: var(--scene-inspector-name-background-color);
376
+ color: #fff;
377
+ border-radius: 999px;
378
+ padding: 0 6px;
379
+ }
380
+ .group-hidden,
381
+ .group-dim {
382
+ font-size: 1em;
383
+ opacity: 0.35;
384
+ cursor: pointer;
385
+ }
386
+ .group-hidden {
387
+ margin: 0 2px 0 6px;
388
+ }
389
+ .group-dim {
390
+ margin: 0 4px 0 2px;
391
+ }
392
+ .group-hidden:hover,
393
+ .group-dim:hover {
394
+ opacity: 0.8;
395
+ }
396
+ .group-header[hidden-group] .group-hidden,
397
+ .group-header[dimmed] .group-dim {
398
+ opacity: 0.95;
399
+ color: var(--md-sys-color-primary, #4a9eff);
400
+ }
401
+ .group-header[dimmed] .group-type {
402
+ opacity: 0.45;
403
+ }
404
+ .group-member {
405
+ padding-left: 8px;
406
+ }
407
+ .muted {
408
+ opacity: 0.4;
409
+ font-style: italic;
410
+ }
411
+ /* 검색 매치 하이라이트 */
412
+ mark {
413
+ background: var(--md-sys-color-tertiary-container, #ffe082);
414
+ color: inherit;
415
+ border-radius: 2px;
416
+ padding: 0 1px;
318
417
  }
319
418
  `
320
419
  ];
@@ -324,4 +423,7 @@ __decorate([
324
423
  __decorate([
325
424
  state()
326
425
  ], SceneInspector.prototype, "searchText", void 0);
426
+ __decorate([
427
+ state()
428
+ ], SceneInspector.prototype, "viewMode", void 0);
327
429
  //# sourceMappingURL=inspector.js.map