@feedmepos/mf-order-setting 0.0.90 → 0.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/DisplaySystemView-n7Kqdw-h.js +6564 -0
  2. package/dist/{KioskDevicesView-WuOTJ9Fi.js → KioskDevicesView-Bz6seroM.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-DdHfATyN.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-I39sE8Pe.js} +3 -3
  4. package/dist/{KioskSettingView-Bsja2OqT.js → KioskSettingView-Cajjh4Zm.js} +3 -3
  5. package/dist/{KioskView-BH5qGTM_.js → KioskView-BPSbsMi5.js} +93 -89
  6. package/dist/{OrderSettingsView-D6V1ztiS.js → OrderSettingsView-jPxZ3JDM.js} +5 -5
  7. package/dist/{app-DsMk7W2h.js → app-DIhv5ssA.js} +266 -254
  8. package/dist/app.js +1 -1
  9. package/dist/frontend/mf-order/src/app.d.ts +18 -6
  10. package/dist/frontend/mf-order/src/main.d.ts +18 -6
  11. package/dist/frontend/mf-order/src/modules/display-system/interface.d.ts +6 -3
  12. package/dist/frontend/mf-order/src/modules/kiosk/bindDeviceError.d.ts +20 -0
  13. package/dist/frontend/mf-order/src/views/display-system/CanvasArea.vue.d.ts +48 -0
  14. package/dist/frontend/mf-order/src/views/display-system/DisplayLayoutCanvas.vue.d.ts +17 -14
  15. package/dist/frontend/mf-order/src/views/display-system/DisplayModulePickerDialog.vue.d.ts +2 -0
  16. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  17. package/dist/{index-DGDiI0fr.js → index-Clp_YRJ2.js} +1 -1
  18. package/dist/{index-BUDM0Rqn.js → index-CmqNrkNl.js} +1 -1
  19. package/dist/{index-C2n1MV_1.js → index-vaSLX6r3.js} +2 -2
  20. package/dist/{order.dto-BRDWWgMV.js → order.dto-DV1qL3Q_.js} +21 -1
  21. package/dist/package/entity/alert/investigation.dto.d.ts +26 -0
  22. package/dist/package/entity/alert/lark-callback.dto.d.ts +19 -0
  23. package/dist/package/entity/alert/record-resolution.dto.d.ts +35 -0
  24. package/dist/package/entity/index.d.ts +1 -0
  25. package/dist/package/entity/kiosk/kiosk.do.d.ts +105 -0
  26. package/dist/package/entity/kiosk/kiosk.dto.d.ts +76 -0
  27. package/dist/style.css +1 -1
  28. package/package.json +3 -1
  29. package/src/locales/en-US.json +11 -4
  30. package/src/locales/zh-CN.json +9 -4
  31. package/src/modules/display-system/interface.spec.ts +90 -5
  32. package/src/modules/display-system/interface.ts +31 -21
  33. package/src/modules/kiosk/bindDeviceError.spec.ts +42 -0
  34. package/src/modules/kiosk/bindDeviceError.ts +40 -0
  35. package/src/views/display-system/CanvasArea.vue +355 -0
  36. package/src/views/display-system/DisplayLayoutCanvas.spec.ts +57 -0
  37. package/src/views/display-system/DisplayLayoutCanvas.vue +173 -298
  38. package/src/views/display-system/DisplayModulePickerDialog.spec.ts +71 -0
  39. package/src/views/display-system/DisplayModulePickerDialog.vue +107 -0
  40. package/src/views/display-system/DisplayTemplateEditor.spec.ts +151 -0
  41. package/src/views/display-system/DisplayTemplateEditor.vue +233 -193
  42. package/src/views/kiosk/KioskView.vue +2 -4
  43. package/vitest.config.ts +2 -0
  44. package/dist/DisplaySystemView-tosCa7Ej.js +0 -6485
  45. package/dist/frontend/mf-order/src/views/display-system/DisplayModulePalette.vue.d.ts +0 -13
  46. package/src/views/display-system/DisplayModulePalette.vue +0 -140
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue'
3
- import { useI18n } from '@feedmepos/mf-common'
3
+ import CanvasArea from './CanvasArea.vue'
4
4
 
