@operato/slot-layout-editor 10.29.56 → 10.29.57

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ### [10.29.57](https://github.com/hatiolab/operato/compare/v10.29.56...v10.29.57) (2026-10-27)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **slot-layout-editor:** start with no numbers to type ([fe55219](https://github.com/hatiolab/operato/commit/fe552194b9a45ea7a3fe2b16427b8a3e9805eb62))
12
+
13
+
14
+ ### :bug: Bug Fix
15
+
16
+ * **slot-layout-editor:** lengths are shown to three decimals ([66bd3af](https://github.com/hatiolab/operato/commit/66bd3af6e5df6860e89f3e706938081370607992))
17
+ * **slot-layout-editor:** words for ACCEPTS_EMPTY, things-scene ^10.1.102 ([ca42efb](https://github.com/hatiolab/operato/commit/ca42efb8b2feb6cc917febd5a0597f6a1360aef9))
18
+
19
+
20
+
6
21
  ### [10.29.56](https://github.com/hatiolab/operato/compare/v10.29.55...v10.29.56) (2026-10-26)
7
22
 
8
23
 
@@ -22,7 +22,7 @@ export const ISSUE_TEXT = {
22
22
  SLOTS_REQUIRED: 'At least one slot is required',
23
23
  SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',
24
24
  NOT_PERSISTABLE: 'Cannot be saved ({value})',
25
- ACCEPTS_REQUIRED: 'Choose at least one type this slot takes'
25
+ ACCEPTS_EMPTY: 'The list of types it takes is empty; choose 「Any type」 to take any item'
26
26
  };
27
27
  export const t = (key, fallback, opts = {}) => i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts });
28
28
  /** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */
@@ -1 +1 @@
1
- {"version":3,"file":"issue-text.js","sourceRoot":"","sources":["../../src/issue-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * 검증 문제(코드·경로·매개변수)를 화면 문구로. 번역 키는 label.slot-layout.issue.{코드} 이고,\n * 번역이 없으면 아래 영어 기본 문구를 쓴다. 호스트 고유 코드는 호스트가 번역을 넣거나 issueText 옵션으로 바꾼다.\n */\n\nimport { i18next } from '@operato/i18n'\nimport type { LayoutIssueCode, SlotSpaceCode } from '@hatiolab/things-scene'\n\n/** 문제 하나. 호스트 고유 코드도 같은 모양으로 온다 */\nexport interface EditorIssue {\n code: string\n path: string\n params?: Record<string, string | number>\n}\n\n/** things-scene 검증 코드의 기본 문구. 자리표시자가 중괄호 하나라서 문구에 중괄호 글자를 쓰지 않는다 */\nexport const ISSUE_TEXT: Record<LayoutIssueCode, string> = {\n TIERS_REQUIRED: 'At least one tier is required',\n MOUNT_INVALID: 'Must be on, in or hang',\n ANCHOR_REQUIRED: 'Choose a face',\n ANCHOR_UNKNOWN: 'This holder has no face {anchorId}',\n TIER_NUMBER_DUPLICATE: 'Tier {tierNo} is used twice',\n FINITE_NUMBER_REQUIRED: 'A number is required',\n POSITIVE_NUMBER_REQUIRED: 'A number greater than 0 is required',\n POSITIVE_INTEGER_REQUIRED: 'A whole number of 1 or more is required',\n ARRAY_REQUIRED: 'A list is required',\n OBJECT_REQUIRED: 'The value is not a layout',\n AXIS_INVALID: 'Must be X or Z',\n PATTERN_INVALID: 'Must be one, row, grid or listed',\n SLOTS_REQUIRED: 'At least one slot is required',\n SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',\n NOT_PERSISTABLE: 'Cannot be saved ({value})',\n ACCEPTS_REQUIRED: 'Choose at least one type this slot takes'\n}\n\nexport const t = (key: string, fallback: string, opts: Record<string, unknown> = {}) =>\n i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts })\n\n/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */\nexport const SPACE_TEXT: Record<SlotSpaceCode, string> = {\n SLOT_OVERLAP: 'Tier {tier} slot {slot} overlaps tier {otherTier} slot {otherSlot}',\n SLOT_OUTSIDE_ANCHOR: 'Tier {tier} slot {slot} reaches past its face',\n ANCHOR_SPACE_UNKNOWN: 'Face {anchorId} has no size: whether slots stay on it is not checked'\n}\n\nexport function spaceWarningText(warning: EditorIssue): string {\n const fallback = (SPACE_TEXT as Record<string, string>)[warning.code] ?? warning.code\n return t(`space.${warning.code}`, fallback, { ...warning.params, interpolation: { escapeValue: false } })\n}\n\n/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */\nexport function issueText(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n const text = t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n return issue.path ? `${issue.path}: ${text}` : text\n}\n\n/** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */\nexport function issueMessage(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n return t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n}\n\n/** Tier 안의 경로 → 편집기 칸(data-field). 칸이 없는 경로(CUSTOM 자리 표 등)는 목록에 보인다 */\nconst FIELD_OF_PATH: Record<string, string> = {\n anchorId: 'anchor',\n mount: 'mount',\n position: 'position-x',\n 'position.x': 'position-x',\n 'position.y': 'position-y',\n 'position.z': 'position-z',\n rotation: 'tier-rotation',\n layout: 'pattern',\n 'layout.pattern': 'pattern',\n 'layout.count': 'count',\n 'layout.pitch': 'pitch',\n 'layout.pitch.x': 'pitch-x',\n 'layout.pitch.z': 'pitch-z',\n 'layout.axis': 'axis',\n 'layout.origin': 'origin',\n 'layout.origin.x': 'origin-x',\n 'layout.origin.z': 'origin-z',\n 'layout.rows': 'rows',\n 'layout.cols': 'cols',\n 'layout.order': 'order',\n 'layout.slot': 'allowed-width',\n 'layout.slot.allowed': 'allowed-width',\n 'layout.slot.allowed.width': 'allowed-width',\n 'layout.slot.allowed.depth': 'allowed-depth',\n 'layout.slot.allowed.height': 'allowed-height',\n 'layout.slot.stackLimit': 'stack-limit',\n 'layout.slot.rotation': 'rotation',\n 'layout.slot.accepts': 'accepts'\n}\n\n/** 문제가 어느 Tier(목록 순서)의 어느 칸인지. Tier 밖이거나 칸이 없으면 field 없음 */\nexport function issueField(issue: EditorIssue): { tierIndex?: number; field?: string } {\n const m = /^tiers\\[(\\d+)\\](?:\\.(.+))?$/.exec(issue.path ?? '')\n if (!m) return {}\n return { tierIndex: Number(m[1]), field: m[2] ? FIELD_OF_PATH[m[2]] : undefined }\n}\n\n/** 실린 아이템 판정 까닭의 기본 문구. 번역 키는 label.slot-layout.fit.{까닭} */\nexport const FIT_TEXT: Record<string, string> = {\n SLOT_NOT_FOUND: 'Slot gone: kept, unplaced',\n TYPE_NOT_ACCEPTED: 'Type not taken here',\n WIDTH_EXCEEDED: 'Wider than the slot',\n DEPTH_EXCEEDED: 'Deeper than the slot',\n HEIGHT_EXCEEDED: 'Taller than the slot',\n STACK_LIMIT_EXCEEDED: 'Over the stack limit',\n SPEC_UNKNOWN: 'Size unknown: not judged'\n}\n\nexport function fitText(reason: string): string {\n return t(`fit.${reason}`, FIT_TEXT[reason] ?? reason)\n}\n\n/** 면에 맞출 수 없는 까닭의 기본 문구. 번역 키는 label.slot-layout.fit-face.{까닭} */\nexport const FIT_FACE_TEXT: Record<string, string> = {\n FACE_UNKNOWN: 'The face size is not known',\n PATTERN_UNSUPPORTED: 'Not for listed slots',\n ROTATION_UNSUPPORTED: 'Only for turns of 90°',\n SLOT_WIDER_THAN_FACE: 'A slot is larger than the face',\n SLOTS_DO_NOT_FIT: 'The slots do not fit on the face'\n}\n\nexport function fitFaceText(reason: string): string {\n return t(`fit-face.${reason}`, FIT_FACE_TEXT[reason] ?? reason)\n}\n"]}
1
+ {"version":3,"file":"issue-text.js","sourceRoot":"","sources":["../../src/issue-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * 검증 문제(코드·경로·매개변수)를 화면 문구로. 번역 키는 label.slot-layout.issue.{코드} 이고,\n * 번역이 없으면 아래 영어 기본 문구를 쓴다. 호스트 고유 코드는 호스트가 번역을 넣거나 issueText 옵션으로 바꾼다.\n */\n\nimport { i18next } from '@operato/i18n'\nimport type { LayoutIssueCode, SlotSpaceCode } from '@hatiolab/things-scene'\n\n/** 문제 하나. 호스트 고유 코드도 같은 모양으로 온다 */\nexport interface EditorIssue {\n code: string\n path: string\n params?: Record<string, string | number>\n}\n\n/** things-scene 검증 코드의 기본 문구. 자리표시자가 중괄호 하나라서 문구에 중괄호 글자를 쓰지 않는다 */\nexport const ISSUE_TEXT: Record<LayoutIssueCode, string> = {\n TIERS_REQUIRED: 'At least one tier is required',\n MOUNT_INVALID: 'Must be on, in or hang',\n ANCHOR_REQUIRED: 'Choose a face',\n ANCHOR_UNKNOWN: 'This holder has no face {anchorId}',\n TIER_NUMBER_DUPLICATE: 'Tier {tierNo} is used twice',\n FINITE_NUMBER_REQUIRED: 'A number is required',\n POSITIVE_NUMBER_REQUIRED: 'A number greater than 0 is required',\n POSITIVE_INTEGER_REQUIRED: 'A whole number of 1 or more is required',\n ARRAY_REQUIRED: 'A list is required',\n OBJECT_REQUIRED: 'The value is not a layout',\n AXIS_INVALID: 'Must be X or Z',\n PATTERN_INVALID: 'Must be one, row, grid or listed',\n SLOTS_REQUIRED: 'At least one slot is required',\n SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',\n NOT_PERSISTABLE: 'Cannot be saved ({value})',\n ACCEPTS_EMPTY: 'The list of types it takes is empty; choose 「Any type」 to take any item'\n}\n\nexport const t = (key: string, fallback: string, opts: Record<string, unknown> = {}) =>\n i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts })\n\n/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */\nexport const SPACE_TEXT: Record<SlotSpaceCode, string> = {\n SLOT_OVERLAP: 'Tier {tier} slot {slot} overlaps tier {otherTier} slot {otherSlot}',\n SLOT_OUTSIDE_ANCHOR: 'Tier {tier} slot {slot} reaches past its face',\n ANCHOR_SPACE_UNKNOWN: 'Face {anchorId} has no size: whether slots stay on it is not checked'\n}\n\nexport function spaceWarningText(warning: EditorIssue): string {\n const fallback = (SPACE_TEXT as Record<string, string>)[warning.code] ?? warning.code\n return t(`space.${warning.code}`, fallback, { ...warning.params, interpolation: { escapeValue: false } })\n}\n\n/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */\nexport function issueText(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n const text = t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n return issue.path ? `${issue.path}: ${text}` : text\n}\n\n/** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */\nexport function issueMessage(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n return t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n}\n\n/** Tier 안의 경로 → 편집기 칸(data-field). 칸이 없는 경로(CUSTOM 자리 표 등)는 목록에 보인다 */\nconst FIELD_OF_PATH: Record<string, string> = {\n anchorId: 'anchor',\n mount: 'mount',\n position: 'position-x',\n 'position.x': 'position-x',\n 'position.y': 'position-y',\n 'position.z': 'position-z',\n rotation: 'tier-rotation',\n layout: 'pattern',\n 'layout.pattern': 'pattern',\n 'layout.count': 'count',\n 'layout.pitch': 'pitch',\n 'layout.pitch.x': 'pitch-x',\n 'layout.pitch.z': 'pitch-z',\n 'layout.axis': 'axis',\n 'layout.origin': 'origin',\n 'layout.origin.x': 'origin-x',\n 'layout.origin.z': 'origin-z',\n 'layout.rows': 'rows',\n 'layout.cols': 'cols',\n 'layout.order': 'order',\n 'layout.slot': 'allowed-width',\n 'layout.slot.allowed': 'allowed-width',\n 'layout.slot.allowed.width': 'allowed-width',\n 'layout.slot.allowed.depth': 'allowed-depth',\n 'layout.slot.allowed.height': 'allowed-height',\n 'layout.slot.stackLimit': 'stack-limit',\n 'layout.slot.rotation': 'rotation',\n 'layout.slot.accepts': 'accepts'\n}\n\n/** 문제가 어느 Tier(목록 순서)의 어느 칸인지. Tier 밖이거나 칸이 없으면 field 없음 */\nexport function issueField(issue: EditorIssue): { tierIndex?: number; field?: string } {\n const m = /^tiers\\[(\\d+)\\](?:\\.(.+))?$/.exec(issue.path ?? '')\n if (!m) return {}\n return { tierIndex: Number(m[1]), field: m[2] ? FIELD_OF_PATH[m[2]] : undefined }\n}\n\n/** 실린 아이템 판정 까닭의 기본 문구. 번역 키는 label.slot-layout.fit.{까닭} */\nexport const FIT_TEXT: Record<string, string> = {\n SLOT_NOT_FOUND: 'Slot gone: kept, unplaced',\n TYPE_NOT_ACCEPTED: 'Type not taken here',\n WIDTH_EXCEEDED: 'Wider than the slot',\n DEPTH_EXCEEDED: 'Deeper than the slot',\n HEIGHT_EXCEEDED: 'Taller than the slot',\n STACK_LIMIT_EXCEEDED: 'Over the stack limit',\n SPEC_UNKNOWN: 'Size unknown: not judged'\n}\n\nexport function fitText(reason: string): string {\n return t(`fit.${reason}`, FIT_TEXT[reason] ?? reason)\n}\n\n/** 면에 맞출 수 없는 까닭의 기본 문구. 번역 키는 label.slot-layout.fit-face.{까닭} */\nexport const FIT_FACE_TEXT: Record<string, string> = {\n FACE_UNKNOWN: 'The face size is not known',\n PATTERN_UNSUPPORTED: 'Not for listed slots',\n ROTATION_UNSUPPORTED: 'Only for turns of 90°',\n SLOT_WIDER_THAN_FACE: 'A slot is larger than the face',\n SLOTS_DO_NOT_FIT: 'The slots do not fit on the face'\n}\n\nexport function fitFaceText(reason: string): string {\n return t(`fit-face.${reason}`, FIT_FACE_TEXT[reason] ?? reason)\n}\n"]}
@@ -34,8 +34,13 @@ export interface SlotLayoutEditorOptions<L extends SlotLayout = SlotLayout> {
34
34
  live?: boolean;
35
35
  /** 호스트가 주는 프리셋 */
36
36
  presets?: SlotLayoutPreset<L>[];
37
- /** 프리셋 크기 초기값 */
37
+ /** 프리셋 크기 초기값 (기준면의 폭·깊이와 기본 자리 높이). 다 주면 프리셋은 누르기만 하고 크기 칸은 접힌다 */
38
38
  presetSize?: Partial<PresetSize>;
39
+ /**
40
+ * 레이아웃이 없을 때 이 프리셋으로 시작한 초안을 바로 보인다 (예: 'single' — 기준면 전체가 자리 하나). [적용]해야 들어간다.
41
+ * 크기를 다 알 때만 시작하고, 바로 적용하는 호스트(live)에서는 하지 않는다
42
+ */
43
+ startFrom?: string;
39
44
  /** 상태 문구 (값의 출처, 단위 상태 등) */
40
45
  notes?: SlotLayoutEditorNote[];
41
46
  /** 홀더가 주는 Anchor. 주면 Tier 는 이 중 하나를 고르고, 없는 Anchor 는 문제로 보인다. part·face 를 주면 면 이름으로 보인다 */
@@ -83,6 +88,7 @@ export declare class OxSlotLayoutEditor extends OxSlotLayoutEditor_base {
83
88
  willUpdate(changed: Map<string, unknown>): void;
84
89
  /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다. 고른 Tier 는 남아 있으면 그대로 */
85
90
  reset(): void;
91
+ private _startFromHost;
86
92
  get issues(): EditorIssue[];
87
93
  /** 자리끼리 겹치거나 면 밖으로 나가는 설정, 면 크기를 몰라 확인하지 못한 Anchor. 적용을 막지 않는다 */
88
94
  get warnings(): EditorIssue[];
@@ -24,7 +24,8 @@ const num = (e) => {
24
24
  const n = e.target.valueAsNumber;
25
25
  return Number.isFinite(n) ? n : NaN;
26
26
  };
27
- const shown = (v) => (v === undefined || v === null || Number.isNaN(v) ? '' : String(v));
27
+ /* 보이는 값: 길이는 소수 셋째 자리까지 — 피규어에서 잰 면은 부동소수 잡음을 싣는다(759.9999998556777 → 760) */
28
+ const shown = (v) => (v === undefined || v === null || Number.isNaN(v) ? '' : typeof v === 'number' ? String(Number(v.toFixed(3))) : String(v));
28
29
  const snap = (v) => Math.round(v / 10) * 10;
29
30
  const ERROR = '#c62828';
30
31
  const SLOT = '#1976d2';
@@ -119,6 +120,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
119
120
  // 값도 기본값도 없는 호스트도 프리셋 크기는 준다. 크기가 실제로 바뀔 때만 받고, 편집 중인 초안은 그대로 둔다
120
121
  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 : {}))
121
122
  this.presetSize = { ...((_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presetSize) !== null && _m !== void 0 ? _m : {}) };
123
+ this._startFromHost();
122
124
  }
123
125
  /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다. 고른 Tier 는 남아 있으면 그대로 */
124
126
  reset() {
@@ -133,6 +135,21 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
133
135
  this.presetSize = { ...((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presetSize) !== null && _k !== void 0 ? _k : {}) };
134
136
  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
137
  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 : '';
138
+ this._startFromHost();
139
+ }
140
+ /* 레이아웃이 없으면 호스트가 고른 프리셋(startFrom)으로 시작한 초안을 보인다. 저장은 [적용] 때 — 끼어들어 쓰지 않는다 */
141
+ _startFromHost() {
142
+ var _a, _b, _c, _d, _e, _f, _g, _h;
143
+ const key = (_a = this.options) === null || _a === void 0 ? void 0 : _a.startFrom;
144
+ if (this.draft || this.value || ((_b = this.options) === null || _b === void 0 ? void 0 : _b.effective) || ((_c = this.options) === null || _c === void 0 ? void 0 : _c.live) || !key)
145
+ return;
146
+ const preset = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.presets) === null || _e === void 0 ? void 0 : _e.find(p => p.key === key);
147
+ const built = preset && !((_f = preset.params) === null || _f === void 0 ? void 0 : _f.length) ? applyPreset(preset, this.presetSize, {}) : undefined;
148
+ if (!(built === null || built === void 0 ? void 0 : built.layout))
149
+ return;
150
+ this.draft = structuredClone(built.layout);
151
+ this.dirty = true;
152
+ this.tier = (_h = (_g = this.draft.tiers[0]) === null || _g === void 0 ? void 0 : _g.tierNo) !== null && _h !== void 0 ? _h : 1;
136
153
  }
