polarvo-layout 1.0.58 → 1.0.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polarvo-layout",
3
- "version": "1.0.58",
3
+ "version": "1.0.60",
4
4
  "type": "module",
5
5
  "author": "unigence <unigencelab@gmail.com>",
6
6
  "repository": {
@@ -18,19 +18,11 @@
18
18
  {{ key }}
19
19
  </div>
20
20
  <template v-if="section.mode === 'grid'">
21
- <GridLayout
22
- :id="`grid-${key}`"
23
- :polarvo="polarvo"
24
- :preview="preview"
25
- :sectionKey="key"
26
- :sectionData="section"
27
- :elementIds="section.elementIds"
28
- />
21
+ <GridLayout :polarvo="polarvo" :preview="preview" :sectionKey="key" :sectionData="section" :elementIds="section.elementIds" />
29
22
  </template>
30
23
 
31
24
  <template v-else>
32
25
  <FreeLayout
33
- :id="`free-${key}`"
34
26
  :polarvo="polarvo"
35
27
  :preview="preview"
36
28
  :sectionKey="key"
@@ -91,6 +83,9 @@ const getSectionStyle = (section) => ({
91
83
  '--grid-rows': section.config?.gridRows ?? 3,
92
84
  '--grid-gap': `${section.config?.gridGap ?? 5}px`,
93
85
  '--grid-row-sizes': section.config?.gridRowRatio?.length ? section.config.gridRowRatio.map((r) => `${r}fr`).join(' ') : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
86
+ '--grid-column-sizes': section.config?.gridColumnRatio?.length
87
+ ? section.config.gridColumnRatio.map((r) => `${r}fr`).join(' ')
88
+ : undefined,
94
89
  position: 'relative',
95
90
  });
96
91
 
@@ -8,29 +8,61 @@
8
8
  :sectionKey="props.sectionKey"
9
9
  ></GridItem>
10
10
 
11
+ <!-- 행 크기 조절 -->
11
12
  <div
12
13
  v-for="r in gridRows - 1"
13
14
  :key="`row-handle-${r}`"
14
15
  :id="`row-handle-${r}`"
15
- class="z-[60] relative self-end justify-self-stretch cursor-s-resize "
16
+ class="z-[60] relative self-end justify-self-stretch cursor-s-resize"
16
17
  :style="{
17
18
  gridRow: r,
18
19
  gridColumn: '1 / -1',
19
20
  height: `${gridGap}px`,
20
21
  transform: 'translateY(100%)',
21
22
  }"
22
- @mousedown="handleMouseDown($event, r)"
23
+ @mousedown="handleMouseDown($event, 'row', r)"
23
24
  >
24
25
  <!-- 점선 -->
25
26
  <div
26
- class="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 border-t border-dashed border-blue-700 "
27
- :class="{ 'opacity-100': rowState.activeRow === r, 'opacity-0': rowState.activeRow !== r }"
27
+ class="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 border-t border-dashed border-blue-700"
28
+ :class="{
29
+ 'opacity-100': dragState.axis === 'row' && dragState.activeIdx === r,
30
+ 'opacity-0': !(dragState.axis === 'row' && dragState.activeIdx === r),
31
+ }"
32
+ ></div>
33
+
34
+ <!-- 핸들 -->
35
+ <!-- <div
36
+ class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3 h-3 cursor-s-resize rounded-full bg-violet-400 opacity-30 hover:opacity-100"
37
+ ></div> -->
38
+ </div>
39
+
40
+ <!-- 열 크기 조절 -->
41
+ <div
42
+ v-for="c in gridColumns - 1"
43
+ :key="`col-handle-${c}`"
44
+ :id="`col-handle-${c}`"
45
+ class="z-[60] relative self-stretch justify-self-end cursor-e-resize"
46
+ :style="{
47
+ gridRow: '1 / -1',
48
+ gridColumn: c,
49
+ width: `${gridGap}px`,
50
+ transform: 'translateX(100%)',
51
+ }"
52
+ @mousedown="handleMouseDown($event, 'col', c)"
53
+ >
54
+ <!-- 점선 -->
55
+ <div
56
+ class="pointer-events-none absolute top-0 bottom-0 left-1/2 -translate-x-1/2 border-l border-dashed border-blue-700"
57
+ :class="{
58
+ 'opacity-100': dragState.axis === 'col' && dragState.activeIdx === c,
59
+ 'opacity-0': !(dragState.axis === 'col' && dragState.activeIdx === c),
60
+ }"
28
61
  ></div>
