@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
- import { computed, ref, watch } from 'vue'
3
- import type { ISelectItem } from '@feedmepos/ui-library'
2
+ import { computed, nextTick, ref, watch } from 'vue'
3
+ import { useDialog, type ISelectItem } from '@feedmepos/ui-library'
4
4
  import { useCoreStore, useI18n } from '@feedmepos/mf-common'
5
5
  import { MediaPickerModal, type MediaSelection } from '@feedmepos/media-picker'
6
6
  import type { BlockEditorControl } from '@entity'
@@ -18,26 +18,26 @@ import type {
18
18
  DisplayTemplatePreset
19
19
  } from '@/modules/display-system/interface'
20
20
  import {
21
- DISPLAY_CONTAINER_CHILD_LIMIT,
22
21
  addArea,
22
+ addAreaBeside,
23
23
  applyDividerRatio,
24
24
  blockDefinition,
25
25
  clearBlock,
26
+ DISPLAY_CONTAINER_CHILD_LIMIT,
26
27
  defaultBlock,
27
28
  defaultDisplayTemplateDraft,
28
29
  displayTemplateToDraft,
29
- mergeGroup,
30
30
  removeArea,
31
31
  setBlock,
32
+ setLayoutDirection,
32
33
  splitArea,
33
34
  swapBlocks,
34
35
  walkZones,
35
- zoneFitsMinimum,
36
- zoneParent
36
+ zoneFitsMinimum
37
37
  } from '@/modules/display-system/interface'
38
38
  import DisplayPreview from './DisplayPreview.vue'
39
39
  import DisplayLayoutCanvas from './DisplayLayoutCanvas.vue'
40
- import DisplayModulePalette from './DisplayModulePalette.vue'
40
+ import DisplayModulePickerDialog from './DisplayModulePickerDialog.vue'
41
41
  import DisplayInspector from './DisplayInspector.vue'
42
42
 
