@hanology/cham-browser 0.4.13 → 0.4.15
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,11 +4,10 @@ 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[]
|
|
10
|
+
headword?: string
|
|
12
11
|
layerLabels?: Record<string, string>
|
|
13
12
|
style?: Record<string, string>
|
|
14
13
|
vertical?: boolean
|
|
@@ -21,25 +20,14 @@ const emit = defineEmits<{
|
|
|
21
20
|
}>()
|
|
22
21
|
|
|
23
22
|
const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
23
|
+
const isMobile = computed(() => ww.value < 768)
|
|
24
24
|
|
|
25
25
|
function onResize() { ww.value = window.innerWidth }
|
|
26
26
|
|
|
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
27
|
const stickyVisible = ref(false)
|
|
35
28
|
|
|
36
29
|
watch(() => props.visible, (v) => {
|
|
37
30
|
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
31
|
})
|
|
44
32
|
|
|
45
33
|
function dismiss() {
|
|
@@ -77,77 +65,81 @@ function kindLabel(ann: Annotation): string {
|
|
|
77
65
|
return map[ann.kind] || ann.kind
|
|
78
66
|
}
|
|
79
67
|
|
|
68
|
+
function dominantKind(): string {
|
|
69
|
+
if (!props.annotations.length) return ''
|
|
70
|
+
const counts: Record<string, number> = {}
|
|
71
|
+
for (const a of props.annotations) {
|
|
72
|
+
counts[a.kind] = (counts[a.kind] || 0) + 1
|
|
73
|
+
}
|
|
74
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]
|
|
75
|
+
}
|
|
76
|
+
|
|
80
77
|
function onDocClick(e: MouseEvent) {
|
|
81
78
|
if (!stickyVisible.value) return
|
|
82
79
|
const el = (e.target as HTMLElement)
|
|
83
|
-
if (el.closest('.ann-
|
|
80
|
+
if (el.closest('.ann-card, .ann-sheet')) return
|
|
84
81
|
if (el.closest('.ann-target')) return
|
|
85
82
|
dismiss()
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
function onDocTouchMove(e: TouchEvent) {
|
|
89
|
-
if (!stickyVisible.value ||
|
|
86
|
+
if (!stickyVisible.value || !isMobile.value) return
|
|
90
87
|
const el = (e.target as HTMLElement)
|
|
91
|
-
if (el.closest('.ann-
|
|
88
|
+
if (el.closest('.ann-sheet')) return
|
|
92
89
|
dismiss()
|
|
93
90
|
}
|
|
94
91
|
|
|
92
|
+
function onKeydown(e: KeyboardEvent) {
|
|
93
|
+
if (e.key === 'Escape' && stickyVisible.value) {
|
|
94
|
+
e.preventDefault()
|
|
95
|
+
dismiss()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
onMounted(() => {
|
|
96
100
|
window.addEventListener('resize', onResize, { passive: true })
|
|
97
101
|
document.addEventListener('click', onDocClick, true)
|
|
98
102
|
document.addEventListener('touchmove', onDocTouchMove, { passive: true })
|
|
103
|
+
document.addEventListener('keydown', onKeydown)
|
|
99
104
|
})
|
|
100
105
|
|
|
101
106
|
onBeforeUnmount(() => {
|
|
102
107
|
window.removeEventListener('resize', onResize)
|
|
103
108
|
document.removeEventListener('click', onDocClick, true)
|
|
104
109
|
document.removeEventListener('touchmove', onDocTouchMove)
|
|
110
|
+
document.removeEventListener('keydown', onKeydown)
|
|
105
111
|
})
|
|
106
112
|
</script>
|
|
107
113
|
|
|
108
114
|
<template>
|
|
109
115
|
<Teleport to="body">
|
|
110
|
-
<!-- Desktop
|
|
111
|
-
<Transition
|
|
116
|
+
<!-- Desktop/Tablet floating card -->
|
|
117
|
+
<Transition name="ann-pop">
|
|
112
118
|
<div
|
|
113
|
-
v-if="
|
|
114
|
-
|
|
119
|
+
v-if="!isMobile && stickyVisible && annotations.length"
|
|
120
|
+
class="ann-card"
|
|
121
|
+
:style="style"
|
|
115
122
|
@mouseenter="emit('tooltipEnter')"
|
|
116
123
|
@mouseleave="emit('tooltipLeave')"
|
|
117
124
|
>
|
|
118
|
-
<
|
|
119
|
-
<
|
|
125
|
+
<div v-if="headword" class="ann-card-head" :class="dominantKind()">
|
|
126
|
+
<span class="ann-headword">{{ headword }}</span>
|
|
127
|
+
<span class="ann-badge-count" v-if="annotations.length > 1">{{ annotations.length }}</span>
|
|
128
|
+
</div>
|
|
129
|
+
<button class="ann-card-close" @click="dismiss" aria-label="關閉">
|
|
130
|
+
<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
131
|
</button>
|
|
121
|
-
<div class="ann-
|
|
122
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-entry"
|
|
123
|
-
<div class="ann-
|
|
132
|
+
<div class="ann-card-scroll">
|
|
133
|
+
<div v-for="ann in annotations" :key="ann.id" class="ann-entry">
|
|
134
|
+
<div class="ann-entry-header">
|
|
124
135
|
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
125
136
|
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
126
137
|
</div>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</div>
|
|
132
|
-
</Transition>
|
|
133
|
-
|
|
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>
|
|
138
|
+
<div class="ann-entry-body">
|
|
139
|
+
<PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
|
|
140
|
+
<div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
|
|
141
|
+
</div>
|
|
148
142
|
</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
143
|
</div>
|
|
152
144
|
</div>
|
|
153
145
|
</Transition>
|
|
@@ -155,20 +147,26 @@ onBeforeUnmount(() => {
|
|
|
155
147
|
<!-- Mobile bottom sheet -->
|
|
156
148
|
<Transition name="ann-sheet">
|
|
157
149
|
<div
|
|
158
|
-
v-if="
|
|
159
|
-
class="ann-
|
|
150
|
+
v-if="isMobile && stickyVisible && annotations.length"
|
|
151
|
+
class="ann-sheet"
|
|
160
152
|
>
|
|
161
153
|
<button class="ann-sheet-handle" @click="dismiss">
|
|
162
154
|
<span class="ann-handle-bar" />
|
|
163
155
|
</button>
|
|
156
|
+
<div v-if="headword" class="ann-sheet-head" :class="dominantKind()">
|
|
157
|
+
<span class="ann-headword">{{ headword }}</span>
|
|
158
|
+
<span class="ann-badge-count" v-if="annotations.length > 1">{{ annotations.length }}</span>
|
|
159
|
+
</div>
|
|
164
160
|
<div class="ann-sheet-scroll">
|
|
165
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-entry"
|
|
166
|
-
<div class="ann-
|
|
161
|
+
<div v-for="ann in annotations" :key="ann.id" class="ann-entry">
|
|
162
|
+
<div class="ann-entry-header">
|
|
167
163
|
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
168
164
|
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
169
165
|
</div>
|
|
170
|
-
<
|
|
171
|
-
|
|
166
|
+
<div class="ann-entry-body">
|
|
167
|
+
<PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
|
|
168
|
+
<div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
|
|
169
|
+
</div>
|
|
172
170
|
</div>
|
|
173
171
|
</div>
|
|
174
172
|
</div>
|
|
@@ -177,7 +175,38 @@ onBeforeUnmount(() => {
|
|
|
177
175
|
</template>
|
|
178
176
|
|
|
179
177
|
<style scoped>
|
|
180
|
-
/* ───
|
|
178
|
+
/* ─── Headword header ─── */
|
|
179
|
+
.ann-card-head,
|
|
180
|
+
.ann-sheet-head {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
padding: 10px 14px;
|
|
185
|
+
border-bottom: 1px solid var(--border-light);
|
|
186
|
+
background: var(--surface);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.ann-headword {
|
|
190
|
+
font-family: var(--serif);
|
|
191
|
+
font-size: 22px;
|
|
192
|
+
font-weight: 900;
|
|
193
|
+
letter-spacing: 4px;
|
|
194
|
+
color: var(--ink);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.ann-badge-count {
|
|
198
|
+
font-family: var(--sans);
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
color: var(--ink-faint);
|
|
202
|
+
background: var(--surface-warm);
|
|
203
|
+
border: 1px solid var(--border-light);
|
|
204
|
+
border-radius: 10px;
|
|
205
|
+
padding: 2px 8px;
|
|
206
|
+
letter-spacing: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ─── Annotation entry ─── */
|
|
181
210
|
.ann-entry {
|
|
182
211
|
padding: 10px 0;
|
|
183
212
|
border-bottom: 1px solid var(--border-light);
|
|
@@ -189,12 +218,15 @@ onBeforeUnmount(() => {
|
|
|
189
218
|
.ann-entry:last-child { border-bottom: none; padding-bottom: 0; }
|
|
190
219
|
.ann-entry:first-child { padding-top: 0; }
|
|
191
220
|
|
|
192
|
-
.ann-
|
|
221
|
+
.ann-entry-header {
|
|
193
222
|
display: inline-flex;
|
|
194
223
|
align-items: center;
|
|
195
224
|
gap: 6px;
|
|
196
|
-
margin-bottom:
|
|
197
|
-
|
|
225
|
+
margin-bottom: 3px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ann-entry-body {
|
|
229
|
+
padding-left: 2px;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
.ann-kind {
|
|
@@ -206,7 +238,6 @@ onBeforeUnmount(() => {
|
|
|
206
238
|
font-weight: 700;
|
|
207
239
|
letter-spacing: 1px;
|
|
208
240
|
line-height: 1.5;
|
|
209
|
-
vertical-align: middle;
|
|
210
241
|
}
|
|
211
242
|
.ann-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
212
243
|
.ann-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
@@ -229,7 +260,6 @@ onBeforeUnmount(() => {
|
|
|
229
260
|
border: 1px solid var(--border-light);
|
|
230
261
|
border-radius: 2px;
|
|
231
262
|
letter-spacing: 0.5px;
|
|
232
|
-
line-height: 1.4;
|
|
233
263
|
}
|
|
234
264
|
|
|
235
265
|
.ann-text {
|
|
@@ -237,128 +267,79 @@ onBeforeUnmount(() => {
|
|
|
237
267
|
line-height: 1.8;
|
|
238
268
|
}
|
|
239
269
|
|
|
240
|
-
/* ───
|
|
241
|
-
.ann-
|
|
270
|
+
/* ─── Floating card ─── */
|
|
271
|
+
.ann-card {
|
|
242
272
|
position: fixed;
|
|
243
|
-
top: 72px;
|
|
244
|
-
left: 20px;
|
|
245
|
-
width: 280px;
|
|
246
|
-
max-height: calc(100vh - 100px);
|
|
247
273
|
background: var(--surface-warm);
|
|
248
274
|
border: 1px solid var(--border);
|
|
249
275
|
border-radius: 10px;
|
|
250
|
-
box-shadow: 0
|
|
276
|
+
box-shadow: 0 12px 48px rgba(var(--shadow-rgb), 0.2), 0 2px 8px rgba(var(--shadow-rgb), 0.06);
|
|
251
277
|
z-index: 1000;
|
|
252
278
|
display: flex;
|
|
253
279
|
flex-direction: column;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
backdrop-filter: blur(8px);
|
|
282
|
+
-webkit-backdrop-filter: blur(8px);
|
|
254
283
|
}
|
|
255
284
|
|
|
256
|
-
.ann-
|
|
285
|
+
.ann-card-close {
|
|
257
286
|
position: absolute;
|
|
258
287
|
top: 8px;
|
|
259
288
|
right: 8px;
|
|
260
|
-
width:
|
|
261
|
-
height:
|
|
289
|
+
width: 22px;
|
|
290
|
+
height: 22px;
|
|
262
291
|
border: none;
|
|
263
292
|
border-radius: 4px;
|
|
264
|
-
background:
|
|
293
|
+
background: transparent;
|
|
265
294
|
color: var(--ink-faint);
|
|
266
295
|
cursor: pointer;
|
|
267
296
|
display: flex;
|
|
268
297
|
align-items: center;
|
|
269
298
|
justify-content: center;
|
|
270
299
|
transition: all 0.15s;
|
|
300
|
+
opacity: 0;
|
|
271
301
|
z-index: 1;
|
|
272
|
-
opacity: 0.6;
|
|
273
302
|
}
|
|
274
|
-
.ann-
|
|
275
|
-
|
|
303
|
+
.ann-card:hover .ann-card-close,
|
|
304
|
+
.ann-card-close:focus-visible {
|
|
305
|
+
opacity: 0.5;
|
|
306
|
+
}
|
|
307
|
+
.ann-card-close:hover {
|
|
308
|
+
opacity: 1 !important;
|
|
276
309
|
background: var(--ink);
|
|
277
310
|
color: var(--paper);
|
|
278
311
|
}
|
|
279
312
|
|
|
280
|
-
.ann-
|
|
281
|
-
padding:
|
|
313
|
+
.ann-card-scroll {
|
|
314
|
+
padding: 10px 14px;
|
|
282
315
|
overflow-y: auto;
|
|
283
316
|
overscroll-behavior: contain;
|
|
284
317
|
flex: 1;
|
|
285
318
|
}
|
|
286
319
|
|
|
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;
|
|
320
|
+
/* Card transition */
|
|
321
|
+
.ann-pop-enter-active {
|
|
322
|
+
transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
318
323
|
}
|
|
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;
|
|
326
|
-
}
|
|
327
|
-
.ann-right-pane .ann-entry:first-child { padding-top: 0; }
|
|
328
|
-
.ann-right-pane .ann-entry:last-child { border-left: none; }
|
|
329
|
-
.ann-right-pane .ann-head {
|
|
330
|
-
flex-direction: column;
|
|
331
|
-
align-items: flex-start;
|
|
332
|
-
margin-bottom: 0;
|
|
333
|
-
margin-left: 4px;
|
|
324
|
+
.ann-pop-leave-active {
|
|
325
|
+
transition: opacity 0.1s ease, transform 0.1s ease;
|
|
334
326
|
}
|
|
335
|
-
.ann-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
line-height: 2;
|
|
327
|
+
.ann-pop-enter-from {
|
|
328
|
+
opacity: 0;
|
|
329
|
+
transform: scale(0.96) translateY(4px);
|
|
339
330
|
}
|
|
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;
|
|
331
|
+
.ann-pop-leave-to {
|
|
332
|
+
opacity: 0;
|
|
333
|
+
transform: scale(0.98);
|
|
353
334
|
}
|
|
354
335
|
|
|
355
|
-
/* ─── Mobile
|
|
356
|
-
.ann-
|
|
336
|
+
/* ─── Mobile bottom sheet ─── */
|
|
337
|
+
.ann-sheet {
|
|
357
338
|
position: fixed;
|
|
358
339
|
left: 0;
|
|
359
340
|
right: 0;
|
|
360
341
|
bottom: 0;
|
|
361
|
-
max-height:
|
|
342
|
+
max-height: 60vh;
|
|
362
343
|
background: var(--surface-warm);
|
|
363
344
|
border-top: 1px solid var(--border);
|
|
364
345
|
border-radius: 14px 14px 0 0;
|
|
@@ -394,53 +375,7 @@ onBeforeUnmount(() => {
|
|
|
394
375
|
flex: 1;
|
|
395
376
|
}
|
|
396
377
|
|
|
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
|
-
|
|
378
|
+
/* Sheet transition */
|
|
444
379
|
.ann-sheet-enter-active {
|
|
445
380
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
446
381
|
}
|
|
@@ -452,11 +387,22 @@ onBeforeUnmount(() => {
|
|
|
452
387
|
transform: translateY(100%);
|
|
453
388
|
}
|
|
454
389
|
|
|
455
|
-
|
|
456
|
-
|
|
390
|
+
/* ─── Active annotation on page ─── */
|
|
391
|
+
:global(.ann-target.ann-active) {
|
|
392
|
+
background: rgba(194, 58, 43, 0.12) !important;
|
|
393
|
+
box-shadow: 0 0 0 2px rgba(194, 58, 43, 0.15);
|
|
394
|
+
border-radius: 2px;
|
|
395
|
+
}
|
|
396
|
+
:global(.ann-target.ann-active.pronunciation) {
|
|
397
|
+
background: rgba(58, 107, 94, 0.12) !important;
|
|
398
|
+
box-shadow: 0 0 0 2px rgba(58, 107, 94, 0.15);
|
|
399
|
+
}
|
|
400
|
+
:global(.ann-target.ann-active.person) {
|
|
401
|
+
background: rgba(58, 90, 140, 0.12) !important;
|
|
402
|
+
box-shadow: 0 0 0 2px rgba(58, 90, 140, 0.15);
|
|
457
403
|
}
|
|
458
404
|
|
|
459
|
-
@media (min-width:
|
|
460
|
-
.ann-
|
|
405
|
+
@media (min-width: 768px) {
|
|
406
|
+
.ann-sheet { display: none; }
|
|
461
407
|
}
|
|
462
408
|
</style>
|
|
@@ -8,19 +8,28 @@ function isMobile(): boolean {
|
|
|
8
8
|
export function useAnnotationInteraction() {
|
|
9
9
|
const tooltip = useAnnotationTooltip()
|
|
10
10
|
let hideTimer: ReturnType<typeof setTimeout> | null = null
|
|
11
|
+
let activeEl: HTMLElement | null = null
|
|
11
12
|
|
|
12
13
|
function cancelHide() {
|
|
13
14
|
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null }
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function setActive(el: HTMLElement | null) {
|
|
18
|
+
if (activeEl) activeEl.classList.remove('ann-active')
|
|
19
|
+
activeEl = el
|
|
20
|
+
if (activeEl) activeEl.classList.add('ann-active')
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
function scheduleHide(delay = 300) {
|
|
17
24
|
cancelHide()
|
|
18
|
-
hideTimer = setTimeout(() => { tooltip.hide(); hideTimer = null }, delay)
|
|
25
|
+
hideTimer = setTimeout(() => { tooltip.hide(); setActive(null); hideTimer = null }, delay)
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
function onHover(event: MouseEvent, annotations: Annotation[]) {
|
|
22
29
|
if (isMobile()) return
|
|
23
30
|
cancelHide()
|
|
31
|
+
const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
|
|
32
|
+
setActive(el)
|
|
24
33
|
tooltip.show(event, annotations)
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -30,6 +39,8 @@ export function useAnnotationInteraction() {
|
|
|
30
39
|
|
|
31
40
|
function onTap(event: MouseEvent, annotations: Annotation[]) {
|
|
32
41
|
cancelHide()
|
|
42
|
+
const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
|
|
43
|
+
setActive(el)
|
|
33
44
|
tooltip.toggle(event, annotations)
|
|
34
45
|
}
|
|
35
46
|
|
|
@@ -43,12 +54,14 @@ export function useAnnotationInteraction() {
|
|
|
43
54
|
|
|
44
55
|
function dismiss() {
|
|
45
56
|
cancelHide()
|
|
57
|
+
setActive(null)
|
|
46
58
|
tooltip.hide()
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
return {
|
|
50
62
|
visible: tooltip.visible,
|
|
51
63
|
items: tooltip.items,
|
|
64
|
+
headword: tooltip.headword,
|
|
52
65
|
style: tooltip.style,
|
|
53
66
|
onHover,
|
|
54
67
|
onLeave,
|
|
@@ -132,6 +132,7 @@ export function useAnnotationTooltip() {
|
|
|
132
132
|
const visible = ref(false)
|
|
133
133
|
const items = ref<Annotation[]>([])
|
|
134
134
|
const style = ref<Record<string, string>>({})
|
|
135
|
+
const headword = ref('')
|
|
135
136
|
const { layout } = useReadingMode()
|
|
136
137
|
|
|
137
138
|
function show(event: MouseEvent, annotations: Annotation[]) {
|
|
@@ -139,51 +140,66 @@ export function useAnnotationTooltip() {
|
|
|
139
140
|
const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
|
|
140
141
|
const rect = (el ?? event.target as HTMLElement).getBoundingClientRect()
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
143
|
+
// Extract headword text (strip the annotation number)
|
|
144
|
+
if (el) {
|
|
145
|
+
const clone = el.cloneNode(true) as HTMLElement
|
|
146
|
+
const nums = clone.querySelectorAll('.ann-num, sup')
|
|
147
|
+
nums.forEach(n => n.remove())
|
|
148
|
+
headword.value = clone.textContent?.trim() || ''
|
|
149
|
+
} else {
|
|
150
|
+
headword.value = ''
|
|
151
|
+
}
|
|
152
|
+
const vw = window.innerWidth
|
|
153
|
+
const vh = window.innerHeight
|
|
154
|
+
|
|
155
|
+
if (vw < 768) {
|
|
156
|
+
// Mobile: bottom sheet
|
|
157
|
+
style.value = {
|
|
158
|
+
left: '0',
|
|
159
|
+
right: '0',
|
|
160
|
+
bottom: '0',
|
|
161
|
+
}
|
|
162
|
+
} else if (layout.value === 'vertical') {
|
|
163
|
+
// Vertical mode: card to the left of the annotation
|
|
164
|
+
const cardW = 340
|
|
165
|
+
const gap = 12
|
|
166
|
+
let left = Math.max(8, rect.left - cardW - gap)
|
|
167
|
+
let top = Math.max(8, Math.min(rect.top, vh - 300))
|
|
168
|
+
|
|
169
|
+
// If not enough room on left, go right
|
|
170
|
+
if (rect.left - cardW - gap < 8) {
|
|
171
|
+
left = Math.min(rect.right + gap, vw - cardW - 8)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
style.value = {
|
|
175
|
+
left: left + 'px',
|
|
176
|
+
top: top + 'px',
|
|
177
|
+
width: cardW + 'px',
|
|
178
|
+
maxHeight: Math.min(vh - 16, 500) + 'px',
|
|
164
179
|
}
|
|
165
180
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
// Horizontal mode: card below/above the annotation
|
|
182
|
+
const cardW = 340
|
|
183
|
+
const gap = 8
|
|
184
|
+
let left = Math.max(8, Math.min(rect.left, vw - cardW - 8))
|
|
185
|
+
let top = rect.bottom + gap
|
|
186
|
+
|
|
187
|
+
// If overflows bottom, show above
|
|
188
|
+
if (top + 200 > vh) {
|
|
189
|
+
top = Math.max(8, rect.top - gap - 200)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
style.value = {
|
|
193
|
+
left: left + 'px',
|
|
194
|
+
top: top + 'px',
|
|
195
|
+
width: cardW + 'px',
|
|
196
|
+
maxHeight: Math.min(vh - top - 16, 500) + 'px',
|
|
181
197
|
}
|
|
182
198
|
}
|
|
183
199
|
visible.value = true
|
|
184
200
|
}
|
|
185
201
|
|
|
186
|
-
function hide() { visible.value = false }
|
|
202
|
+
function hide() { visible.value = false; headword.value = '' }
|
|
187
203
|
function toggle(event: MouseEvent, annotations: Annotation[]) {
|
|
188
204
|
if (visible.value) {
|
|
189
205
|
const currentIds = items.value.map(a => a.id).sort().join(',')
|
|
@@ -198,5 +214,5 @@ export function useAnnotationTooltip() {
|
|
|
198
214
|
}
|
|
199
215
|
}
|
|
200
216
|
|
|
201
|
-
return { visible, items, style, show, hide, toggle }
|
|
217
|
+
return { visible, items, style, headword, show, hide, toggle }
|
|
202
218
|
}
|
|
@@ -434,6 +434,7 @@ function tcy(n: number): string {
|
|
|
434
434
|
<AnnotationTooltip
|
|
435
435
|
:visible="interaction.visible"
|
|
436
436
|
:annotations="interaction.items"
|
|
437
|
+
:headword="interaction.headword"
|
|
437
438
|
:layer-labels="layerLabels"
|
|
438
439
|
:style="interaction.style"
|
|
439
440
|
:vertical="true"
|
|
@@ -441,10 +442,6 @@ function tcy(n: number): string {
|
|
|
441
442
|
@tooltip-enter="interaction.onTooltipEnter"
|
|
442
443
|
@tooltip-leave="interaction.onTooltipLeave"
|
|
443
444
|
/>
|
|
444
|
-
|
|
445
|
-
<Teleport to="body">
|
|
446
|
-
<Transition name="overlay">
|
|
447
|
-
<div v-if="authorPaneOpen" class="v-overlay" @click="closeAuthorPane">
|
|
448
445
|
<div class="v-author-pane" @click.stop>
|
|
449
446
|
<button class="v-pane-close" @click="closeAuthorPane">✕</button>
|
|
450
447
|
<div class="v-pane-header">
|
|
@@ -602,6 +599,7 @@ function tcy(n: number): string {
|
|
|
602
599
|
<AnnotationTooltip
|
|
603
600
|
:visible="interaction.visible"
|
|
604
601
|
:annotations="interaction.items"
|
|
602
|
+
:headword="interaction.headword"
|
|
605
603
|
:layer-labels="layerLabels"
|
|
606
604
|
:style="interaction.style"
|
|
607
605
|
:vertical="false"
|