29
62
 
30
63
  <!-- 핸들 -->
31
64
  <!-- <div
32
65
  class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3 h-3 cursor-s-resize rounded-full bg-violet-400 opacity-30 hover:opacity-100"
33
- @mousedown="handleMouseDown($event, r)"
34
66
  ></div> -->
35
67
  </div>
36
68
 
@@ -81,36 +113,62 @@ const gridColumns = ref(3); // 동적으로 계산
81
113
  const gridRows = ref(1); // 동적으로 계산
82
114
  const gridGap = computed(() => props.sectionData?.config?.gridGap ?? 5); // BaseLayout.vue --grid-gap과 동일한 소스/기본값
83
115
 
84
- // row Resize
116
+ // row, col Resize
85
117
  const MIN_ROW_PX = 20; // 행 최소 높이(px)
118
+ const MIN_COL_PX = 20; // 열 최소 너비(px)
119
+
120
+ const DRAG_CONFIG = {
121
+ row: {
122
+ ref: gridRows,
123
+ minPx: MIN_ROW_PX,
124
+ ratioKey: 'gridRowRatio',
125
+ gridTemplateKey: 'gridTemplateRows',
126
+ clientKey: 'clientY',
127
+ positionKey: 'y',
128
+ cssVar: '--grid-row-sizes',
129
+ },
130
+ col: {
131
+ ref: gridColumns,
132
+ minPx: MIN_COL_PX,
133
+ ratioKey: 'gridColumnRatio',
134
+ gridTemplateKey: 'gridTemplateColumns',
135
+ clientKey: 'clientX',
136
+ positionKey: 'x',
137
+ cssVar: '--grid-column-sizes',
138
+ },
139
+ };
86
140
 
