@operato/slot-layout-editor 10.29.52 → 10.29.56

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.
@@ -1,25 +1,61 @@
1
1
  /**
2
2
  * @license Copyright © HatioLab Inc. All rights reserved.
3
3
  *
4
- * 자리 레이아웃 편집기. 홀더의 Tier·Slot 구성을 고치고, 검증하고, 미리 보고, [적용]으로 알린다.
4
+ * 자리 레이아웃 편집기. 홀더의 Tier·Slot 구성을 고치고, 검증하고, 미리 보고, 호스트에 알린다.
5
5
  * Tier 는 홀더가 주는 Anchor 하나에 서고, 그 Anchor 기준 위치·회전과 적재 방식(mount)을 갖는다.
6
6
  *
7
+ * 맨 위 그림은 Anchor 면(호스트가 크기를 주면)과 그 위의 자리, 실린 아이템을 Anchor 기준으로 보인다. 자리를 끌면 Tier 위치가,
8
+ * 끝 자리의 점을 끌면 간격이 바뀐다. 끄는 동안은 그림만 바뀌고, 놓을 때 한 번 편집이 된다. Esc 는 끌기 전으로 돌린다.
9
+ *
7
10
  * 컴포넌트 상태를 직접 바꾸지 않는다. 호스트에 두 가지만 알린다:
8
- * change — 적용한 레이아웃 (this.value)
11
+ * change — 적용한 레이아웃 (this.value). live 호스트에는 맞는 편집마다, 아니면 [적용]으로
9
12
  * slot-layout-use-default — 개별 설정을 지우고 기본값을 쓰겠다는 의도 (값을 비우는 편집과 다르다)
13
+ * 맞지 않는 초안은 알리지 않는다: 칸 옆에 문제를 보이고, 장면에는 마지막으로 알린 값이 남는다.
10
14
  * 기본값을 쓰게 된 뒤 보일 값은 호스트가 options.effective 로 다시 준다.
11
15
  */
12
16
  import { __decorate } from "tslib";
13
17
  import { css, html, nothing, LitElement, svg } from 'lit';
14
18
  import { customElement, property, state } from 'lit/decorators.js';
15
19
  import { i18next, localize } from '@operato/i18n';
16
- import { persistenceIssues, slotSpaceWarnings, validateSlotLayout } from '@hatiolab/things-scene';
17
- import { addCustomSlot, addressImpact, applyPreset, copyTier, previewTier, removeCustomSlot, removeTier, setTierPattern, updatePattern, updateSlotSpec, updateTier } from './draft.js';
18
- import { issueText as defaultIssueText, spaceWarningText, t } from './issue-text.js';
20
+ import { persistenceIssues, rotatedFootprint, slotSpaceWarnings, validateSlotLayout } from '@hatiolab/things-scene';
21
+ import { addCustomSlot, addressImpact, anchorFaceSlots, applyPreset, copyTier, fitTierToFace, heldItemStatuses, pitchToPoint, removeCustomSlot, removeTier, setTierPattern, updatePattern, updateSlotSpec, updateTier } from './draft.js';
22
+ import { fitFaceText, fitText, issueField, issueMessage, issueText as defaultIssueText, ISSUE_TEXT, spaceWarningText, t } from './issue-text.js';
19
23
  const num = (e) => {
20
24
  const n = e.target.valueAsNumber;
21
25
  return Number.isFinite(n) ? n : NaN;
22
26
  };
27
+ const shown = (v) => (v === undefined || v === null || Number.isNaN(v) ? '' : String(v));
28
+ const snap = (v) => Math.round(v / 10) * 10;
29
+ const ERROR = '#c62828';
30
+ const SLOT = '#1976d2';
31
+ /** 칸 이름 (칸 옆에 오지 못한 문제를 목록에 적을 때) */
32
+ const FIELD_LABEL = {
33
+ anchor: ['face', 'Face'],
34
+ mount: ['mount', 'Mount'],
35
+ 'position-x': ['position-x', 'Position X'],
36
+ 'position-y': ['position-y', 'Position Y (height)'],
37
+ 'position-z': ['position-z', 'Position Z'],
38
+ 'tier-rotation': ['tier-rotation', 'Tier rotation (deg)'],
39
+ pattern: ['pattern', 'Arrangement'],
40
+ count: ['count', 'Slots'],
41
+ pitch: ['pitch', 'Pitch'],
42
+ 'pitch-x': ['pitch-x', 'Pitch X'],
43
+ 'pitch-z': ['pitch-z', 'Pitch Z'],
44
+ axis: ['axis', 'Direction'],
45
+ origin: ['origin', 'Slot 1 at'],
46
+ 'origin-x': ['origin-x', 'Slot 1 at X'],
47
+ 'origin-z': ['origin-z', 'Slot 1 at Z'],
48
+ rows: ['rows', 'Rows (Z)'],
49
+ cols: ['cols', 'Columns (X)'],
50
+ order: ['order', 'Numbering'],
51
+ 'allowed-width': ['allowed-width', 'Width (X)'],
52
+ 'allowed-depth': ['allowed-depth', 'Depth (Z)'],
53
+ 'allowed-height': ['allowed-height', 'Height'],
54
+ 'stack-limit': ['stack-limit', 'Stack limit'],
55
+ rotation: ['rotation', 'Slot rotation (deg)'],
56
+ accepts: ['accepts', 'Accepts']
57
+ };
58
+ const fieldLabel = (field) => (FIELD_LABEL[field] ? t(FIELD_LABEL[field][0], FIELD_LABEL[field][1]) : field);
23
59
  let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitElement) {
24
60
  constructor() {
25
61
  super(...arguments);
@@ -29,6 +65,46 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
29
65
  this.presetKey = '';
30
66
  this.presetSize = {};
31
67
  this.presetParams = {};
68
+ this._onDragKey = (e) => {
69
+ if (e.key !== 'Escape' || !this._drag)
70
+ return;
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ this._cancelDrag();
74
+ };
75
+ this._onDragMove = (e) => {
76
+ const d = this._drag;
77
+ const p = d && this._point(e);
78
+ if (!d || !p)
79
+ return;
80
+ const tier = d.before.tiers.find(x => x.tierNo === d.tierNo);
81
+ if (d.kind === 'move') {
82
+ const position = { ...tier.position, x: snap(tier.position.x + p.x - d.from.x), z: snap(tier.position.z + p.z - d.from.z) };
83
+ this.draft = updateTier(d.before, d.tierNo, { position });
84
+ }
85
+ else {
86
+ const pitch = pitchToPoint(d.before, d.tierNo, p);
87
+ if (pitch !== null)
88
+ this.draft = updatePattern(d.before, d.tierNo, { pitch: Math.max(10, snap(pitch)) });
89
+ }
90
+ };
91
+ /** 놓으면 한 번의 편집. 움직이지 않았으면 아무 일도 없다 */
92
+ this._onDragEnd = () => {
93
+ const d = this._endDrag();
94
+ if (!d)
95
+ return;
96
+ if (JSON.stringify(this.draft) === JSON.stringify(d.before)) {
97
+ this.draft = d.before;
98
+ return;
99
+ }
100
+ this._edit(this.draft);
101
+ };
102
+ this._cancelDrag = () => {
103
+ const d = this._endDrag();
104
+ if (!d)
105
+ return;
106
+ this.draft = d.before;
107
+ };
32
108
  }
33
109
  willUpdate(changed) {
34
110
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
@@ -44,16 +120,19 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
44
120
  if (JSON.stringify((_h = before === null || before === void 0 ? void 0 : before.presetSize) !== null && _h !== void 0 ? _h : {}) !== JSON.stringify((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presetSize) !== null && _k !== void 0 ? _k : {}))
45
121
  this.presetSize = { ...((_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presetSize) !== null && _m !== void 0 ? _m : {}) };
46
122
  }
47
- /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다 */
123
+ /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다. 고른 Tier 는 남아 있으면 그대로 */
48
124
  reset() {
49
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
125
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
50
126
  const start = (_a = this.value) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.effective;
51
127
  this.draft = start ? structuredClone(start) : undefined;
52
- this.tier = (_f = (_e = (_d = (_c = this.draft) === null || _c === void 0 ? void 0 : _c.tiers) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.tierNo) !== null && _f !== void 0 ? _f : 1;
128
+ if (!((_d = (_c = this.draft) === null || _c === void 0 ? void 0 : _c.tiers) === null || _d === void 0 ? void 0 : _d.some(x => (x === null || x === void 0 ? void 0 : x.tierNo) === this.tier)))
129
+ this.tier = (_h = (_g = (_f = (_e = this.draft) === null || _e === void 0 ? void 0 : _e.tiers) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.tierNo) !== null && _h !== void 0 ? _h : 1;
53
130
  this.dirty = false;
54
- this.presetSize = { ...((_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.presetSize) !== null && _h !== void 0 ? _h : {}) };
55
- if (!((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presets) === null || _k === void 0 ? void 0 : _k.some(p => p.key === this.presetKey)))
56
- this.presetKey = (_p = (_o = (_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presets) === null || _m === void 0 ? void 0 : _m[0]) === null || _o === void 0 ? void 0 : _o.key) !== null && _p !== void 0 ? _p : '';
131
+ this.fitNote = undefined;
132
+ this._endDrag();
133
+ this.presetSize = { ...((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presetSize) !== null && _k !== void 0 ? _k : {}) };
134
+ if (!((_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presets) === null || _m === void 0 ? void 0 : _m.some(p => p.key === this.presetKey)))
135
+ this.presetKey = (_r = (_q = (_p = (_o = this.options) === null || _o === void 0 ? void 0 : _o.presets) === null || _p === void 0 ? void 0 : _p[0]) === null || _q === void 0 ? void 0 : _q.key) !== null && _r !== void 0 ? _r : '';
57
136
  }
58
137
  get issues() {
59
138
  var _a, _b, _c, _d, _e;
@@ -61,58 +140,95 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
61
140
  return [];
62
141
  const anchors = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors) === null || _b === void 0 ? void 0 : _b.map(a => a.id);
63
142
  const invalid = [...validateSlotLayout(this.draft, { anchors }), ...((_e = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.validate) === null || _d === void 0 ? void 0 : _d.call(_c, this.draft)) !== null && _e !== void 0 ? _e : [])];
143
+ // 호스트 검증이 같은 문제를 다시 내도 한 번만 적는다
144
+ const seen = new Set();
145
+ const unique = invalid.filter(i => !seen.has(`${i.code}@${i.path}`) && !!seen.add(`${i.code}@${i.path}`));
64
146
  // 저장 불가 검사는 검증을 통과한 값에만 보인다 (같은 문제를 두 번 적지 않는다)
65
- return invalid.length ? invalid : persistenceIssues(this.draft);
147
+ return unique.length ? unique : persistenceIssues(this.draft);
66
148
  }
67
- /** 자리끼리 겹치거나 면 밖으로 나가는 설정 경고. 적용을 막지 않는다. 적재면 크기를 주지 않은 호스트에는 확인 불가를 보이지 않는다 */
149
+ /** 자리끼리 겹치거나 면 밖으로 나가는 설정, 면 크기를 몰라 확인하지 못한 Anchor. 적용을 막지 않는다 */
68
150
  get warnings() {
69
151
  var _a;
70
152
  if (!this.draft || this.issues.length)
71
153
  return [];
72
- const faces = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchorFaces;
73
- return slotSpaceWarnings(this.draft, { anchorFaces: faces }).filter(w => faces || w.code !== 'ANCHOR_SPACE_UNKNOWN');
154
+ return slotSpaceWarnings(this.draft, { anchorFaces: (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchorFaces });
74
155
  }
75
156
  /** 적용하면 뜻이 바뀌는 주소 (지금 적용된 값 기준) */
76
157
  get impact() {
77
158
  var _a, _b;
78
159
  return this.dirty && !this.issues.length ? addressImpact((_a = this.value) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.effective, this.draft) : { moved: [], gone: [] };
79
160
  }
161
+ _message(issue) {
162
+ var _a, _b, _c;
163
+ // 편집기가 아는 코드는 칸 옆에 위치 없이 보인다. 호스트 고유 코드는 호스트 문구
164
+ if (issue.code in ISSUE_TEXT)
165
+ return issueMessage(issue);
166
+ return (_c = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.issueText) === null || _b === void 0 ? void 0 : _b.call(_a, issue)) !== null && _c !== void 0 ? _c : issueMessage(issue);
167
+ }
80
168
  _text(issue) {
81
169
  var _a, _b, _c;
82
170
  return (_c = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.issueText) === null || _b === void 0 ? void 0 : _b.call(_a, issue)) !== null && _c !== void 0 ? _c : defaultIssueText(issue);
83
171
  }
