@operato/board 10.0.0-beta.49 → 10.0.0-beta.50

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,30 @@
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.0.0-beta.50](https://github.com/hatiolab/operato/compare/v10.0.0-beta.49...v10.0.0-beta.50) (2026-05-12)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **board:** bulk-paste (Ctrl+Shift+V) + zorder Photoshop 컨벤션 ([9a1bb03](https://github.com/hatiolab/operato/commit/9a1bb031e157b15cabff0403ad27fbdeb559fa55)), closes [#4a9](https://github.com/hatiolab/operato/issues/4a9)
12
+
13
+
14
+ ### :mega: Other
15
+
16
+ * **board:** bulk-paste dialog 디버깅 console.log 제거 ([e1b88d5](https://github.com/hatiolab/operato/commit/e1b88d511ebdc106a1cda77ee6893f79775afd99))
17
+
18
+
19
+ ### :house: Code Refactoring
20
+
21
+ * **board:** bulk-paste 영역 드래그를 things-scene AddLayer 로 위임 ([9efdac7](https://github.com/hatiolab/operato/commit/9efdac7831a47072a81e5d4122ecf321de47c378)), closes [#2964c2](https://github.com/hatiolab/operato/issues/2964c2)
22
+
23
+
24
+ ### :bug: Bug Fix
25
+
26
+ * **board:** bulk-create-dialog 4꼭지점 흰색 사각형 제거 ([e8bd653](https://github.com/hatiolab/operato/commit/e8bd653a81a1c2c4886546a9c9a261bfb0159795))
27
+
28
+
29
+
6
30
  ## [10.0.0-beta.49](https://github.com/hatiolab/operato/compare/v10.0.0-beta.48...v10.0.0-beta.49) (2026-05-07)
7
31
 
8
32
  **Note:** Version bump only for package @operato/board
@@ -0,0 +1,51 @@
1
+ import { LitElement } from 'lit';
2
+ import { generateBulkComponents, type BulkCreateArea as Area } from '@hatiolab/things-scene';
3
+ export type ConflictPolicy = 'skip' | 'duplicate';
4
+ export interface BulkCreateDialogProps {
5
+ /** 템플릿 컴포넌트 state (read-only 표시용). */
6
+ template: Record<string, any>;
7
+ /** 초기 영역 (보드 좌표). 사용자가 dialog 내에서 조정 가능. */
8
+ initialArea: Area;
9
+ /** 기존 scene 의 모든 ID — 충돌 검사. */
10
+ existingIds?: Set<string>;
11
+ /** OK 클릭 시 콜백 — 생성할 model 배열 전달. */
12
+ onConfirm: (models: ReturnType<typeof generateBulkComponents>) => void;
13
+ /** 취소 / 닫기 콜백. */
14
+ onCancel?: () => void;
15
+ }
16
+ export declare class BulkCreateDialog extends LitElement {
17
+ static styles: import("lit").CSSResult;
18
+ template: Record<string, any>;
19
+ initialArea: Area;
20
+ existingIds: Set<string>;
21
+ onConfirm: BulkCreateDialogProps['onConfirm'];
22
+ onCancel?: BulkCreateDialogProps['onCancel'];
23
+ private pattern;
24
+ private count;
25
+ private rows;
26
+ private cols;
27
+ private idPattern;
28
+ private idStart;
29
+ private idPadding;
30
+ private area;
31
+ private conflictPolicy;
32
+ countInput: HTMLInputElement;
33
+ connectedCallback(): void;
34
+ /** rows/cols 가 미입력 (undefined) 이면 영역 종횡비 + count 로 자동 계산해 표시. */
35
+ private get displayRows();
36
+ private get displayCols();
37
+ firstUpdated(): void;
38
+ private updateArea;
39
+ private get previewModels();
40
+ private get conflicts();
41
+ /** 충돌 정책 적용 후 실제 생성될 model 배열. */
42
+ private get effectiveModels();
43
+ private onOk;
44
+ private onCancelClick;
45
+ render(): import("lit-html").TemplateResult<1>;
46
+ }
47
+ declare global {
48
+ interface HTMLElementTagNameMap {
49
+ 'bulk-create-dialog': BulkCreateDialog;
50
+ }
51
+ }
@@ -0,0 +1,531 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * 멀티 컴포넌트 생성 dialog — 템플릿 + 영역 + 옵션 입력 → OK 시 콜백 호출.
5
+ */
6
+ import { __decorate } from "tslib";
7
+ import { LitElement, css, html } from 'lit';
8
+ import { customElement, property, query, state } from 'lit/decorators.js';
9
+ import { generateBulkComponents, findIdConflicts, inferIdPattern, nextAvailableStart, deriveGridShape } from '@hatiolab/things-scene';
10
+ let BulkCreateDialog = class BulkCreateDialog extends LitElement {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.existingIds = new Set();
14
+ this.pattern = 'grid';
15
+ this.count = 12;
16
+ this.rows = undefined;
17
+ this.cols = undefined;
18
+ this.idPattern = '{i}';
19
+ this.idStart = 1;
20
+ this.idPadding = 0;
21
+ this.conflictPolicy = 'skip';
22
+ this.onOk = () => {
23
+ const models = this.effectiveModels;
24
+ if (models.length === 0)
25
+ return;
26
+ this.onConfirm(models);
27
+ };
28
+ this.onCancelClick = () => {
29
+ var _a;
30
+ (_a = this.onCancel) === null || _a === void 0 ? void 0 : _a.call(this);
31
+ };
32
+ }
33
+ connectedCallback() {
34
+ var _a;
35
+ super.connectedCallback();
36
+ // OxPopup wrapper (ox-popup) 의 흰색 배경 + box-shadow 가 dialog 라운드 코너
37
+ // 밖으로 비쳐서 4 꼭지점에 흰색 사각형이 보임. 부모를 투명/그림자 없이 만들어
38
+ // dialog 자체의 라운드 + 그림자만 보이게.
39
+ const parent = this.parentElement;
40
+ if (parent) {
41
+ parent.style.background = 'transparent';
42
+ parent.style.boxShadow = 'none';
43
+ parent.style.border = 'none';
44
+ }
45
+ this.area = { ...this.initialArea };
46
+ // 1) 영역 종횡비로 레이아웃 기본 선택:
47
+ // 너비 ≫ 높이 → line-h (가로), 높이 ≫ 너비 → line-v (세로), 비슷하면 grid.
48
+ const w = Math.max(this.area.width, 1);
49
+ const h = Math.max(this.area.height, 1);
50
+ const aspect = w / h;
51
+ if (aspect >= 2.5)
52
+ this.pattern = 'line-h';
53
+ else if (aspect <= 0.4)
54
+ this.pattern = 'line-v';
55
+ else
56
+ this.pattern = 'grid';
57
+ // 2) 템플릿 id 로부터 ID 패턴 / padding 자동 추론.
58
+ const inferred = inferIdPattern((_a = this.template) === null || _a === void 0 ? void 0 : _a.id);
59
+ this.idPattern = inferred.pattern;
60
+ this.idPadding = inferred.padding;
61
+ // 3) 시작 번호 = existingIds 에 패턴 매칭되는 *최대 번호 + 1*.
62
+ // 반복 bulk-paste 시 이전 batch 의 끝 다음으로 자동 advance.
63
+ // 매칭 ID 가 없으면 inferIdPattern 의 fallback (= template num+1) 사용.
64
+ this.idStart = nextAvailableStart(inferred.pattern, this.existingIds, inferred.start);
65
+ }
66
+ /** rows/cols 가 미입력 (undefined) 이면 영역 종횡비 + count 로 자동 계산해 표시. */
67
+ get displayRows() {
68
+ if (this.rows != null)
69
+ return this.rows;
70
+ return deriveGridShape(this.count, this.area, undefined, this.cols).rows;
71
+ }
72
+ get displayCols() {
73
+ if (this.cols != null)
74
+ return this.cols;
75
+ return deriveGridShape(this.count, this.area, this.rows, undefined).cols;
76
+ }
77
+ firstUpdated() {
78
+ var _a, _b;
79
+ (_a = this.countInput) === null || _a === void 0 ? void 0 : _a.focus();
80
+ (_b = this.countInput) === null || _b === void 0 ? void 0 : _b.select();
81
+ }
82
+ updateArea(field, value) {
83
+ this.area = { ...this.area, [field]: value };
84
+ }
85
+ get previewModels() {
86
+ var _a, _b;
87
+ if (!this.template)
88
+ return [];
89
+ return generateBulkComponents({
90
+ template: this.template,
91
+ area: this.area,
92
+ pattern: this.pattern,
93
+ count: this.count,
94
+ rows: this.rows,
95
+ cols: this.cols,
96
+ idPattern: this.idPattern.replace(/\{type\}/g, String((_b = (_a = this.template) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'C')),
97
+ idStart: this.idStart,
98
+ idPadding: this.idPadding
99
+ });
100
+ }
101
+ get conflicts() {
102
+ return findIdConflicts(this.previewModels, this.existingIds);
103
+ }
104
+ /** 충돌 정책 적용 후 실제 생성될 model 배열. */
105
+ get effectiveModels() {
106
+ const all = this.previewModels;
107
+ if (this.conflictPolicy === 'skip' && this.existingIds.size > 0) {
108
+ return all.filter(m => !this.existingIds.has(m.id));
109
+ }
110
+ return all;
111
+ }
112
+ render() {
113
+ var _a, _b, _c, _d;
114
+ const tplType = (_b = (_a = this.template) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : '?';
115
+ const tplId = (_d = (_c = this.template) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : '?';
116
+ const models = this.previewModels;
117
+ return html `
118
+ <h3>Bulk Paste — ${tplType}</h3>
119
+
120
+ <div class="row">
121
+ <label>템플릿</label>
122
+ <span style="color:#b6d4fe;">${tplId} <span style="color:rgba(255,255,255,0.4);">(${tplType})</span></span>
123
+ </div>
124
+
125
+ <div class="row">
126
+ <label>레이아웃</label>
127
+ <div class="pattern-picker">
128
+ <button
129
+ type="button"
130
+ class="pattern-btn ${this.pattern === 'grid' ? 'active' : ''}"
131
+ @click=${() => (this.pattern = 'grid')}
132
+ title="격자"
133
+ >
134
+ <svg viewBox="0 0 24 24">
135
+ <circle cx="6" cy="6" r="1.6" /><circle cx="12" cy="6" r="1.6" /><circle cx="18" cy="6" r="1.6" />
136
+ <circle cx="6" cy="12" r="1.6" /><circle cx="12" cy="12" r="1.6" /><circle cx="18" cy="12" r="1.6" />
137
+ <circle cx="6" cy="18" r="1.6" /><circle cx="12" cy="18" r="1.6" /><circle cx="18" cy="18" r="1.6" />
138
+ </svg>
139
+ <span>격자</span>
140
+ </button>
141
+ <button
142
+ type="button"
143
+ class="pattern-btn ${this.pattern === 'line-h' ? 'active' : ''}"
144
+ @click=${() => (this.pattern = 'line-h')}
145
+ title="가로 1줄"
146
+ >
147
+ <svg viewBox="0 0 24 24">
148
+ <circle cx="3" cy="12" r="1.6" /><circle cx="8" cy="12" r="1.6" /><circle cx="13" cy="12" r="1.6" />
149
+ <circle cx="18" cy="12" r="1.6" /><circle cx="22" cy="12" r="1.4" />
150
+ </svg>
151
+ <span>가로</span>
152
+ </button>
153
+ <button
154
+ type="button"
155
+ class="pattern-btn ${this.pattern === 'line-v' ? 'active' : ''}"
156
+ @click=${() => (this.pattern = 'line-v')}
157
+ title="세로 1줄"
158
+ >
159
+ <svg viewBox="0 0 24 24">
160
+ <circle cx="12" cy="3" r="1.6" /><circle cx="12" cy="8" r="1.6" /><circle cx="12" cy="13" r="1.6" />
161
+ <circle cx="12" cy="18" r="1.6" /><circle cx="12" cy="22" r="1.4" />
162
+ </svg>
163
+ <span>세로</span>
164
+ </button>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="row">
169
+ <label>개수</label>
170
+ <input
171
+ id="count-input"
172
+ type="number"
173
+ min="1"
174
+ .value=${String(this.count)}
175
+ @input=${(e) => (this.count = Math.max(1, +e.target.value))}
176
+ />
177
+ </div>
178
+
179
+ ${this.pattern === 'grid'
180
+ ? html `
181
+ <div class="row">
182
+ <label>행 × 열</label>
183
+ <div class="grid-shape">
184
+ <input
185
+ type="number"
186
+ min="1"
187
+ .value=${String(this.displayRows)}
188
+ @input=${(e) => {
189
+ const v = e.target.value;
190
+ this.rows = v ? Math.max(1, +v) : undefined;
191
+ }}
192
+ />
193
+ <span>×</span>
194
+ <input
195
+ type="number"
196
+ min="1"
197
+ .value=${String(this.displayCols)}
198
+ @input=${(e) => {
199
+ const v = e.target.value;
200
+ this.cols = v ? Math.max(1, +v) : undefined;
201
+ }}
202
+ />
203
+ </div>
204
+ </div>
205
+ `
206
+ : ''}
207
+
208
+ <div class="row">
209
+ <label>ID 패턴</label>
210
+ <input
211
+ type="text"
212
+ .value=${this.idPattern}
213
+ @input=${(e) => (this.idPattern = e.target.value)}
214
+ />
215
+ </div>
216
+ <div class="row">
217
+ <span></span>
218
+ <div class="hint">placeholder: <code>{i}</code> 일련번호, <code>{r}</code> 행, <code>{c}</code> 열 (grid)</div>
219
+ </div>
220
+
221
+ <div class="row">
222
+ <label>시작 번호</label>
223
+ <input
224
+ type="number"
225
+ .value=${String(this.idStart)}
226
+ @input=${(e) => (this.idStart = +e.target.value)}
227
+ />
228
+ </div>
229
+
230
+ <div class="row">
231
+ <label>Zero-pad 자릿수</label>
232
+ <input
233
+ type="number"
234
+ min="0"
235
+ .value=${String(this.idPadding)}
236
+ @input=${(e) => (this.idPadding = Math.max(0, +e.target.value))}
237
+ />
238
+ </div>
239
+
240
+ <div class="preview">
241
+ ${models.length > 0
242
+ ? html `총 ${models.length}개 — 예: <code>${models[0].id}</code> ... <code>${models[models.length - 1].id}</code>`
243
+ : html `<span style="color:#ff8080;">생성될 컴포넌트 없음</span>`}
244
+ </div>
245
+
246
+ ${this.conflicts.length > 0
247
+ ? html `
248
+ <div class="conflict">
249
+ <strong>ID 충돌 ${this.conflicts.length}개</strong>
250
+ <code>${this.conflicts.slice(0, 5).join(', ')}${this.conflicts.length > 5 ? ', ...' : ''}</code>
251
+ <div class="conflict-hint" style="margin-top:6px;">
252
+ <div class="policy-radios">
253
+ <label>
254
+ <input
255
+ type="radio"
256
+ name="conflict-policy"
257
+ value="skip"
258
+ .checked=${this.conflictPolicy === 'skip'}
259
+ @change=${() => (this.conflictPolicy = 'skip')}
260
+ />
261
+ 충돌 ID 스킵 (기본)
262
+ </label>
263
+ <label>
264
+ <input
265
+ type="radio"
266
+ name="conflict-policy"
267
+ value="duplicate"
268
+ .checked=${this.conflictPolicy === 'duplicate'}
269
+ @change=${() => (this.conflictPolicy = 'duplicate')}
270
+ />
271
+ 중복 그대로 생성
272
+ </label>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ `
277
+ : ''}
278
+
279
+ <div class="actions">
280
+ <button @click=${this.onCancelClick}>취소</button>
281
+ <button
282
+ class="primary"
283
+ @click=${this.onOk}
284
+ ?disabled=${this.effectiveModels.length === 0}
285
+ >
286
+ 생성 (${this.effectiveModels.length}${this.conflicts.length > 0 && this.conflictPolicy === 'skip'
287
+ ? html `<span style="opacity:0.6;"> / ${models.length}</span>`
288
+ : ''})
289
+ </button>
290
+ </div>
291
+ `;
292
+ }
293
+ };
294
+ BulkCreateDialog.styles = css `
295
+ :host {
296
+ display: block;
297
+ padding: 16px 20px;
298
+ min-width: 340px;
299
+ max-width: 480px;
300
+ background: rgba(30, 30, 30, 0.94);
301
+ border: 1px solid rgba(255, 255, 255, 0.08);
302
+ border-radius: 10px;
303
+ backdrop-filter: blur(8px);
304
+ -webkit-backdrop-filter: blur(8px);
305
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
306
+ color: rgba(255, 255, 255, 0.85);
307
+ font: 12px/1.6 -apple-system, BlinkMacSystemFont, sans-serif;
308
+ }
309
+ h3 {
310
+ margin: 0 0 12px;
311
+ font-size: 13px;
312
+ font-weight: 600;
313
+ color: #4a9eff;
314
+ }
315
+ .row {
316
+ display: grid;
317
+ grid-template-columns: 100px 1fr;
318
+ align-items: center;
319
+ gap: 8px;
320
+ margin: 6px 0;
321
+ }
322
+ label {
323
+ font-size: 12px;
324
+ color: rgba(255, 255, 255, 0.55);
325
+ }
326
+ input,
327
+ select {
328
+ padding: 4px 6px;
329
+ font: inherit;
330
+ font-size: 12px;
331
+ background: rgba(255, 255, 255, 0.06);
332
+ border: 1px solid rgba(255, 255, 255, 0.12);
333
+ border-radius: 3px;
334
+ width: 100%;
335
+ box-sizing: border-box;
336
+ color: rgba(255, 255, 255, 0.9);
337
+ outline: none;
338
+ }
339
+ input:focus,
340
+ select:focus {
341
+ border-color: #4a9eff;
342
+ background: rgba(74, 158, 255, 0.08);
343
+ }
344
+ select option {
345
+ background: rgb(30, 30, 30);
346
+ color: rgba(255, 255, 255, 0.9);
347
+ }
348
+ .grid-shape {
349
+ display: grid;
350
+ grid-template-columns: 1fr 20px 1fr;
351
+ gap: 4px;
352
+ align-items: center;
353
+ }
354
+ .grid-shape > span {
355
+ text-align: center;
356
+ color: rgba(255, 255, 255, 0.35);
357
+ }
358
+ .pattern-picker {
359
+ display: flex;
360
+ gap: 6px;
361
+ }
362
+ .pattern-btn {
363
+ flex: 1;
364
+ display: flex;
365
+ flex-direction: column;
366
+ align-items: center;
367
+ gap: 2px;
368
+ padding: 6px 4px;
369
+ background: rgba(255, 255, 255, 0.04);
370
+ border: 1px solid rgba(255, 255, 255, 0.1);
371
+ border-radius: 4px;
372
+ cursor: pointer;
373
+ color: rgba(255, 255, 255, 0.6);
374
+ transition: background 0.1s, border-color 0.1s;
375
+ }
376
+ .pattern-btn:hover {
377
+ background: rgba(255, 255, 255, 0.08);
378
+ }
379
+ .pattern-btn.active {
380
+ border-color: #4a9eff;
381
+ background: rgba(74, 158, 255, 0.15);
382
+ color: rgba(255, 255, 255, 0.95);
383
+ }
384
+ .pattern-btn svg {
385
+ width: 28px;
386
+ height: 28px;
387
+ }
388
+ .pattern-btn svg circle {
389
+ fill: currentColor;
390
+ }
391
+ .pattern-btn span {
392
+ font-size: 10.5px;
393
+ }
394
+ .policy-radios {
395
+ display: flex;
396
+ gap: 12px;
397
+ align-items: center;
398
+ }
399
+ .policy-radios label {
400
+ display: flex;
401
+ align-items: center;
402
+ gap: 4px;
403
+ cursor: pointer;
404
+ color: rgba(255, 255, 255, 0.75);
405
+ }
406
+ .policy-radios input[type='radio'] {
407
+ width: auto;
408
+ accent-color: #4a9eff;
409
+ margin: 0;
410
+ }
411
+ code {
412
+ font-family: ui-monospace, SFMono-Regular, monospace;
413
+ background: rgba(255, 255, 255, 0.08);
414
+ padding: 1px 4px;
415
+ border-radius: 3px;
416
+ color: #b6d4fe;
417
+ font-size: 11px;
418
+ }
419
+ .hint {
420
+ font-size: 11px;
421
+ color: rgba(255, 255, 255, 0.4);
422
+ margin-top: 2px;
423
+ grid-column: 2;
424
+ }
425
+ .preview {
426
+ font-size: 11.5px;
427
+ color: rgba(255, 255, 255, 0.65);
428
+ margin-top: 10px;
429
+ padding: 6px 8px;
430
+ background: rgba(255, 255, 255, 0.04);
431
+ border-radius: 4px;
432
+ }
433
+ .conflict {
434
+ margin-top: 8px;
435
+ padding: 8px 10px;
436
+ background: rgba(255, 100, 100, 0.12);
437
+ border: 1px solid rgba(255, 100, 100, 0.3);
438
+ border-radius: 4px;
439
+ color: #ffb3b3;
440
+ font-size: 11.5px;
441
+ }
442
+ .conflict strong {
443
+ color: #ff8080;
444
+ }
445
+ .conflict-hint {
446
+ margin-top: 4px;
447
+ color: rgba(255, 255, 255, 0.45);
448
+ font-size: 11px;
449
+ }
450
+ .actions {
451
+ margin-top: 16px;
452
+ display: flex;
453
+ justify-content: flex-end;
454
+ gap: 8px;
455
+ }
456
+ button {
457
+ padding: 5px 14px;
458
+ font: inherit;
459
+ font-size: 12px;
460
+ border: 1px solid rgba(255, 255, 255, 0.15);
461
+ background: rgba(255, 255, 255, 0.06);
462
+ color: rgba(255, 255, 255, 0.85);
463
+ cursor: pointer;
464
+ border-radius: 4px;
465
+ }
466
+ button:hover:not(:disabled) {
467
+ background: rgba(255, 255, 255, 0.1);
468
+ }
469
+ button:disabled {
470
+ opacity: 0.4;
471
+ cursor: not-allowed;
472
+ }
473
+ button.primary {
474
+ background: #4a9eff;
475
+ color: white;
476
+ border-color: #4a9eff;
477
+ }
478
+ button.primary:hover:not(:disabled) {
479
+ background: #6db1ff;
480
+ }
481
+ `;
482
+ __decorate([
483
+ property({ type: Object })
484
+ ], BulkCreateDialog.prototype, "template", void 0);
485
+ __decorate([
486
+ property({ type: Object })
487
+ ], BulkCreateDialog.prototype, "initialArea", void 0);
488
+ __decorate([
489
+ property({ attribute: false })
490
+ ], BulkCreateDialog.prototype, "existingIds", void 0);
491
+ __decorate([
492
+ property({ attribute: false })
493
+ ], BulkCreateDialog.prototype, "onConfirm", void 0);
494
+ __decorate([
495
+ property({ attribute: false })
496
+ ], BulkCreateDialog.prototype, "onCancel", void 0);
497
+ __decorate([
498
+ state()
499
+ ], BulkCreateDialog.prototype, "pattern", void 0);
500
+ __decorate([
501
+ state()
502
+ ], BulkCreateDialog.prototype, "count", void 0);
503
+ __decorate([
504
+ state()
505
+ ], BulkCreateDialog.prototype, "rows", void 0);
506
+ __decorate([
507
+ state()
508
+ ], BulkCreateDialog.prototype, "cols", void 0);
509
+ __decorate([
510
+ state()
511
+ ], BulkCreateDialog.prototype, "idPattern", void 0);
512
+ __decorate([
513
+ state()
514
+ ], BulkCreateDialog.prototype, "idStart", void 0);
515
+ __decorate([
516
+ state()
517
+ ], BulkCreateDialog.prototype, "idPadding", void 0);
518
+ __decorate([
519
+ state()
520
+ ], BulkCreateDialog.prototype, "area", void 0);
521
+ __decorate([
522
+ state()
523
+ ], BulkCreateDialog.prototype, "conflictPolicy", void 0);
524
+ __decorate([
525
+ query('#count-input')
526
+ ], BulkCreateDialog.prototype, "countInput", void 0);
527
+ BulkCreateDialog = __decorate([
528
+ customElement('bulk-create-dialog')
529
+ ], BulkCreateDialog);
530
+ export { BulkCreateDialog };
531
+ //# sourceMappingURL=bulk-create-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulk-create-dialog.js","sourceRoot":"","sources":["../../../src/modeller/bulk-create-dialog.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * 멀티 컴포넌트 생성 dialog — 템플릿 + 영역 + 옵션 입력 → OK 시 콜백 호출.\n */\n\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport {\n generateBulkComponents,\n findIdConflicts,\n inferIdPattern,\n nextAvailableStart,\n deriveGridShape,\n type BulkCreateOptions,\n type BulkCreatePattern as Pattern,\n type BulkCreateArea as Area\n} from '@hatiolab/things-scene'\n\nexport type ConflictPolicy = 'skip' | 'duplicate'\n\nexport interface BulkCreateDialogProps {\n /** 템플릿 컴포넌트 state (read-only 표시용). */\n template: Record<string, any>\n /** 초기 영역 (보드 좌표). 사용자가 dialog 내에서 조정 가능. */\n initialArea: Area\n /** 기존 scene 의 모든 ID — 충돌 검사. */\n existingIds?: Set<string>\n /** OK 클릭 시 콜백 — 생성할 model 배열 전달. */\n onConfirm: (models: ReturnType<typeof generateBulkComponents>) => void\n /** 취소 / 닫기 콜백. */\n onCancel?: () => void\n}\n\n@customElement('bulk-create-dialog')\nexport class BulkCreateDialog extends LitElement {\n static styles = css`\n :host {\n display: block;\n padding: 16px 20px;\n min-width: 340px;\n max-width: 480px;\n background: rgba(30, 30, 30, 0.94);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 10px;\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);\n color: rgba(255, 255, 255, 0.85);\n font: 12px/1.6 -apple-system, BlinkMacSystemFont, sans-serif;\n }\n h3 {\n margin: 0 0 12px;\n font-size: 13px;\n font-weight: 600;\n color: #4a9eff;\n }\n .row {\n display: grid;\n grid-template-columns: 100px 1fr;\n align-items: center;\n gap: 8px;\n margin: 6px 0;\n }\n label {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.55);\n }\n input,\n select {\n padding: 4px 6px;\n font: inherit;\n font-size: 12px;\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 3px;\n width: 100%;\n box-sizing: border-box;\n color: rgba(255, 255, 255, 0.9);\n outline: none;\n }\n input:focus,\n select:focus {\n border-color: #4a9eff;\n background: rgba(74, 158, 255, 0.08);\n }\n select option {\n background: rgb(30, 30, 30);\n color: rgba(255, 255, 255, 0.9);\n }\n .grid-shape {\n display: grid;\n grid-template-columns: 1fr 20px 1fr;\n gap: 4px;\n align-items: center;\n }\n .grid-shape > span {\n text-align: center;\n color: rgba(255, 255, 255, 0.35);\n }\n .pattern-picker {\n display: flex;\n gap: 6px;\n }\n .pattern-btn {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 6px 4px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n cursor: pointer;\n color: rgba(255, 255, 255, 0.6);\n transition: background 0.1s, border-color 0.1s;\n }\n .pattern-btn:hover {\n background: rgba(255, 255, 255, 0.08);\n }\n .pattern-btn.active {\n border-color: #4a9eff;\n background: rgba(74, 158, 255, 0.15);\n color: rgba(255, 255, 255, 0.95);\n }\n .pattern-btn svg {\n width: 28px;\n height: 28px;\n }\n .pattern-btn svg circle {\n fill: currentColor;\n }\n .pattern-btn span {\n font-size: 10.5px;\n }\n .policy-radios {\n display: flex;\n gap: 12px;\n align-items: center;\n }\n .policy-radios label {\n display: flex;\n align-items: center;\n gap: 4px;\n cursor: pointer;\n color: rgba(255, 255, 255, 0.75);\n }\n .policy-radios input[type='radio'] {\n width: auto;\n accent-color: #4a9eff;\n margin: 0;\n }\n code {\n font-family: ui-monospace, SFMono-Regular, monospace;\n background: rgba(255, 255, 255, 0.08);\n padding: 1px 4px;\n border-radius: 3px;\n color: #b6d4fe;\n font-size: 11px;\n }\n .hint {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.4);\n margin-top: 2px;\n grid-column: 2;\n }\n .preview {\n font-size: 11.5px;\n color: rgba(255, 255, 255, 0.65);\n margin-top: 10px;\n padding: 6px 8px;\n background: rgba(255, 255, 255, 0.04);\n border-radius: 4px;\n }\n .conflict {\n margin-top: 8px;\n padding: 8px 10px;\n background: rgba(255, 100, 100, 0.12);\n border: 1px solid rgba(255, 100, 100, 0.3);\n border-radius: 4px;\n color: #ffb3b3;\n font-size: 11.5px;\n }\n .conflict strong {\n color: #ff8080;\n }\n .conflict-hint {\n margin-top: 4px;\n color: rgba(255, 255, 255, 0.45);\n font-size: 11px;\n }\n .actions {\n margin-top: 16px;\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n }\n button {\n padding: 5px 14px;\n font: inherit;\n font-size: 12px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n background: rgba(255, 255, 255, 0.06);\n color: rgba(255, 255, 255, 0.85);\n cursor: pointer;\n border-radius: 4px;\n }\n button:hover:not(:disabled) {\n background: rgba(255, 255, 255, 0.1);\n }\n button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n button.primary {\n background: #4a9eff;\n color: white;\n border-color: #4a9eff;\n }\n button.primary:hover:not(:disabled) {\n background: #6db1ff;\n }\n `\n\n @property({ type: Object }) template!: Record<string, any>\n @property({ type: Object }) initialArea!: Area\n @property({ attribute: false }) existingIds: Set<string> = new Set()\n @property({ attribute: false }) onConfirm!: BulkCreateDialogProps['onConfirm']\n @property({ attribute: false }) onCancel?: BulkCreateDialogProps['onCancel']\n\n @state() private pattern: Pattern = 'grid'\n @state() private count = 12\n @state() private rows: number | undefined = undefined\n @state() private cols: number | undefined = undefined\n @state() private idPattern = '{i}'\n @state() private idStart = 1\n @state() private idPadding = 0\n @state() private area!: Area\n @state() private conflictPolicy: ConflictPolicy = 'skip'\n\n @query('#count-input') countInput!: HTMLInputElement\n\n connectedCallback() {\n super.connectedCallback()\n // OxPopup wrapper (ox-popup) 의 흰색 배경 + box-shadow 가 dialog 라운드 코너\n // 밖으로 비쳐서 4 꼭지점에 흰색 사각형이 보임. 부모를 투명/그림자 없이 만들어\n // dialog 자체의 라운드 + 그림자만 보이게.\n const parent = this.parentElement as HTMLElement | null\n if (parent) {\n parent.style.background = 'transparent'\n parent.style.boxShadow = 'none'\n parent.style.border = 'none'\n }\n this.area = { ...this.initialArea }\n\n // 1) 영역 종횡비로 레이아웃 기본 선택:\n // 너비 ≫ 높이 → line-h (가로), 높이 ≫ 너비 → line-v (세로), 비슷하면 grid.\n const w = Math.max(this.area.width, 1)\n const h = Math.max(this.area.height, 1)\n const aspect = w / h\n if (aspect >= 2.5) this.pattern = 'line-h'\n else if (aspect <= 0.4) this.pattern = 'line-v'\n else this.pattern = 'grid'\n\n // 2) 템플릿 id 로부터 ID 패턴 / padding 자동 추론.\n const inferred = inferIdPattern(this.template?.id)\n this.idPattern = inferred.pattern\n this.idPadding = inferred.padding\n\n // 3) 시작 번호 = existingIds 에 패턴 매칭되는 *최대 번호 + 1*.\n // 반복 bulk-paste 시 이전 batch 의 끝 다음으로 자동 advance.\n // 매칭 ID 가 없으면 inferIdPattern 의 fallback (= template num+1) 사용.\n this.idStart = nextAvailableStart(inferred.pattern, this.existingIds, inferred.start)\n }\n\n /** rows/cols 가 미입력 (undefined) 이면 영역 종횡비 + count 로 자동 계산해 표시. */\n private get displayRows(): number {\n if (this.rows != null) return this.rows\n return deriveGridShape(this.count, this.area, undefined, this.cols).rows\n }\n\n private get displayCols(): number {\n if (this.cols != null) return this.cols\n return deriveGridShape(this.count, this.area, this.rows, undefined).cols\n }\n\n firstUpdated() {\n this.countInput?.focus()\n this.countInput?.select()\n }\n\n private updateArea(field: keyof Area, value: number) {\n this.area = { ...this.area, [field]: value }\n }\n\n private get previewModels() {\n if (!this.template) return []\n return generateBulkComponents({\n template: this.template,\n area: this.area,\n pattern: this.pattern,\n count: this.count,\n rows: this.rows,\n cols: this.cols,\n idPattern: this.idPattern.replace(/\\{type\\}/g, String(this.template?.type ?? 'C')),\n idStart: this.idStart,\n idPadding: this.idPadding\n })\n }\n\n private get conflicts(): string[] {\n return findIdConflicts(this.previewModels, this.existingIds)\n }\n\n /** 충돌 정책 적용 후 실제 생성될 model 배열. */\n private get effectiveModels() {\n const all = this.previewModels\n if (this.conflictPolicy === 'skip' && this.existingIds.size > 0) {\n return all.filter(m => !this.existingIds.has(m.id))\n }\n return all\n }\n\n private onOk = () => {\n const models = this.effectiveModels\n if (models.length === 0) return\n this.onConfirm(models)\n }\n\n private onCancelClick = () => {\n this.onCancel?.()\n }\n\n render() {\n const tplType = this.template?.type ?? '?'\n const tplId = this.template?.id ?? '?'\n const models = this.previewModels\n\n return html`\n <h3>Bulk Paste — ${tplType}</h3>\n\n <div class=\"row\">\n <label>템플릿</label>\n <span style=\"color:#b6d4fe;\">${tplId} <span style=\"color:rgba(255,255,255,0.4);\">(${tplType})</span></span>\n </div>\n\n <div class=\"row\">\n <label>레이아웃</label>\n <div class=\"pattern-picker\">\n <button\n type=\"button\"\n class=\"pattern-btn ${this.pattern === 'grid' ? 'active' : ''}\"\n @click=${() => (this.pattern = 'grid')}\n title=\"격자\"\n >\n <svg viewBox=\"0 0 24 24\">\n <circle cx=\"6\" cy=\"6\" r=\"1.6\" /><circle cx=\"12\" cy=\"6\" r=\"1.6\" /><circle cx=\"18\" cy=\"6\" r=\"1.6\" />\n <circle cx=\"6\" cy=\"12\" r=\"1.6\" /><circle cx=\"12\" cy=\"12\" r=\"1.6\" /><circle cx=\"18\" cy=\"12\" r=\"1.6\" />\n <circle cx=\"6\" cy=\"18\" r=\"1.6\" /><circle cx=\"12\" cy=\"18\" r=\"1.6\" /><circle cx=\"18\" cy=\"18\" r=\"1.6\" />\n </svg>\n <span>격자</span>\n </button>\n <button\n type=\"button\"\n class=\"pattern-btn ${this.pattern === 'line-h' ? 'active' : ''}\"\n @click=${() => (this.pattern = 'line-h')}\n title=\"가로 1줄\"\n >\n <svg viewBox=\"0 0 24 24\">\n <circle cx=\"3\" cy=\"12\" r=\"1.6\" /><circle cx=\"8\" cy=\"12\" r=\"1.6\" /><circle cx=\"13\" cy=\"12\" r=\"1.6\" />\n <circle cx=\"18\" cy=\"12\" r=\"1.6\" /><circle cx=\"22\" cy=\"12\" r=\"1.4\" />\n </svg>\n <span>가로</span>\n </button>\n <button\n type=\"button\"\n class=\"pattern-btn ${this.pattern === 'line-v' ? 'active' : ''}\"\n @click=${() => (this.pattern = 'line-v')}\n title=\"세로 1줄\"\n >\n <svg viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"3\" r=\"1.6\" /><circle cx=\"12\" cy=\"8\" r=\"1.6\" /><circle cx=\"12\" cy=\"13\" r=\"1.6\" />\n <circle cx=\"12\" cy=\"18\" r=\"1.6\" /><circle cx=\"12\" cy=\"22\" r=\"1.4\" />\n </svg>\n <span>세로</span>\n </button>\n </div>\n </div>\n\n <div class=\"row\">\n <label>개수</label>\n <input\n id=\"count-input\"\n type=\"number\"\n min=\"1\"\n .value=${String(this.count)}\n @input=${(e: Event) => (this.count = Math.max(1, +(e.target as HTMLInputElement).value))}\n />\n </div>\n\n ${this.pattern === 'grid'\n ? html`\n <div class=\"row\">\n <label>행 × 열</label>\n <div class=\"grid-shape\">\n <input\n type=\"number\"\n min=\"1\"\n .value=${String(this.displayRows)}\n @input=${(e: Event) => {\n const v = (e.target as HTMLInputElement).value\n this.rows = v ? Math.max(1, +v) : undefined\n }}\n />\n <span>×</span>\n <input\n type=\"number\"\n min=\"1\"\n .value=${String(this.displayCols)}\n @input=${(e: Event) => {\n const v = (e.target as HTMLInputElement).value\n this.cols = v ? Math.max(1, +v) : undefined\n }}\n />\n </div>\n </div>\n `\n : ''}\n\n <div class=\"row\">\n <label>ID 패턴</label>\n <input\n type=\"text\"\n .value=${this.idPattern}\n @input=${(e: Event) => (this.idPattern = (e.target as HTMLInputElement).value)}\n />\n </div>\n <div class=\"row\">\n <span></span>\n <div class=\"hint\">placeholder: <code>{i}</code> 일련번호, <code>{r}</code> 행, <code>{c}</code> 열 (grid)</div>\n </div>\n\n <div class=\"row\">\n <label>시작 번호</label>\n <input\n type=\"number\"\n .value=${String(this.idStart)}\n @input=${(e: Event) => (this.idStart = +(e.target as HTMLInputElement).value)}\n />\n </div>\n\n <div class=\"row\">\n <label>Zero-pad 자릿수</label>\n <input\n type=\"number\"\n min=\"0\"\n .value=${String(this.idPadding)}\n @input=${(e: Event) => (this.idPadding = Math.max(0, +(e.target as HTMLInputElement).value))}\n />\n </div>\n\n <div class=\"preview\">\n ${models.length > 0\n ? html`총 ${models.length}개 — 예: <code>${models[0].id}</code> ... <code>${models[models.length - 1].id}</code>`\n : html`<span style=\"color:#ff8080;\">생성될 컴포넌트 없음</span>`}\n </div>\n\n ${this.conflicts.length > 0\n ? html`\n <div class=\"conflict\">\n <strong>ID 충돌 ${this.conflicts.length}개</strong>\n <code>${this.conflicts.slice(0, 5).join(', ')}${this.conflicts.length > 5 ? ', ...' : ''}</code>\n <div class=\"conflict-hint\" style=\"margin-top:6px;\">\n <div class=\"policy-radios\">\n <label>\n <input\n type=\"radio\"\n name=\"conflict-policy\"\n value=\"skip\"\n .checked=${this.conflictPolicy === 'skip'}\n @change=${() => (this.conflictPolicy = 'skip')}\n />\n 충돌 ID 스킵 (기본)\n </label>\n <label>\n <input\n type=\"radio\"\n name=\"conflict-policy\"\n value=\"duplicate\"\n .checked=${this.conflictPolicy === 'duplicate'}\n @change=${() => (this.conflictPolicy = 'duplicate')}\n />\n 중복 그대로 생성\n </label>\n </div>\n </div>\n </div>\n `\n : ''}\n\n <div class=\"actions\">\n <button @click=${this.onCancelClick}>취소</button>\n <button\n class=\"primary\"\n @click=${this.onOk}\n ?disabled=${this.effectiveModels.length === 0}\n >\n 생성 (${this.effectiveModels.length}${\n this.conflicts.length > 0 && this.conflictPolicy === 'skip'\n ? html`<span style=\"opacity:0.6;\"> / ${models.length}</span>`\n : ''\n })\n </button>\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'bulk-create-dialog': BulkCreateDialog\n }\n}\n"]}