87
- const rowState = ref({
88
- activeRow: null,
141
+ const dragState = ref({
142
+ axis: null, // 'row' | 'col'
143
+ activeIdx: null,
89
144
  isDragging: false,
90
145
  });
91
146
  const startPos = ref({ x: 0, y: 0 });
92
147
 
93
- function handleMouseDown(event, row) {
94
- if (!row) return;
148
+ function handleMouseDown(event, axis, index) {
149
+ if (!index) return;
95
150
  event.preventDefault();
96
151
  event.stopPropagation();
97
152
 
98
- rowState.value.activeRow = row;
153
+ const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar } = DRAG_CONFIG[axis];
154
+
155
+ dragState.value.axis = axis;
156
+ dragState.value.activeIdx = index;
99
157
  startPos.value = { x: event.clientX, y: event.clientY };
100
158
 
101
159
  setActiveMenu(null); // 메뉴 닫기
102
160
  setActiveDesign(null); // 디자인 닫기
103
161
 
104
162
  let dragSnapShot = null; // 드래그 시작 시 스냅샷
105
- let liveRowRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
106
- let pendingClientY = null;
163
+ let liveRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
164
+ let pendingClient = null;
107
165
  let rafId = null;
108
166
 
109
167
  document.addEventListener('mousemove', _detectDragIntent);
110
168
  document.addEventListener('mouseup', _cancelDragIntent);
111
169
 
112
170
  function _detectDragIntent(e) {
113
- if (rowState.value.activeRow !== row) return;
171
+ if (dragState.value.axis !== axis || dragState.value.activeIdx !== index) return;
114
172
  const deltaX = Math.abs(e.clientX - startPos.value.x);
115
173
  const deltaY = Math.abs(e.clientY - startPos.value.y);
116
174
 
@@ -127,26 +185,27 @@ function handleMouseDown(event, row) {
127
185
  }
128
186
 
129
187
  function _cancelDragIntent() {
130
- rowState.value.activeRow = null;
188
+ dragState.value.axis = null;
189
+ dragState.value.activeIdx = null;
131
190
 
132
191
  document.removeEventListener('mousemove', _detectDragIntent);
133
192
  document.removeEventListener('mouseup', _cancelDragIntent);
134
193
  }
135
194
 
136
195
  function _startDrag() {
137
- rowState.value.isDragging = true;
196
+ dragState.value.isDragging = true;
138
197
 
139
198
  const container = document.getElementById(`${props.sectionKey}Layout`);
140
199
  if (!container) return _stopDrag();
141
200
 
142
- const configured = props.sectionData?.config?.gridRowRatio;
201
+ const configured = props.sectionData?.config?.[ratioKey];
143
202
  const baselineRatio =
144
- (Array.isArray(configured) && configured.length === gridRows.value) || configured.some((v) => typeof v !== 'number' || !(v > 0))
203
+ (Array.isArray(configured) && configured.length === ref.value) || configured.some((v) => typeof v !== 'number' || !(v > 0))
145
204
  ? [...configured]
146
- : Array(gridRows.value).fill(1);
205
+ : Array(ref.value).fill(1);
147
206
 
148
207
  const trackPx = getComputedStyle(container)
149
- .gridTemplateRows.split(' ')
208
+ [gridTemplateKey].split(' ')
150
209
  .map((v) => parseFloat(v));
151
210
 
152
211
  // 총 Fr 계산
@@ -160,7 +219,7 @@ function handleMouseDown(event, row) {
160
219
  container,
161
220
  baselineRatio,
162
221
  frPerPx,
163
- minFr: MIN_ROW_PX * frPerPx, // 최소 Fr 계산
222
+ minFr: minPx * frPerPx, // 최소 Fr 계산
164
223
  };
165
224
 
166
225
  document.addEventListener('mousemove', _startDragMove);
@@ -169,7 +228,7 @@ function handleMouseDown(event, row) {
169
228
 
170
229
  function _startDragMove(e) {
171
230
  if (!dragSnapShot) return;
172
- pendingClientY = e.clientY;
231
+ pendingClient = e[clientKey];
173
232
 
174
233
  // rafId 중복 예약 방지
175
234
  if (rafId) return;
@@ -178,39 +237,40 @@ function handleMouseDown(event, row) {
178
237
  rafId = null;
179
238
 
180
239
  const { baselineRatio, frPerPx, minFr, container } = dragSnapShot;
181
- const topIdx = row - 1; // 드래그 행 위쪽 행 인덱스
182
- const bottomIdx = row; // 드래그 행 인덱스
240
+ const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
241
+ const nextIdx = index; // 드래그 행 인덱스
183
242
 
184
243
  // 순수 이동량
185
- let delta = (pendingClientY - startPos.value.y) * frPerPx;
186
- // 위쪽 행 작아지는 것 방지: 가장 커져도 minFr - baselineRatio[topIdx] (음수값 = 위로 이동)
187
- delta = Math.max(minFr - baselineRatio[topIdx], delta);
244
+ let delta = (pendingClient - startPos.value[positionKey]) * frPerPx;
245
+ // 위쪽 행 작아지는 것 방지: 가장 커져도 minFr - baselineRatio[prevIdx] (음수값 = 위로 이동)
246
+ delta = Math.max(minFr - baselineRatio[prevIdx], delta);
188
247
  // 아래쪽 행 작아지는 것 방지: 가장 작아져도 minFr (양수값 = 아래로 이동)
189
- delta = Math.min(baselineRatio[bottomIdx] - minFr, delta);
248
+ delta = Math.min(baselineRatio[nextIdx] - minFr, delta);
190
249
 
191
250
  // 미리보기용
192
251
  const preview = [...baselineRatio];
193
- preview[topIdx] += delta;
194
- preview[bottomIdx] -= delta;
252
+ preview[prevIdx] += delta;
253
+ preview[nextIdx] -= delta;
195
254
 
196
- liveRowRatio = preview;
255
+ liveRatio = preview;
197
256
 
198
257
  // 화면에만 즉시 반영
199
- container.style.setProperty('--grid-row-sizes', preview.map((v) => `${v}fr`).join(' '));
258
+ container.style.setProperty(cssVar, preview.map((v) => `${v}fr`).join(' '));
200
259
  });
201
260
  }
202
261
  function _stopDrag() {
203
- rowState.value.isDragging = false;
204
- rowState.value.activeRow = null;
262
+ dragState.value.isDragging = false;
263
+ dragState.value.axis = null;
264
+ dragState.value.activeIdx = null;
205
265
 
206
- if (liveRowRatio) {
207
- setSectionConfig('gridRowRatio', liveRowRatio);
266
+ if (liveRatio) {
267
+ setSectionConfig(ratioKey, liveRatio);
208
268
  }
209
269
 
210
270
  startPos.value = { x: 0, y: 0 };
211
271
  dragSnapShot = null;
212
- liveRowRatio = null;
213
- pendingClientY = null;
272
+ liveRatio = null;
273
+ pendingClient = null;
214
274
  rafId = null;
215
275
 
216
276
  document.removeEventListener('mousemove', _startDragMove);
@@ -17,7 +17,7 @@ const props = defineProps({
17
17
  <style scoped>
18
18
  .polar-grid {
19
19
  display: grid;
20
- grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
20
+ grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 3), 1fr));
21
21
  /* grid-template-rows: repeat(var(--grid-rows, 3), 1fr); */
22
22
  grid-template-rows: var(--grid-row-sizes, repeat(var(--grid-rows, 3), 1fr));
23
23
  gap: var(--grid-gap, 5px);
@@ -156,14 +156,14 @@ class EngineManager {
156
156
  * @param {string} type - 설정 타입 (mode, config)
157
157
  * @param {object} updates - 업데이트할 설정 값 객체
158
158
  */
159
- _updateActiveSectionConfig(type, updates) {
160
- if (!this.state.layoutData || !this.state.activeSection) {
159
+ _updateActiveSectionConfig(type, updates, section = this.state.activeSection) {
160
+ if (!this.state.layoutData || !section) {
161
161
  console.error('[EngineManager] activeData를 업데이트할 수 없습니다.');
162
162
  return;
163
163
  }
164
164
 
165
165
  this.eventBus.emit('system:updateActiveSectionConfig', {
166
- $section: this.state.activeSection,
166
+ $section: section,
167
167
  changes: { type: type, ...updates },
168
168
  timestamp: Date.now(),
169
169
  });
@@ -460,7 +460,7 @@ class EngineManager {
460
460
  * @param {number|boolean} config - 새로운 설정 값
461
461
  */
462
462
  setSectionConfig(type, config) {
463
- if (!['column', 'row', 'gap', 'guideLine', 'gridRowRatio'].includes(type)) {
463
+ if (!['column', 'row', 'gap', 'guideLine', 'gridRowRatio', 'gridColumnRatio'].includes(type)) {
464
464
  return { result: false, message: '입력할 수 없는 config 타입입니다.' };
465
465
  }
466
466
 
@@ -486,6 +486,14 @@ class EngineManager {
486
486
  }
487
487
 
488
488
  newConfig = { gridRowRatio: value };
489
+ } else if (type === 'gridColumnRatio') {
490
+ // gridColumnRatio 배열은 gridColumns 길이와 일치해야 함
491
+ const gridColumns = this.activeData.config?.gridColumns || 1;
492
+ if (!Array.isArray(value) || value.length !== gridColumns || value.some((v) => typeof v !== 'number' || !(v > 0))) {
493
+ return { result: false, message: '잘못된 열 비율 값입니다.' };
494
+ }
495
+
496
+ newConfig = { gridColumnRatio: value };
489
497
  } else {
490
498
  const limit = CONFIG_LIMITS[type];
491
499
 
@@ -500,7 +508,8 @@ class EngineManager {
500
508
  value = Math.min(Math.max(limit.min, value), limit.max);
501
509
 
502
510
  if (type === 'column') {
503
- newConfig = { gridColumns: value };
511
+ // gridColumns 변경되면 gridColumnRatio 초기화
512
+ newConfig = { gridColumns: value, gridColumnRatio: [] };
504
513
  } else if (type === 'row') {
505
514
  // gridRows 변경되면 gridRowRatio 초기화
506
515
  newConfig = { gridRows: value, gridRowRatio: [] };
@@ -525,7 +534,8 @@ class EngineManager {
525
534
  }
526
535
 
527
536
  this.state.elements.push(element);
528
- this._updateActiveSectionConfig('elementIds', { elementIds: [...(this.activeData?.elementIds || []), element.id] });
537
+ const targetSectionData = this.state.layoutData?.[element.section];
538
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...(targetSectionData?.elementIds || []), element.id] }, element.section);
529
539
 
530
540
  this.eventBus.emit('system:requestUpdateElement', {
531
541
  action: 'add',
@@ -542,10 +552,12 @@ class EngineManager {
542
552
 
543
553
  // 배치요소 삭제 요청
544
554
  this._subscribe('freeDrop:requestDeleteElement', ({ elementId }) => {
555
+ const target = this.state.elements.find((el) => el.id === elementId);
545
556
  this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
557
+ const targetSectionData = this.state.layoutData?.[target?.section];
546
558
  this._updateActiveSectionConfig('elementIds', {
547
- elementIds: (this.activeData?.elementIds || []).filter((elId) => elId !== elementId),
548
- });
559
+ elementIds: (targetSectionData?.elementIds || []).filter((elId) => elId !== elementId),
560
+ }, target?.section);
549
561
  // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
550
562
 
551
563
  this.eventBus.emit('system:requestUpdateElement', {
@@ -557,11 +569,7 @@ class EngineManager {
557
569
 
558
570
  this.eventBus.emit('system:historyCheckpoint', {
559
571
  domain: 'manager',
560
- state: {
561
- manager: this.getState() || null,
562
- layout: this.getEngine('layout')?.getState() || null,
563
- display: this.getEngine('display')?.getState() || null,
564
- },
572
+ state: this.getState(),
565
573
  });
566
574
  });
567
575
 
@@ -600,7 +608,8 @@ class EngineManager {
600
608
  }
601
609
 
602
610
  this.state.elements.push(element);
603
- this._updateActiveSectionConfig('elementIds', { elementIds: [...(this.activeData?.elementIds || []), element.id] });
611
+ const targetSectionData = this.state.layoutData?.[element.section];
612
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...(targetSectionData?.elementIds || []), element.id] }, element.section);
604
613
  // this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
605
614
 
606
615
  this.eventBus.emit('system:requestUpdateElement', {
@@ -618,10 +627,12 @@ class EngineManager {
618
627
 
619
628
  // 배치요소 삭제 요청
620
629
  this._subscribe('gridDrop:requestDeleteElement', ({ elementId }) => {
630
+ const target = this.state.elements.find((el) => el.id === elementId);
621
631
  this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
632
+ const targetSectionData = this.state.layoutData?.[target?.section];
622
633
  this._updateActiveSectionConfig('elementIds', {
623
- elementIds: (this.activeData?.elementIds || []).filter((elId) => elId !== elementId),
624
- });
634
+ elementIds: (targetSectionData?.elementIds || []).filter((elId) => elId !== elementId),
635
+ }, target?.section);
625
636
  // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
626
637
 
627
638
  this.eventBus.emit('system:requestUpdateElement', {
@@ -120,11 +120,11 @@ function useLayout(api) {
120
120
 
121
121
  _subscribe('system:restoredState', ({ domain, state: snapShot }) => {
122
122
  if (domain === 'all'|| domain === 'draft') {
123
- state.elements = snapShot.manager.elements;
123
+ state.elements = cloneDeep(snapShot.manager.elements);
124
124
  // state.layoutData = snapShot.manager.layoutData
125
125
 
126
126
  state.layoutName = snapShot.layout.layoutName;
127
- state.layoutData = snapShot.layout.layoutData;
127
+ state.layoutData = cloneDeep(snapShot.layout.layoutData);
128
128
  state.gridNumber = { ...snapShot.layout.gridNumber };
129
129
  state.gridRatio = {
130
130
  column: [...snapShot.layout.gridRatio.column],
@@ -134,13 +134,13 @@ function useLayout(api) {
134
134
  }
135
135
 
136
136
  if (domain === 'manager') {
137
- state.elements = snapShot.elements;
138
- state.layoutData = snapShot.layoutData;
137
+ state.elements = cloneDeep(snapShot.elements);
138
+ state.layoutData = cloneDeep(snapShot.layoutData);
139
139
  }
140
140
 
141
141
  if (domain === 'layout') {
142
142
  state.layoutName = snapShot.layoutName;
143
- state.layoutData = snapShot.layoutData;
143
+ state.layoutData = cloneDeep(snapShot.layoutData);
144
144
  state.gridNumber = { ...snapShot.gridNumber };
145
145
  state.gridRatio = {
146
146
  column: [...snapShot.gridRatio.column],