84
172
  _edit(next) {
85
- var _a, _b;
173
+ var _a, _b, _c;
86
174
  this.draft = next;
87
175
  this.dirty = true;
176
+ this.fitNote = undefined;
88
177
  if (!next.tiers.some(x => x.tierNo === this.tier))
89
178
  this.tier = (_b = (_a = next.tiers[next.tiers.length - 1]) === null || _a === void 0 ? void 0 : _a.tierNo) !== null && _b !== void 0 ? _b : 1;
179
+ if (((_c = this.options) === null || _c === void 0 ? void 0 : _c.live) && !this.issues.length)
180
+ this._apply();
90
181
  }
91
182
  _apply() {
92
183
  this.value = structuredClone(this.draft);
184
+ this.dirty = false;
93
185
  this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
94
186
  }
95
187
  _useDefault() {
96
188
  this.dispatchEvent(new CustomEvent('slot-layout-use-default', { bubbles: true, composed: true }));
97
189
  }
98
190
  render() {
99
- var _a, _b, _c, _d;
191
+ var _a, _b, _c;
100
192
  const issues = this.issues;
101
- const canUseDefault = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.canUseDefault) !== null && _b !== void 0 ? _b : this.value !== undefined;
193
+ const live = !!((_a = this.options) === null || _a === void 0 ? void 0 : _a.live);
194
+ const canUseDefault = (_c = (_b = this.options) === null || _b === void 0 ? void 0 : _b.canUseDefault) !== null && _c !== void 0 ? _c : this.value !== undefined;
102
195
  return html `
103
- ${((_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.notes) === null || _d === void 0 ? void 0 : _d.length)
104
- ? html `<div class="notes">
105
- ${this.options.notes.map(n => html `<div class=${n.level === 'error' ? 'error' : 'note'}>${n.text}</div>`)}
106
- </div>`
107
- : nothing}
108
- ${this._renderPreset()} ${this.draft ? this._renderDraft(issues) : nothing} ${this._renderImpact()}
196
+ ${this._renderNotes()} ${this.draft ? this._renderDraft(issues) : nothing} ${this._renderPreset()} ${live ? nothing : this._renderImpact()}
109
197
  <div class="actions">
110
198
  ${canUseDefault ? html `<button data-action="use-default" @click=${this._useDefault}>${t('use-default', 'Use default')}</button>` : nothing}
111
- <button data-action="discard" ?disabled=${!this.dirty} @click=${() => this.reset()}>${t('discard', 'Discard')}</button>
112
- <button data-action="apply" ?disabled=${!this.dirty || !this.draft || issues.length > 0} @click=${this._apply}>${t('apply', 'Apply')}</button>
199
+ ${live
200
+ ? nothing
201
+ : html `
202
+ <button data-action="discard" ?disabled=${!this.dirty} @click=${() => this.reset()}>${t('discard', 'Discard')}</button>
203
+ <button data-action="apply" ?disabled=${!this.dirty || !this.draft || issues.length > 0} @click=${this._apply}>${t('apply', 'Apply')}</button>
204
+ `}
113
205
  </div>
114
206
  `;
115
207
  }
208
+ _renderNotes() {
209
+ var _a, _b;
210
+ const notes = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.notes) !== null && _b !== void 0 ? _b : [];
211
+ if (!notes.length)
212
+ return nothing;
213
+ const text = (n) => {
214
+ var _a;
215
+ return (_a = n.text) !== null && _a !== void 0 ? _a : (n.code === 'OVERRIDE_INVALID' && n.issue
216
+ ? t('override-invalid', "This placement's layout does not hold and offers no slots: {text}", { text: this._located(n.issue), interpolation: { escapeValue: false } })
217
+ : n.issue
218
+ ? this._located(n.issue)
219
+ : '');
220
+ };
221
+ return html `<div class="notes">${notes.map(n => html `<div class=${n.level === 'error' ? 'error' : 'note'}>${text(n)}</div>`)}</div>`;
222
+ }
223
+ /** 문제를 Tier·칸 이름과 함께. 칸을 모르면 경로 그대로 */
224
+ _located(issue) {
225
+ var _a, _b, _c, _d;
226
+ const { tierIndex, field } = issueField(issue);
227
+ const tierNo = tierIndex === undefined ? undefined : (_d = (_c = (_b = (_a = this.draft) === null || _a === void 0 ? void 0 : _a.tiers) === null || _b === void 0 ? void 0 : _b[tierIndex]) === null || _c === void 0 ? void 0 : _c.tierNo) !== null && _d !== void 0 ? _d : tierIndex + 1;
228
+ if (tierNo === undefined || !field)
229
+ return this._text(issue);
230
+ return `${t('tier-n', 'Tier {n}', { n: tierNo })} · ${fieldLabel(field)}: ${this._message(issue)}`;
231
+ }
116
232
  _renderPreset() {
117
233
  var _a, _b, _c, _d;
118
234
  const presets = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.presets) !== null && _b !== void 0 ? _b : [];
@@ -121,29 +237,29 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
121
237
  const preset = (_c = presets.find(p => p.key === this.presetKey)) !== null && _c !== void 0 ? _c : presets[0];
122
238
  const built = applyPreset(preset, this.presetSize, this.presetParams);
123
239
  const numberRow = (label, value, onChange, key) => html `
124
- <div class="row">
125
- <label>${label}</label>
126
- <input type="number" data-preset=${key} .value=${value === undefined || Number.isNaN(value) ? '' : String(value)} @change=${(e) => onChange(num(e))} />
127
- </div>
240
+ <label class="l">${label}</label>
241
+ <input type="number" data-preset=${key} .value=${shown(value)} @change=${(e) => onChange(num(e))} />
128
242
  `;