5
5
  import type {
6
6
  DisplayLayoutDraft,
@@ -8,7 +8,7 @@ import type {
8
8
  DisplayZoneDraft
9
9
  } from '@/modules/display-system/interface'
10
10
  import {
11
- blockDefinition,
11
+ DISPLAY_CONTAINER_CHILD_LIMIT,
12
12
  crossDirection,
13
13
  walkZones,
14
14
  weightPercents
@@ -18,8 +18,7 @@ const props = defineProps<{
18
18
  root: DisplayLayoutDraft
19
19
  selectedZoneId: string
20
20
  selectedBlockId: string
21
- /** Set while a palette card is being dragged, so areas can advertise
22
- * themselves as drop targets. */
21
+ /** Set while an existing module is dragged between areas. */
23
22
  dragging: boolean
24
23
  /** Which scope the inspector is editing. Selecting a module also selects its
25
24
  * area, so without this both would light up the same way and there would be
@@ -30,23 +29,34 @@ const props = defineProps<{
30
29
  const emit = defineEmits<{
31
30
  (event: 'select-zone', zoneId: string): void
32
31
  (event: 'select-block', payload: { zoneId: string; blockId: string }): void
33
- (event: 'drop-block', payload: { zoneId: string }): void
32
+ /** Clicking an empty area opens its module picker. */
33
+ (event: 'open-module-picker', zoneId: string): void
34
34
  (event: 'move-block', payload: { fromZoneId: string; toZoneId: string }): void
35
35
  /** `path` is empty for the top-level container and holds the slot index of
36
36
  * the group when the divider is inside one. */
37
37
  (event: 'resize', payload: { path: number[]; index: number; ratio: number }): void
38
+ (event: 'add-area', direction: DisplaySplitDraft['direction']): void
38
39
  (event: 'split-area', zoneId: string): void
39
- (event: 'merge-group', zoneId: string): void
40
+ (event: 'extend-area', zoneId: string): void
41
+ (event: 'remove-area', zoneId: string): void
40
42
  (event: 'block-dragstart'): void
41
43
  (event: 'block-dragend'): void
44
+ /** A divider drag can carry the pointer outside the preview frame the
45
+ * overlay is pinned to, so it needs its own signal to stay visible --
46
+ * hover alone would drop it mid-drag. */
47
+ (event: 'resize-start'): void
48
+ (event: 'resize-end'): void
42
49
  }>()
43
50
 
44
- const { t } = useI18n()
45
-
46
51
  interface Slot {
47
52
  zone?: DisplayZoneDraft
48
53
  group?: DisplaySplitDraft
54
+ /** Rounded, for the "N%" label only. */
49
55
  percent: number
56
+ /** Raw, for `flex-grow`: the live renderer uses the weight itself rather
57
+ * than a rounded percentage, so rendering from the rounded number would let
58
+ * the overlay's boundaries drift a few pixels from the real ones underneath. */
59
+ weight: number
50
60
  }
51
61
 
52
62
  const topSplit = computed(() => (props.root.kind === 'SPLIT' ? props.root : undefined))
@@ -54,12 +64,14 @@ const topDirection = computed(() => topSplit.value?.direction ?? 'ROW')
54
64
 
55
65
  const slots = computed<Slot[]>(() => {
56
66
  if (!topSplit.value) {
57
- return props.root.kind === 'ZONE' ? [{ zone: props.root, percent: 100 }] : []
67
+ return props.root.kind === 'ZONE' ? [{ zone: props.root, percent: 100, weight: 1 }] : []
58
68
  }
59
69
  const percents = weightPercents(topSplit.value.children)
60
- return topSplit.value.children.map((child, index) => child.node.kind === 'ZONE'
61
- ? { zone: child.node, percent: percents[index] ?? 100 }
62
- : { group: child.node, percent: percents[index] ?? 100 })
70
+ return topSplit.value.children.map((child, index) =>
71
+ child.node.kind === 'ZONE'
72
+ ? { zone: child.node, percent: percents[index] ?? 100, weight: child.weight }
73
+ : { group: child.node, percent: percents[index] ?? 100, weight: child.weight }
74
+ )
63
75
  })
64
76
 
65
77
  /** "Area N" is the render order across the whole screen, so the number a
@@ -70,10 +82,68 @@ const areaNumbers = computed(() => {
70
82
  return map
71
83
  })
72
84
 
85
+ /** Which border an area offers a "+" on, and what it does there. The border a
86
+ * direction sits on is always the same regardless of why it is offered, so
87
+ * placement and semantics never need to be chosen separately: ROW lives on
88
+ * the right edge, COLUMN on the bottom edge. */
89
+ interface EdgeAction {
90
+ direction: DisplaySplitDraft['direction']
91
+ kind: 'bootstrap' | 'split' | 'extend'
92
+ }
93
+
94
+ /** A lone, not-yet-split area offers both directions as the template's first
95
+ * split. Once split, every top-level area can still nest a group across its
96
+ * parent's direction, and only the last one can also extend the top-level
97
+ * container -- that is the only place a new sibling can land. */
98
+ function topEdgeActions(index: number): EdgeAction[] {
99
+ if (!topSplit.value)
100
+ return [
101
+ { direction: 'ROW', kind: 'bootstrap' },
102
+ { direction: 'COLUMN', kind: 'bootstrap' }
103
+ ]
104
+ const actions: EdgeAction[] = [{ direction: crossDirection(topDirection.value), kind: 'split' }]
105
+ if (
106
+ index === slots.value.length - 1 &&
107
+ topSplit.value.children.length < DISPLAY_CONTAINER_CHILD_LIMIT
108
+ ) {
109
+ actions.push({ direction: topDirection.value, kind: 'extend' })
110
+ }
111
+ return actions
112
+ }
113
+
114
+ /** A nested group has no further level to split into, so its own last area
115
+ * only ever offers to grow the group itself, same as the top level does. */
116
+ function groupEdgeActions(group: DisplaySplitDraft, position: number): EdgeAction[] {
117
+ if (
118
+ position !== group.children.length - 1 ||
119
+ group.children.length >= DISPLAY_CONTAINER_CHILD_LIMIT
120
+ )
121
+ return []
122
+ return [{ direction: group.direction, kind: 'extend' }]
123
+ }
124
+
125
+ function onEdgeClick(action: EdgeAction, zoneId: string): void {
126
+ if (action.kind === 'bootstrap') emit('add-area', action.direction)
127
+ else if (action.kind === 'split') emit('split-area', zoneId)
128
+ else emit('extend-area', zoneId)
129
+ }
130
+
73
131
  function groupPercents(group: DisplaySplitDraft): number[] {
74
132
  return weightPercents(group.children)
75
133
  }
76
134
 
135
+ /** Where a divider sits, as a percent of the container -- from the raw
136
+ * weights rather than the rounded per-child percentages, so it lands on
137
+ * exactly the same boundary the areas' own flex-grow renders, not a
138
+ * fraction of a percent off it. */
139
+ function dividerPercent(children: DisplaySplitDraft['children'], beforeIndex: number): number {
140
+ const total = children.reduce((sum, child) => sum + Math.max(1, child.weight), 0)
141
+ const before = children
142
+ .slice(0, beforeIndex)
143
+ .reduce((sum, child) => sum + Math.max(1, child.weight), 0)
144
+ return total > 0 ? (before / total) * 100 : 0
145
+ }
146
+
77
147
  function groupZones(group: DisplaySplitDraft): DisplayZoneDraft[] {
78
148
  return group.children
79
149
  .map((child) => child.node)
@@ -86,15 +156,6 @@ const groupSurfaces = ref<Record<number, HTMLElement | undefined>>({})
86
156
  let dragged: { fromZoneId: string } | undefined
87
157
  let resizing: { path: number[]; index: number } | undefined
88
158
 
89
- function blockLabel(type: string): string {
90
- const definition = blockDefinition(type)
91
- return definition ? t(definition.editor.labelKey) : type
92
- }
93
-
94
- function blockIcon(type: string): string {
95
- return blockDefinition(type)?.editor.icon ?? 'widgets'
96
- }
97
-
98
159
  function onBlockDragStart(event: DragEvent, zone: DisplayZoneDraft): void {
99
160
  dragged = { fromZoneId: zone.id }
100
161
  event.dataTransfer?.setData('application/x-display-block-id', zone.block?.id ?? '')
@@ -108,16 +169,8 @@ function onBlockDragEnd(): void {
108
169
  emit('block-dragend')
109
170
  }
110
171
 
111
- function onZoneDragOver(event: DragEvent, zone: DisplayZoneDraft): void {
112
- event.preventDefault()
113
- dropZoneId.value = zone.id
114
- }
115
-
116
- function onZoneDrop(event: DragEvent, zone: DisplayZoneDraft): void {
117
- event.preventDefault()
118
- const newType = event.dataTransfer?.getData('application/x-display-block-type')
119
- if (newType) emit('drop-block', { zoneId: zone.id })
120
- else if (dragged && dragged.fromZoneId !== zone.id) {
172
+ function onZoneDrop(zone: DisplayZoneDraft): void {
173
+ if (dragged && dragged.fromZoneId !== zone.id) {
121
174
  emit('move-block', { fromZoneId: dragged.fromZoneId, toZoneId: zone.id })
122
175
  }
123
176
  dropZoneId.value = ''
@@ -131,25 +184,26 @@ function onZoneDragLeave(zone: DisplayZoneDraft): void {
131
184
  function startResize(event: PointerEvent, path: number[], index: number): void {
132
185
  resizing = { path, index }
133
186
  ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
187
+ emit('resize-start')
134
188
  }
135
189
 
136
190
  function onResizeMove(event: PointerEvent): void {
137
191
  if (!resizing) return
138
- const group = resizing.path.length
139
- ? slots.value[resizing.path[0]!]?.group
140
- : undefined
192
+ const group = resizing.path.length ? slots.value[resizing.path[0]!]?.group : undefined
141
193
  const container = resizing.path.length ? group : topSplit.value
142
194
  const element = resizing.path.length ? groupSurfaces.value[resizing.path[0]!] : surface.value
143
195
  if (!container || !element) return
144
196
  const bounds = element.getBoundingClientRect()
145
- const percents = weightPercents(container.children)
146
- const before = percents.slice(0, resizing.index).reduce((sum, value) => sum + value, 0)
147
- const pair = (percents[resizing.index] ?? 0) + (percents[resizing.index + 1] ?? 0)
197
+ const weights = container.children.map((child) => Math.max(1, child.weight))
198
+ const total = weights.reduce((sum, value) => sum + value, 0)
199
+ const before = weights.slice(0, resizing.index).reduce((sum, value) => sum + value, 0)
200
+ const pair = (weights[resizing.index] ?? 0) + (weights[resizing.index + 1] ?? 0)
148
201
  if (pair <= 0) return
149
- const along = container.direction === 'ROW'
150
- ? (event.clientX - bounds.left) / Math.max(1, bounds.width)
151
- : (event.clientY - bounds.top) / Math.max(1, bounds.height)
152
- const ratio = (along * 100 - before) / pair
202
+ const along =
203
+ container.direction === 'ROW'
204
+ ? (event.clientX - bounds.left) / Math.max(1, bounds.width)
205
+ : (event.clientY - bounds.top) / Math.max(1, bounds.height)
206
+ const ratio = (along * total - before) / pair
153
207
  if (container.children.length > resizing.index + 1) {
154
208
  emit('resize', { path: resizing.path, index: resizing.index, ratio })
155
209
  }
@@ -159,13 +213,12 @@ function endResize(event: PointerEvent): void {
159
213
  if (!resizing) return
160
214
  ;(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId)
161
215
  resizing = undefined
216
+ emit('resize-end')
162
217
  }
163
218
 
164
219
  function setGroupSurface(index: number, element: unknown): void {
165
220
  groupSurfaces.value[index] = (element as HTMLElement | null) ?? undefined
166
221
  }
167
-
168
- const areaCount = computed(() => walkZones(props.root).length)
169
222
  </script>
170
223
 
171
224
  <template>
@@ -173,15 +226,21 @@ const areaCount = computed(() => walkZones(props.root).length)
173
226
  ref="surface"
174
227
  class="canvas"
175
228
  :class="topDirection === 'COLUMN' ? 'canvas-column' : 'canvas-row'"
176
- :style="{ '--area-count': areaCount }"
177
229
  >
178
230
  <template v-for="(slot, index) in slots" :key="slot.zone?.id ?? `group-${index}`">
179
- <!-- Dividers sit between slots and drive the weights directly, so the
180
- proportion is adjusted where it is seen. -->
231
+ <!-- Floats on the boundary rather than sitting in the flex flow: the
232
+ real areas touch with zero gap between them, so a divider that
233
+ consumed its own layout space would sit off-center from that
234
+ boundary by half its own width. -->
181
235
  <div
182
236
  v-if="index > 0 && topSplit"
183
237
  class="canvas-divider"
184
238
  :class="topDirection === 'COLUMN' ? 'divider-horizontal' : 'divider-vertical'"
239
+ :style="
240
+ topDirection === 'COLUMN'
241
+ ? { top: `${dividerPercent(topSplit.children, index)}%` }
242
+ : { left: `${dividerPercent(topSplit.children, index)}%` }
243
+ "
185
244
  role="separator"
186
245
  @pointerdown="startResize($event, [], index - 1)"
187
246
  @pointermove="onResizeMove"
@@ -191,78 +250,54 @@ const areaCount = computed(() => walkZones(props.root).length)
191
250
  <span class="canvas-divider-grip" />
192
251
  </div>
193
252
 
194
- <!-- A top-level area. -->
195
- <section
253
+ <!-- A top-level area. The card that carries the border/padding is a
254
+ child of this, not this element itself: with border-box sizing,
255
+ flex-grow can't actually shrink a flex-basis: 0 item's border and
256
+ padding away, so a chrome-bearing item ends up a few pixels wider
257
+ than its weight says -- keeping this outer sizing box chrome-free
258
+ is what makes the divider land exactly on the true boundary. -->
259
+ <CanvasArea
196
260
  v-if="slot.zone"
197
- class="canvas-area"
198
- :class="{
199
- 'canvas-area-active': slot.zone.id === selectedZoneId && scope === 'area',
200
- 'canvas-area-context': slot.zone.id === selectedZoneId && scope === 'module',
201
- 'canvas-area-droppable': dragging,
202
- 'canvas-area-drop-over': dropZoneId === slot.zone.id
203
- }"
204
- :style="{ flexGrow: slot.percent, flexBasis: '0' }"
205
- @click="emit('select-zone', slot.zone.id)"
206
- @dragover="onZoneDragOver($event, slot.zone)"
261
+ :zone="slot.zone"
262
+ :area-number="areaNumbers.get(slot.zone.id) ?? index + 1"
263
+ :percent="slot.percent"
264
+ :weight="slot.weight"
265
+ :removable="Boolean(topSplit)"
266
+ :edge-actions="topEdgeActions(index)"
267
+ :selected-zone-id="selectedZoneId"
268
+ :selected-block-id="selectedBlockId"
269
+ :scope="scope"
270
+ :dragging="dragging"
271
+ :drop-zone-id="dropZoneId"
272
+ @select-zone="emit('select-zone', $event)"
273
+ @select-block="emit('select-block', $event)"
274
+ @open-module-picker="emit('open-module-picker', $event)"
275
+ @remove-area="emit('remove-area', $event)"
276
+ @edge-click="onEdgeClick($event, slot.zone.id)"
277
+ @block-dragstart="onBlockDragStart($event, slot.zone)"
278
+ @block-dragend="onBlockDragEnd"
279
+ @dragover="dropZoneId = slot.zone.id"
207
280
  @dragleave="onZoneDragLeave(slot.zone)"
208
- @drop="onZoneDrop($event, slot.zone)"
209
- >
210
- <header class="canvas-area-head">
211
- <span class="canvas-area-name">
212
- {{ t('order.displaySystem.editor.ui.area', { index: areaNumbers.get(slot.zone.id) ?? index + 1 }) }}
213
- </span>
214
- <span class="canvas-area-share">{{ slot.percent }}%</span>
215
- <!-- Splitting is offered on the area itself: the nested container runs
216
- across the parent, so there is nothing to choose. -->
217
- <button
218
- type="button"
219
- class="canvas-area-action"
220
- :title="t('order.displaySystem.editor.ui.splitAreaHint', {
221
- direction: t(`order.displaySystem.editor.values.${crossDirection(topDirection).toLowerCase()}`)
222
- })"
223
- @click.stop="emit('split-area', slot.zone!.id)"
224
- >
225
- <FmIcon :name="crossDirection(topDirection) === 'COLUMN' ? 'splitscreen' : 'splitscreen_vertical'" size="sm" />
226
- </button>
227
- </header>
228
-
229
- <div class="canvas-chips">
230
- <button
231
- v-if="slot.zone.block"
232
- type="button"
233
- class="canvas-chip"
234
- :class="{
235
- 'canvas-chip-active': slot.zone.block.id === selectedBlockId && scope === 'module',
236
- 'canvas-chip-context': slot.zone.block.id === selectedBlockId && scope === 'area'
237
- }"
238
- draggable="true"
239
- @click.stop="emit('select-block', { zoneId: slot.zone!.id, blockId: slot.zone!.block!.id })"
240
- @dragstart="onBlockDragStart($event, slot.zone!)"
241
- @dragend="onBlockDragEnd"
242
- >
243
- <FmIcon name="drag_indicator" size="sm" class="canvas-chip-grip" />
244
- <FmIcon :name="blockIcon(slot.zone.block.type)" size="sm" />
245
- <span class="canvas-chip-label">{{ blockLabel(slot.zone.block.type) }}</span>
246
- </button>
247
- <p v-else class="canvas-area-empty">
248
- {{ t('order.displaySystem.editor.ui.emptyArea') }}
249
- </p>
250
- </div>
251
- </section>
252
-
281
+ @drop="onZoneDrop(slot.zone)"
282
+ />
253
283
  <!-- A nested container, drawn as areas running across the parent. -->
254
284
  <div
255
285
  v-else-if="slot.group"
256
286
  :ref="(element) => setGroupSurface(index, element)"
257
287
  class="canvas-group"
258
288
  :class="slot.group.direction === 'COLUMN' ? 'canvas-column' : 'canvas-row'"
259
- :style="{ flexGrow: slot.percent, flexBasis: '0' }"
289
+ :style="{ flexGrow: slot.weight, flexBasis: '0' }"
260
290
  >
261
291
  <template v-for="(zone, position) in groupZones(slot.group)" :key="zone.id">
262
292
  <div
263
293
  v-if="position > 0"
264
294
  class="canvas-divider"
265
295
  :class="slot.group.direction === 'COLUMN' ? 'divider-horizontal' : 'divider-vertical'"
296
+ :style="
297
+ slot.group.direction === 'COLUMN'
298
+ ? { top: `${dividerPercent(slot.group.children, position)}%` }
299
+ : { left: `${dividerPercent(slot.group.children, position)}%` }
300
+ "
266
301
  role="separator"
267
302
  @pointerdown="startResize($event, [index], position - 1)"
268
303
  @pointermove="onResizeMove"
@@ -272,58 +307,29 @@ const areaCount = computed(() => walkZones(props.root).length)
272
307
  <span class="canvas-divider-grip" />
273
308
  </div>
274
309
 
275
- <section
276
- class="canvas-area canvas-area-nested"
277
- :class="{
278
- 'canvas-area-active': zone.id === selectedZoneId && scope === 'area',
279
- 'canvas-area-context': zone.id === selectedZoneId && scope === 'module',
280
- 'canvas-area-droppable': dragging,
281
- 'canvas-area-drop-over': dropZoneId === zone.id
282
- }"
283
- :style="{ flexGrow: groupPercents(slot.group)[position] ?? 100, flexBasis: '0' }"
284
- @click="emit('select-zone', zone.id)"
285
- @dragover="onZoneDragOver($event, zone)"
310
+ <CanvasArea
311
+ :zone="zone"
312
+ :area-number="areaNumbers.get(zone.id) ?? position + 1"
313
+ :percent="groupPercents(slot.group)[position] ?? 100"
314
+ :weight="slot.group.children[position]?.weight ?? 1"
315
+ :removable="true"
316
+ :edge-actions="groupEdgeActions(slot.group, position)"
317
+ :selected-zone-id="selectedZoneId"
318
+ :selected-block-id="selectedBlockId"
319
+ :scope="scope"
320
+ :dragging="dragging"
321
+ :drop-zone-id="dropZoneId"
322
+ @select-zone="emit('select-zone', $event)"
323
+ @select-block="emit('select-block', $event)"
324
+ @open-module-picker="emit('open-module-picker', $event)"
325
+ @remove-area="emit('remove-area', $event)"
326
+ @edge-click="onEdgeClick($event, zone.id)"
327
+ @block-dragstart="onBlockDragStart($event, zone)"
328
+ @block-dragend="onBlockDragEnd"
329
+ @dragover="dropZoneId = zone.id"
286
330
  @dragleave="onZoneDragLeave(zone)"
287
- @drop="onZoneDrop($event, zone)"
288
- >
289
- <header class="canvas-area-head">
290
- <span class="canvas-area-name">
291
- {{ t('order.displaySystem.editor.ui.area', { index: areaNumbers.get(zone.id) ?? position + 1 }) }}
292
- </span>
293
- <span class="canvas-area-share">{{ groupPercents(slot.group)[position] ?? 100 }}%</span>
294
- <button
295
- type="button"
296
- class="canvas-area-action"
297
- :title="t('order.displaySystem.editor.ui.mergeGroupHint')"
298
- @click.stop="emit('merge-group', zone.id)"
299
- >
300
- <FmIcon name="close_fullscreen" size="sm" />
301
- </button>
302
- </header>
303
-
304
- <div class="canvas-chips">
305
- <button
306
- v-if="zone.block"
307
- type="button"
308
- class="canvas-chip"
309
- :class="{
310
- 'canvas-chip-active': zone.block.id === selectedBlockId && scope === 'module',
311
- 'canvas-chip-context': zone.block.id === selectedBlockId && scope === 'area'
312
- }"
313
- draggable="true"
314
- @click.stop="emit('select-block', { zoneId: zone.id, blockId: zone.block!.id })"
315
- @dragstart="onBlockDragStart($event, zone)"
316
- @dragend="onBlockDragEnd"
317
- >
318
- <FmIcon name="drag_indicator" size="sm" class="canvas-chip-grip" />
319
- <FmIcon :name="blockIcon(zone.block.type)" size="sm" />
320
- <span class="canvas-chip-label">{{ blockLabel(zone.block.type) }}</span>
321
- </button>
322
- <p v-else class="canvas-area-empty">
323
- {{ t('order.displaySystem.editor.ui.emptyArea') }}
324
- </p>
325
- </div>
326
- </section>
331
+ @drop="onZoneDrop(zone)"
332
+ />
327
333
  </template>
328
334
  </div>
329
335
  </template>
@@ -332,12 +338,10 @@ const areaCount = computed(() => walkZones(props.root).length)
332
338
 
333
339
  <style scoped>
334
340
  .canvas {
341
+ position: absolute;
342
+ inset: 0;
335
343
  display: flex;
336
344
  min-height: 0;
337
- flex: 1;
338
- border-radius: 12px;
339
- background: #f3f4f6;
340
- padding: 8px;
341
345
  }
342
346
  .canvas-row {
343
347
  flex-direction: row;
@@ -345,160 +349,31 @@ const areaCount = computed(() => walkZones(props.root).length)
345
349
  .canvas-column {
346
350
  flex-direction: column;
347
351
  }
348
- /* A group is a container, not an area: it gets no border of its own so the
349
- areas inside it read as siblings of the areas outside. */
350
352
  .canvas-group {
353
+ position: relative;
351
354
  display: flex;
352
355
  min-width: 0;
353
356
  min-height: 0;
354
- gap: 0;
355
- }
356
- .canvas-area {
357
- display: flex;
358
- min-width: 0;
359
- min-height: 78px;
360
- flex-direction: column;
361
- gap: 8px;
362
- overflow: hidden;
363
- border: 1.5px dashed #d1d5db;
364
- border-radius: 10px;
365
- padding: 10px;
366
- background: white;
367
- text-align: left;
368
- }
369
- .canvas-area-nested {
370
- min-height: 62px;
371
- }
372
- .canvas-area:hover {
373
- border-color: #fdba74;
374
- }
375
- /* Editing the area itself: the area carries the emphasis. */
376
- .canvas-area-active {
377
- border-style: solid;
378
- border-color: #f97316;
379
- background: #fffbf5;
380
- box-shadow: 0 0 0 3px #f973161f;
381
- }
382
- /* Editing a module inside it: the area only says "you are in here". */
383
- .canvas-area-context {
384
- border-style: solid;
385
- border-color: #fed7aa;
386
- background: #fffdfa;
387
- }
388
- .canvas-area-droppable {
389
- border-color: #f97316;
390
- background: #fff7ed;
391
- }
392
- .canvas-area-drop-over {
393
- border-style: solid;
394
- box-shadow: 0 0 0 3px #f9731633;
395
- }
396
- .canvas-area-head {
397
- display: flex;
398
- align-items: center;
399
- gap: 8px;
400
- }
401
- .canvas-area-name {
402
- color: #6b7280;
403
- font-size: 10px;
404
- font-weight: 700;
405
- letter-spacing: 0.06em;
406
- text-transform: uppercase;
407
- }
408
- .canvas-area-share {
409
- margin-left: auto;
410
- color: #374151;
411
- font-size: 12px;
412
- font-variant-numeric: tabular-nums;
413
- font-weight: 700;
414
- }
415
- .canvas-area-action {
416
- display: grid;
417
- flex: 0 0 auto;
418
- place-items: center;
419
- border-radius: 6px;
420
- padding: 2px;
421
- color: #9ca3af;
422
- }
423
- .canvas-area-action:hover {
424
- background: #fff7ed;
425
- color: #f97316;
426
- }
427
- .canvas-chips {
428
- display: flex;
429
- min-height: 0;
430
- flex: 1;
431
- flex-direction: column;
432
- gap: 4px;
433
- }
434
- .canvas-chip {
435
- display: flex;
436
- align-items: center;
437
- gap: 6px;
438
- border: 1px solid #e5e7eb;
439
- border-radius: 8px;
440
- padding: 6px 8px;
441
- background: white;
442
- color: #374151;
443
- font-size: 12px;
444
- text-align: left;
445
- cursor: grab;
446
- }
447
- .canvas-chip:hover {
448
- border-color: #fdba74;
449
- }
450
- /* Editing this module: it carries the emphasis, with a solid accent edge that
451
- the area's own highlight never uses. */
452
- .canvas-chip-active {
453
- border-color: #f97316;
454
- background: #ffffff;
455
- box-shadow: inset 4px 0 0 #f97316, 0 0 0 3px #f973161f;
456
- font-weight: 600;
457
- }
458
- .canvas-chip-active .canvas-chip-grip {
459
- color: #f97316;
460
- }
461
- /* Selected, but the panel is on the area — keep it findable, not shouting. */
462
- .canvas-chip-context {
463
- border-color: #fed7aa;
464
- background: #fffdfa;
465
- }
466
- .canvas-chip:active {
467
- cursor: grabbing;
468
- }
469
- .canvas-chip-grip {
470
- flex: 0 0 auto;
471
- color: #9ca3af;
472
- }
473
- .canvas-chip-label {
474
- min-width: 0;
475
- flex: 1;
476
- overflow: hidden;
477
- text-overflow: ellipsis;
478
- white-space: nowrap;
479
- }
480
- /* An empty area is a legitimate editing state, so it should look like an
481
- invitation rather than a rendering failure. */
482
- .canvas-area-empty {
483
- display: grid;
484
- flex: 1;
485
- place-items: center;
486
- border: 1px dashed #d1d5db;
487
- border-radius: 8px;
488
- color: #9ca3af;
489
- font-size: 11px;
490
357
  }
491
358
  .canvas-divider {
359
+ position: absolute;
360
+ z-index: 2;
492
361
  display: grid;
493
- flex: 0 0 auto;
494
362
  place-items: center;
363
+ touch-action: none;
495
364
  }
496
365
  .divider-vertical {
366
+ top: 0;
367
+ bottom: 0;
497
368
  width: 10px;
369
+ transform: translateX(-50%);
498
370
  cursor: col-resize;
499
371
  }
500
372
  .divider-horizontal {
373
+ left: 0;
374
+ right: 0;
501
375
  height: 10px;
376
+ transform: translateY(-50%);
502
377
  cursor: row-resize;
503
378
  }
504
379
  .canvas-divider-grip {