@vue-lynx-example/7guis 0.2.0 → 0.2.1
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/dist/cells.lynx.bundle +0 -0
- package/dist/cells.web.bundle +1 -1
- package/dist/circle-drawer.lynx.bundle +0 -0
- package/dist/circle-drawer.web.bundle +1 -1
- package/dist/counter.lynx.bundle +0 -0
- package/dist/counter.web.bundle +1 -1
- package/dist/crud.lynx.bundle +0 -0
- package/dist/crud.web.bundle +1 -1
- package/dist/flight-booker.lynx.bundle +0 -0
- package/dist/flight-booker.web.bundle +1 -1
- package/dist/temperature-converter.lynx.bundle +0 -0
- package/dist/temperature-converter.web.bundle +1 -1
- package/dist/timer.lynx.bundle +0 -0
- package/dist/timer.web.bundle +1 -1
- package/lynx.config.ts +7 -0
- package/package.json +2 -2
- package/src/circle-drawer/App.vue +208 -47
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, shallowReactive } from 'vue'
|
|
2
|
+
import { ref, shallowReactive, computed } from 'vue'
|
|
3
3
|
|
|
4
4
|
const history = shallowReactive([[]])
|
|
5
5
|
const historyIndex = ref(0)
|
|
6
6
|
const circles = ref([])
|
|
7
7
|
const selectedId = ref(-1)
|
|
8
|
-
const
|
|
8
|
+
const showModal = ref(false)
|
|
9
9
|
|
|
10
10
|
let nextId = 0
|
|
11
11
|
|
|
12
|
+
// Modal slider state
|
|
13
|
+
const sliderWidth = 200 // Width of slider track in px
|
|
14
|
+
const minRadius = 5
|
|
15
|
+
const maxRadius = 150
|
|
16
|
+
|
|
17
|
+
// Double tap detection
|
|
18
|
+
const lastTapTime = ref(0)
|
|
19
|
+
const lastTapId = ref(-1)
|
|
20
|
+
const DOUBLE_TAP_DELAY = 300 // ms
|
|
21
|
+
|
|
22
|
+
const selectedCircle = computed(() => {
|
|
23
|
+
return circles.value.find(c => c.id === selectedId.value)
|
|
24
|
+
})
|
|
25
|
+
|
|
12
26
|
function onCanvasTap(e) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
push()
|
|
27
|
+
// If modal is open, close it without creating new circle
|
|
28
|
+
if (showModal.value) {
|
|
29
|
+
closeModal()
|
|
17
30
|
return
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
//
|
|
33
|
+
// Get touch coordinates - relative to the full view
|
|
21
34
|
const x = e.detail?.x ?? e.touches?.[0]?.pageX ?? 0
|
|
22
35
|
const y = e.detail?.y ?? e.touches?.[0]?.pageY ?? 0
|
|
23
|
-
console.log('[circle-drawer] tap event:', { x, y, detail: e.detail, type: e.type, raw: { clientX: e.clientX, clientY: e.clientY } })
|
|
24
36
|
|
|
37
|
+
// Check if tapped on an existing circle
|
|
25
38
|
const hit = [...circles.value].reverse().find((c) => {
|
|
26
39
|
const dx = c.x - x
|
|
27
40
|
const dy = c.y - y
|
|
@@ -29,7 +42,7 @@ function onCanvasTap(e) {
|
|
|
29
42
|
})
|
|
30
43
|
|
|
31
44
|
if (hit) {
|
|
32
|
-
|
|
45
|
+
handleCircleTap(hit.id)
|
|
33
46
|
} else {
|
|
34
47
|
circles.value.push({ id: nextId++, x, y, r: 30 })
|
|
35
48
|
selectedId.value = -1
|
|
@@ -37,37 +50,108 @@ function onCanvasTap(e) {
|
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
function handleCircleTap(circleId) {
|
|
54
|
+
const now = Date.now()
|
|
55
|
+
const timeSinceLastTap = now - lastTapTime.value
|
|
56
|
+
|
|
57
|
+
if (lastTapId.value === circleId && timeSinceLastTap < DOUBLE_TAP_DELAY) {
|
|
58
|
+
// Double tap detected - open modal
|
|
59
|
+
showModal.value = true
|
|
60
|
+
lastTapId.value = -1
|
|
61
|
+
lastTapTime.value = 0
|
|
62
|
+
} else {
|
|
63
|
+
// Single tap - just select
|
|
64
|
+
selectedId.value = circleId
|
|
65
|
+
lastTapId.value = circleId
|
|
66
|
+
lastTapTime.value = now
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
function onCircleTap(e, circle) {
|
|
71
|
+
// Use catchtap to stop event bubbling to parent
|
|
72
|
+
e?.stopPropagation?.()
|
|
73
|
+
handleCircleTap(circle.id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function closeModal() {
|
|
77
|
+
if (showModal.value) {
|
|
78
|
+
showModal.value = false
|
|
79
|
+
push()
|
|
50
80
|
}
|
|
51
81
|
}
|
|
52
82
|
|
|
83
|
+
function onModalTap(e) {
|
|
84
|
+
// Stop propagation to prevent canvas interaction
|
|
85
|
+
e?.stopPropagation?.()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onOverlayTap(e) {
|
|
89
|
+
// Close modal when tapping overlay background
|
|
90
|
+
e?.stopPropagation?.()
|
|
91
|
+
closeModal()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Slider drag handling
|
|
95
|
+
const isDragging = ref(false)
|
|
96
|
+
const sliderValue = computed({
|
|
97
|
+
get() {
|
|
98
|
+
return selectedCircle.value?.r ?? minRadius
|
|
99
|
+
},
|
|
100
|
+
set(val) {
|
|
101
|
+
if (selectedCircle.value) {
|
|
102
|
+
selectedCircle.value.r = Math.max(minRadius, Math.min(maxRadius, val))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function onSliderTouchStart(e) {
|
|
108
|
+
isDragging.value = true
|
|
109
|
+
updateSliderFromTouch(e)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onSliderTouchMove(e) {
|
|
113
|
+
if (isDragging.value) {
|
|
114
|
+
updateSliderFromTouch(e)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onSliderTouchEnd(e) {
|
|
119
|
+
isDragging.value = false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function updateSliderFromTouch(e) {
|
|
123
|
+
const touch = e.touches?.[0] ?? e.changedTouches?.[0]
|
|
124
|
+
if (!touch) return
|
|
125
|
+
|
|
126
|
+
// Calculate position relative to slider track
|
|
127
|
+
const trackLeft = (globalThis.window?.innerWidth ?? 375) / 2 - sliderWidth / 2
|
|
128
|
+
const relativeX = touch.pageX - trackLeft
|
|
129
|
+
const percentage = Math.max(0, Math.min(1, relativeX / sliderWidth))
|
|
130
|
+
sliderValue.value = minRadius + percentage * (maxRadius - minRadius)
|
|
131
|
+
}
|
|
132
|
+
|
|
53
133
|
function push() {
|
|
54
134
|
history.length = ++historyIndex.value
|
|
55
135
|
history.push(circles.value.map((c) => ({ ...c })))
|
|
56
136
|
}
|
|
57
137
|
|
|
58
|
-
function undo() {
|
|
138
|
+
function undo(e) {
|
|
139
|
+
// Use catchtap to prevent event from reaching canvas
|
|
140
|
+
e?.stopPropagation?.()
|
|
59
141
|
if (historyIndex.value > 0) {
|
|
60
142
|
circles.value = history[--historyIndex.value].map((c) => ({ ...c }))
|
|
61
143
|
selectedId.value = -1
|
|
62
|
-
|
|
144
|
+
showModal.value = false
|
|
63
145
|
}
|
|
64
146
|
}
|
|
65
147
|
|
|
66
|
-
function redo() {
|
|
148
|
+
function redo(e) {
|
|
149
|
+
// Use catchtap to prevent event from reaching canvas
|
|
150
|
+
e?.stopPropagation?.()
|
|
67
151
|
if (historyIndex.value < history.length - 1) {
|
|
68
152
|
circles.value = history[++historyIndex.value].map((c) => ({ ...c }))
|
|
69
153
|
selectedId.value = -1
|
|
70
|
-
|
|
154
|
+
showModal.value = false
|
|
71
155
|
}
|
|
72
156
|
}
|
|
73
157
|
</script>
|
|
@@ -78,7 +162,7 @@ function redo() {
|
|
|
78
162
|
:style="{ width: '100%', height: '100vh', minHeight: '400px', backgroundColor: '#f0f0f0', position: 'relative' }"
|
|
79
163
|
@tap="onCanvasTap"
|
|
80
164
|
>
|
|
81
|
-
<!-- Hint text -->
|
|
165
|
+
<!-- Hint text for empty state -->
|
|
82
166
|
<text
|
|
83
167
|
v-if="circles.length === 0"
|
|
84
168
|
:style="{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: '#bbb', fontSize: 14, textAlign: 'center' }"
|
|
@@ -97,10 +181,11 @@ function redo() {
|
|
|
97
181
|
width: circle.r * 2 + 'px',
|
|
98
182
|
height: circle.r * 2 + 'px',
|
|
99
183
|
borderRadius: circle.r + 'px',
|
|
100
|
-
backgroundColor: circle.id === selectedId ? '#
|
|
101
|
-
borderWidth: 1,
|
|
102
|
-
borderColor: '#333',
|
|
184
|
+
backgroundColor: circle.id === selectedId ? '#0077ff' : '#fff',
|
|
185
|
+
borderWidth: circle.id === selectedId ? 3 : 1,
|
|
186
|
+
borderColor: circle.id === selectedId ? '#0055cc' : '#333',
|
|
103
187
|
}"
|
|
188
|
+
@tap="(e) => onCircleTap(e, circle)"
|
|
104
189
|
/>
|
|
105
190
|
|
|
106
191
|
<!-- Undo / Redo overlaid at the top -->
|
|
@@ -119,33 +204,109 @@ function redo() {
|
|
|
119
204
|
</view>
|
|
120
205
|
</view>
|
|
121
206
|
|
|
122
|
-
<!--
|
|
123
|
-
<
|
|
124
|
-
|
|
207
|
+
<!-- Hint for interaction -->
|
|
208
|
+
<text
|
|
209
|
+
v-if="circles.length > 0 && !showModal"
|
|
210
|
+
:style="{ position: 'absolute', bottom: 20, left: 0, right: 0, textAlign: 'center', color: '#666', fontSize: 12 }"
|
|
211
|
+
>
|
|
212
|
+
Double-tap a circle to adjust radius
|
|
213
|
+
</text>
|
|
214
|
+
|
|
215
|
+
<!-- Modal Overlay -->
|
|
216
|
+
<view
|
|
217
|
+
v-if="showModal"
|
|
218
|
+
:style="{
|
|
219
|
+
position: 'absolute',
|
|
220
|
+
top: 0,
|
|
221
|
+
left: 0,
|
|
222
|
+
right: 0,
|
|
223
|
+
bottom: 0,
|
|
224
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
225
|
+
display: 'flex',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
justifyContent: 'center',
|
|
228
|
+
zIndex: 100,
|
|
229
|
+
}"
|
|
230
|
+
@tap="onOverlayTap"
|
|
231
|
+
>
|
|
232
|
+
<!-- Modal Content -->
|
|
233
|
+
<view
|
|
234
|
+
:style="{
|
|
235
|
+
backgroundColor: '#fff',
|
|
236
|
+
borderRadius: 12,
|
|
237
|
+
padding: '24px 32px',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
minWidth: '280px',
|
|
240
|
+
}"
|
|
241
|
+
@tap="onModalTap"
|
|
242
|
+
>
|
|
243
|
+
<text :style="{ fontSize: 18, fontWeight: 'bold', color: '#333', marginBottom: 16 }">
|
|
244
|
+
Adjust Radius
|
|
245
|
+
</text>
|
|
246
|
+
|
|
247
|
+
<text :style="{ fontSize: 24, fontWeight: 'bold', color: '#0077ff', marginBottom: 20 }">
|
|
248
|
+
{{ Math.round(selectedCircle?.r ?? 0) }}px
|
|
249
|
+
</text>
|
|
250
|
+
|
|
251
|
+
<!-- Slider Track -->
|
|
125
252
|
<view
|
|
126
|
-
:style="{
|
|
127
|
-
|
|
253
|
+
:style="{
|
|
254
|
+
width: sliderWidth + 'px',
|
|
255
|
+
height: 6,
|
|
256
|
+
backgroundColor: '#e0e0e0',
|
|
257
|
+
borderRadius: 3,
|
|
258
|
+
position: 'relative',
|
|
259
|
+
}"
|
|
260
|
+
@touchstart="onSliderTouchStart"
|
|
261
|
+
@touchmove="onSliderTouchMove"
|
|
262
|
+
@touchend="onSliderTouchEnd"
|
|
128
263
|
>
|
|
129
|
-
|
|
130
|
-
</view>
|
|
131
|
-
</view>
|
|
132
|
-
<view v-else :style="{ alignItems: 'center', gap: 6 }">
|
|
133
|
-
<text :style="{ fontSize: 14 }">
|
|
134
|
-
Radius: {{ circles.find(c => c.id === selectedId)?.r ?? 0 }}
|
|
135
|
-
</text>
|
|
136
|
-
<view :style="{ display: 'flex', flexDirection: 'row', gap: 8 }">
|
|
264
|
+
<!-- Filled portion -->
|
|
137
265
|
<view
|
|
138
|
-
:style="{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
266
|
+
:style="{
|
|
267
|
+
position: 'absolute',
|
|
268
|
+
left: 0,
|
|
269
|
+
top: 0,
|
|
270
|
+
height: 6,
|
|
271
|
+
width: (((selectedCircle?.r ?? minRadius) - minRadius) / (maxRadius - minRadius)) * sliderWidth + 'px',
|
|
272
|
+
backgroundColor: '#0077ff',
|
|
273
|
+
borderRadius: 3,
|
|
274
|
+
}"
|
|
275
|
+
/>
|
|
276
|
+
<!-- Thumb -->
|
|
143
277
|
<view
|
|
144
|
-
:style="{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
278
|
+
:style="{
|
|
279
|
+
position: 'absolute',
|
|
280
|
+
top: -7,
|
|
281
|
+
left: (((selectedCircle?.r ?? minRadius) - minRadius) / (maxRadius - minRadius)) * sliderWidth - 10 + 'px',
|
|
282
|
+
width: 20,
|
|
283
|
+
height: 20,
|
|
284
|
+
backgroundColor: '#0077ff',
|
|
285
|
+
borderRadius: 10,
|
|
286
|
+
borderWidth: 2,
|
|
287
|
+
borderColor: '#fff',
|
|
288
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|
289
|
+
}"
|
|
290
|
+
/>
|
|
291
|
+
</view>
|
|
292
|
+
|
|
293
|
+
<!-- Slider Labels -->
|
|
294
|
+
<view :style="{ width: sliderWidth + 'px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }">
|
|
295
|
+
<text :style="{ fontSize: 12, color: '#999' }">{{ minRadius }}px</text>
|
|
296
|
+
<text :style="{ fontSize: 12, color: '#999' }">{{ maxRadius }}px</text>
|
|
297
|
+
</view>
|
|
298
|
+
|
|
299
|
+
<!-- Done Button -->
|
|
300
|
+
<view
|
|
301
|
+
:style="{
|
|
302
|
+
marginTop: 24,
|
|
303
|
+
padding: '10px 32px',
|
|
304
|
+
backgroundColor: '#0077ff',
|
|
305
|
+
borderRadius: 6,
|
|
306
|
+
}"
|
|
307
|
+
@tap="closeModal"
|
|
308
|
+
>
|
|
309
|
+
<text :style="{ color: '#fff', fontSize: 16, fontWeight: 'bold' }">Done</text>
|
|
149
310
|
</view>
|
|
150
311
|
</view>
|
|
151
312
|
</view>
|