129
243
  return html `
130
- <h4>${t('preset', 'Preset')}</h4>
131
- <div class="row">
132
- <label>${t('preset-kind', 'Kind')}</label>
133
- <select data-preset="kind" @change=${(e) => (this.presetKey = e.target.value)}>
134
- ${presets.map(p => html `<option value=${p.key} ?selected=${p.key === preset.key}>${t(`preset.${p.key}`, p.key)}</option>`)}
135
- </select>
136
- </div>
137
- ${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
138
- ${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
139
- ${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
140
- ${((_d = preset.params) !== null && _d !== void 0 ? _d : []).map(p => numberRow(t(`preset-param.${p.key}`, p.key), this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
141
- <div class="note">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
142
- <div class="actions">
143
- <button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
144
- ${t('start-from-preset', 'Start from preset')}
145
- </button>
146
- </div>
244
+ <details data-section="preset" ?open=${!this.draft}>
245
+ <summary>${t('preset', 'Start again from a preset')}</summary>
246
+ <section>
247
+ <label class="l">${t('preset-kind', 'Kind')}</label>
248
+ <select data-preset="kind" @change=${(e) => (this.presetKey = e.target.value)}>
249
+ ${presets.map(p => html `<option value=${p.key} ?selected=${p.key === preset.key}>${t(`preset.${p.key}`, p.key)}</option>`)}
250
+ </select>
251
+ ${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
252
+ ${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
253
+ ${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
254
+ ${((_d = preset.params) !== null && _d !== void 0 ? _d : []).map(p => numberRow(t(`preset-param.${p.key}`, p.key), this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
255
+ <div class="hint">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
256
+ </section>
257
+ <div class="actions">
258
+ <button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
259
+ ${t('start-from-preset', 'Start from preset')}
260
+ </button>
261
+ </div>
262
+ </details>
147
263
  `;
148
264
  }
149
265
  /** 적용 전에 뜻이 바뀌는 주소를 보인다. 놓인 Carrier 는 주소로 다시 읽혀 다른 자리에 그려지거나 예외가 된다 */
@@ -164,174 +280,432 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
164
280
  this._edit({ ...extra, ...structuredClone(layout) });
165
281
  }
166
282
  _renderDraft(issues) {
167
- var _a, _b, _c, _d, _e, _f;
283
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
168
284
  const draft = this.draft;
169
285
  const tier = (_b = (_a = draft.tiers) === null || _a === void 0 ? void 0 : _a.find(x => x.tierNo === this.tier)) !== null && _b !== void 0 ? _b : (_c = draft.tiers) === null || _c === void 0 ? void 0 : _c[0];
170
- const warnings = this.warnings;
171
- const issueList = issues.length
172
- ? html `<div class="error" data-issues>${issues.map(i => html `<div>· ${this._text(i)}</div>`)}</div>`
173
- : warnings.length
174
- ? html `<div class="note" data-warnings>${warnings.map(w => html `<div>· ${spaceWarningText(w)}</div>`)}</div>`
175
- : nothing;
176
286
  // 층이 없는 값(잘못된 저장값)은 편집할 층이 없으므로 문제만 보인다. 프리셋으로 다시 시작한다
177
287
  if (!tier)
178
- return issueList;
288
+ return issues.length ? html `<div class="error" data-issues>${issues.map(i => html `<div>· ${this._text(i)}</div>`)}</div>` : nothing;
289
+ const index = draft.tiers.indexOf(tier);
290
+ // 고른 Tier 의 문제는 칸 옆에, 나머지(다른 Tier, 칸이 없는 경로)는 목록에
291
+ const errors = new Map();
292
+ const listed = [];
293
+ for (const issue of issues) {
294
+ const { tierIndex, field } = issueField(issue);
295
+ if (tierIndex === index && field)
296
+ errors.set(field, [...((_d = errors.get(field)) !== null && _d !== void 0 ? _d : []), this._message(issue)]);
297
+ else
298
+ listed.push(issue);
299
+ }
179
300
  return html `
180
- <h4>${t('tiers', 'Tiers')}</h4>
181
- <div class="chips">
182
- ${draft.tiers.map(x => html `<button data-tier=${x.tierNo} ?active=${x.tierNo === tier.tierNo} @click=${() => (this.tier = x.tierNo)}>${t('tier-n', 'Tier {n}', { n: x.tierNo })}</button>`)}
183
- </div>
184
- <div class="actions">
185
- <button data-action="copy-tier" @click=${() => this._copyTier(draft, tier.tierNo)}>${t('copy-tier', 'Copy tier')}</button>
301
+ ${this._renderPicture(draft, tier.tierNo)}
302
+ <div class="tiers">
303
+ ${draft.tiers.map(x => html `<button data-tier=${x.tierNo} aria-pressed=${x.tierNo === tier.tierNo ? 'true' : 'false'} @click=${() => (this.tier = x.tierNo)}>
304
+ ${t('tier-n', 'Tier {n}', { n: x.tierNo })}
305
+ </button>`)}
306
+ <button class="end" data-action="copy-tier" @click=${() => this._copyTier(draft, tier.tierNo)}>${t('copy-tier', 'Copy tier')}</button>
186
307
  <button data-action="remove-tier" ?disabled=${draft.tiers.length <= 1} @click=${() => this._edit(removeTier(draft, tier.tierNo))}>${t('remove-tier', 'Remove tier')}</button>
187
308
  </div>
188
- ${this._renderTier(draft, tier.tierNo)} ${(_f = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.renderExtra) === null || _e === void 0 ? void 0 : _e.call(_d, draft, next => this._edit(next))) !== null && _f !== void 0 ? _f : nothing}
189
- <h4>${t('preview', 'Preview (top view, tier {n}, in the tier)', { n: tier.tierNo })}</h4>
190
- ${this._renderPreview(draft, tier.tierNo)} ${issueList}
309
+ ${listed.length || (issues.length && ((_e = this.options) === null || _e === void 0 ? void 0 : _e.live))
310
+ ? html `<div class="error" data-issues>
311
+ ${listed.map(i => html `<div>· ${this._located(i)}</div>`)}
312
+ ${((_f = this.options) === null || _f === void 0 ? void 0 : _f.live) && issues.length ? html `<div class="note" data-held>${t('held-back', 'Not applied until fixed: the scene keeps the last sound layout.')}</div>` : nothing}
313
+ </div>`
314
+ : nothing}
315
+ ${this._renderTier(draft, tier.tierNo, errors)} ${(_j = (_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.renderExtra) === null || _h === void 0 ? void 0 : _h.call(_g, draft, next => this._edit(next))) !== null && _j !== void 0 ? _j : nothing}
191
316
  `;
192
317
  }
193
318
  /** 복사한 Tier 를 골라 둔다. 같은 자리에 겹쳐 생기므로 바로 이어서 배치를 고친다 */
194
319
  _copyTier(draft, tierNo) {
195
320
  const next = copyTier(draft, tierNo);
196
- this._edit(next);
197
321
  this.tier = next.tiers[next.tiers.length - 1].tierNo;
322
+ this._edit(next);
323
+ }
324
+ // ── 그림 ──────────────────────────────────────────────────────────────────
325
+ _anchorName(id) {
326
+ var _a, _b, _c, _d;
327
+ const a = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors) === null || _b === void 0 ? void 0 : _b.find(x => x.id === id);
328
+ if (a === null || a === void 0 ? void 0 : a.label)
329
+ return a.label;
330
+ if (a === null || a === void 0 ? void 0 : a.part)
331
+ return `${id} · ${t(`face-${(_c = a.face) !== null && _c !== void 0 ? _c : 'top'}`, `{part} ${(_d = a.face) !== null && _d !== void 0 ? _d : 'top'}`, { part: a.part })}`;
332
+ return id;
333
+ }
334
+ _unit() {
335
+ var _a;
336
+ return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.unit) ? html ` <span class="unit">${this.options.unit}</span>` : nothing;
198
337
  }
