@hanology/cham-browser 0.4.12 → 0.4.14
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
|
@@ -4,8 +4,6 @@ import { annotationToPronSegment } from '../utils/annotationParser'
|
|
|
4
4
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
5
5
|
import type { Annotation } from '../types'
|
|
6
6
|
|
|
7
|
-
type DisplayMode = 'pane' | 'popup' | 'sheet'
|
|
8
|
-
|
|
9
7
|
const props = defineProps<{
|
|
10
8
|
visible: boolean
|
|
11
9
|
annotations: Annotation[]
|
|
@@ -21,25 +19,14 @@ const emit = defineEmits<{
|
|
|
21
19
|
}>()
|
|
22
20
|
|
|
23
21
|
const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
22
|
+
const isMobile = computed(() => ww.value < 768)
|
|
24
23
|
|
|
25
24
|
function onResize() { ww.value = window.innerWidth }
|
|
26
25
|
|
|
27
|
-
const mode = computed<DisplayMode>(() => {
|
|
28
|
-
const w = ww.value
|
|
29
|
-
if (w < 768) return 'sheet'
|
|
30
|
-
if (w >= 1024) return 'pane'
|
|
31
|
-
return 'popup'
|
|
32
|
-
})
|
|
33
|
-
|
|
34
26
|
const stickyVisible = ref(false)
|
|
35
27
|
|
|
36
28
|
watch(() => props.visible, (v) => {
|
|
37
29
|
if (v) stickyVisible.value = true
|
|
38
|
-
else if (mode.value === 'popup') stickyVisible.value = false
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
watch(mode, () => {
|
|
42
|
-
if (mode.value === 'popup' && !props.visible) stickyVisible.value = false
|
|
43
30
|
})
|
|
44
31
|
|
|
45
32
|
function dismiss() {
|
|
@@ -80,15 +67,15 @@ function kindLabel(ann: Annotation): string {
|
|
|
80
67
|
function onDocClick(e: MouseEvent) {
|
|
81
68
|
if (!stickyVisible.value) return
|
|
82
69
|
const el = (e.target as HTMLElement)
|
|
83
|
-
if (el.closest('.ann-
|
|
70
|
+
if (el.closest('.ann-card, .ann-sheet')) return
|
|
84
71
|
if (el.closest('.ann-target')) return
|
|
85
72
|
dismiss()
|
|
86
73
|
}
|
|
87
74
|
|
|
88
75
|
function onDocTouchMove(e: TouchEvent) {
|
|
89
|
-
if (!stickyVisible.value ||
|
|
76
|
+
if (!stickyVisible.value || !isMobile.value) return
|
|
90
77
|
const el = (e.target as HTMLElement)
|
|
91
|
-
if (el.closest('.ann-
|
|
78
|
+
if (el.closest('.ann-sheet')) return
|
|
92
79
|
dismiss()
|
|
93
80
|
}
|
|
94
81
|
|
|
@@ -107,18 +94,19 @@ onBeforeUnmount(() => {
|
|
|
107
94
|
|
|
108
95
|
<template>
|
|
109
96
|
<Teleport to="body">
|
|
110
|
-
<!-- Desktop
|
|
111
|
-
<Transition
|
|
97
|
+
<!-- Desktop/Tablet floating card -->
|
|
98
|
+
<Transition name="ann-pop">
|
|
112
99
|
<div
|
|
113
|
-
v-if="
|
|
114
|
-
|
|
100
|
+
v-if="!isMobile && stickyVisible && annotations.length"
|
|
101
|
+
class="ann-card"
|
|
102
|
+
:style="style"
|
|
115
103
|
@mouseenter="emit('tooltipEnter')"
|
|
116
104
|
@mouseleave="emit('tooltipLeave')"
|
|
117
105
|
>
|
|
118
|
-
<button class="ann-
|
|
119
|
-
<svg width="
|
|
106
|
+
<button class="ann-card-close" @click="dismiss" aria-label="關閉">
|
|
107
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
120
108
|
</button>
|
|
121
|
-
<div class="ann-
|
|
109
|
+
<div class="ann-card-scroll">
|
|
122
110
|
<div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
|
|
123
111
|
<div class="ann-head">
|
|
124
112
|
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
@@ -131,32 +119,11 @@ onBeforeUnmount(() => {
|
|
|
131
119
|
</div>
|
|
132
120
|
</Transition>
|
|
133
121
|
|
|
134
|
-
<!-- Tablet popup -->
|
|
135
|
-
<Transition name="ann-fade">
|
|
136
|
-
<div
|
|
137
|
-
v-if="mode === 'popup' && visible && annotations.length"
|
|
138
|
-
class="ann-popup"
|
|
139
|
-
:class="{ 'ann-popup--vertical': vertical }"
|
|
140
|
-
:style="style"
|
|
141
|
-
@mouseenter="emit('tooltipEnter')"
|
|
142
|
-
@mouseleave="emit('tooltipLeave')"
|
|
143
|
-
>
|
|
144
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
|
|
145
|
-
<div class="ann-head">
|
|
146
|
-
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
147
|
-
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
148
|
-
</div>
|
|
149
|
-
<PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
|
|
150
|
-
<div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</Transition>
|
|
154
|
-
|
|
155
122
|
<!-- Mobile bottom sheet -->
|
|
156
123
|
<Transition name="ann-sheet">
|
|
157
124
|
<div
|
|
158
|
-
v-if="
|
|
159
|
-
class="ann-
|
|
125
|
+
v-if="isMobile && stickyVisible && annotations.length"
|
|
126
|
+
class="ann-sheet"
|
|
160
127
|
>
|
|
161
128
|
<button class="ann-sheet-handle" @click="dismiss">
|
|
162
129
|
<span class="ann-handle-bar" />
|
|
@@ -177,7 +144,7 @@ onBeforeUnmount(() => {
|
|
|
177
144
|
</template>
|
|
178
145
|
|
|
179
146
|
<style scoped>
|
|
180
|
-
/* ───
|
|
147
|
+
/* ─── Shared annotation entry ─── */
|
|
181
148
|
.ann-entry {
|
|
182
149
|
padding: 10px 0;
|
|
183
150
|
border-bottom: 1px solid var(--border-light);
|
|
@@ -194,7 +161,6 @@ onBeforeUnmount(() => {
|
|
|
194
161
|
align-items: center;
|
|
195
162
|
gap: 6px;
|
|
196
163
|
margin-bottom: 2px;
|
|
197
|
-
vertical-align: baseline;
|
|
198
164
|
}
|
|
199
165
|
|
|
200
166
|
.ann-kind {
|
|
@@ -206,7 +172,6 @@ onBeforeUnmount(() => {
|
|
|
206
172
|
font-weight: 700;
|
|
207
173
|
letter-spacing: 1px;
|
|
208
174
|
line-height: 1.5;
|
|
209
|
-
vertical-align: middle;
|
|
210
175
|
}
|
|
211
176
|
.ann-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
212
177
|
.ann-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
@@ -229,7 +194,6 @@ onBeforeUnmount(() => {
|
|
|
229
194
|
border: 1px solid var(--border-light);
|
|
230
195
|
border-radius: 2px;
|
|
231
196
|
letter-spacing: 0.5px;
|
|
232
|
-
line-height: 1.4;
|
|
233
197
|
}
|
|
234
198
|
|
|
235
199
|
.ann-text {
|
|
@@ -237,123 +201,68 @@ onBeforeUnmount(() => {
|
|
|
237
201
|
line-height: 1.8;
|
|
238
202
|
}
|
|
239
203
|
|
|
240
|
-
/* ─── Desktop
|
|
241
|
-
.ann-
|
|
204
|
+
/* ─── Desktop/Tablet floating card ─── */
|
|
205
|
+
.ann-card {
|
|
242
206
|
position: fixed;
|
|
243
|
-
top: 72px;
|
|
244
|
-
left: 20px;
|
|
245
|
-
width: 280px;
|
|
246
|
-
max-height: calc(100vh - 100px);
|
|
247
207
|
background: var(--surface-warm);
|
|
248
208
|
border: 1px solid var(--border);
|
|
249
209
|
border-radius: 10px;
|
|
250
|
-
box-shadow: 0
|
|
210
|
+
box-shadow: 0 12px 40px rgba(var(--shadow-rgb), 0.18);
|
|
251
211
|
z-index: 1000;
|
|
252
212
|
display: flex;
|
|
253
213
|
flex-direction: column;
|
|
214
|
+
overflow: hidden;
|
|
254
215
|
}
|
|
255
216
|
|
|
256
|
-
.ann-
|
|
217
|
+
.ann-card-close {
|
|
257
218
|
position: absolute;
|
|
258
|
-
top:
|
|
259
|
-
right:
|
|
260
|
-
width:
|
|
261
|
-
height:
|
|
219
|
+
top: 6px;
|
|
220
|
+
right: 6px;
|
|
221
|
+
width: 22px;
|
|
222
|
+
height: 22px;
|
|
262
223
|
border: none;
|
|
263
224
|
border-radius: 4px;
|
|
264
|
-
background:
|
|
225
|
+
background: transparent;
|
|
265
226
|
color: var(--ink-faint);
|
|
266
227
|
cursor: pointer;
|
|
267
228
|
display: flex;
|
|
268
229
|
align-items: center;
|
|
269
230
|
justify-content: center;
|
|
270
231
|
transition: all 0.15s;
|
|
232
|
+
opacity: 0.4;
|
|
271
233
|
z-index: 1;
|
|
272
|
-
opacity: 0.6;
|
|
273
234
|
}
|
|
274
|
-
.ann-
|
|
235
|
+
.ann-card-close:hover {
|
|
275
236
|
opacity: 1;
|
|
276
237
|
background: var(--ink);
|
|
277
238
|
color: var(--paper);
|
|
278
239
|
}
|
|
279
240
|
|
|
280
|
-
.ann-
|
|
241
|
+
.ann-card-scroll {
|
|
281
242
|
padding: 12px 14px;
|
|
282
243
|
overflow-y: auto;
|
|
283
244
|
overscroll-behavior: contain;
|
|
284
245
|
flex: 1;
|
|
285
246
|
}
|
|
286
247
|
|
|
287
|
-
/*
|
|
288
|
-
.ann-
|
|
289
|
-
|
|
290
|
-
top: 72px;
|
|
291
|
-
right: 20px;
|
|
292
|
-
height: calc(100vh - 100px);
|
|
293
|
-
width: auto;
|
|
294
|
-
max-width: 280px;
|
|
295
|
-
writing-mode: vertical-rl;
|
|
296
|
-
text-orientation: mixed;
|
|
297
|
-
background: var(--surface-warm);
|
|
298
|
-
border: 1px solid var(--border);
|
|
299
|
-
border-radius: 10px;
|
|
300
|
-
box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
|
|
301
|
-
z-index: 1000;
|
|
302
|
-
display: flex;
|
|
303
|
-
flex-direction: column;
|
|
304
|
-
}
|
|
305
|
-
.ann-right-pane .ann-pane-close {
|
|
306
|
-
position: static;
|
|
307
|
-
margin: 8px 8px 0;
|
|
308
|
-
opacity: 0.5;
|
|
309
|
-
flex-shrink: 0;
|
|
310
|
-
}
|
|
311
|
-
.ann-right-pane .ann-pane-scroll {
|
|
312
|
-
writing-mode: vertical-rl;
|
|
313
|
-
text-orientation: mixed;
|
|
314
|
-
overflow-x: auto;
|
|
315
|
-
overflow-y: hidden;
|
|
316
|
-
padding: 12px 14px;
|
|
317
|
-
flex: 1;
|
|
248
|
+
/* Card transition */
|
|
249
|
+
.ann-pop-enter-active {
|
|
250
|
+
transition: opacity 0.15s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
318
251
|
}
|
|
319
|
-
.ann-
|
|
320
|
-
|
|
321
|
-
text-orientation: mixed;
|
|
322
|
-
border-bottom: none;
|
|
323
|
-
border-left: 1px solid var(--border-light);
|
|
324
|
-
padding: 0 0 0 12px;
|
|
325
|
-
margin-left: 8px;
|
|
252
|
+
.ann-pop-leave-active {
|
|
253
|
+
transition: opacity 0.12s ease, transform 0.12s ease;
|
|
326
254
|
}
|
|
327
|
-
.ann-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
flex-direction: column;
|
|
331
|
-
align-items: flex-start;
|
|
332
|
-
margin-bottom: 0;
|
|
333
|
-
margin-left: 4px;
|
|
334
|
-
}
|
|
335
|
-
.ann-right-pane .ann-text {
|
|
336
|
-
writing-mode: vertical-rl;
|
|
337
|
-
text-orientation: mixed;
|
|
338
|
-
line-height: 2;
|
|
255
|
+
.ann-pop-enter-from {
|
|
256
|
+
opacity: 0;
|
|
257
|
+
transform: scale(0.95) translateY(6px);
|
|
339
258
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
.
|
|
343
|
-
position: fixed;
|
|
344
|
-
padding: 12px 14px;
|
|
345
|
-
background: var(--surface-warm);
|
|
346
|
-
border: 1px solid var(--border);
|
|
347
|
-
border-radius: 8px;
|
|
348
|
-
box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
|
|
349
|
-
max-width: 300px;
|
|
350
|
-
max-height: 50vh;
|
|
351
|
-
overflow-y: auto;
|
|
352
|
-
z-index: 1000;
|
|
259
|
+
.ann-pop-leave-to {
|
|
260
|
+
opacity: 0;
|
|
261
|
+
transform: scale(0.97);
|
|
353
262
|
}
|
|
354
263
|
|
|
355
|
-
/* ─── Mobile
|
|
356
|
-
.ann-
|
|
264
|
+
/* ─── Mobile bottom sheet ─── */
|
|
265
|
+
.ann-sheet {
|
|
357
266
|
position: fixed;
|
|
358
267
|
left: 0;
|
|
359
268
|
right: 0;
|
|
@@ -394,53 +303,7 @@ onBeforeUnmount(() => {
|
|
|
394
303
|
flex: 1;
|
|
395
304
|
}
|
|
396
305
|
|
|
397
|
-
/*
|
|
398
|
-
|
|
399
|
-
.ann-slide-right-enter-active {
|
|
400
|
-
transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
401
|
-
}
|
|
402
|
-
.ann-slide-right-leave-active {
|
|
403
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
404
|
-
}
|
|
405
|
-
.ann-slide-right-enter-from {
|
|
406
|
-
opacity: 0;
|
|
407
|
-
transform: translateX(-20px);
|
|
408
|
-
}
|
|
409
|
-
.ann-slide-right-leave-to {
|
|
410
|
-
opacity: 0;
|
|
411
|
-
transform: translateX(-12px);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.ann-slide-left-enter-active {
|
|
415
|
-
transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
416
|
-
}
|
|
417
|
-
.ann-slide-left-leave-active {
|
|
418
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
419
|
-
}
|
|
420
|
-
.ann-slide-left-enter-from {
|
|
421
|
-
opacity: 0;
|
|
422
|
-
transform: translateX(20px);
|
|
423
|
-
}
|
|
424
|
-
.ann-slide-left-leave-to {
|
|
425
|
-
opacity: 0;
|
|
426
|
-
transform: translateX(12px);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.ann-fade-enter-active {
|
|
430
|
-
transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
431
|
-
}
|
|
432
|
-
.ann-fade-leave-active {
|
|
433
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
434
|
-
}
|
|
435
|
-
.ann-fade-enter-from {
|
|
436
|
-
opacity: 0;
|
|
437
|
-
transform: scale(0.94) translateY(4px);
|
|
438
|
-
}
|
|
439
|
-
.ann-fade-leave-to {
|
|
440
|
-
opacity: 0;
|
|
441
|
-
transform: scale(0.96);
|
|
442
|
-
}
|
|
443
|
-
|
|
306
|
+
/* Sheet transition */
|
|
444
307
|
.ann-sheet-enter-active {
|
|
445
308
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
446
309
|
}
|
|
@@ -452,11 +315,7 @@ onBeforeUnmount(() => {
|
|
|
452
315
|
transform: translateY(100%);
|
|
453
316
|
}
|
|
454
317
|
|
|
455
|
-
@media (
|
|
456
|
-
.ann-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
@media (min-width: 1024px) {
|
|
460
|
-
.ann-bottom-sheet { display: none; }
|
|
318
|
+
@media (min-width: 768px) {
|
|
319
|
+
.ann-sheet { display: none; }
|
|
461
320
|
}
|
|
462
321
|
</style>
|
|
@@ -138,46 +138,51 @@ export function useAnnotationTooltip() {
|
|
|
138
138
|
items.value = annotations
|
|
139
139
|
const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
|
|
140
140
|
const rect = (el ?? event.target as HTMLElement).getBoundingClientRect()
|
|
141
|
+
const vw = window.innerWidth
|
|
142
|
+
const vh = window.innerHeight
|
|
143
|
+
|
|
144
|
+
if (vw < 768) {
|
|
145
|
+
// Mobile: bottom sheet
|
|
146
|
+
style.value = {
|
|
147
|
+
left: '0',
|
|
148
|
+
right: '0',
|
|
149
|
+
bottom: '0',
|
|
150
|
+
}
|
|
151
|
+
} else if (layout.value === 'vertical') {
|
|
152
|
+
// Vertical mode: card to the left of the annotation
|
|
153
|
+
const cardW = 340
|
|
154
|
+
const gap = 12
|
|
155
|
+
let left = Math.max(8, rect.left - cardW - gap)
|
|
156
|
+
let top = Math.max(8, Math.min(rect.top, vh - 300))
|
|
157
|
+
|
|
158
|
+
// If not enough room on left, go right
|
|
159
|
+
if (rect.left - cardW - gap < 8) {
|
|
160
|
+
left = Math.min(rect.right + gap, vw - cardW - 8)
|
|
161
|
+
}
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
right: '4vw',
|
|
148
|
-
bottom: '0',
|
|
149
|
-
maxWidth: 'none',
|
|
150
|
-
}
|
|
151
|
-
} else if (window.innerWidth >= 1024) {
|
|
152
|
-
style.value = {
|
|
153
|
-
right: '20px',
|
|
154
|
-
top: '72px',
|
|
155
|
-
maxHeight: 'calc(100vh - 100px)',
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
const right = window.innerWidth - rect.left + 8
|
|
159
|
-
style.value = {
|
|
160
|
-
right: Math.min(right, window.innerWidth - 40) + 'px',
|
|
161
|
-
top: '50%',
|
|
162
|
-
transform: 'translateY(-50%)',
|
|
163
|
-
}
|
|
163
|
+
style.value = {
|
|
164
|
+
left: left + 'px',
|
|
165
|
+
top: top + 'px',
|
|
166
|
+
width: cardW + 'px',
|
|
167
|
+
maxHeight: Math.min(vh - 16, 500) + 'px',
|
|
164
168
|
}
|
|
165
169
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
// Horizontal mode: card below/above the annotation
|
|
171
|
+
const cardW = 340
|
|
172
|
+
const gap = 8
|
|
173
|
+
let left = Math.max(8, Math.min(rect.left, vw - cardW - 8))
|
|
174
|
+
let top = rect.bottom + gap
|
|
175
|
+
|
|
176
|
+
// If overflows bottom, show above
|
|
177
|
+
if (top + 200 > vh) {
|
|
178
|
+
top = Math.max(8, rect.top - gap - 200)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
style.value = {
|
|
182
|
+
left: left + 'px',
|
|
183
|
+
top: top + 'px',
|
|
184
|
+
width: cardW + 'px',
|
|
185
|
+
maxHeight: Math.min(vh - top - 16, 500) + 'px',
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
visible.value = true
|
|
@@ -778,16 +778,13 @@ function tcy(n: number): string {
|
|
|
778
778
|
.v-nav-btn {
|
|
779
779
|
writing-mode: vertical-rl;
|
|
780
780
|
text-orientation: mixed;
|
|
781
|
-
|
|
782
|
-
flex-direction: row;
|
|
783
|
-
align-items: center;
|
|
784
|
-
gap: 8px;
|
|
785
|
-
padding: 16px 8px;
|
|
781
|
+
padding: 20px 12px;
|
|
786
782
|
background: var(--surface);
|
|
787
783
|
border: 1px solid var(--border-light);
|
|
788
784
|
border-radius: 6px;
|
|
789
785
|
cursor: pointer;
|
|
790
786
|
transition: all 0.2s;
|
|
787
|
+
line-height: 1.6;
|
|
791
788
|
}
|
|
792
789
|
.v-nav-btn:hover {
|
|
793
790
|
border-color: var(--gold);
|
|
@@ -795,10 +792,12 @@ function tcy(n: number): string {
|
|
|
795
792
|
}
|
|
796
793
|
.v-nav-dir {
|
|
797
794
|
font-size: 16px; color: var(--vermillion);
|
|
795
|
+
margin-bottom: 0.5em;
|
|
798
796
|
}
|
|
799
797
|
.v-nav-label {
|
|
800
798
|
font-size: 11px; color: var(--ink-faint);
|
|
801
799
|
font-family: var(--sans); letter-spacing: 2px;
|
|
800
|
+
margin-bottom: 0.5em;
|
|
802
801
|
}
|
|
803
802
|
.v-nav-title {
|
|
804
803
|
font-size: 18px; font-weight: 700;
|