stellar-ui-plus 1.24.26 → 1.25.0

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 (44) hide show
  1. package/components/ste-app-update/method.ts +1 -0
  2. package/components/ste-app-update/props.ts +11 -11
  3. package/components/ste-app-update/ste-app-update.vue +2 -7
  4. package/components/ste-radio/README.md +1 -1
  5. package/components/ste-radio/ste-radio.vue +2 -7
  6. package/components/ste-radio-group/ste-radio-group.vue +2 -1
  7. package/components/ste-select-seat/ATTRIBUTES.md +18 -0
  8. package/components/ste-select-seat/README.md +280 -0
  9. package/components/ste-select-seat/canvasUtils.ts +42 -0
  10. package/components/ste-select-seat/config.json +5 -0
  11. package/components/ste-select-seat/internals/gridUtils.ts +23 -0
  12. package/components/ste-select-seat/internals/seatLayout.ts +169 -0
  13. package/components/ste-select-seat/internals/useSeatInteraction.ts +540 -0
  14. package/components/ste-select-seat/props.ts +37 -0
  15. package/components/ste-select-seat/ste-select-seat.easycom.json +62 -0
  16. package/components/ste-select-seat/ste-select-seat.vue +517 -0
  17. package/components/ste-select-seat/types.d.ts +33 -0
  18. package/components/ste-select-seat/useData.ts +179 -0
  19. package/components/ste-select-seat/useTouchCompat.ts +89 -0
  20. package/components/ste-simple-calendar/ATTRIBUTES.md +17 -0
  21. package/components/ste-simple-calendar/README.md +112 -0
  22. package/components/ste-simple-calendar/config.json +5 -0
  23. package/components/ste-simple-calendar/props.ts +32 -0
  24. package/components/ste-simple-calendar/ste-simple-calendar.easycom.json +60 -0
  25. package/components/ste-simple-calendar/ste-simple-calendar.vue +265 -0
  26. package/components/ste-simple-calendar/type.d.ts +30 -0
  27. package/components/ste-simple-calendar/useData.ts +60 -0
  28. package/components/ste-skeleton/ATTRIBUTES.md +7 -0
  29. package/components/ste-skeleton/README.md +82 -0
  30. package/components/ste-skeleton/config.json +5 -0
  31. package/components/ste-skeleton/props.ts +7 -0
  32. package/components/ste-skeleton/ste-skeleton.easycom.json +38 -0
  33. package/components/ste-skeleton/ste-skeleton.vue +90 -0
  34. package/components/ste-slide-verify/ATTRIBUTES.md +27 -0
  35. package/components/ste-slide-verify/README.md +118 -0
  36. package/components/ste-slide-verify/config.json +5 -0
  37. package/components/ste-slide-verify/props.ts +43 -0
  38. package/components/ste-slide-verify/ste-slide-verify.easycom.json +119 -0
  39. package/components/ste-slide-verify/ste-slide-verify.vue +535 -0
  40. package/index.ts +8 -0
  41. package/package.json +1 -1
  42. package/types/components.d.ts +8 -0
  43. package/types/index.d.ts +2 -0
  44. package/types/refComponents.d.ts +8 -0