199
- _renderTier(draft, tierNo) {
338
+ /** 고른 Tier 의 Anchor 면 위에서 본 자리 (Anchor 기준). 오른쪽이 +X, 위쪽이 −Z */
339
+ _renderPicture(draft, tierNo) {
200
340
  var _a, _b, _c, _d, _e, _f, _g, _h;
201
341
  const tier = draft.tiers.find(x => x.tierNo === tierNo);
202
- const l = tier.layout;
203
- const patternInput = (k, label, value, parse = num) => html `
204
- <div class="row">
205
- <label>${label}</label>
206
- <input type="number" data-field=${k} .value=${value === undefined || Number.isNaN(value) ? '' : String(value)} @change=${(e) => this._edit(updatePattern(draft, tierNo, { [k]: parse(e) }))} />
207
- </div>
208
- `;
209
- const select = (field, label, value, options, onChange) => html `
210
- <div class="row">
211
- <label>${label}</label>
212
- <select data-field=${field} @change=${(e) => onChange(e.target.value)}>
213
- ${options.map(o => html `<option value=${o} ?selected=${o === value}>${o}</option>`)}
214
- </select>
215
- </div>
216
- `;
342
+ const face = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchorFaces) === null || _b === void 0 ? void 0 : _b[tier.anchorId];
343
+ const slots = anchorFaceSlots(draft, tier.anchorId);
344
+ const mine = slots.filter(s => s.tier === tierNo);
345
+ if (!mine.length)
346
+ return html `<div class="note" data-preview="none">${t('preview-none', 'No slots to show with the current values.')}</div>`;
347
+ const boxes = slots.map(s => ({ s, fp: rotatedFootprint({ width: s.width, depth: s.depth, height: 0 }, s.rotation) }));
348
+ const xs = boxes.flatMap(({ s, fp }) => [s.x - fp.width / 2, s.x + fp.width / 2]).concat(face ? [-face.width / 2, face.width / 2] : []);
349
+ const zs = boxes.flatMap(({ s, fp }) => [s.z - fp.depth / 2, s.z + fp.depth / 2]).concat(face ? [-face.depth / 2, face.depth / 2] : []);
350
+ const [minX, maxX, minZ, maxZ] = [Math.min(...xs), Math.max(...xs), Math.min(...zs), Math.max(...zs)];
351
+ const size = Math.max(maxX - minX, maxZ - minZ, 1);
352
+ const pad = size * 0.06;
353
+ const viewBox = (_d = (_c = this._drag) === null || _c === void 0 ? void 0 : _c.viewBox) !== null && _d !== void 0 ? _d : `${minX - pad} ${minZ - pad} ${maxX - minX + pad * 2} ${maxZ - minZ + pad * 2}`;
354
+ const font = size * 0.045;
355
+ const stroke = size * 0.004;
356
+ const flagged = new Map();
357
+ for (const w of this.warnings) {
358
+ const id = `t${(_e = w.params) === null || _e === void 0 ? void 0 : _e.tier}_s${(_f = w.params) === null || _f === void 0 ? void 0 : _f.slot}`;
359
+ if (w.code === 'SLOT_OUTSIDE_ANCHOR')
360
+ flagged.set(id, 'out');
361
+ else if (w.code === 'SLOT_OVERLAP' && !flagged.has(id))
362
+ flagged.set(id, 'overlap');
363
+ }
364
+ const items = (_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.items) !== null && _h !== void 0 ? _h : [];
365
+ const onSlot = (id) => items.filter(i => i.slot === id).map(i => i.id);
366
+ const tl = tier.layout;
367
+ const last = tl.pattern === 'ROW' && tl.count >= 2 ? mine.find(s => s.slotNo === tl.count) : undefined;
368
+ /* 간격 손잡이: 끝 자리 안, 줄과 직각으로 가장자리 쪽에 둔다(글자를 가리지 않게). 간격은 줄 방향 거리로만 정해진다 */
369
+ const handle = last && (() => {
370
+ const first = mine.find(s => s.slotNo === 1);
371
+ const length = Math.hypot(last.x - first.x, last.z - first.z) || 1;
372
+ const across = { x: -(last.z - first.z) / length, z: (last.x - first.x) / length };
373
+ const room = Math.min(last.width, last.depth) / 2 - font;
374
+ return { x: last.x + across.x * room, z: last.z + across.z * room };
375
+ })();
217
376
  return html `
218
- ${this._renderPlacement(draft, tierNo)}
219
- ${select('pattern', t('pattern', 'Pattern'), l.pattern, ['SINGLE', 'ROW', 'GRID', 'CUSTOM'], v => this._edit(setTierPattern(draft, tierNo, v)))}
220
- ${l.pattern === 'ROW'
221
- ? html `
222
- ${patternInput('count', t('count', 'Slots'), l.count)} ${patternInput('pitch', t('pitch', 'Pitch'), l.pitch)}
223
- ${select('axis', t('axis', 'Axis'), l.axis, ['X', 'Z'], v => this._edit(updatePattern(draft, tierNo, { axis: v })))}
224
- ${select('origin', t('origin', 'Slot 1 at'), (_a = l.origin) !== null && _a !== void 0 ? _a : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: v })))}
225
- `
377
+ <svg
378
+ data-preview
379
+ viewBox=${viewBox}
380
+ preserveAspectRatio="xMidYMid meet"
381
+ tabindex="0"
382
+ @pointermove=${this._onDragMove}
383
+ @pointerup=${this._onDragEnd}
384
+ @pointercancel=${this._cancelDrag}
385
+ >
386
+ ${face
387
+ ? svg `<rect data-face x=${-face.width / 2} y=${-face.depth / 2} width=${face.width} height=${face.depth}
388
+ fill="rgba(0,0,0,0.04)" stroke="rgba(0,0,0,0.45)" stroke-width=${stroke} />`
226
389
  : nothing}
227
- ${l.pattern === 'GRID'
228
- ? html `
229
- ${patternInput('rows', t('rows', 'Rows (Z)'), l.rows)} ${patternInput('cols', t('cols', 'Columns (X)'), l.cols)}
230
- ${patternInput('pitch', t('pitch-x', 'Pitch X'), (_b = l.pitch) === null || _b === void 0 ? void 0 : _b.x, e => ({ ...l.pitch, x: num(e) }))}
231
- ${patternInput('pitch', t('pitch-z', 'Pitch Z'), (_c = l.pitch) === null || _c === void 0 ? void 0 : _c.z, e => ({ ...l.pitch, z: num(e) }))}
232
- ${select('order', t('order', 'Numbering'), (_d = l.order) !== null && _d !== void 0 ? _d : 'ROW_MAJOR', ['ROW_MAJOR', 'COL_MAJOR', 'SERPENTINE_ROW', 'SERPENTINE_COL'], v => this._edit(updatePattern(draft, tierNo, { order: v })))}
233
- ${select('origin-x', t('origin-x', 'Slot 1 at X'), (_f = (_e = l.origin) === null || _e === void 0 ? void 0 : _e.x) !== null && _f !== void 0 ? _f : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: { z: 'MIN', ...l.origin, x: v } })))}
234
- ${select('origin-z', t('origin-z', 'Slot 1 at Z'), (_h = (_g = l.origin) === null || _g === void 0 ? void 0 : _g.z) !== null && _h !== void 0 ? _h : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: { x: 'MIN', ...l.origin, z: v } })))}
235
- `
390
+ ${slots
391
+ .filter(s => s.tier !== tierNo)
392
+ .map(s => svg `<g transform="translate(${s.x} ${s.z}) rotate(${-s.rotation})">
393
+ <rect data-other-slot=${s.id} x=${-s.width / 2} y=${-s.depth / 2} width=${s.width} height=${s.depth}
394
+ fill="none" stroke="rgba(0,0,0,0.35)" stroke-dasharray="${stroke * 4} ${stroke * 3}" stroke-width=${stroke} />
395
+ </g>`)}
396
+ ${mine.map(s => {
397
+ const flag = flagged.get(s.id);
398
+ const colour = flag ? ERROR : SLOT;
399
+ return svg `<g transform="translate(${s.x} ${s.z}) rotate(${-s.rotation})">
400
+ <rect data-slot=${s.slotNo} data-slot-id=${s.id} data-flag=${flag !== null && flag !== void 0 ? flag : ''} x=${-s.width / 2} y=${-s.depth / 2} width=${s.width} height=${s.depth}
401
+ fill=${flag ? 'rgba(198,40,40,0.1)' : 'rgba(25,118,210,0.1)'} stroke=${colour} stroke-width=${stroke}
402
+ stroke-dasharray=${flag ? `${stroke * 4} ${stroke * 3}` : 'none'}
403
+ @pointerdown=${(e) => this._startDrag(e, 'move', tierNo)} />
404
+ </g>`;
405
+ })}
406
+ ${mine.map(s => {
407
+ const held = onSlot(s.id);
408
+ return svg `<text x=${s.x} y=${s.z - (held.length ? font * 0.6 : 0)} font-size=${font} text-anchor="middle" dominant-baseline="central"
409
+ fill=${flagged.has(s.id) ? '#791f1f' : '#0d47a1'} pointer-events="none">${s.id}</text>
410
+ ${held.length
411
+ ? svg `<text data-held-on=${s.id} x=${s.x} y=${s.z + font * 0.7} font-size=${font * 0.85} text-anchor="middle" dominant-baseline="central"
412
+ fill="rgba(0,0,0,0.6)" pointer-events="none">${held.length > 2 ? `${held.slice(0, 2).join(', ')} +${held.length - 2}` : held.join(', ')}</text>`
413
+ : nothing}`;
414
+ })}
415
+ ${handle
416
+ ? svg `<circle data-handle="pitch" cx=${handle.x} cy=${handle.z} r=${font * 0.45} fill=${SLOT} stroke="#fff" stroke-width=${stroke}
417
+ @pointerdown=${(e) => this._startDrag(e, 'pitch', tierNo)} />`
236
418
  : nothing}
237
- ${l.pattern === 'CUSTOM' ? this._renderCustom(draft, tierNo) : this._renderSpec(draft, tierNo, l.slot)}
419
+ </svg>
420
+ <div class="caption">
421
+ <span data-face-caption>
422
+ ${face
423
+ ? html `${this._anchorName(tier.anchorId)} · ${face.width} × ${face.depth}${this._unit()}`
424
+ : html `<span class="error">${t('face-unknown', 'Face size unknown: whether slots stay on it is not checked')}</span>`}
425
+ </span>
426
+ <span>${t('preview-axes', 'Right = +X, up = −Z')}</span>
427
+ </div>
428
+ ${this._renderSpaceWarnings(tier.anchorId, !!face)} ${this._renderItems(draft)}
238
429
  `;
239
430
  }