137
154
  get issues() {
138
155
  var _a, _b, _c, _d, _e;
@@ -236,29 +253,57 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
236
253
  return nothing;
237
254
  const preset = (_c = presets.find(p => p.key === this.presetKey)) !== null && _c !== void 0 ? _c : presets[0];
238
255
  const built = applyPreset(preset, this.presetSize, this.presetParams);
256
+ const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
257
+ const sized = positive(this.presetSize.width) && positive(this.presetSize.depth) && positive(this.presetSize.slotHeight);
239
258
  const numberRow = (label, value, onChange, key) => html `
240
259
  <label class="l">${label}</label>
241
260
  <input type="number" data-preset=${key} .value=${shown(value)} @change=${(e) => onChange(num(e))} />
242
261
  `;
262
+ /* 값을 더 묻지 않는 프리셋은 누르면 바로 시작한다. 값을 묻는 프리셋(행 수 등)은 그 칸과 [시작]을 연다 */
263
+ const choose = (p) => {
264
+ var _a;
265
+ this.presetKey = p.key;
266
+ if ((_a = p.params) === null || _a === void 0 ? void 0 : _a.length)
267
+ return;
268
+ const made = applyPreset(p, this.presetSize, {});
269
+ if (made.layout)
270
+ this._startFrom(made.layout);
271
+ };
243
272
  return html `
244
273
  <details data-section="preset" ?open=${!this.draft}>
245
274
  <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>
275
+ <div class="tiers" data-preset="kind" role="group">
276
+ ${presets.map(p => {
277
+ var _a;
278
+ return html `<button
279
+ data-preset-key=${p.key}
280
+ aria-pressed=${((_a = p.params) === null || _a === void 0 ? void 0 : _a.length) && p.key === preset.key ? 'true' : 'false'}
281
+ ?disabled=${!sized}
282
+ @click=${() => choose(p)}
283
+ >
284
+ ${t(`preset.${p.key}`, p.key)}
285
+ </button>`;
286
+ })}
261
287
  </div>
288
+ ${((_d = preset.params) === null || _d === void 0 ? void 0 : _d.length)
289
+ ? html `<section>
290
+ ${preset.params.map(p => numberRow(t(`preset-param.${p.key}`, p.key), this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
291
+ </section>
292
+ <div class="actions">
293
+ <button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
294
+ ${t('start-from-preset', 'Start from preset')}
295
+ </button>
296
+ </div>`
297
+ : nothing}
298
+ <details data-section="preset-size" ?open=${!sized}>
299
+ <summary>${t('preset-size', 'Size (enter by hand)')}</summary>
300
+ <section>
301
+ ${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
302
+ ${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
303
+ ${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
304
+ </section>
305
+ </details>
306
+ <div class="hint">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
262
307
  </details>
263
308
  `;
264
309
  }
@@ -420,7 +465,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
420
465
  <div class="caption">
421
466
  <span data-face-caption>
422
467
  ${face
423
- ? html `${this._anchorName(tier.anchorId)} · ${face.width} × ${face.depth}${this._unit()}`
468
+ ? html `${this._anchorName(tier.anchorId)} · ${shown(face.width)} × ${shown(face.depth)}${this._unit()}`
424
469
  : html `<span class="error">${t('face-unknown', 'Face size unknown: whether slots stay on it is not checked')}</span>`}
425
470
  </span>
426
471
  <span>${t('preview-axes', 'Right = +X, up = −Z')}</span>
@@ -708,7 +753,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
708
753
  var _a, _b, _c;
709
754
  const tier = draft.tiers.find(x => x.tierNo === tierNo);
710
755
  const slots = [...tier.layout.slots].sort((a, b) => a.slotNo - b.slotNo);
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>`;
756
+ const cell = (slotNo, value, patch) => html `<td><input type="number" .value=${shown(value)} @change=${(e) => this._edit(updateSlotSpec(draft, tierNo, patch(e), slotNo))} /></td>`;
712
757
  return html `
713
758
  <h4>${t('custom-slots', 'Slots')}</h4>
714
759
  <table>