@@ -0,0 +1,169 @@
1
+ export interface SteSelectSeatContentSize {
2
+ width: number
3
+ height: number
4
+ }
5
+
6
+ export interface SteSelectSeatViewport {
7
+ scale: number
8
+ translateX: number
9
+ translateY: number
10
+ }
11
+
12
+ export interface SteSelectSeatRowLabelItem {
13
+ row: number
14
+ top: number
15
+ rowHeight: number
16
+ style: {
17
+ top: string
18
+ height: string
19
+ lineHeight: string
20
+ fontSize: string
21
+ }
22
+ }
23
+
24
+ export const INTERNAL_MAX_SCALE = 3
25
+
26
+ export const getLabelWidth = (showRowLabels: boolean, seatSize: number, seatGap: number) => {
27
+ return showRowLabels ? seatSize + seatGap : 0
28
+ }
29
+
30
+ export const getSeatContentSize = (params: {
31
+ rows: number
32
+ cols: number
33
+ seatSize: number
34
+ seatGap: number
35
+ labelWidth: number
36
+ }): SteSelectSeatContentSize => {
37
+ const { rows, cols, seatSize, seatGap, labelWidth } = params
38
+ if (!rows || !cols) return { width: 0, height: 0 }
39
+
40
+ return {
41
+ width: labelWidth + cols * (seatSize + seatGap) + seatGap,
42
+ height: rows * (seatSize + seatGap) + seatGap,
43
+ }
44
+ }
45
+
46
+ export const getFitScale = (params: {
47
+ width: number
48
+ height: number
49
+ contentWidth: number
50
+ contentHeight: number
51
+ }) => {
52
+ const { width, height, contentWidth, contentHeight } = params
53
+ if (!contentWidth || !contentHeight) return 1
54
+ return Math.min(1, width / contentWidth, height / contentHeight)
55
+ }
56
+
57
+ export const clampSeatScale = (scale: number, fitScale: number, maxScale = INTERNAL_MAX_SCALE) => {
58
+ return Math.min(Math.max(scale, fitScale), maxScale)
59
+ }
60
+
61
+ export const getSeatTranslateBounds = (params: {
62
+ scale: number
63
+ width: number
64
+ height: number
65
+ contentWidth: number
66
+ contentHeight: number
67
+ }) => {
68
+ const { scale, width, height, contentWidth, contentHeight } = params
69
+ if (!contentWidth || !contentHeight) {
70
+ return { minX: 0, maxX: 0, minY: 0, maxY: 0 }
71
+ }
72
+
73
+ const scaledWidth = contentWidth * scale
74
+ const scaledHeight = contentHeight * scale
75
+ const centerX = (width / scale - contentWidth) / 2
76
+ const centerY = (height / scale - contentHeight) / 2
77
+ const marginX = width * 0.2 / scale
78
+ const marginY = height * 0.2 / scale
79
+
80
+ return {
81
+ minX: scaledWidth > width ? width / scale - contentWidth - marginX : centerX - marginX,
82
+ maxX: scaledWidth > width ? marginX : centerX + marginX,
83
+ minY: scaledHeight > height ? height / scale - contentHeight - marginY : centerY - marginY,
84
+ maxY: scaledHeight > height ? marginY : centerY + marginY,
85
+ }
86
+ }
87
+
88
+ export const getDefaultSeatViewport = (params: {
89
+ fitScale: number
90
+ width: number
91
+ height: number
92
+ contentWidth: number
93
+ contentHeight: number
94
+ maxScale?: number
95
+ }): SteSelectSeatViewport => {
96
+ const { fitScale, width, height, contentWidth, contentHeight, maxScale = INTERNAL_MAX_SCALE } = params
97
+ if (!contentWidth || !contentHeight) {
98
+ return {
99
+ scale: 1,
100
+ translateX: 0,
101
+ translateY: 0,
102
+ }
103
+ }
104
+
105
+ const scale = clampSeatScale(fitScale, fitScale, maxScale)
106
+ return {
107
+ scale,
108
+ translateX: (width / scale - contentWidth) / 2,
109
+ translateY: (height / scale - contentHeight) / 2,
110
+ }
111
+ }
112
+
113
+ export const getScreenTranslateX = (params: {
114
+ scale: number
115
+ translateX: number
116
+ width: number
117
+ defaultViewport: SteSelectSeatViewport
118
+ }) => {
119
+ const { scale, translateX, width, defaultViewport } = params
120
+ const anchorX = width / (2 * defaultViewport.scale) - defaultViewport.translateX
121
+ return (anchorX + translateX) * scale - width / 2
122
+ }
123
+
124
+ export const buildRowLabelItems = (params: {
125
+ rows: number
126
+ height: number
127
+ seatSize: number
128
+ seatGap: number
129
+ translateY: number
130
+ scale: number
131
+ }): SteSelectSeatRowLabelItem[] => {
132
+ const { rows, height, seatSize, seatGap, translateY, scale } = params
133
+ const rowHeight = seatSize * scale
134
+ const fontSize = Math.max(10, Math.min(13, rowHeight * 0.3))
135
+
136
+ return Array.from({ length: rows }, (_, row) => {
137
+ const top = (row * (seatSize + seatGap) + seatGap / 2 + translateY) * scale
138
+ return {
139
+ row,
140
+ top,
141
+ rowHeight,
142
+ style: {
143
+ top: `${top}px`,
144
+ height: `${rowHeight}px`,
145
+ lineHeight: `${rowHeight}px`,
146
+ fontSize: `${fontSize}px`,
147
+ },
148
+ }
149
+ }).filter(item => item.top + rowHeight > 0 && item.top < height)
150
+ }
151
+
152
+ export const getRowLabelTrackStyle = (items: SteSelectSeatRowLabelItem[], height: number) => {
153
+ if (!items.length) {
154
+ return {
155
+ display: 'none',
156
+ }
157
+ }
158
+
159
+ const first = items[0]
160
+ const last = items[items.length - 1]
161
+ const padding = 8
162
+ const top = Math.max(0, first.top - padding)
163
+ const bottom = Math.min(height, last.top + last.rowHeight + padding)
164
+
165
+ return {
166
+ top: `${top}px`,
167
+ height: `${Math.max(32, bottom - top)}px`,
168
+ }
169
+ }
@@ -0,0 +1,540 @@
1
+ import { ref } from 'vue'
2
+ import type { ComponentPublicInstance } from 'vue'
3
+ import type { UniTouch, UniTouchEvent } from '../../../types/event'
4
+ import type { SteSelectSeatItem, SteSelectSeatValue } from '../types'
5
+ import { getTouchCenter, getTouchDistance, getTouchIdentifier, getTouchX, getTouchY, toTouchArray } from '../useTouchCompat'
6
+
7
+ interface UseSeatInteractionOptions {
8
+ instance: ComponentPublicInstance
9
+ canvasId: string
10
+ getShowRowLabels: () => boolean
11
+ touchHandler: {
12
+ scale: number
13
+ baseScale: number
14
+ translateX: number
15
+ translateY: number
16
+ baseTranslateX: number
17
+ baseTranslateY: number
18
+ reset: () => void
19
+ }
20
+ clampScale: (scale: number) => number
21
+ applyTranslateResistance: (x: number, y: number, scale?: number) => { x: number; y: number }
22
+ clampTranslate: (x: number, y: number, scale?: number) => { x: number; y: number }
23
+ getTouchSeat: (touchX: number, touchY: number) => SteSelectSeatItem | null
24
+ getTouchLocalPoint: (touch: UniTouch | undefined | null, rect?: { left?: number; top?: number } | null) => { x: number; y: number }
25
+ applyDefaultViewport: () => void
26
+ draw: () => void
27
+ emitMove: () => void
28
+ emitSeatClick: (seat: SteSelectSeatItem) => void
29
+ emitModelValue: (value: SteSelectSeatValue[]) => void
30
+ toggleSeat: (row: number, col: number) => SteSelectSeatValue[]
31
+ }
32
+
33
+ interface PointerEventLike {
34
+ clientX: number
35
+ clientY: number
36
+ target?: EventTarget | null
37
+ }
38
+
39
+ export function useSeatInteraction(options: UseSeatInteractionOptions) {
40
+ const {
41
+ instance,
42
+ canvasId,
43
+ getShowRowLabels,
44
+ touchHandler,
45
+ clampScale,
46
+ applyTranslateResistance,
47
+ clampTranslate,
48
+ getTouchSeat,
49
+ getTouchLocalPoint,
50
+ applyDefaultViewport,
51
+ draw,
52
+ emitMove,
53
+ emitSeatClick,
54
+ emitModelValue,
55
+ toggleSeat,
56
+ } = options
57
+
58
+ const panThreshold = 4
59
+ const reboundThreshold = 0.5
60
+ const reboundDuration = 180
61
+ const momentumMinVelocity = 0.02
62
+ const momentumDecayPerFrame = 0.92
63
+
64
+ const rowLabelsVisible = ref(getShowRowLabels())
65
+ const activeTouches = new Map<number | string, UniTouch>()
66
+
67
+ let dragMoved = false
68
+ let gestureMode: 'none' | 'pan' | 'pinch' = 'none'
69
+ let panStartX = 0
70
+ let panStartY = 0
71
+ let panBaseTranslateX = 0
72
+ let panBaseTranslateY = 0
73
+ let pinchStartDistance = 0
74
+ let pinchStartScale = 1
75
+ let pinchStartCenterX = 0
76
+ let pinchStartCenterY = 0
77
+ let pinchLockedTranslateX = 0
78
+ let pinchLockedTranslateY = 0
79
+ let reboundTimer: ReturnType<typeof setTimeout> | null = null
80
+ let momentumTimer: ReturnType<typeof setTimeout> | null = null
81
+ let rowLabelTimer: ReturnType<typeof setTimeout> | null = null
82
+ let lastPanSampleTime = 0
83
+ let lastPanSampleX = 0
84
+ let lastPanSampleY = 0
85
+ let panVelocityX = 0
86
+ let panVelocityY = 0
87
+ let mouseDown = false
88
+ let mouseStartX = 0
89
+ let mouseStartY = 0
90
+
91
+ // ─── Touch State Sync ─────────────────────────────────────────────────────
92
+
93
+ const syncActiveTouches = (touches: UniTouch[]) => {
94
+ activeTouches.clear()
95
+ touches.forEach((touch, index) => {
96
+ activeTouches.set(getTouchIdentifier(touch, index), touch)
97
+ })
98
+ }
99
+
100
+ const patchActiveTouches = (touches: UniTouch[]) => {
101
+ touches.forEach((touch, index) => {
102
+ activeTouches.set(getTouchIdentifier(touch, index), touch)
103
+ })
104
+ }
105
+
106
+ const removeActiveTouches = (touches: UniTouch[]) => {
107
+ touches.forEach((touch, index) => {
108
+ activeTouches.delete(getTouchIdentifier(touch, index))
109
+ })
110
+ }
111
+
112
+ const getEventTouches = (e: UniTouchEvent, phase: 'start' | 'move' | 'end') => {
113
+ const touches = toTouchArray(e.touches)
114
+ const changedTouches = toTouchArray(e.changedTouches)
115
+
116
+ if (phase === 'end') {
117
+ if (touches.length) {
118
+ syncActiveTouches(touches)
119
+ } else {
120
+ removeActiveTouches(changedTouches)
121
+ if (!activeTouches.size) {
122
+ activeTouches.clear()
123
+ }
124
+ }
125
+ return Array.from(activeTouches.values())
126
+ }
127
+
128
+ if (touches.length) {
129
+ syncActiveTouches(touches)
130
+ } else {
131
+ patchActiveTouches(changedTouches)
132
+ }
133
+ return Array.from(activeTouches.values())
134
+ }
135
+
136
+ const getChangedTouches = (e: UniTouchEvent) => toTouchArray(e.changedTouches)
137
+
138
+ // ─── Overlay State ────────────────────────────────────────────────────────
139
+
140
+ const clearRowLabelTimer = () => {
141
+ if (rowLabelTimer) {
142
+ clearTimeout(rowLabelTimer)
143
+ rowLabelTimer = null
144
+ }
145
+ }
146
+
147
+ const setRowLabelsVisible = (visible: boolean) => {
148
+ clearRowLabelTimer()
149
+ rowLabelsVisible.value = visible
150
+ }
151
+
152
+ const showRowLabelOverlay = () => {
153
+ clearRowLabelTimer()
154
+ rowLabelsVisible.value = getShowRowLabels()
155
+ }
156
+
157
+ // ─── Motion State ─────────────────────────────────────────────────────────
158
+
159
+ const resetPanVelocity = () => {
160
+ panVelocityX = 0
161
+ panVelocityY = 0
162
+ lastPanSampleTime = 0
163
+ lastPanSampleX = 0
164
+ lastPanSampleY = 0
165
+ }
166
+
167
+ const recordPanVelocity = (x: number, y: number) => {
168
+ const now = Date.now()
169
+ if (!lastPanSampleTime) {
170
+ lastPanSampleTime = now
171
+ lastPanSampleX = x
172
+ lastPanSampleY = y
173
+ return
174
+ }
175
+
176
+ const dt = Math.max(1, now - lastPanSampleTime)
177
+ const nextVelocityX = (x - lastPanSampleX) / dt
178
+ const nextVelocityY = (y - lastPanSampleY) / dt
179
+
180
+ panVelocityX = panVelocityX * 0.35 + nextVelocityX * 0.65
181
+ panVelocityY = panVelocityY * 0.35 + nextVelocityY * 0.65
182
+ lastPanSampleTime = now
183
+ lastPanSampleX = x
184
+ lastPanSampleY = y
185
+ }
186
+
187
+ const stopMomentum = () => {
188
+ if (!momentumTimer) return
189
+ clearTimeout(momentumTimer)
190
+ momentumTimer = null
191
+ }
192
+
193
+ const stopRebound = () => {
194
+ if (!reboundTimer) return
195
+ clearTimeout(reboundTimer)
196
+ reboundTimer = null
197
+ }
198
+
199
+ const stopMotion = () => {
200
+ stopMomentum()
201
+ stopRebound()
202
+ }
203
+
204
+ const updateBaseTransform = () => {
205
+ touchHandler.baseScale = touchHandler.scale
206
+ touchHandler.baseTranslateX = touchHandler.translateX
207
+ touchHandler.baseTranslateY = touchHandler.translateY
208
+ }
209
+
210
+ const applyTranslate = (x: number, y: number, scale = touchHandler.scale) => {
211
+ const nextTranslate = applyTranslateResistance(x, y, scale)
212
+ touchHandler.translateX = nextTranslate.x
213
+ touchHandler.translateY = nextTranslate.y
214
+ }
215
+
216
+ const beginPan = (touch: UniTouch, moved: boolean) => {
217
+ gestureMode = 'pan'
218
+ dragMoved = moved
219
+ panStartX = getTouchX(touch)
220
+ panStartY = getTouchY(touch)
221
+ panBaseTranslateX = touchHandler.translateX
222
+ panBaseTranslateY = touchHandler.translateY
223
+ resetPanVelocity()
224
+ recordPanVelocity(panStartX, panStartY)
225
+ }
226
+
227
+ const beginPinch = (touches: UniTouch[]) => {
228
+ gestureMode = 'pinch'
229
+ dragMoved = true
230
+ resetPanVelocity()
231
+ pinchStartDistance = getTouchDistance(touches)
232
+ pinchStartScale = touchHandler.scale
233
+ const center = getTouchCenter(touches)
234
+ pinchStartCenterX = center.x
235
+ pinchStartCenterY = center.y
236
+ pinchLockedTranslateX = touchHandler.translateX
237
+ pinchLockedTranslateY = touchHandler.translateY
238
+ }
239
+
240
+ // ─── Motion Animation ─────────────────────────────────────────────────────
241
+
242
+ const reboundToBounds = (onComplete?: () => void) => {
243
+ stopMomentum()
244
+ stopRebound()
245
+ const fromX = touchHandler.translateX
246
+ const fromY = touchHandler.translateY
247
+ const target = clampTranslate(fromX, fromY)
248
+
249
+ if (Math.abs(target.x - fromX) <= reboundThreshold && Math.abs(target.y - fromY) <= reboundThreshold) {
250
+ touchHandler.translateX = target.x
251
+ touchHandler.translateY = target.y
252
+ updateBaseTransform()
253
+ draw()
254
+ emitMove()
255
+ onComplete?.()
256
+ return
257
+ }
258
+
259
+ const startTime = Date.now()
260
+ const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
261
+
262
+ const animate = () => {
263
+ const elapsed = Date.now() - startTime
264
+ const progress = Math.min(1, elapsed / reboundDuration)
265
+ const eased = easeOutCubic(progress)
266
+
267
+ touchHandler.translateX = fromX + (target.x - fromX) * eased
268
+ touchHandler.translateY = fromY + (target.y - fromY) * eased
269
+ updateBaseTransform()
270
+ draw()
271
+ emitMove()
272
+
273
+ if (progress >= 1) {
274
+ touchHandler.translateX = target.x
275
+ touchHandler.translateY = target.y
276
+ updateBaseTransform()
277
+ draw()
278
+ emitMove()
279
+ reboundTimer = null
280
+ onComplete?.()
281
+ return
282
+ }
283
+
284
+ reboundTimer = setTimeout(animate, 16)
285
+ }
286
+
287
+ animate()
288
+ }
289
+
290
+ const startMomentum = (onComplete?: () => void) => {
291
+ stopMomentum()
292
+
293
+ if (Math.abs(panVelocityX) < momentumMinVelocity && Math.abs(panVelocityY) < momentumMinVelocity) {
294
+ reboundToBounds(onComplete)
295
+ return
296
+ }
297
+
298
+ let velocityX = panVelocityX
299
+ let velocityY = panVelocityY
300
+ let lastTime = Date.now()
301
+
302
+ const animate = () => {
303
+ const now = Date.now()
304
+ const dt = Math.min(24, Math.max(8, now - lastTime))
305
+ lastTime = now
306
+
307
+ const decay = Math.pow(momentumDecayPerFrame, dt / 16)
308
+ velocityX *= decay
309
+ velocityY *= decay
310
+
311
+ const nextTranslate = applyTranslateResistance(
312
+ touchHandler.translateX + velocityX * dt,
313
+ touchHandler.translateY + velocityY * dt,
314
+ )
315
+
316
+ touchHandler.translateX = nextTranslate.x
317
+ touchHandler.translateY = nextTranslate.y
318
+ updateBaseTransform()
319
+ draw()
320
+ emitMove()
321
+
322
+ const clamped = clampTranslate(touchHandler.translateX, touchHandler.translateY)
323
+ const outOfBounds =
324
+ Math.abs(clamped.x - touchHandler.translateX) > reboundThreshold ||
325
+ Math.abs(clamped.y - touchHandler.translateY) > reboundThreshold
326
+
327
+ if ((Math.abs(velocityX) < momentumMinVelocity && Math.abs(velocityY) < momentumMinVelocity) || outOfBounds) {
328
+ momentumTimer = null
329
+ panVelocityX = velocityX
330
+ panVelocityY = velocityY
331
+ reboundToBounds(onComplete)
332
+ return
333
+ }
334
+
335
+ momentumTimer = setTimeout(animate, 16)
336
+ }
337
+
338
+ animate()
339
+ }
340
+
341
+ const emitSeatSelection = (seat: SteSelectSeatItem) => {
342
+ emitSeatClick(seat)
343
+ emitModelValue(toggleSeat(seat.row, seat.col))
344
+ }
345
+
346
+ const selectSeatByPoint = (x: number, y: number) => {
347
+ const seat = getTouchSeat(x, y)
348
+ if (seat && !seat.disabled && !seat.empty) {
349
+ emitSeatSelection(seat)
350
+ }
351
+ }
352
+
353
+ const selectSeatFromTouch = (touch: UniTouch) => {
354
+ uni.createSelectorQuery()
355
+ .in(instance)
356
+ .select(`#${canvasId}`)
357
+ .boundingClientRect((rect: any) => {
358
+ if (!rect) return
359
+ const { x: localX, y: localY } = getTouchLocalPoint(touch, rect)
360
+ selectSeatByPoint(localX, localY)
361
+ })
362
+ .exec()
363
+ }
364
+
365
+ const selectSeatFromMouse = (event: PointerEventLike) => {
366
+ const rect = (event.target as HTMLElement | null)?.getBoundingClientRect?.()
367
+ if (!rect) return
368
+ selectSeatByPoint(event.clientX - rect.left, event.clientY - rect.top)
369
+ }
370
+
371
+ // ─── Pointer Events ───────────────────────────────────────────────────────
372
+
373
+ const onTouchStart = (e: UniTouchEvent) => {
374
+ stopMotion()
375
+ const touches = getEventTouches(e, 'start')
376
+ const touchCount = touches.length
377
+
378
+ if (touchCount >= 2) {
379
+ beginPinch(touches)
380
+ return
381
+ }
382
+
383
+ beginPan(touches[0], false)
384
+ }
385
+
386
+ const onTouchMove = (e: UniTouchEvent) => {
387
+ const touches = getEventTouches(e, 'move')
388
+ const touchCount = touches.length
389
+
390
+ if (touchCount >= 2) {
391
+ if (!pinchStartDistance) {
392
+ beginPinch(touches)
393
+ }
394
+
395
+ const currentDistance = getTouchDistance(touches)
396
+ const currentCenter = getTouchCenter(touches)
397
+ if (pinchStartDistance > 0 && currentDistance > 0) {
398
+ const nextScale = clampScale((currentDistance / pinchStartDistance) * pinchStartScale)
399
+ touchHandler.scale = nextScale
400
+
401
+ const nextTranslateX = currentCenter.x / nextScale - pinchStartCenterX / pinchStartScale + pinchLockedTranslateX
402
+ const nextTranslateY = currentCenter.y / nextScale - pinchStartCenterY / pinchStartScale + pinchLockedTranslateY
403
+ applyTranslate(nextTranslateX, nextTranslateY, nextScale)
404
+ }
405
+ draw()
406
+ emitMove()
407
+ return
408
+ }
409
+
410
+ if (gestureMode !== 'pan' || touchCount !== 1) return
411
+
412
+ const touch = touches[0]
413
+ const currentX = getTouchX(touch)
414
+ const currentY = getTouchY(touch)
415
+ const dx = currentX - panStartX
416
+ const dy = currentY - panStartY
417
+
418
+ if (Math.abs(dx) > panThreshold || Math.abs(dy) > panThreshold) {
419
+ dragMoved = true
420
+ }
421
+
422
+ applyTranslate(panBaseTranslateX + dx, panBaseTranslateY + dy)
423
+ recordPanVelocity(currentX, currentY)
424
+ draw()
425
+ emitMove()
426
+ }
427
+
428
+ const onTouchEnd = (e: UniTouchEvent) => {
429
+ const touches = getEventTouches(e, 'end')
430
+ const changedTouches = getChangedTouches(e)
431
+ const touchCount = touches.length
432
+
433
+ if (gestureMode === 'pinch') {
434
+ touchHandler.scale = clampScale(touchHandler.scale)
435
+ applyTranslate(touchHandler.translateX, touchHandler.translateY, touchHandler.scale)
436
+ pinchStartDistance = 0
437
+ }
438
+
439
+ updateBaseTransform()
440
+
441
+ if (touchCount >= 2) {
442
+ beginPinch(touches)
443
+ draw()
444
+ return
445
+ }
446
+
447
+ if (touchCount === 1) {
448
+ beginPan(touches[0], true)
449
+ } else if (touchCount === 0) {
450
+ gestureMode = 'none'
451
+ }
452
+
453
+ draw()
454
+ if (touchCount === 0 && dragMoved) {
455
+ startMomentum(() => showRowLabelOverlay())
456
+ } else if (touchCount === 0 || gestureMode === 'pinch') {
457
+ reboundToBounds(() => {
458
+ if (touchCount === 0) showRowLabelOverlay()
459
+ })
460
+ }
461
+
462
+ if (!dragMoved && changedTouches.length === 1) {
463
+ selectSeatFromTouch(changedTouches[0])
464
+ }
465
+ }
466
+
467
+ const onMouseDown = (e: PointerEventLike) => {
468
+ stopMotion()
469
+ mouseDown = true
470
+ dragMoved = false
471
+ mouseStartX = e.clientX
472
+ mouseStartY = e.clientY
473
+ resetPanVelocity()
474
+ recordPanVelocity(mouseStartX, mouseStartY)
475
+ }
476
+
477
+ const onMouseMove = (e: PointerEventLike) => {
478
+ if (!mouseDown) return
479
+ const dx = e.clientX - mouseStartX
480
+ const dy = e.clientY - mouseStartY
481
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
482
+ dragMoved = true
483
+ }
484
+ applyTranslate(touchHandler.baseTranslateX + dx, touchHandler.baseTranslateY + dy)
485
+ recordPanVelocity(e.clientX, e.clientY)
486
+ draw()
487
+ emitMove()
488
+ }
489
+
490
+ const onMouseUp = (e: PointerEventLike) => {
491
+ if (!mouseDown) return
492
+ mouseDown = false
493
+ updateBaseTransform()
494
+ if (dragMoved) {
495
+ startMomentum(() => showRowLabelOverlay())
496
+ } else {
497
+ reboundToBounds(() => showRowLabelOverlay())
498
+ }
499
+
500
+ if (!dragMoved) {
501
+ selectSeatFromMouse(e)
502
+ }
503
+ }
504
+
505
+ // ─── Public API ───────────────────────────────────────────────────────────
506
+
507
+ const reset = () => {
508
+ stopMotion()
509
+ showRowLabelOverlay()
510
+ activeTouches.clear()
511
+ touchHandler.reset()
512
+ gestureMode = 'none'
513
+ dragMoved = false
514
+ pinchStartDistance = 0
515
+ pinchStartScale = 1
516
+ pinchStartCenterX = 0
517
+ pinchStartCenterY = 0
518
+ pinchLockedTranslateX = 0
519
+ pinchLockedTranslateY = 0
520
+ panStartX = 0
521
+ panStartY = 0
522
+ panBaseTranslateX = 0
523
+ panBaseTranslateY = 0
524
+ resetPanVelocity()
525
+ applyDefaultViewport()
526
+ draw()
527
+ }
528
+
529
+ return {
530
+ rowLabelsVisible,
531
+ setShowRowLabelsVisible: setRowLabelsVisible,
532
+ onTouchStart,
533
+ onTouchMove,
534
+ onTouchEnd,
535
+ onMouseDown,
536
+ onMouseMove,
537
+ onMouseUp,
538
+ reset,
539
+ }
540
+ }