240
- /** Tier 가 선 Anchor, 적재 방식, Anchor 기준 위치·회전 */
241
- _renderPlacement(draft, tierNo) {
242
- var _a, _b, _c, _d, _e, _f;
431
+ /** 면 밖·겹침 경고. 고른 Tier 의 Anchor 면을 모른다는 것은 그림 아래에 이미 적었다 */
432
+ _renderSpaceWarnings(anchorId, faceKnown) {
433
+ const warnings = this.warnings.filter(w => { var _a; return !(w.code === 'ANCHOR_SPACE_UNKNOWN' && !faceKnown && ((_a = w.params) === null || _a === void 0 ? void 0 : _a.anchorId) === anchorId); });
434
+ if (!warnings.length)
435
+ return nothing;
436
+ return html `<div class="error" data-warnings>${warnings.map(w => html `<div>· ${spaceWarningText(w)}</div>`)}</div>`;
437
+ }
438
+ /**
439
+ * 실린 아이템이 편집 중인 레이아웃에서 어떻게 되는지. 자리가 없어지면 미배치로 남고(지우거나 옮기지 않는다), 받는 타입·규격은
440
+ * 공통 판정으로 본다. 치수를 모르는 아이템은 판정하지 않았다고 적는다 — 모양을 자리에 맞춰 그리는 것은 판정이 아니다.
441
+ */
442
+ _renderItems(draft) {
443
+ var _a, _b;
444
+ const items = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : [];
445
+ if (!items.length)
446
+ return nothing;
447
+ const statuses = heldItemStatuses(draft, items);
448
+ const shownItems = statuses.slice(0, 12);
449
+ return html `<div class="items" data-items>
450
+ ${shownItems.map(s => {
451
+ var _a;
452
+ const line = [];
453
+ if (!s.placed)
454
+ line.push(html `<span class="bad">${fitText('SLOT_NOT_FOUND')}</span>`);
455
+ else {
456
+ if (s.unregistered)
457
+ line.push(html `<span class="warn">${t('unregistered-type', 'Type not registered')}</span>`);
458
+ for (const r of s.reasons)
459
+ line.push(html `<span class=${r === 'SPEC_UNKNOWN' ? 'note' : 'bad'}>${fitText(r)}</span>`);
460
+ if (s.verdict === 'OK')
461
+ line.push(html `<span class="note">OK</span>`);
462
+ }
463
+ return html `<div data-item=${s.item.id} data-verdict=${s.placed ? s.verdict : 'UNPLACED'}>
464
+ <b>${s.item.id}</b> · ${(_a = s.item.slot) !== null && _a !== void 0 ? _a : '—'}${s.item.code ? html ` · ${s.item.code}` : nothing} — ${line.map((x, i) => html `${i ? ', ' : ''}${x}`)}
465
+ </div>`;
466
+ })}
467
+ ${statuses.length > shownItems.length ? html `<div class="note">… (${statuses.length})</div>` : nothing}
468
+ </div>`;
469
+ }
470
+ // ── 끌기 ──────────────────────────────────────────────────────────────────
471
+ _point(e) {
472
+ const svgEl = this.renderRoot.querySelector('svg[data-preview]');
473
+ const m = svgEl === null || svgEl === void 0 ? void 0 : svgEl.getScreenCTM();
474
+ if (!m)
475
+ return null;
476
+ const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(m.inverse());
477
+ return { x: p.x, z: p.y };
478
+ }
479
+ _startDrag(e, kind, tierNo) {
480
+ var _a;
481
+ const from = this._point(e);
482
+ const svgEl = this.renderRoot.querySelector('svg[data-preview]');
483
+ if (!from || !svgEl || !this.draft)
484
+ return;
485
+ e.preventDefault();
486
+ e.stopPropagation();
487
+ try {
488
+ svgEl.setPointerCapture(e.pointerId);
489
+ }
490
+ catch (_b) {
491
+ // 이미 놓인 포인터: 끌기는 svg 안에서만 이어진다
492
+ }
493
+ this._drag = { kind, tierNo, from, before: structuredClone(this.draft), viewBox: (_a = svgEl.getAttribute('viewBox')) !== null && _a !== void 0 ? _a : '' };
494
+ // 끄는 동안의 Esc 는 포커스가 어디 있든 끌기를 거둔다
495
+ window.addEventListener('keydown', this._onDragKey, true);
496
+ }
497
+ _endDrag() {
498
+ const d = this._drag;
499
+ this._drag = undefined;
500
+ window.removeEventListener('keydown', this._onDragKey, true);
501
+ return d;
502
+ }
503
+ disconnectedCallback() {
504
+ super.disconnectedCallback();
505
+ this._endDrag();
506
+ }
507
+ // ── 칸 ────────────────────────────────────────────────────────────────────
508
+ _row(field, label, control, errors) {
509
+ const messages = errors.get(field);
510
+ return html `
511
+ <label class="l">${label}</label>
512
+ ${control} ${messages ? html `<div class="err" data-error=${field}>${messages.join(' · ')}</div>` : nothing}
513
+ `;
514
+ }
515
+ _num(field, value, onChange, errors, extra = {}) {
516
+ var _a, _b;
517
+ return html `<input
518
+ type="number"
519
+ data-field=${field}
520
+ ?invalid=${errors.has(field)}
521
+ min=${(_a = extra.min) !== null && _a !== void 0 ? _a : nothing}
522
+ step=${(_b = extra.step) !== null && _b !== void 0 ? _b : nothing}
523
+ .value=${shown(value)}
524
+ @change=${(e) => onChange(num(e))}
525
+ />`;
526
+ }
527
+ _seg(field, value, options, onChange, errors) {
528
+ return html `<div class="seg" role="radiogroup" data-field=${field} ?invalid=${errors.has(field)}>
529
+ ${options.map(([v, label]) => html `<button type="button" role="radio" data-value=${v} aria-checked=${v === value ? 'true' : 'false'} title=${label} @click=${() => v !== value && onChange(v)}>
530
+ ${label}
531
+ </button>`)}
532
+ </div>`;
533
+ }
534
+ _renderTier(draft, tierNo, errors) {
535
+ var _a, _b, _c, _d, _e;
243
536
  const tier = draft.tiers.find(x => x.tierNo === tierNo);
244
537
  const anchors = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors;
245
- const edit = (patch) => this._edit(updateTier(draft, tierNo, patch));
538
+ const editTier = (patch) => this._edit(updateTier(draft, tierNo, patch));
246
539
  const position = (_b = tier.position) !== null && _b !== void 0 ? _b : { x: 0, y: 0, z: 0 };
247
- const axis = (k, label) => {
248
- var _a;
249
- return html `
250
- <div class="row">
251
- <label>${label}</label>
252
- <input type="number" data-field=${`position-${k}`} .value=${String((_a = position[k]) !== null && _a !== void 0 ? _a : '')} @change=${(e) => edit({ position: { ...position, [k]: num(e) } })} />
253
- </div>
540
+ const fine = ['position-x', 'position-y', 'position-z', 'tier-rotation'].some(f => errors.has(f));
541
+ const u = this._unit();
542
+ return html `
543
+ <section data-section="where">
544
+ <h4>${t('where', 'Where')}</h4>
545
+ ${this._row('anchor', t('face', 'Face'), (anchors === null || anchors === void 0 ? void 0 : anchors.length)
546
+ ? html `<select data-field="anchor" ?invalid=${errors.has('anchor')} @change=${(e) => editTier({ anchorId: e.target.value })}>
547
+ ${anchors.some(a => a.id === tier.anchorId) ? nothing : html `<option value=${(_c = tier.anchorId) !== null && _c !== void 0 ? _c : ''} selected>${(_d = tier.anchorId) !== null && _d !== void 0 ? _d : ''}</option>`}
548
+ ${anchors.map(a => html `<option value=${a.id} ?selected=${a.id === tier.anchorId}>${this._anchorName(a.id)}</option>`)}
549
+ </select>`
550
+ : html `<input data-field="anchor" ?invalid=${errors.has('anchor')} .value=${(_e = tier.anchorId) !== null && _e !== void 0 ? _e : ''} @change=${(e) => editTier({ anchorId: e.target.value.trim() })} />`, errors)}
551
+ ${this._row('mount', t('mount', 'Mount'), this._seg('mount', tier.mount, [
552
+ ['ON', t('mount.ON', 'On')],
553
+ ['IN', t('mount.IN', 'In')],
554
+ ['HANG', t('mount.HANG', 'Hang')]
555
+ ], mount => editTier({ mount }), errors), errors)}
556
+ </section>
557
+ ${this._renderArrangement(draft, tierNo, errors)} ${this._renderSlot(draft, tierNo, errors)}
558
+ <details data-section="fine" ?open=${fine}>
559
+ <summary>${t('fine', 'Position and rotation')}</summary>
560
+ <section>
561
+ ${this._row('position-x', html `${t('position-x', 'Position X')}${u}`, this._num('position-x', position.x, n => editTier({ position: { ...position, x: n } }), errors), errors)}
562
+ ${this._row('position-y', html `${t('position-y', 'Position Y (height)')}${u}`, this._num('position-y', position.y, n => editTier({ position: { ...position, y: n } }), errors), errors)}
563
+ ${this._row('position-z', html `${t('position-z', 'Position Z')}${u}`, this._num('position-z', position.z, n => editTier({ position: { ...position, z: n } }), errors), errors)}
564
+ ${this._row('tier-rotation', t('tier-rotation', 'Tier rotation (deg)'), this._num('tier-rotation', tier.rotation, n => editTier({ rotation: n }), errors), errors)}
565
+ </section>
566
+ </details>
254
567
  `;
568
+ }
569
+ /** 자리를 어떻게 늘어놓는가: 패턴, 수, 간격, 방향, 1번 자리. 그리고 [면에 맞춤] */
570
+ _renderArrangement(draft, tierNo, errors) {
571
+ var _a, _b, _c, _d, _e, _f, _g;
572
+ const tier = draft.tiers.find(x => x.tierNo === tierNo);
573
+ const l = tier.layout;
574
+ const edit = (patch) => this._edit(updatePattern(draft, tierNo, patch));
575
+ const u = this._unit();
576
+ const side = (axis) => axis === 'X'
577
+ ? [
578
+ ['MIN', t('side.left', 'Left')],
579
+ ['MAX', t('side.right', 'Right')]
580
+ ]
581
+ : [
582
+ ['MIN', t('side.top', 'Top')],
583
+ ['MAX', t('side.bottom', 'Bottom')]
584
+ ];
585
+ const fit = () => {
586
+ var _a, _b;
587
+ const result = fitTierToFace(draft, tierNo, (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchorFaces) === null || _b === void 0 ? void 0 : _b[tier.anchorId]);
588
+ if ('layout' in result)
589
+ this._edit(result.layout);
590
+ else
591
+ this.fitNote = fitFaceText(result.reason);
255
592
  };
593
+ const fitButton = html `<button data-action="fit-face" title=${t('fit-face-tip', 'Centre on the face and spread the slots end to end, once')} @click=${fit}>
594
+ ${t('fit-face', 'Fit to face')}
595
+ </button>`;
256
596
  return html `
257
- <div class="row">
258
- <label>${t('anchor', 'Anchor')}</label>
259
- ${(anchors === null || anchors === void 0 ? void 0 : anchors.length)
260
- ? html `<select data-field="anchor" @change=${(e) => edit({ anchorId: e.target.value })}>
261
- ${anchors.some(a => a.id === tier.anchorId) ? nothing : html `<option value=${(_c = tier.anchorId) !== null && _c !== void 0 ? _c : ''} selected>${(_d = tier.anchorId) !== null && _d !== void 0 ? _d : ''}</option>`}
262
- ${anchors.map(a => { var _a; return html `<option value=${a.id} ?selected=${a.id === tier.anchorId}>${(_a = a.label) !== null && _a !== void 0 ? _a : a.id}</option>`; })}
263
- </select>`
264
- : html `<input data-field="anchor" .value=${(_e = tier.anchorId) !== null && _e !== void 0 ? _e : ''} @change=${(e) => edit({ anchorId: e.target.value.trim() })} />`}
265
- </div>
266
- <div class="row">
267
- <label>${t('mount', 'Mount')}</label>
268
- <select data-field="mount" @change=${(e) => edit({ mount: e.target.value })}>
269
- ${['ON', 'IN', 'HANG'].map(m => html `<option value=${m} ?selected=${m === tier.mount}>${m}</option>`)}
270
- </select>
271
- </div>
272
- ${axis('x', t('position-x', 'Position X'))} ${axis('y', t('position-y', 'Position Y (height)'))} ${axis('z', t('position-z', 'Position Z'))}
273
- <div class="row">
274
- <label>${t('tier-rotation', 'Tier rotation (deg)')}</label>
275
- <input type="number" data-field="tier-rotation" .value=${String((_f = tier.rotation) !== null && _f !== void 0 ? _f : '')} @change=${(e) => edit({ rotation: num(e) })} />
276
- </div>
597
+ <section data-section="arrangement">
598
+ <h4>${t('arrangement', 'Arrangement')}</h4>
599
+ ${this._row('pattern', t('pattern', 'Arrangement'), this._seg('pattern', l.pattern, [
600
+ ['SINGLE', t('pattern.SINGLE', 'One')],
601
+ ['ROW', t('pattern.ROW', 'Row')],
602
+ ['GRID', t('pattern.GRID', 'Grid')],
603
+ ['CUSTOM', t('pattern.CUSTOM', 'Listed')]
604
+ ], v => this._edit(setTierPattern(draft, tierNo, v)), errors), errors)}
605
+ ${l.pattern === 'ROW'
606
+ ? html `
607
+ ${this._row('count', t('count', 'Slots'), this._num('count', l.count, n => edit({ count: n }), errors, { min: '1', step: '1' }), errors)}
608
+ ${this._row('pitch', html `${t('pitch', 'Pitch')}${u}`, html `<div class="inline">${this._num('pitch', l.pitch, n => edit({ pitch: n }), errors)} ${fitButton}</div>`, errors)}
609
+ ${this._row('axis', t('axis', 'Direction'), this._seg('axis', l.axis, [
610
+ ['X', t('axis.X', 'Across (X)')],
611
+ ['Z', t('axis.Z', 'Down (Z)')]
612
+ ], axis => edit({ axis }), errors), errors)}
613
+ ${this._row('origin', t('origin', 'Slot 1 at'), this._seg('origin', (_a = l.origin) !== null && _a !== void 0 ? _a : 'MIN', side(l.axis), origin => edit({ origin }), errors), errors)}
614
+ `
615
+ : nothing}
616
+ ${l.pattern === 'GRID'
617
+ ? html `
618
+ ${this._row('rows', t('rows', 'Rows (Z)'), this._num('rows', l.rows, n => edit({ rows: n }), errors, { min: '1', step: '1' }), errors)}
619
+ ${this._row('cols', t('cols', 'Columns (X)'), this._num('cols', l.cols, n => edit({ cols: n }), errors, { min: '1', step: '1' }), errors)}
620
+ ${this._row('pitch-x', html `${t('pitch-x', 'Pitch X')}${u}`, this._num('pitch-x', (_b = l.pitch) === null || _b === void 0 ? void 0 : _b.x, n => edit({ pitch: { ...l.pitch, x: n } }), errors), errors)}
621
+ ${this._row('pitch-z', html `${t('pitch-z', 'Pitch Z')}${u}`, html `<div class="inline">${this._num('pitch-z', (_c = l.pitch) === null || _c === void 0 ? void 0 : _c.z, n => edit({ pitch: { ...l.pitch, z: n } }), errors)} ${fitButton}</div>`, errors)}
622
+ ${this._row('order', t('order', 'Numbering'), html `<select data-field="order" @change=${(e) => edit({ order: e.target.value })}>
623
+ ${['ROW_MAJOR', 'COL_MAJOR', 'SERPENTINE_ROW', 'SERPENTINE_COL'].map(o => { var _a; return html `<option value=${o} ?selected=${o === ((_a = l.order) !== null && _a !== void 0 ? _a : 'ROW_MAJOR')}>${t(`order.${o}`, o)}</option>`; })}
624
+ </select>`, errors)}
625
+ ${this._row('origin-x', t('origin-x', 'Slot 1 at X'), this._seg('origin-x', (_e = (_d = l.origin) === null || _d === void 0 ? void 0 : _d.x) !== null && _e !== void 0 ? _e : 'MIN', side('X'), x => edit({ origin: { z: 'MIN', ...l.origin, x } }), errors), errors)}
626
+ ${this._row('origin-z', t('origin-z', 'Slot 1 at Z'), this._seg('origin-z', (_g = (_f = l.origin) === null || _f === void 0 ? void 0 : _f.z) !== null && _g !== void 0 ? _g : 'MIN', side('Z'), z => edit({ origin: { x: 'MIN', ...l.origin, z } }), errors), errors)}
627
+ `
628
+ : nothing}
629
+ ${l.pattern === 'SINGLE' ? html `<label class="l"></label><div class="inline">${fitButton}</div>` : nothing}
630
+ ${this.fitNote ? html `<div class="err" data-fit-note>${this.fitNote}</div>` : nothing}
631
+ </section>
632
+ ${l.pattern === 'CUSTOM' ? this._renderCustom(draft, tierNo) : nothing}
277
633
  `;
278
634
  }
279
- /** 받는 타입: 모든 타입(accepts 생략) / 지정 타입만(빈 목록이면 아무것도 받지 않음) */
280
- _acceptsEditor(spec, onChange) {
281
- var _a;
282
- const restricted = Array.isArray(spec.accepts);
283
- return html `
284
- <div class="row">
285
- <label>${t('accepts', 'Accepts')}</label>
286
- <select data-field="accepts-mode" @change=${(e) => { var _a; return onChange(e.target.value === 'ALL' ? undefined : (_a = spec.accepts) !== null && _a !== void 0 ? _a : []); }}>
287
- <option value="ALL" ?selected=${!restricted}>${t('accepts-all', 'Any type')}</option>
288
- <option value="LIST" ?selected=${restricted}>${t('accepts-list', 'Listed types only')}</option>
289
- </select>
290
- </div>
291
- ${restricted
292
- ? html `<div class="row">
293
- <label>${t('accepts-types', 'Types (comma)')}</label>
294
- <input
295
- data-field="accepts"
296
- .value=${((_a = spec.accepts) !== null && _a !== void 0 ? _a : []).join(', ')}
297
- placeholder=${t('accepts-none', 'Empty = accepts nothing')}
298
- @change=${(e) => onChange(e.target.value
635
+ /** 받는 타입: 목록에서 고른 코드(목록에 없는 코드는 그대로 두고 표시), 목록이 없으면 직접 적는다. 필수가 아니면 [모든 타입]도 */
636
+ _acceptsEditor(spec, onChange, errors) {
637
+ var _a, _b, _c;
638
+ const required = !!((_a = this.options) === null || _a === void 0 ? void 0 : _a.acceptsRequired);
639
+ const restricted = required || Array.isArray(spec.accepts);
640
+ const types = (_b = this.options) === null || _b === void 0 ? void 0 : _b.itemTypes;
641
+ const accepts = (_c = spec.accepts) !== null && _c !== void 0 ? _c : [];
642
+ const mode = required
643
+ ? nothing
644
+ : this._row('accepts-mode', t('accepts', 'Accepts'), this._seg('accepts-mode', restricted ? 'LIST' : 'ALL', [
645
+ ['ALL', t('accepts-all', 'Any type')],
646
+ ['LIST', t('accepts-list', 'Listed types only')]
647
+ ], v => { var _a; return onChange(v === 'ALL' ? undefined : ((_a = spec.accepts) !== null && _a !== void 0 ? _a : [])); }, errors), errors);
648
+ if (!restricted)
649
+ return mode;
650
+ const label = required ? t('accepts', 'Accepts') : t('accepts-types', 'Types');
651
+ const control = types
652
+ ? html `<div class="chips" data-field="accepts" ?invalid=${errors.has('accepts')}>
653
+ ${accepts.map(code => {
654
+ const known = types.find(x => x.code === code);
655
+ return html `<span class="chip" data-code=${code} ?unregistered=${!known} title=${known ? code : t('unregistered-type', 'Type not registered')}>
656
+ ${(known === null || known === void 0 ? void 0 : known.name) && known.name !== code ? `${known.name} (${code})` : code}${known ? nothing : ' ⚠'}
657
+ <button aria-label=${t('remove', 'Remove')} @click=${() => onChange(accepts.filter(c => c !== code))}>×</button>
658
+ </span>`;
659
+ })}
660
+ <select
661
+ data-field="accepts-add"
662
+ @change=${(e) => {
663
+ const select = e.target;
664
+ const code = select.value;
665
+ select.value = '';
666
+ if (code)
667
+ onChange([...accepts, code]);
668
+ }}
669
+ >
670
+ <option value="">${t('accepts-add', '+ Add type')}</option>
671
+ ${types.filter(x => !accepts.includes(x.code)).map(x => html `<option value=${x.code}>${x.name && x.name !== x.code ? `${x.name} (${x.code})` : x.code}</option>`)}
672
+ </select>
673
+ </div>`
674
+ : html `<input
675
+ data-field="accepts"
676
+ ?invalid=${errors.has('accepts')}
677
+ .value=${accepts.join(', ')}
678
+ placeholder=${t('accepts-none', 'Empty = accepts nothing')}
679
+ @change=${(e) => onChange(e.target.value
299
680
  .split(',')
300
681
  .map(s => s.trim())
301
682
  .filter(Boolean))}
302
- />
303
- </div>`
304
- : nothing}
305
- `;
683
+ />`;
684
+ return html `${mode} ${this._row('accepts', label, control, errors)}`;
306
685
  }
307
- _renderSpec(draft, tierNo, spec) {
308
- var _a, _b;
686
+ /** 자리 하나의 크기·받는 것·쌓기 (이 Tier 의 모든 자리 공통). CUSTOM 은 자리 표에서 */
687
+ _renderSlot(draft, tierNo, errors) {
688
+ var _a, _b, _c;
689
+ const tier = draft.tiers.find(x => x.tierNo === tierNo);
690
+ if (tier.layout.pattern === 'CUSTOM')
691
+ return nothing;
692
+ const spec = tier.layout.slot;
309
693
  const edit = (patch) => this._edit(updateSlotSpec(draft, tierNo, patch));
310
- const allowed = (k, label) => {
311
- var _a, _b;
312
- return html `
313
- <div class="row">
314
- <label>${label}</label>
315
- <input type="number" data-field=${`allowed-${k}`} .value=${String((_b = (_a = spec.allowed) === null || _a === void 0 ? void 0 : _a[k]) !== null && _b !== void 0 ? _b : '')} @change=${(e) => edit({ allowed: { ...spec.allowed, [k]: num(e) } })} />
316
- </div>
317
- `;
318
- };
694
+ const u = this._unit();
695
+ const allowed = (k, field, label) => { var _a; return this._row(field, html `${label}${u}`, this._num(field, (_a = spec.allowed) === null || _a === void 0 ? void 0 : _a[k], n => edit({ allowed: { ...spec.allowed, [k]: n } }), errors), errors); };
319
696
  return html `
320
- <h4>${t('slot-spec', 'Slot (same for every slot in this tier)')}</h4>
321
- ${allowed('width', t('allowed-width', 'Allowed width'))} ${allowed('depth', t('allowed-depth', 'Allowed depth'))}
322
- ${allowed('height', t('allowed-height', 'Allowed height'))}
323
- <div class="row">
324
- <label>${t('stack-limit', 'Stack limit')}</label>
325
- <input type="number" data-field="stack-limit" min="1" step="1" .value=${String((_a = spec.stackLimit) !== null && _a !== void 0 ? _a : 1)} @change=${(e) => edit({ stackLimit: num(e) })} />
326
- </div>
327
- <div class="row">
328
- <label>${t('rotation', 'Rotation (deg)')}</label>
329
- <input type="number" data-field="rotation" .value=${String((_b = spec.rotation) !== null && _b !== void 0 ? _b : 0)} @change=${(e) => edit({ rotation: num(e) || undefined })} />
330
- </div>
331
- ${this._acceptsEditor(spec, accepts => edit({ accepts }))}
697
+ <section data-section="slot">
698
+ <h4>${t('slot-spec', 'Each slot')}</h4>
699
+ ${this._acceptsEditor(spec, accepts => edit({ accepts }), errors)} ${allowed('width', 'allowed-width', t('allowed-width', 'Width (X)'))}
700
+ ${allowed('depth', 'allowed-depth', t('allowed-depth', 'Depth (Z)'))} ${allowed('height', 'allowed-height', t('allowed-height', 'Height'))}
701
+ ${((_a = this.options) === null || _a === void 0 ? void 0 : _a.itemTypes) ? html `<div class="hint">${t('drawn-at-slot', 'An item that gives no size is drawn at this size.')}</div>` : nothing}
702
+ ${this._row('stack-limit', t('stack-limit', 'Stack limit'), this._num('stack-limit', (_b = spec.stackLimit) !== null && _b !== void 0 ? _b : 1, n => edit({ stackLimit: n }), errors, { min: '1', step: '1' }), errors)}
703
+ ${this._row('rotation', t('rotation', 'Slot rotation (deg)'), this._num('rotation', (_c = spec.rotation) !== null && _c !== void 0 ? _c : 0, n => edit({ rotation: n || undefined }), errors), errors)}
704
+ </section>
332
705
  `;
333
706
  }
334
707
  _renderCustom(draft, tierNo) {
708
+ var _a, _b, _c;
335
709
  const tier = draft.tiers.find(x => x.tierNo === tierNo);
336
710
  const slots = [...tier.layout.slots].sort((a, b) => a.slotNo - b.slotNo);
337
711
  const cell = (slotNo, value, patch) => html `<td><input type="number" .value=${String(value !== null && value !== void 0 ? value : '')} @change=${(e) => this._edit(updateSlotSpec(draft, tierNo, patch(e), slotNo))} /></td>`;
@@ -366,30 +740,12 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
366
740
  })}
367
741
  </table>
368
742
  <div class="actions"><button data-action="add-slot" @click=${() => this._edit(addCustomSlot(draft, tierNo))}>${t('add-slot', 'Add slot')}</button></div>
369
- `;
370
- }
371
- /** 위에서 본 자리 배치. 오른쪽이 +X, 위쪽이 −Z */
372
- _renderPreview(draft, tierNo) {
373
- const slots = previewTier(draft, tierNo);
374
- if (!slots.length)
375
- return html `<div class="note" data-preview="none">${t('preview-none', 'No slots to show with the current values.')}</div>`;
376
- const minX = Math.min(...slots.map(s => s.x - s.width / 2));
377
- const maxX = Math.max(...slots.map(s => s.x + s.width / 2));
378
- const minZ = Math.min(...slots.map(s => s.z - s.depth / 2));
379
- const maxZ = Math.max(...slots.map(s => s.z + s.depth / 2));
380
- const w = Math.max(maxX - minX, 1);
381
- const h = Math.max(maxZ - minZ, 1);
382
- const pad = Math.max(w, h) * 0.08;
383
- const font = Math.max(w, h) * 0.07;
384
- return html `
385
- <svg data-preview viewBox="${minX - pad} ${minZ - pad} ${w + pad * 2} ${h + pad * 2}" preserveAspectRatio="xMidYMid meet">
386
- ${slots.map(s => svg `
387
- <rect data-slot=${s.slotNo} x=${s.x - s.width / 2} y=${s.z - s.depth / 2} width=${s.width} height=${s.depth}
388
- fill="rgba(25,118,210,0.08)" stroke="rgba(25,118,210,0.8)" stroke-width=${Math.max(w, h) * 0.006} />
389
- <text x=${s.x} y=${s.z} font-size=${font} text-anchor="middle" dominant-baseline="central" fill="#0d47a1">${s.slotNo}</text>
390
- `)}
391
- </svg>
392
- <div class="note">${t('preview-axes', 'Right = +X, up = −Z')}</div>
743
+ ${this._acceptsEditor({ allowed: (_b = (_a = slots[0]) === null || _a === void 0 ? void 0 : _a.allowed) !== null && _b !== void 0 ? _b : { width: 1, depth: 1, height: 1 }, accepts: (_c = slots[0]) === null || _c === void 0 ? void 0 : _c.accepts }, accepts => {
744
+ let next = draft;
745
+ for (const s of slots)
746
+ next = updateSlotSpec(next, tierNo, { accepts }, s.slotNo);
747
+ this._edit(next);
748
+ }, new Map())}
393
749
  `;
394
750
  }
395
751
  };
@@ -397,14 +753,42 @@ OxSlotLayoutEditor.styles = css `
397
753
  :host {
398
754
  display: flex;
399
755
  flex-direction: column;
400
- gap: 6px;
756
+ gap: 8px;
757
+ min-width: 0;
401
758
  font-size: 12px;
759
+ color: var(--property-editor-color, inherit);
402
760
  }
403
- .row {
761
+ section {
404
762
  display: grid;
405
- grid-template-columns: 40% 1fr;
763
+ grid-template-columns: minmax(64px, 34%) minmax(0, 1fr);
764
+ gap: 4px 6px;
406
765
  align-items: center;
407
- gap: 4px;
766
+ }
767
+ h4,
768
+ summary {
769
+ grid-column: 1 / -1;
770
+ margin: 4px 0 0;
771
+ font-size: 12px;
772
+ font-weight: bold;
773
+ }
774
+ summary {
775
+ cursor: pointer;
776
+ }
777
+ details > section {
778
+ margin-top: 4px;
779
+ }
780
+ label.l {
781
+ text-align: right;
782
+ font: var(--property-editor-label-font, inherit);
783
+ color: var(--property-editor-label-color, inherit);
784
+ }
785
+ .unit {
786
+ opacity: 0.6;
787
+ }
788
+ .err {
789
+ grid-column: 2;
790
+ color: #c62828;
791
+ font-size: 11px;
408
792
  }
409
793
  .notes {
410
794
  padding: 4px 6px;
@@ -414,63 +798,176 @@ OxSlotLayoutEditor.styles = css `
414
798
  .error {
415
799
  color: #c62828;
416
800
  }
417
- .note {
801
+ .note,
802
+ .hint {
418
803
  color: rgba(0, 0, 0, 0.55);
419
804
  }
420
- .chips {
421
- display: flex;
422
- flex-wrap: wrap;
423
- gap: 4px;
805
+ .hint {
806
+ grid-column: 2;
807
+ font-size: 11px;
424
808
  }
425
- .chips button[active] {
426
- border-color: var(--md-sys-color-primary, #1976d2);
427
- color: var(--md-sys-color-primary, #1976d2);
428
- font-weight: bold;
809
+ input,
810
+ select {
811
+ box-sizing: border-box;
812
+ width: 100%;
813
+ min-width: 0;
814
+ min-height: var(--property-editor-field-height, 24px);
815
+ padding: var(--property-editor-field-padding, 1px 4px);
816
+ border: var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2));
817
+ border-radius: var(--property-editor-border-radius, 4px);
818
+ background-color: var(--property-editor-background, field);
819
+ color: inherit;
820
+ font: var(--property-editor-field-font, inherit);
821
+ outline: none;
822
+ }
823
+ input[type='number'] {
824
+ font-variant-numeric: tabular-nums;
825
+ }
826
+ input[invalid],
827
+ select[invalid],
828
+ .seg[invalid],
829
+ .chips[invalid] {
830
+ border-color: #c62828;
831
+ }
832
+ .seg {
833
+ display: flex;
834
+ box-sizing: border-box;
835
+ min-height: var(--property-editor-field-height, 24px);
836
+ border: var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2));
837
+ border-radius: var(--property-editor-border-radius, 4px);
838
+ background-color: var(--property-editor-background, field);
839
+ overflow: hidden;
840
+ }
841
+ .seg button {
842
+ flex: 1;
843
+ min-width: 0;
844
+ min-height: 0;
845
+ padding: 0 4px;
846
+ border: 0;
847
+ border-left: 1px solid rgba(0, 0, 0, 0.12);
848
+ border-radius: 0;
849
+ background: transparent;
850
+ white-space: nowrap;
851
+ overflow: hidden;
852
+ text-overflow: ellipsis;
853
+ }
854
+ .seg button:first-child {
855
+ border-left: 0;
856
+ }
857
+ .seg button[aria-checked='true'] {
858
+ background: var(--md-sys-color-primary, #1976d2);
859
+ color: var(--md-sys-color-on-primary, #fff);
429
860
  }
430
861
  button {
431
- font-size: 12px;
432
- padding: 2px 8px;
433
- border: 1px solid rgba(0, 0, 0, 0.2);
434
- border-radius: 4px;
435
- background: #fff;
862
+ box-sizing: border-box;
863
+ min-height: var(--property-editor-field-height, 24px);
864
+ padding: 0 8px;
865
+ border: var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2));
866
+ border-radius: var(--property-editor-border-radius, 4px);
867
+ background-color: var(--property-editor-background, field);
868
+ color: inherit;
869
+ font: var(--property-editor-field-font, inherit);
436
870
  cursor: pointer;
437
871
  }
438
872
  button[disabled] {
439
873
  opacity: 0.4;
440
874
  cursor: default;
441
875
  }
442
- input,
443
- select {
444
- font-size: 12px;
445
- min-width: 0;
876
+ .tiers,
877
+ .chips,
878
+ .actions,
879
+ .inline {
880
+ display: flex;
881
+ flex-wrap: wrap;
882
+ gap: 4px;
883
+ align-items: center;
446
884
  }
447
- table {
448
- border-collapse: collapse;
449
- width: 100%;
885
+ .actions {
886
+ justify-content: flex-end;
450
887
  }
451
- td,
452
- th {
453
- border-bottom: 1px solid rgba(0, 0, 0, 0.08);
454
- padding: 1px;
888
+ .tiers button[aria-pressed='true'] {
889
+ border-color: var(--md-sys-color-primary, #1976d2);
890
+ color: var(--md-sys-color-primary, #1976d2);
891
+ font-weight: bold;
455
892
  }
456
- td input {
457
- width: 100%;
893
+ .tiers .end {
894
+ margin-left: auto;
895
+ }
896
+ .inline > input {
897
+ flex: 1;
898
+ }
899
+ .chips {
458
900
  box-sizing: border-box;
901
+ padding: 2px;
902
+ border: 1px solid transparent;
903
+ border-radius: var(--property-editor-border-radius, 4px);
459
904
  }
460
- h4 {
461
- margin: 6px 0 2px;
462
- font-size: 12px;
905
+ .chip {
906
+ display: inline-flex;
907
+ align-items: center;
908
+ gap: 2px;
909
+ padding: 1px 2px 1px 6px;
910
+ border-radius: 10px;
911
+ background: rgba(25, 118, 210, 0.1);
912
+ color: #0d47a1;
913
+ }
914
+ .chip[unregistered] {
915
+ background: rgba(239, 159, 39, 0.18);
916
+ color: #633806;
917
+ }
918
+ .chip button {
919
+ min-height: 0;
920
+ padding: 0 4px;
921
+ border: 0;
922
+ background: transparent;
923
+ }
924
+ .chips select {
925
+ flex: 1;
926
+ min-width: 80px;
927
+ width: auto;
463
928
  }
464
929
  svg {
930
+ display: block;
465
931
  width: 100%;
466
- height: 160px;
932
+ height: auto;
933
+ max-height: 240px;
934
+ touch-action: none;
467
935
  background: rgba(0, 0, 0, 0.02);
468
936
  border: 1px solid rgba(0, 0, 0, 0.08);
937
+ border-radius: 4px;
469
938
  }
470
- .actions {
939
+ svg [data-slot] {
940
+ cursor: move;
941
+ }
942
+ svg [data-handle] {
943
+ cursor: ew-resize;
944
+ }
945
+ .caption {
471
946
  display: flex;
472
- gap: 4px;
473
- justify-content: flex-end;
947
+ justify-content: space-between;
948
+ gap: 6px;
949
+ color: rgba(0, 0, 0, 0.55);
950
+ font-size: 11px;
951
+ }
952
+ .items {
953
+ display: flex;
954
+ flex-direction: column;
955
+ gap: 2px;
956
+ }
957
+ .items .bad {
958
+ color: #c62828;
959
+ }
960
+ .items .warn {
961
+ color: #854f0b;
962
+ }
963
+ table {
964
+ border-collapse: collapse;
965
+ width: 100%;
966
+ }
967
+ td,
968
+ th {
969
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
970
+ padding: 1px;
474
971
  }
475
972
  `;
476
973
  __decorate([
@@ -497,6 +994,9 @@ __decorate([
497
994
  __decorate([
498
995
  state()
499
996
  ], OxSlotLayoutEditor.prototype, "presetParams", void 0);
997
+ __decorate([
998
+ state()
999
+ ], OxSlotLayoutEditor.prototype, "fitNote", void 0);
500
1000
  OxSlotLayoutEditor = __decorate([
501
1001
  customElement('ox-slot-layout-editor')
502
1002
  ], OxSlotLayoutEditor);