@hanology/cham-browser 0.4.14 → 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
|
@@ -7,6 +7,7 @@ import type { Annotation } from '../types'
|
|
|
7
7
|
const props = defineProps<{
|
|
8
8
|
visible: boolean
|
|
9
9
|
annotations: Annotation[]
|
|
10
|
+
headword?: string
|
|
10
11
|
layerLabels?: Record<string, string>
|
|
11
12
|
style?: Record<string, string>
|
|
12
13
|
vertical?: boolean
|
|
@@ -64,6 +65,15 @@ function kindLabel(ann: Annotation): string {
|
|
|
64
65
|
return map[ann.kind] || ann.kind
|
|
65
66
|
}
|
|
66
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
|
+
|
|
67
77
|
function onDocClick(e: MouseEvent) {
|
|
68
78
|
if (!stickyVisible.value) return
|
|
69
79
|
const el = (e.target as HTMLElement)
|
|
@@ -79,16 +89,25 @@ function onDocTouchMove(e: TouchEvent) {
|
|
|
79
89
|
dismiss()
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
function onKeydown(e: KeyboardEvent) {
|
|
93
|
+
if (e.key === 'Escape' && stickyVisible.value) {
|
|
94
|
+
e.preventDefault()
|
|
95
|
+
dismiss()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
onMounted(() => {
|
|
83
100
|
window.addEventListener('resize', onResize, { passive: true })
|
|
84
101
|
document.addEventListener('click', onDocClick, true)
|
|
85
102
|
document.addEventListener('touchmove', onDocTouchMove, { passive: true })
|
|
103
|
+
document.addEventListener('keydown', onKeydown)
|
|
86
104
|
})
|
|
87
105
|
|
|
88
106
|
onBeforeUnmount(() => {
|
|
89
107
|
window.removeEventListener('resize', onResize)
|
|
90
108
|
document.removeEventListener('click', onDocClick, true)
|
|
91
109
|
document.removeEventListener('touchmove', onDocTouchMove)
|
|
110
|
+
document.removeEventListener('keydown', onKeydown)
|
|
92
111
|
})
|
|
93
112
|
</script>
|
|
94
113
|
|
|
@@ -103,17 +122,23 @@ onBeforeUnmount(() => {
|
|
|
103
122
|
@mouseenter="emit('tooltipEnter')"
|
|
104
123
|
@mouseleave="emit('tooltipLeave')"
|
|
105
124
|
>
|
|
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>
|
|
106
129
|
<button class="ann-card-close" @click="dismiss" aria-label="關閉">
|
|
107
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>
|
|
108
131
|
</button>
|
|
109
132
|
<div class="ann-card-scroll">
|
|
110
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-entry"
|
|
111
|
-
<div class="ann-
|
|
133
|
+
<div v-for="ann in annotations" :key="ann.id" class="ann-entry">
|
|
134
|
+
<div class="ann-entry-header">
|
|
112
135
|
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
113
136
|
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
114
137
|
</div>
|
|
115
|
-
<
|
|
116
|
-
|
|
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>
|
|
117
142
|
</div>
|
|
118
143
|
</div>
|
|
119
144
|
</div>
|
|
@@ -128,14 +153,20 @@ onBeforeUnmount(() => {
|
|
|
128
153
|
<button class="ann-sheet-handle" @click="dismiss">
|
|
129
154
|
<span class="ann-handle-bar" />
|
|
130
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>
|
|
131
160
|
<div class="ann-sheet-scroll">
|
|
132
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-entry"
|
|
133
|
-
<div class="ann-
|
|
161
|
+
<div v-for="ann in annotations" :key="ann.id" class="ann-entry">
|
|
162
|
+
<div class="ann-entry-header">
|
|
134
163
|
<span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
|
|
135
164
|
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
136
165
|
</div>
|
|
137
|
-
<
|
|
138
|
-
|
|
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>
|
|
139
170
|
</div>
|
|
140
171
|
</div>
|
|
141
172
|
</div>
|
|
@@ -144,7 +175,38 @@ onBeforeUnmount(() => {
|
|
|
144
175
|
</template>
|
|
145
176
|
|
|
146
177
|
<style scoped>
|
|
147
|
-
/* ───
|
|
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 ─── */
|
|
148
210
|
.ann-entry {
|
|
149
211
|
padding: 10px 0;
|
|
150
212
|
border-bottom: 1px solid var(--border-light);
|
|
@@ -156,11 +218,15 @@ onBeforeUnmount(() => {
|
|
|
156
218
|
.ann-entry:last-child { border-bottom: none; padding-bottom: 0; }
|
|
157
219
|
.ann-entry:first-child { padding-top: 0; }
|
|
158
220
|
|
|
159
|
-
.ann-
|
|
221
|
+
.ann-entry-header {
|
|
160
222
|
display: inline-flex;
|
|
161
223
|
align-items: center;
|
|
162
224
|
gap: 6px;
|
|
163
|
-
margin-bottom:
|
|
225
|
+
margin-bottom: 3px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ann-entry-body {
|
|
229
|
+
padding-left: 2px;
|
|
164
230
|
}
|
|
165
231
|
|
|
166
232
|
.ann-kind {
|
|
@@ -201,23 +267,25 @@ onBeforeUnmount(() => {
|
|
|
201
267
|
line-height: 1.8;
|
|
202
268
|
}
|
|
203
269
|
|
|
204
|
-
/* ───
|
|
270
|
+
/* ─── Floating card ─── */
|
|
205
271
|
.ann-card {
|
|
206
272
|
position: fixed;
|
|
207
273
|
background: var(--surface-warm);
|
|
208
274
|
border: 1px solid var(--border);
|
|
209
275
|
border-radius: 10px;
|
|
210
|
-
box-shadow: 0 12px
|
|
276
|
+
box-shadow: 0 12px 48px rgba(var(--shadow-rgb), 0.2), 0 2px 8px rgba(var(--shadow-rgb), 0.06);
|
|
211
277
|
z-index: 1000;
|
|
212
278
|
display: flex;
|
|
213
279
|
flex-direction: column;
|
|
214
280
|
overflow: hidden;
|
|
281
|
+
backdrop-filter: blur(8px);
|
|
282
|
+
-webkit-backdrop-filter: blur(8px);
|
|
215
283
|
}
|
|
216
284
|
|
|
217
285
|
.ann-card-close {
|
|
218
286
|
position: absolute;
|
|
219
|
-
top:
|
|
220
|
-
right:
|
|
287
|
+
top: 8px;
|
|
288
|
+
right: 8px;
|
|
221
289
|
width: 22px;
|
|
222
290
|
height: 22px;
|
|
223
291
|
border: none;
|
|
@@ -229,17 +297,21 @@ onBeforeUnmount(() => {
|
|
|
229
297
|
align-items: center;
|
|
230
298
|
justify-content: center;
|
|
231
299
|
transition: all 0.15s;
|
|
232
|
-
opacity: 0
|
|
300
|
+
opacity: 0;
|
|
233
301
|
z-index: 1;
|
|
234
302
|
}
|
|
303
|
+
.ann-card:hover .ann-card-close,
|
|
304
|
+
.ann-card-close:focus-visible {
|
|
305
|
+
opacity: 0.5;
|
|
306
|
+
}
|
|
235
307
|
.ann-card-close:hover {
|
|
236
|
-
opacity: 1;
|
|
308
|
+
opacity: 1 !important;
|
|
237
309
|
background: var(--ink);
|
|
238
310
|
color: var(--paper);
|
|
239
311
|
}
|
|
240
312
|
|
|
241
313
|
.ann-card-scroll {
|
|
242
|
-
padding:
|
|
314
|
+
padding: 10px 14px;
|
|
243
315
|
overflow-y: auto;
|
|
244
316
|
overscroll-behavior: contain;
|
|
245
317
|
flex: 1;
|
|
@@ -247,18 +319,18 @@ onBeforeUnmount(() => {
|
|
|
247
319
|
|
|
248
320
|
/* Card transition */
|
|
249
321
|
.ann-pop-enter-active {
|
|
250
|
-
transition: opacity 0.15s ease, transform 0.
|
|
322
|
+
transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
251
323
|
}
|
|
252
324
|
.ann-pop-leave-active {
|
|
253
|
-
transition: opacity 0.
|
|
325
|
+
transition: opacity 0.1s ease, transform 0.1s ease;
|
|
254
326
|
}
|
|
255
327
|
.ann-pop-enter-from {
|
|
256
328
|
opacity: 0;
|
|
257
|
-
transform: scale(0.
|
|
329
|
+
transform: scale(0.96) translateY(4px);
|
|
258
330
|
}
|
|
259
331
|
.ann-pop-leave-to {
|
|
260
332
|
opacity: 0;
|
|
261
|
-
transform: scale(0.
|
|
333
|
+
transform: scale(0.98);
|
|
262
334
|
}
|
|
263
335
|
|
|
264
336
|
/* ─── Mobile bottom sheet ─── */
|
|
@@ -267,7 +339,7 @@ onBeforeUnmount(() => {
|
|
|
267
339
|
left: 0;
|
|
268
340
|
right: 0;
|
|
269
341
|
bottom: 0;
|
|
270
|
-
max-height:
|
|
342
|
+
max-height: 60vh;
|
|
271
343
|
background: var(--surface-warm);
|
|
272
344
|
border-top: 1px solid var(--border);
|
|
273
345
|
border-radius: 14px 14px 0 0;
|
|
@@ -315,6 +387,21 @@ onBeforeUnmount(() => {
|
|
|
315
387
|
transform: translateY(100%);
|
|
316
388
|
}
|
|
317
389
|
|
|
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);
|
|
403
|
+
}
|
|
404
|
+
|
|
318
405
|
@media (min-width: 768px) {
|
|
319
406
|
.ann-sheet { display: none; }
|
|
320
407
|
}
|
|
@@ -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,12 +132,23 @@ 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[]) {
|
|
138
139
|
items.value = annotations
|
|
139
140
|
const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
|
|
140
141
|
const rect = (el ?? event.target as HTMLElement).getBoundingClientRect()
|
|
142
|
+
|
|
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
|
+
}
|
|
141
152
|
const vw = window.innerWidth
|
|
142
153
|
const vh = window.innerHeight
|
|
143
154
|
|
|
@@ -188,7 +199,7 @@ export function useAnnotationTooltip() {
|
|
|
188
199
|
visible.value = true
|
|
189
200
|
}
|
|
190
201
|
|
|
191
|
-
function hide() { visible.value = false }
|
|
202
|
+
function hide() { visible.value = false; headword.value = '' }
|
|
192
203
|
function toggle(event: MouseEvent, annotations: Annotation[]) {
|
|
193
204
|
if (visible.value) {
|
|
194
205
|
const currentIds = items.value.map(a => a.id).sort().join(',')
|
|
@@ -203,5 +214,5 @@ export function useAnnotationTooltip() {
|
|
|
203
214
|
}
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
return { visible, items, style, show, hide, toggle }
|
|
217
|
+
return { visible, items, style, headword, show, hide, toggle }
|
|
207
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"
|