43
43
  const props = defineProps<{
@@ -58,6 +58,7 @@ const emit = defineEmits<{
58
58
 
59
59
  const { t } = useI18n()
60
60
  const { currentBusiness, currentCountry, namespace, restaurants } = useCoreStore()
61
+ const Dialog = useDialog()
61
62
 
62
63
  const draft = ref(defaultDisplayTemplateDraft())
63
64
  const previewBrandKit = ref<RestaurantDisplayBrandKit | null>()
@@ -67,8 +68,9 @@ const inspectorScope = ref<'area' | 'module'>('module')
67
68
  const presetsOpen = ref(false)
68
69
  const error = ref('')
69
70
  const dragging = ref(false)
70
- /** Type carried by the palette drag, so a drop knows what to insert. */
71
- const draggedType = ref('')
71
+ /** A divider drag, kept separate from `dragging`: that flag also lights up
72
+ * every area as a drop target, which a resize must not do. */
73
+ const resizingLayout = ref(false)
72
74
  const mediaPickerOpen = ref(false)
73
75
  const mediaTarget = ref<DisplayMediaTarget>({ kind: 'background' })
74
76
  const brokenImages = ref(new Set<string>())
@@ -115,14 +117,24 @@ const PRESETS: PresetOption[] = [
115
117
  { key: 'media', labelKey: 'presetMedia', direction: 'ROW', cells: [100] }
116
118
  ]
117
119
 
120
+ /** Mirrors `syncDisplayScale` in display-system-v3's App.vue: the brand
121
+ * footer is a fixed 3.6rem there, and that root font-size is
122
+ * `16 * iframeWidth * scale / baseWidth` (1920 landscape, 1080 portrait). As a
123
+ * share of the FRAME's own height rather than its width, that only depends on
124
+ * scale and orientation, never on how large the frame itself renders here --
125
+ * so the overlay can reserve the same footer strip without asking the iframe
126
+ * for it. */
127
+ function previewFooterHeightPercent(orientation: DisplayTemplateDraft['orientation'], scale: number): number {
128
+ const baseWidth = orientation === 'PORTRAIT' ? 1080 : 1920
129
+ const aspect = orientation === 'PORTRAIT' ? 9 / 16 : 16 / 9
130
+ return ((3.6 * 16 * scale) / baseWidth) * aspect * 100
131
+ }
132
+
133
+ const previewFooterPercent = computed(() => draft.value.hideFooter
134
+ ? 0
135
+ : previewFooterHeightPercent(draft.value.orientation, draft.value.scale ?? 1))
136
+
118
137
  const splitRoot = computed(() => (draft.value.root.kind === 'SPLIT' ? draft.value.root : undefined))
119
- const canAddArea = computed(
120
- () => draft.value.root.kind === 'ZONE'
121
- || (splitRoot.value?.children.length ?? 0) < DISPLAY_CONTAINER_CHILD_LIMIT
122
- )
123
- const canRemoveArea = computed(() => Boolean(
124
- selectedZone.value && zoneParent(draft.value.root, selectedZone.value.id)
125
- ))
126
138
  const isColumn = computed(() => splitRoot.value?.direction === 'COLUMN')
127
139
 
128
140
  const tooSmallFor = computed(() => {
@@ -213,13 +225,44 @@ function selectBlock(payload: { zoneId: string; blockId: string }): void {
213
225
  inspectorScope.value = 'module'
214
226
  }
215
227
 
216
- /** An area holds one module, so adding one replaces what is there. */
217
- function addModule(type: string, zoneId?: string): void {
218
- const target = zoneId ?? selectedZone.value?.id
219
- if (!target) return
220
- const block = defaultBlock(type)
221
- if (!setBlock(draft.value, target, block)) return
222
- selectBlock({ zoneId: target, blockId: block.id })
228
+ /** An area holds one module, so adding one replaces what is there -- silently
229
+ * for an empty area, but only after confirming when it would discard one
230
+ * that is already set up. */
231
+ function addModule(type: string, zoneId: string): void {
232
+ const existing = zones.value.find((zone) => zone.id === zoneId)?.block
233
+ const apply = () => {
234
+ const block = defaultBlock(type)
235
+ if (!setBlock(draft.value, zoneId, block)) return
236
+ selectBlock({ zoneId, blockId: block.id })
237
+ }
238
+ if (!existing) {
239
+ apply()
240
+ return
241
+ }
242
+ Dialog.open({
243
+ title: t('order.displaySystem.editor.ui.replaceModuleConfirmTitle'),
244
+ message: t('order.displaySystem.editor.ui.replaceModuleConfirmMessage', {
245
+ current: t(blockDefinition(existing.type)?.editor.labelKey ?? existing.type)
246
+ }),
247
+ primaryActions: { text: t('order.displaySystem.editor.ui.replaceModuleConfirmAction'), variant: 'destructive', close: true },
248
+ secondaryActions: { text: t('order.displaySystem.common.cancel'), variant: 'tertiary', close: true },
249
+ // Without this it falls back to Tailwind's z-50, behind this editor's own
250
+ // z-[60] overlay -- the same reason MediaPickerModal below needs its :z-index.
251
+ zIndex: 70
252
+ }).onPrimary(apply)
253
+ }
254
+
255
+ /** The full grouped picker, opened right on the area it will fill -- an empty
256
+ * area is never more than one click from getting its module. */
257
+ function openModulePicker(zoneId: string): void {
258
+ Dialog.open({
259
+ title: t('order.displaySystem.editor.ui.pickModuleTitle'),
260
+ contentComponent: DisplayModulePickerDialog,
261
+ zIndex: 70
262
+ }).onPrimary((type: string) => {
263
+ Dialog.close()
264
+ void nextTick(() => addModule(type, zoneId))
265
+ })
223
266
  }
224
267
 
225
268
  function onMoveBlock(payload: { fromZoneId: string; toZoneId: string }): void {
@@ -241,31 +284,44 @@ function onResize(payload: { path: number[]; index: number; ratio: number }): vo
241
284
  if (container) applyDividerRatio(container.children, payload.index, payload.ratio)
242
285
  }
243
286
 
244
- function onAddArea(): void {
245
- // A new area starts empty: the merchant picks its module from the palette,
246
- // which is one step instead of replacing a filler nobody asked for.
247
- const created = addArea(draft.value, splitRoot.value?.direction ?? 'ROW')
248
- if (created) selectZone(created)
287
+ function onAddArea(direction: DisplaySplitDraft['direction']): void {
288
+ // A new area starts empty, so the picker opens on it right away instead of
289
+ // leaving a filler nobody asked for until the merchant notices it.
290
+ const created = addArea(draft.value, direction)
291
+ if (created) { selectZone(created); openModulePicker(created) }
249
292
  }
250
293
 
251
294
  function onSplitArea(zoneId: string): void {
252
295
  const created = splitArea(draft.value, zoneId)
253
- if (created) selectZone(created)
296
+ if (created) { selectZone(created); openModulePicker(created) }
254
297
  else error.value = t('order.displaySystem.editor.ui.splitLimit')
255
298
  }
256
299
 
257
- function onMergeGroup(zoneId: string): void {
258
- const kept = mergeGroup(draft.value, zoneId)
259
- if (kept) selectZone(kept)
300
+ function onExtendArea(zoneId: string): void {
301
+ const created = addAreaBeside(draft.value, zoneId)
302
+ if (created) { selectZone(created); openModulePicker(created) }
260
303
  }
261
304
 
262
- function onRemoveArea(): void {
263
- const zone = selectedZone.value
264
- if (zone && removeArea(draft.value, zone.id)) selectFirst()
305
+ function onRemoveArea(zoneId: string): void {
306
+ const zone = zones.value.find((item) => item.id === zoneId)
307
+ const apply = () => {
308
+ if (removeArea(draft.value, zoneId)) selectFirst()
309
+ }
310
+ if (!zone?.block) {
311
+ apply()
312
+ return
313
+ }
314
+ Dialog.open({
315
+ title: t('order.displaySystem.editor.ui.removeAreaConfirmTitle'),
316
+ message: t('order.displaySystem.editor.ui.removeAreaConfirmMessage'),
317
+ primaryActions: { text: t('order.displaySystem.editor.ui.removeArea'), variant: 'destructive', close: true },
318
+ secondaryActions: { text: t('order.displaySystem.common.cancel'), variant: 'tertiary', close: true },
319
+ zIndex: 70
320
+ }).onPrimary(apply)
265
321
  }
266
322
 
267
323
  function toggleDirection(): void {
268
- if (splitRoot.value) splitRoot.value.direction = isColumn.value ? 'ROW' : 'COLUMN'
324
+ if (splitRoot.value) setLayoutDirection(splitRoot.value, isColumn.value ? 'ROW' : 'COLUMN')
269
325
  }
270
326
 
271
327
  function removeModule(): void {
@@ -517,113 +573,112 @@ function save(): void {
517
573
  />
518
574
  <p v-if="footerWarning" class="rail-scale-warning">{{ footerWarning }}</p>
519
575
  </div>
520
- <DisplayModulePalette
521
- :can-add="Boolean(selectedZone)"
522
- @add="addModule($event)"
523
- @dragstart="(type) => { draggedType = type; dragging = true }"
524
- @dragend="dragging = false; draggedType = ''"
525
- />
526
576
  </aside>
527
577
 
528
- <main class="editor-stage" :class="{ 'editor-stage-portrait': draft.orientation === 'PORTRAIT' }">
529
- <section class="stage-preview">
530
- <div
531
- class="preview-frame"
532
- :class="draft.orientation === 'PORTRAIT' ? 'preview-portrait' : 'preview-landscape'"
533
- >
534
- <DisplayPreview :config="draft" :restaurant="previewRestaurant" />
578
+ <main class="editor-stage">
579
+ <div class="layout-bar">
580
+ <div class="layout-bar-title">
581
+ <h3 class="layout-title">{{ t('order.displaySystem.editor.ui.layoutTitle') }}</h3>
582
+ <p class="layout-hint">{{ t('order.displaySystem.editor.ui.layoutHint') }}</p>
535
583
  </div>
536
- </section>
537
-
538
- <section class="stage-layout">
539
- <!-- Layout controls belong to the canvas, so they sit on its edge
540
- rather than in a toolbar far from what they change. -->
541
- <div class="layout-bar">
542
- <div class="layout-bar-title">
543
- <h3 class="layout-title">{{ t('order.displaySystem.editor.ui.layoutTitle') }}</h3>
544
- <p class="layout-hint">{{ t('order.displaySystem.editor.ui.layoutHint') }}</p>
545
- </div>
546
- <div class="layout-actions">
547
- <FmTooltip
548
- :text="isColumn
549
- ? t('order.displaySystem.editor.ui.directionRow')
550
- : t('order.displaySystem.editor.ui.directionColumn')"
551
- >
552
- <FmButton
553
- v-if="splitRoot"
554
- variant="secondary"
555
- :prepend-icon="isColumn ? 'view_column' : 'view_agenda'"
556
- :label="isColumn
557
- ? t('order.displaySystem.editor.ui.directionColumn')
558
- : t('order.displaySystem.editor.ui.directionRow')"
559
- @click="toggleDirection"
560
- />
561
- </FmTooltip>
584
+ <div class="layout-actions">
585
+ <FmButton
586
+ variant="secondary"
587
+ prepend-icon="add"
588
+ :label="t('order.displaySystem.editor.ui.addArea')"
589
+ :disabled="Boolean(splitRoot && splitRoot.children.length >= DISPLAY_CONTAINER_CHILD_LIMIT)"
590
+ @click="onAddArea(splitRoot?.direction ?? 'ROW')"
591
+ />
592
+ <FmTooltip
593
+ :text="isColumn
594
+ ? t('order.displaySystem.editor.ui.directionRow')
595
+ : t('order.displaySystem.editor.ui.directionColumn')"
596
+ >
562
597
  <FmButton
598
+ v-if="splitRoot"
563
599
  variant="secondary"
564
- prepend-icon="add"
565
- :label="t('order.displaySystem.editor.ui.addArea')"
566
- :disabled="!canAddArea"
567
- @click="onAddArea"
568
- />
569
- <FmTooltip :text="t('order.displaySystem.editor.ui.removeAreaTooltip')">
570
- <FmButton
571
- variant="plain"
572
- prepend-icon="delete"
573
- :disabled="!canRemoveArea"
574
- @click="onRemoveArea"
575
- />
576
- </FmTooltip>
577
- <FmButton
578
- variant="plain"
579
- prepend-icon="dashboard_customize"
580
- :label="t('order.displaySystem.editor.ui.resetLayout')"
581
- @click="presetsOpen = !presetsOpen"
600
+ :prepend-icon="isColumn ? 'view_column' : 'view_agenda'"
601
+ :label="isColumn
602
+ ? t('order.displaySystem.editor.ui.directionColumn')
603
+ : t('order.displaySystem.editor.ui.directionRow')"
604
+ @click="toggleDirection"
582
605
  />
583
- </div>
606
+ </FmTooltip>
607
+ <FmButton
608
+ variant="plain"
609
+ prepend-icon="dashboard_customize"
610
+ :label="t('order.displaySystem.editor.ui.resetLayout')"
611
+ @click="presetsOpen = !presetsOpen"
612
+ />
584
613
  </div>
614
+ </div>
585
615
 
586
- <div v-if="presetsOpen" class="preset-row">
587
- <button
588
- v-for="preset in PRESETS"
589
- :key="preset.key"
590
- type="button"
591
- class="preset-card"
592
- @click="applyPreset(preset.key)"
616
+ <div v-if="presetsOpen" class="preset-row">
617
+ <button
618
+ v-for="preset in PRESETS"
619
+ :key="preset.key"
620
+ type="button"
621
+ class="preset-card"
622
+ @click="applyPreset(preset.key)"
623
+ >
624
+ <span
625
+ class="preset-diagram"
626
+ :class="preset.direction === 'ROW' ? 'preset-row-dir' : 'preset-column-dir'"
593
627
  >
594
628
  <span
595
- class="preset-diagram"
596
- :class="preset.direction === 'ROW' ? 'preset-row-dir' : 'preset-column-dir'"
597
- >
598
- <span
599
- v-for="(cell, index) in preset.cells"
600
- :key="index"
601
- class="preset-cell"
602
- :style="{ flexGrow: cell }"
603
- />
604
- </span>
605
- <span class="preset-label">
606
- {{ t(`order.displaySystem.editor.ui.${preset.labelKey}`) }}
607
- </span>
608
- </button>
609
- </div>
629
+ v-for="(cell, index) in preset.cells"
630
+ :key="index"
631
+ class="preset-cell"
632
+ :style="{ flexGrow: cell }"
633
+ />
634
+ </span>
635
+ <span class="preset-label">
636
+ {{ t(`order.displaySystem.editor.ui.${preset.labelKey}`) }}
637
+ </span>
638
+ </button>
639
+ </div>
610
640
 
611
- <DisplayLayoutCanvas
612
- :root="draft.root"
613
- :selected-zone-id="selectedZone?.id ?? ''"
614
- :selected-block-id="selectedBlockId"
615
- :scope="inspectorScope"
616
- :dragging="dragging"
617
- @select-zone="selectZone"
618
- @select-block="selectBlock"
619
- @drop-block="(payload) => draggedType && addModule(draggedType, payload.zoneId)"
620
- @move-block="onMoveBlock"
621
- @resize="onResize"
622
- @split-area="onSplitArea"
623
- @merge-group="onMergeGroup"
624
- @block-dragstart="dragging = true"
625
- @block-dragend="dragging = false"
626
- />
641
+ <!-- The editor lives directly on the preview it edits: a translucent
642
+ layer that surfaces on hover or stays visible on touch devices.
643
+ At rest, a mouse user sees what the screen will actually show. It sits beside the clipped
644
+ preview frame rather than inside it, so the frame keeps rounding
645
+ off the real content while the overlay's own border controls
646
+ stay free to straddle an area's edge without being cut off. -->
647
+ <section class="stage-preview">
648
+ <div
649
+ class="preview-box"
650
+ :class="draft.orientation === 'PORTRAIT' ? 'preview-portrait' : 'preview-landscape'"
651
+ >
652
+ <div class="preview-frame">
653
+ <DisplayPreview :config="draft" :restaurant="previewRestaurant" />
654
+ </div>
655
+ <div
656
+ class="preview-edit-overlay"
657
+ :class="{ 'preview-edit-overlay-active': dragging || resizingLayout }"
658
+ :style="{ bottom: `${previewFooterPercent}%` }"
659
+ >
660
+ <DisplayLayoutCanvas
661
+ :root="draft.root"
662
+ :selected-zone-id="selectedZone?.id ?? ''"
663
+ :selected-block-id="selectedBlockId"
664
+ :scope="inspectorScope"
665
+ :dragging="dragging"
666
+ @select-zone="selectZone"
667
+ @select-block="selectBlock"
668
+ @open-module-picker="openModulePicker"
669
+ @move-block="onMoveBlock"
670
+ @resize="onResize"
671
+ @add-area="onAddArea"
672
+ @split-area="onSplitArea"
673
+ @extend-area="onExtendArea"
674
+ @remove-area="onRemoveArea"
675
+ @block-dragstart="dragging = true"
676
+ @block-dragend="dragging = false"
677
+ @resize-start="resizingLayout = true"
678
+ @resize-end="resizingLayout = false"
679
+ />
680
+ </div>
681
+ </div>
627
682
  </section>
628
683
  </main>
629
684
 
@@ -753,85 +808,70 @@ function save(): void {
753
808
  overflow: auto;
754
809
  padding: 16px;
755
810
  }
756
- /* A portrait preview is narrow, so stacking the canvas under it wastes the
757
- width beside it. Sit them side by side instead. */
758
- .editor-stage-portrait {
759
- flex-direction: row;
760
- align-items: flex-start;
761
- gap: 16px;
762
- }
763
- .editor-stage-portrait {
764
- /* Side by side, so the preview is free to use the full stage height. */
765
- height: 100%;
766
- }
767
- .editor-stage-portrait .stage-preview {
768
- min-height: 0;
769
- flex: 0 0 auto;
770
- align-self: stretch;
771
- }
772
- .editor-stage-portrait .preview-portrait {
773
- height: 100%;
774
- max-height: none;
775
- }
776
- .editor-stage-portrait .stage-layout {
777
- min-width: 0;
778
- flex: 1;
779
- }
780
- /* The column is narrower than in landscape, so the title and the actions no
781
- longer fit on one line without crushing the hint. */
782
- .editor-stage-portrait .layout-bar {
783
- flex-direction: column;
784
- align-items: stretch;
785
- gap: 8px;
786
- }
787
- .editor-stage-portrait .layout-actions {
788
- flex-wrap: wrap;
789
- }
790
811
  .stage-preview {
791
812
  display: flex;
792
- flex-direction: column;
813
+ min-height: 0;
814
+ flex: 1;
815
+ align-items: center;
816
+ justify-content: center;
793
817
  }
794
- .preview-frame {
795
- overflow: hidden;
796
- border: 1px solid #e5e7eb;
797
- border-radius: 12px;
798
- background: #101828;
818
+ /* The sizing box: the frame and the overlay both sit inside this at
819
+ `inset: 0`, so they always share exactly the same bounds without either
820
+ one clipping the other. */
821
+ .preview-box {
822
+ position: relative;
799
823
  margin: 0 auto;
800
824
  }
801
825
  /* Landscape is width-driven; portrait must be height-driven. Giving portrait
802
826
  `width: 100%` too would pin the width and let max-height clamp the height,
803
827
  which drops the aspect ratio entirely and leaves the frame looking landscape. */
804
- .preview-landscape {
828
+ .preview-box.preview-landscape {
805
829
  width: 100%;
806
- max-height: 52vh;
830
+ max-height: 72vh;
807
831
  aspect-ratio: 16 / 9;
808
832
  }
809
- .preview-portrait {
810
- height: 52vh;
833
+ .preview-box.preview-portrait {
834
+ height: 72vh;
811
835
  max-width: 100%;
812
836
  aspect-ratio: 9 / 16;
813
837
  }
838
+ /* Clips the real preview to its rounded corners. The editor overlay lives
839
+ outside this box (a sibling, not a child) precisely so it is never subject
840
+ to this clipping -- its own border controls need to straddle an area's
841
+ edge without being cut off, which content inside the frame never does. */
842
+ .preview-frame {
843
+ position: absolute;
844
+ inset: 0;
845
+ overflow: hidden;
846
+ border: 1px solid #e5e7eb;
847
+ border-radius: 12px;
848
+ background: #101828;
849
+ }
814
850
  .preview-frame :deep(> *) {
815
851
  width: 100%;
816
852
  height: 100%;
817
853
  }
818
-
819
- .stage-layout {
820
- display: flex;
821
- min-height: 0;
822
- flex: 0 0 auto;
823
- flex-direction: column;
824
- gap: 10px;
825
- }
826
- /* Side by side, every area shares the full canvas height, so a cap keeps the
827
- canvas from leaving dead space. Stacked, the areas divide that height between
828
- them, so the canvas has to grow with how many there are. */
829
- .stage-layout :deep(.canvas-row) {
830
- min-height: 176px;
831
- max-height: 260px;
832
- }
833
- .stage-layout :deep(.canvas-column) {
834
- min-height: calc(var(--area-count, 2) * 96px + 16px);
854
+ /* The translucent editing layer: invisible until the merchant is actually
855
+ pointing at the preview (or mid-drag), so idle the screen reads as a plain
856
+ preview rather than a diagram. */
857
+ .preview-edit-overlay {
858
+ position: absolute;
859
+ inset: 0;
860
+ opacity: 0;
861
+ pointer-events: none;
862
+ transition: opacity 0.15s ease;
863
+ }
864
+ .preview-box:hover .preview-edit-overlay,
865
+ .preview-box:focus-within .preview-edit-overlay,
866
+ .preview-edit-overlay.preview-edit-overlay-active {
867
+ opacity: 1;
868
+ pointer-events: auto;
869
+ }
870
+ @media (hover: none), (pointer: coarse) {
871
+ .preview-edit-overlay {
872
+ opacity: 1;
873
+ pointer-events: auto;
874
+ }
835
875
  }
836
876
  .layout-bar {
837
877
  display: flex;
@@ -61,6 +61,7 @@ import { useKioskStore } from '@/stores/kiosk'
61
61
  import type { MfKioskDevice } from '@/modules/kiosk/interface'
62
62
  import KioskOtpDialog from '@/views/kiosk/devices/KioskOtpDialog.vue'
63
63
  import { SvcCompanyApiError } from '@/api'
64
+ import { bindDeviceErrorKey } from '@/modules/kiosk/bindDeviceError'
64
65
  import { useCoreStore, useI18n } from '@feedmepos/mf-common'
65
66
  import { useOrderSettings } from '@/stores/order-setting'
66
67
  import { usePaymentStore } from '@/stores/payment'
@@ -185,10 +186,7 @@ async function requestOtp() {
185
186
  await KioskStore.requestOtp()
186
187
  } catch (e) {
187
188
  SnackBar.open({
188
- title:
189
- e instanceof SvcCompanyApiError && e.status === 403
190
- ? t('order.bindDevicePermissionDenied')
191
- : t('order.bindDeviceFailed'),
189
+ title: t(bindDeviceErrorKey(e instanceof SvcCompanyApiError ? e : null)),
192
190
  type: 'error'
193
191
  })
194
192
  return
package/vitest.config.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url'
2
+ import vue from '@vitejs/plugin-vue'
2
3
  import { defineConfig } from 'vitest/config'
3
4
 
4
5
  export default defineConfig({
6
+ plugins: [vue()],
5
7
  resolve: {
6
8
  alias: {
7
9
  '@': fileURLToPath(new URL('./src', import.meta.url)),