@hanology/cham-browser 0.4.63 → 0.4.65
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 +1 -1
- package/template/index.html +1 -1
- package/template/src/App.vue +7 -0
- package/template/src/components/AnnotationPane.vue +11 -11
- package/template/src/components/AnnotationTooltip.vue +45 -17
- package/template/src/components/BackToTop.vue +3 -3
- package/template/src/components/HorizontalDisplay.vue +18 -5
- package/template/src/components/PoemCard.vue +2 -2
- package/template/src/components/PronunciationGroup.vue +13 -3
- package/template/src/components/ReadingToolbar.vue +4 -3
- package/template/src/components/SectionBlock.vue +12 -3
- package/template/src/components/SideNav.vue +4 -4
- package/template/src/components/VerticalScroll.vue +19 -6
- package/template/src/composables/useI18n.ts +10 -2
- package/template/src/composables/useReadingMode.ts +0 -7
- package/template/src/styles/annotation-targets.css +26 -14
- package/template/src/styles/main.css +12 -2
- package/template/src/utils/annotationLabels.ts +3 -5
- package/template/src/views/AuthorView.vue +2 -3
- package/template/src/views/BookHome.vue +21 -1
- package/template/src/views/PieceView.vue +9 -9
package/package.json
CHANGED
package/template/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="zh-Hant">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<title>CHAM</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
package/template/src/App.vue
CHANGED
|
@@ -146,7 +146,7 @@ onBeforeUnmount(() => {
|
|
|
146
146
|
<Teleport to="body">
|
|
147
147
|
<Transition name="ann-dim">
|
|
148
148
|
<div
|
|
149
|
-
v-if="visible && annotations.length && vertical &&
|
|
149
|
+
v-if="visible && annotations.length && vertical && ww < 768"
|
|
150
150
|
class="ann-pane-dim"
|
|
151
151
|
@click="emit('close')"
|
|
152
152
|
/>
|
|
@@ -341,18 +341,18 @@ onBeforeUnmount(() => {
|
|
|
341
341
|
line-height: 1.5;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
.ann-pane-kind.pronunciation { background: var(--jade); color:
|
|
345
|
-
.ann-pane-kind.semantic { background: var(--vermillion); color:
|
|
346
|
-
.ann-pane-kind.etymology { background: var(--ann-etymology); color:
|
|
344
|
+
.ann-pane-kind.pronunciation { background: var(--jade); color: var(--paper); }
|
|
345
|
+
.ann-pane-kind.semantic { background: var(--vermillion); color: var(--paper); }
|
|
346
|
+
.ann-pane-kind.etymology { background: var(--ann-etymology); color: var(--paper); }
|
|
347
347
|
.ann-pane-kind.note,
|
|
348
348
|
.ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
|
|
349
|
-
.ann-pane-kind.commentary { background: var(--ann-commentary); color:
|
|
350
|
-
.ann-pane-kind.translation { background: var(--ann-translation); color:
|
|
351
|
-
.ann-pane-kind.person { background: var(--ann-person); color:
|
|
352
|
-
.ann-pane-kind.place { background: var(--ann-place); color:
|
|
353
|
-
.ann-pane-kind.event { background: var(--ann-event); color:
|
|
354
|
-
.ann-pane-kind.date { background: var(--ann-date); color:
|
|
355
|
-
.ann-pane-kind.allusion { background: var(--ann-allusion); color:
|
|
349
|
+
.ann-pane-kind.commentary { background: var(--ann-commentary); color: var(--paper); }
|
|
350
|
+
.ann-pane-kind.translation { background: var(--ann-translation); color: var(--paper); }
|
|
351
|
+
.ann-pane-kind.person { background: var(--ann-person); color: var(--paper); }
|
|
352
|
+
.ann-pane-kind.place { background: var(--ann-place); color: var(--paper); }
|
|
353
|
+
.ann-pane-kind.event { background: var(--ann-event); color: var(--paper); }
|
|
354
|
+
.ann-pane-kind.date { background: var(--ann-date); color: var(--paper); }
|
|
355
|
+
.ann-pane-kind.allusion { background: var(--ann-allusion); color: var(--paper); }
|
|
356
356
|
|
|
357
357
|
.ann-pane-layer {
|
|
358
358
|
font-size: 10px;
|
|
@@ -231,18 +231,18 @@ onBeforeUnmount(() => {
|
|
|
231
231
|
letter-spacing: 1px;
|
|
232
232
|
line-height: 1.5;
|
|
233
233
|
}
|
|
234
|
-
.ann-kind.pronunciation { background: var(--jade); color:
|
|
235
|
-
.ann-kind.semantic { background: var(--vermillion); color:
|
|
236
|
-
.ann-kind.etymology { background: var(--ann-etymology); color:
|
|
234
|
+
.ann-kind.pronunciation { background: var(--jade); color: var(--paper); }
|
|
235
|
+
.ann-kind.semantic { background: var(--vermillion); color: var(--paper); }
|
|
236
|
+
.ann-kind.etymology { background: var(--ann-etymology); color: var(--paper); }
|
|
237
237
|
.ann-kind.note,
|
|
238
238
|
.ann-kind.definition { background: var(--ink); color: var(--paper); }
|
|
239
|
-
.ann-kind.commentary { background: var(--ann-commentary); color:
|
|
240
|
-
.ann-kind.translation { background: var(--ann-translation); color:
|
|
241
|
-
.ann-kind.person { background: var(--ann-person); color:
|
|
242
|
-
.ann-kind.place { background: var(--ann-place); color:
|
|
243
|
-
.ann-kind.event { background: var(--ann-event); color:
|
|
244
|
-
.ann-kind.date { background: var(--ann-date); color:
|
|
245
|
-
.ann-kind.allusion { background: var(--ann-allusion); color:
|
|
239
|
+
.ann-kind.commentary { background: var(--ann-commentary); color: var(--paper); }
|
|
240
|
+
.ann-kind.translation { background: var(--ann-translation); color: var(--paper); }
|
|
241
|
+
.ann-kind.person { background: var(--ann-person); color: var(--paper); }
|
|
242
|
+
.ann-kind.place { background: var(--ann-place); color: var(--paper); }
|
|
243
|
+
.ann-kind.event { background: var(--ann-event); color: var(--paper); }
|
|
244
|
+
.ann-kind.date { background: var(--ann-date); color: var(--paper); }
|
|
245
|
+
.ann-kind.allusion { background: var(--ann-allusion); color: var(--paper); }
|
|
246
246
|
|
|
247
247
|
.ann-layer {
|
|
248
248
|
font-size: 10px;
|
|
@@ -364,7 +364,7 @@ onBeforeUnmount(() => {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
.ann-sheet-scroll {
|
|
367
|
-
padding: 4px 16px 24px;
|
|
367
|
+
padding: 4px 16px max(24px, env(safe-area-inset-bottom, 0px));
|
|
368
368
|
overflow-y: auto;
|
|
369
369
|
overscroll-behavior: contain;
|
|
370
370
|
flex: 1;
|
|
@@ -384,17 +384,45 @@ onBeforeUnmount(() => {
|
|
|
384
384
|
|
|
385
385
|
/* ─── Active annotation on page ─── */
|
|
386
386
|
:global(.ann-target.ann-active) {
|
|
387
|
-
background:
|
|
388
|
-
box-shadow: 0 0 0 2px
|
|
387
|
+
background: color-mix(in srgb, var(--vermillion) 12%, transparent) !important;
|
|
388
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vermillion) 15%, transparent);
|
|
389
389
|
border-radius: 2px;
|
|
390
390
|
}
|
|
391
391
|
:global(.ann-target.ann-active.pronunciation) {
|
|
392
|
-
background:
|
|
393
|
-
box-shadow: 0 0 0 2px
|
|
392
|
+
background: color-mix(in srgb, var(--jade) 12%, transparent) !important;
|
|
393
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--jade) 15%, transparent);
|
|
394
394
|
}
|
|
395
395
|
:global(.ann-target.ann-active.person) {
|
|
396
|
-
background:
|
|
397
|
-
box-shadow: 0 0 0 2px
|
|
396
|
+
background: color-mix(in srgb, var(--ann-person) 12%, transparent) !important;
|
|
397
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-person) 15%, transparent);
|
|
398
|
+
}
|
|
399
|
+
:global(.ann-target.ann-active.place) {
|
|
400
|
+
background: color-mix(in srgb, var(--ann-place) 12%, transparent) !important;
|
|
401
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-place) 15%, transparent);
|
|
402
|
+
}
|
|
403
|
+
:global(.ann-target.ann-active.event) {
|
|
404
|
+
background: color-mix(in srgb, var(--ann-event) 12%, transparent) !important;
|
|
405
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-event) 15%, transparent);
|
|
406
|
+
}
|
|
407
|
+
:global(.ann-target.ann-active.date) {
|
|
408
|
+
background: color-mix(in srgb, var(--ann-date) 12%, transparent) !important;
|
|
409
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-date) 15%, transparent);
|
|
410
|
+
}
|
|
411
|
+
:global(.ann-target.ann-active.allusion) {
|
|
412
|
+
background: color-mix(in srgb, var(--ann-allusion) 12%, transparent) !important;
|
|
413
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-allusion) 15%, transparent);
|
|
414
|
+
}
|
|
415
|
+
:global(.ann-target.ann-active.etymology) {
|
|
416
|
+
background: color-mix(in srgb, var(--ann-etymology) 12%, transparent) !important;
|
|
417
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-etymology) 15%, transparent);
|
|
418
|
+
}
|
|
419
|
+
:global(.ann-target.ann-active.commentary) {
|
|
420
|
+
background: color-mix(in srgb, var(--ann-commentary) 12%, transparent) !important;
|
|
421
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-commentary) 15%, transparent);
|
|
422
|
+
}
|
|
423
|
+
:global(.ann-target.ann-active.translation) {
|
|
424
|
+
background: color-mix(in srgb, var(--ann-translation) 12%, transparent) !important;
|
|
425
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ann-translation) 15%, transparent);
|
|
398
426
|
}
|
|
399
427
|
|
|
400
428
|
@media (min-width: 768px) {
|
|
@@ -61,7 +61,7 @@ onUnmounted(detach)
|
|
|
61
61
|
|
|
62
62
|
<template>
|
|
63
63
|
<Transition name="btt">
|
|
64
|
-
<button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" :aria-label="vertical ? t('action.backToStart') : t('action.
|
|
64
|
+
<button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" :aria-label="vertical ? t('action.backToStart') : t('action.backToTop')">
|
|
65
65
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
66
66
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
67
67
|
</svg>
|
|
@@ -72,7 +72,7 @@ onUnmounted(detach)
|
|
|
72
72
|
<style scoped>
|
|
73
73
|
.btt {
|
|
74
74
|
position: fixed;
|
|
75
|
-
bottom: 80px;
|
|
75
|
+
bottom: max(80px, calc(72px + env(safe-area-inset-bottom, 0px)));
|
|
76
76
|
right: 24px;
|
|
77
77
|
width: 40px;
|
|
78
78
|
height: 40px;
|
|
@@ -98,7 +98,7 @@ onUnmounted(detach)
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
@media (max-width: 768px) {
|
|
101
|
-
.btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
|
|
101
|
+
.btt { bottom: max(88px, calc(80px + env(safe-area-inset-bottom, 0px))); right: 16px; width: 36px; height: 36px; }
|
|
102
102
|
}
|
|
103
103
|
.btt:hover {
|
|
104
104
|
background: var(--vermillion);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import type { Annotation, VerseLine } from '../types'
|
|
3
|
-
import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations
|
|
4
|
+
import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations } from '../composables/useAnnotationRenderer'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
title: string
|
|
@@ -15,11 +16,23 @@ const emit = defineEmits<{
|
|
|
15
16
|
annotationTap: [event: MouseEvent, annotations: Annotation[]]
|
|
16
17
|
}>()
|
|
17
18
|
|
|
19
|
+
const allVerseSpans = computed(() =>
|
|
20
|
+
props.verses.map((_, i) => buildVerseAnnotations(props.annotations, i))
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const verseOffsets = computed(() => {
|
|
24
|
+
const offsets: number[] = []
|
|
25
|
+
let acc = 0
|
|
26
|
+
for (const spans of allVerseSpans.value) {
|
|
27
|
+
offsets.push(acc)
|
|
28
|
+
acc += spans.length
|
|
29
|
+
}
|
|
30
|
+
return offsets
|
|
31
|
+
})
|
|
32
|
+
|
|
18
33
|
function verseHtml(index: number): string {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const spans = buildVerseAnnotations(props.annotations, index)
|
|
22
|
-
return renderAnnotatedText(props.verses[index].text, spans, false, offset)
|
|
34
|
+
const spans = allVerseSpans.value[index]
|
|
35
|
+
return renderAnnotatedText(props.verses[index].text, spans, false, verseOffsets.value[index])
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
function onHover(event: MouseEvent) {
|
|
@@ -97,8 +97,8 @@ const preview = computed(() => {
|
|
|
97
97
|
box-sizing: border-box;
|
|
98
98
|
overflow: hidden;
|
|
99
99
|
height: auto;
|
|
100
|
-
-webkit-mask-image: linear-gradient(to left, black
|
|
101
|
-
mask-image: linear-gradient(to left, black
|
|
100
|
+
-webkit-mask-image: linear-gradient(to left, black 80%, transparent);
|
|
101
|
+
mask-image: linear-gradient(to left, black 80%, transparent);
|
|
102
102
|
}
|
|
103
103
|
.pc-vertical .pc-num {
|
|
104
104
|
font-size: 11px;
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import type { PronSegment } from '../types'
|
|
4
|
+
import { useI18n } from '../composables/useI18n'
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
const { t } = useI18n()
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
5
9
|
segment: PronSegment
|
|
6
10
|
}>()
|
|
11
|
+
|
|
12
|
+
const label = computed(() => {
|
|
13
|
+
if (props.segment.lang === 'yue') return t('pron.yue')
|
|
14
|
+
if (props.segment.lang === 'cmn') return t('pron.cmn')
|
|
15
|
+
return props.segment.label
|
|
16
|
+
})
|
|
7
17
|
</script>
|
|
8
18
|
|
|
9
19
|
<template>
|
|
10
20
|
<span class="pron-group">
|
|
11
21
|
<span class="pron-badge" :class="segment.lang === 'yue' ? 'pron-yue' : 'pron-cmn'">
|
|
12
|
-
{{
|
|
22
|
+
{{ label }}
|
|
13
23
|
</span>
|
|
14
24
|
<span class="pron-text">{{ segment.parts.join(' ') }}</span>
|
|
15
25
|
</span>
|
|
@@ -39,7 +49,7 @@ defineProps<{
|
|
|
39
49
|
}
|
|
40
50
|
.pron-yue {
|
|
41
51
|
background: var(--jade);
|
|
42
|
-
color:
|
|
52
|
+
color: var(--paper);
|
|
43
53
|
}
|
|
44
54
|
.pron-cmn {
|
|
45
55
|
background: var(--ink);
|
|
@@ -15,7 +15,7 @@ function close() { open.value = false }
|
|
|
15
15
|
<template>
|
|
16
16
|
<div class="rt" :class="{ open }">
|
|
17
17
|
<button class="rt-fab" @click="toggle" :aria-label="open ? t('settings.close') : t('settings.reading')">
|
|
18
|
-
<span v-if="!open" class="rt-icon"
|
|
18
|
+
<span v-if="!open" class="rt-icon">{{ t('settings.shortTitle').charAt(0) }}</span>
|
|
19
19
|
<span v-else class="rt-icon">✕</span>
|
|
20
20
|
</button>
|
|
21
21
|
<div v-if="open" class="rt-panel" @click.stop>
|
|
@@ -117,13 +117,13 @@ function close() { open.value = false }
|
|
|
117
117
|
<style scoped>
|
|
118
118
|
.rt {
|
|
119
119
|
position: fixed;
|
|
120
|
-
bottom: 24px;
|
|
120
|
+
bottom: max(24px, calc(16px + env(safe-area-inset-bottom, 0px)));
|
|
121
121
|
right: 24px;
|
|
122
122
|
z-index: 500;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
@media (max-width: 768px) {
|
|
126
|
-
.rt { bottom: 16px; right: 16px; }
|
|
126
|
+
.rt { bottom: max(16px, calc(12px + env(safe-area-inset-bottom, 0px))); right: 16px; }
|
|
127
127
|
.rt-panel {
|
|
128
128
|
position: fixed;
|
|
129
129
|
bottom: 0;
|
|
@@ -134,6 +134,7 @@ function close() { open.value = false }
|
|
|
134
134
|
max-height: 60vh;
|
|
135
135
|
overflow-y: auto;
|
|
136
136
|
overscroll-behavior: contain;
|
|
137
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
|
137
138
|
animation: slideUpMobile 0.3s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
138
139
|
}
|
|
139
140
|
@keyframes slideUpMobile {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { parseAnnotationBlock } from '../utils/annotationParser'
|
|
4
|
+
import { useI18n } from '../composables/useI18n'
|
|
4
5
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
5
6
|
|
|
7
|
+
const { locale } = useI18n()
|
|
8
|
+
|
|
6
9
|
const props = defineProps<{
|
|
7
10
|
num: string
|
|
8
11
|
label: string
|
|
@@ -49,6 +52,12 @@ const displayNum = computed(() => {
|
|
|
49
52
|
return props.num
|
|
50
53
|
})
|
|
51
54
|
|
|
55
|
+
const displayLabel = computed(() => {
|
|
56
|
+
if (!props.special) return props.label
|
|
57
|
+
if (locale.value === 'en') return props.label
|
|
58
|
+
return '【' + props.label + '】'
|
|
59
|
+
})
|
|
60
|
+
|
|
52
61
|
const entries = computed(() =>
|
|
53
62
|
props.isAnnotations ? parseAnnotationBlock(props.text) : []
|
|
54
63
|
)
|
|
@@ -64,7 +73,7 @@ const paragraphsHtml = computed(() => {
|
|
|
64
73
|
<div v-if="text" ref="rootRef" class="sb-root" :class="{ 'sb-vertical': vertical, 'sb-visible': visible }">
|
|
65
74
|
<div class="sb-header">
|
|
66
75
|
<span v-if="displayNum" class="sb-num" :class="{ special }">{{ displayNum }}</span>
|
|
67
|
-
<h3>{{
|
|
76
|
+
<h3>{{ displayLabel }}</h3>
|
|
68
77
|
</div>
|
|
69
78
|
<div v-if="isAnnotations" class="sb-text sb-ann-list">
|
|
70
79
|
<div v-for="entry in entries" :key="entry.num" class="sb-ann-entry">
|
|
@@ -104,7 +113,7 @@ const paragraphsHtml = computed(() => {
|
|
|
104
113
|
.sb-num {
|
|
105
114
|
display: inline-flex; align-items: center; justify-content: center;
|
|
106
115
|
width: 28px; height: 28px; border-radius: 50%;
|
|
107
|
-
background: var(--vermillion); color:
|
|
116
|
+
background: var(--vermillion); color: var(--paper);
|
|
108
117
|
font-family: var(--sans); font-size: 13px; font-weight: 700;
|
|
109
118
|
flex-shrink: 0;
|
|
110
119
|
}
|
|
@@ -142,7 +151,7 @@ const paragraphsHtml = computed(() => {
|
|
|
142
151
|
height: 22px;
|
|
143
152
|
border-radius: 4px;
|
|
144
153
|
background: var(--vermillion);
|
|
145
|
-
color:
|
|
154
|
+
color: var(--paper);
|
|
146
155
|
font-family: var(--sans);
|
|
147
156
|
font-size: 12px;
|
|
148
157
|
font-weight: 700;
|
|
@@ -135,7 +135,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
135
135
|
border-left: 1px solid var(--border);
|
|
136
136
|
display: flex; flex-direction: column;
|
|
137
137
|
align-items: center;
|
|
138
|
-
padding: 12px 0;
|
|
138
|
+
padding: max(12px, env(safe-area-inset-top, 0px)) 0 max(12px, env(safe-area-inset-bottom, 0px));
|
|
139
139
|
z-index: 200;
|
|
140
140
|
gap: 8px;
|
|
141
141
|
}
|
|
@@ -347,11 +347,11 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
@media (max-width: 768px) {
|
|
350
|
-
.sidenav { width: 44px; padding: 8px 0; gap: 5px; }
|
|
350
|
+
.sidenav { width: 44px; padding: max(8px, env(safe-area-inset-top, 0px)) 0 max(8px, env(safe-area-inset-bottom, 0px)); gap: 5px; }
|
|
351
351
|
.sn-brand { width: 30px; height: 36px; margin-bottom: 2px; }
|
|
352
352
|
.sn-seal { font-size: 14px; }
|
|
353
|
-
.sn-btn { width:
|
|
354
|
-
.sn-btn svg { width:
|
|
353
|
+
.sn-btn { width: 32px; height: 32px; }
|
|
354
|
+
.sn-btn svg { width: 16px; height: 16px; }
|
|
355
355
|
.sn-context { font-size: 10px; max-height: 70px; }
|
|
356
356
|
.sn-settings { width: 180px; right: 52px; padding: 12px; }
|
|
357
357
|
.sn-layout-tag { width: 20px; height: 20px; font-size: 10px; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import type { Annotation, VerseLine } from '../types'
|
|
3
|
-
import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations
|
|
4
|
+
import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations } from '../composables/useAnnotationRenderer'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
title: string
|
|
@@ -17,11 +18,23 @@ const emit = defineEmits<{
|
|
|
17
18
|
openAuthor: [name: string]
|
|
18
19
|
}>()
|
|
19
20
|
|
|
21
|
+
const allVerseSpans = computed(() =>
|
|
22
|
+
props.verses.map((_, i) => buildVerseAnnotations(props.annotations, i))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const verseOffsets = computed(() => {
|
|
26
|
+
const offsets: number[] = []
|
|
27
|
+
let acc = 0
|
|
28
|
+
for (const spans of allVerseSpans.value) {
|
|
29
|
+
offsets.push(acc)
|
|
30
|
+
acc += spans.length
|
|
31
|
+
}
|
|
32
|
+
return offsets
|
|
33
|
+
})
|
|
34
|
+
|
|
20
35
|
function verseHtml(index: number): string {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const spans = buildVerseAnnotations(props.annotations, index)
|
|
24
|
-
return renderAnnotatedText(props.verses[index].text, spans, true, offset)
|
|
36
|
+
const spans = allVerseSpans.value[index]
|
|
37
|
+
return renderAnnotatedText(props.verses[index].text, spans, true, verseOffsets.value[index])
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
function onHover(event: MouseEvent) {
|
|
@@ -60,7 +73,7 @@ function onTap(event: MouseEvent) {
|
|
|
60
73
|
--ann-shadow-y: -2px;
|
|
61
74
|
writing-mode: vertical-rl;
|
|
62
75
|
text-orientation: mixed;
|
|
63
|
-
height: calc(100vh -
|
|
76
|
+
height: calc(100vh - var(--nav-width, 56px));
|
|
64
77
|
padding: 32px 24px;
|
|
65
78
|
background: var(--surface);
|
|
66
79
|
border: 1px solid var(--border);
|
|
@@ -78,6 +78,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
78
78
|
'annotation.noteCount': '{count}注',
|
|
79
79
|
'action.close': '關閉',
|
|
80
80
|
'action.backToStart': '回到起始',
|
|
81
|
+
'action.backToTop': '回到頂部',
|
|
81
82
|
'shortcut.toggleLayout': '直/橫',
|
|
82
83
|
'shortcut.toggleTheme': '主題',
|
|
83
84
|
'shortcut.goHome': '首頁',
|
|
@@ -93,8 +94,9 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
93
94
|
'theme.sepia': '暖',
|
|
94
95
|
'theme.dark': '暗',
|
|
95
96
|
'theme.oled': '黑',
|
|
96
|
-
'
|
|
97
|
-
'
|
|
97
|
+
'pron.yue': '粵',
|
|
98
|
+
'pron.cmn': '普',
|
|
99
|
+
'annotation.kind.pronunciation': '讀音', 'annotation.kind.semantic': '釋義',
|
|
98
100
|
'annotation.kind.etymology': '詞源',
|
|
99
101
|
'annotation.kind.note': '備注',
|
|
100
102
|
'annotation.kind.definition': '釋義',
|
|
@@ -175,6 +177,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
175
177
|
'annotation.noteCount': '{count}注',
|
|
176
178
|
'action.close': '关闭',
|
|
177
179
|
'action.backToStart': '回到起始',
|
|
180
|
+
'action.backToTop': '回到顶部',
|
|
178
181
|
'shortcut.toggleLayout': '直/横',
|
|
179
182
|
'shortcut.toggleTheme': '主题',
|
|
180
183
|
'shortcut.goHome': '首页',
|
|
@@ -190,6 +193,8 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
190
193
|
'theme.sepia': '暖',
|
|
191
194
|
'theme.dark': '暗',
|
|
192
195
|
'theme.oled': '黑',
|
|
196
|
+
'pron.yue': '粤',
|
|
197
|
+
'pron.cmn': '普',
|
|
193
198
|
'annotation.kind.pronunciation': '读音',
|
|
194
199
|
'annotation.kind.semantic': '释义',
|
|
195
200
|
'annotation.kind.etymology': '词源',
|
|
@@ -272,6 +277,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
272
277
|
'annotation.noteCount': '{count} notes',
|
|
273
278
|
'action.close': 'Close',
|
|
274
279
|
'action.backToStart': 'Back to start',
|
|
280
|
+
'action.backToTop': 'Back to top',
|
|
275
281
|
'shortcut.toggleLayout': 'V/H',
|
|
276
282
|
'shortcut.toggleTheme': 'Theme',
|
|
277
283
|
'shortcut.goHome': 'Home',
|
|
@@ -287,6 +293,8 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
287
293
|
'theme.sepia': 'Warm',
|
|
288
294
|
'theme.dark': 'Dark',
|
|
289
295
|
'theme.oled': 'Black',
|
|
296
|
+
'pron.yue': 'Yue',
|
|
297
|
+
'pron.cmn': 'Man',
|
|
290
298
|
'annotation.kind.pronunciation': 'Pronunciation',
|
|
291
299
|
'annotation.kind.semantic': 'Definition',
|
|
292
300
|
'annotation.kind.etymology': 'Etymology',
|
|
@@ -5,13 +5,6 @@ export type LayoutMode = 'horizontal' | 'vertical'
|
|
|
5
5
|
|
|
6
6
|
export const THEMES: Theme[] = ['light', 'sepia', 'dark', 'oled']
|
|
7
7
|
|
|
8
|
-
export const THEME_LABELS: Record<Theme, string> = {
|
|
9
|
-
light: '亮',
|
|
10
|
-
sepia: '暖',
|
|
11
|
-
dark: '暗',
|
|
12
|
-
oled: '黑',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
export const FONT_SIZES = [12, 14, 16, 18, 20, 22, 24, 28, 32] as const
|
|
16
9
|
export type FontSize = typeof FONT_SIZES[number]
|
|
17
10
|
|
|
@@ -18,32 +18,44 @@
|
|
|
18
18
|
|
|
19
19
|
/* ─── Annotation kind hover states ─── */
|
|
20
20
|
.ann-target:hover {
|
|
21
|
-
background:
|
|
22
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
21
|
+
background: color-mix(in srgb, var(--vermillion) 10%, transparent);
|
|
22
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--vermillion) 8%, transparent);
|
|
23
23
|
}
|
|
24
24
|
.ann-target.pronunciation:hover {
|
|
25
|
-
background:
|
|
26
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
25
|
+
background: color-mix(in srgb, var(--jade) 10%, transparent);
|
|
26
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--jade) 8%, transparent);
|
|
27
27
|
}
|
|
28
28
|
.ann-target.person:hover {
|
|
29
|
-
background:
|
|
30
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
29
|
+
background: color-mix(in srgb, var(--ann-person) 10%, transparent);
|
|
30
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-person) 8%, transparent);
|
|
31
31
|
}
|
|
32
32
|
.ann-target.place:hover {
|
|
33
|
-
background:
|
|
34
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
33
|
+
background: color-mix(in srgb, var(--ann-place) 10%, transparent);
|
|
34
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-place) 8%, transparent);
|
|
35
35
|
}
|
|
36
36
|
.ann-target.event:hover {
|
|
37
|
-
background:
|
|
38
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
37
|
+
background: color-mix(in srgb, var(--ann-event) 10%, transparent);
|
|
38
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-event) 8%, transparent);
|
|
39
39
|
}
|
|
40
40
|
.ann-target.date:hover {
|
|
41
|
-
background:
|
|
42
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
41
|
+
background: color-mix(in srgb, var(--ann-date) 10%, transparent);
|
|
42
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-date) 8%, transparent);
|
|
43
43
|
}
|
|
44
44
|
.ann-target.allusion:hover {
|
|
45
|
-
background:
|
|
46
|
-
box-shadow: 0 var(--ann-shadow-y, 2px) 8px
|
|
45
|
+
background: color-mix(in srgb, var(--ann-allusion) 10%, transparent);
|
|
46
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-allusion) 8%, transparent);
|
|
47
|
+
}
|
|
48
|
+
.ann-target.etymology:hover {
|
|
49
|
+
background: color-mix(in srgb, var(--ann-etymology) 10%, transparent);
|
|
50
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-etymology) 8%, transparent);
|
|
51
|
+
}
|
|
52
|
+
.ann-target.commentary:hover {
|
|
53
|
+
background: color-mix(in srgb, var(--ann-commentary) 10%, transparent);
|
|
54
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-commentary) 8%, transparent);
|
|
55
|
+
}
|
|
56
|
+
.ann-target.translation:hover {
|
|
57
|
+
background: color-mix(in srgb, var(--ann-translation) 10%, transparent);
|
|
58
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px color-mix(in srgb, var(--ann-translation) 8%, transparent);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/* ─── Annotation number ─── */
|
|
@@ -167,7 +167,7 @@ button:focus-visible {
|
|
|
167
167
|
|
|
168
168
|
/* ===== ACTIVE ANNOTATION FLASH ===== */
|
|
169
169
|
@keyframes ann-flash-anim {
|
|
170
|
-
0% { background:
|
|
170
|
+
0% { background: color-mix(in srgb, var(--vermillion) 25%, transparent); box-shadow: 0 0 12px color-mix(in srgb, var(--vermillion) 15%, transparent); }
|
|
171
171
|
100% { background: transparent; box-shadow: none; }
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -251,7 +251,7 @@ button:focus-visible {
|
|
|
251
251
|
flex-direction: row-reverse;
|
|
252
252
|
overflow-x: auto;
|
|
253
253
|
overflow-y: hidden;
|
|
254
|
-
margin-right: var(--nav-width, 56px);
|
|
254
|
+
margin-right: calc(var(--nav-width, 56px) + env(safe-area-inset-right, 0px));
|
|
255
255
|
scrollbar-width: thin;
|
|
256
256
|
scrollbar-color: var(--gold) transparent;
|
|
257
257
|
scroll-snap-type: x proximity;
|
|
@@ -328,3 +328,13 @@ button:focus-visible {
|
|
|
328
328
|
[data-theme="oled"] .sb-vertical {
|
|
329
329
|
border-right-color: var(--border-light);
|
|
330
330
|
}
|
|
331
|
+
|
|
332
|
+
/* ===== REDUCED MOTION ===== */
|
|
333
|
+
@media (prefers-reduced-motion: reduce) {
|
|
334
|
+
*, *::before, *::after {
|
|
335
|
+
animation-duration: 0.01ms !important;
|
|
336
|
+
animation-iteration-count: 1 !important;
|
|
337
|
+
transition-duration: 0.01ms !important;
|
|
338
|
+
scroll-behavior: auto !important;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Annotation } from '../types'
|
|
2
2
|
import { useI18n } from '../composables/useI18n'
|
|
3
3
|
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
|
|
4
6
|
const KIND_I18N_KEYS: Record<string, string> = {
|
|
5
7
|
pronunciation: 'annotation.kind.pronunciation',
|
|
6
8
|
semantic: 'annotation.kind.semantic',
|
|
@@ -18,9 +20,5 @@ const KIND_I18N_KEYS: Record<string, string> = {
|
|
|
18
20
|
|
|
19
21
|
export function kindLabel(ann: Annotation): string {
|
|
20
22
|
const key = KIND_I18N_KEYS[ann.kind]
|
|
21
|
-
|
|
22
|
-
const { t } = useI18n()
|
|
23
|
-
return t(key)
|
|
24
|
-
}
|
|
25
|
-
return ann.kind
|
|
23
|
+
return key ? t(key) : ann.kind
|
|
26
24
|
}
|
|
@@ -177,8 +177,7 @@ function goHome() { router.push('/') }
|
|
|
177
177
|
display: flex; align-items: center; justify-content: center;
|
|
178
178
|
font-size: 28px; font-weight: 900;
|
|
179
179
|
color: var(--vermillion);
|
|
180
|
-
margin-left: 24px;
|
|
181
|
-
border-left: 3px solid var(--vermillion);
|
|
180
|
+
margin-left: 24px;
|
|
182
181
|
}
|
|
183
182
|
.v-name {
|
|
184
183
|
font-size: 48px; font-weight: 900;
|
|
@@ -251,7 +250,7 @@ function goHome() { router.push('/') }
|
|
|
251
250
|
.h-page { min-height: 100vh; }
|
|
252
251
|
.h-nav {
|
|
253
252
|
position: sticky; top: 0; z-index: 100;
|
|
254
|
-
background: var(--paper);
|
|
253
|
+
background: var(--paper);
|
|
255
254
|
backdrop-filter: blur(20px);
|
|
256
255
|
border-bottom: 1px solid var(--border-light);
|
|
257
256
|
padding: 0 40px;
|
|
@@ -78,7 +78,7 @@ function scrollToCatalog() {
|
|
|
78
78
|
<span class="v-ch-line"> </span>
|
|
79
79
|
<span class="v-count">{{ t('catalog.total', { count: filtered.length }) }}</span>
|
|
80
80
|
<span class="v-search-wrap">
|
|
81
|
-
<input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" aria-label="search" />
|
|
81
|
+
<input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" :aria-label="t('catalog.search')" />
|
|
82
82
|
</span>
|
|
83
83
|
</section>
|
|
84
84
|
|
|
@@ -92,6 +92,9 @@ function scrollToCatalog() {
|
|
|
92
92
|
@click="openPiece(piece.num)"
|
|
93
93
|
/>
|
|
94
94
|
</div>
|
|
95
|
+
<div v-if="searchQuery && filtered.length === 0" class="v-empty">
|
|
96
|
+
<span class="v-empty-text">{{ t('catalog.noResults', { query: searchQuery }) }}</span>
|
|
97
|
+
</div>
|
|
95
98
|
</div>
|
|
96
99
|
</div>
|
|
97
100
|
|
|
@@ -379,6 +382,23 @@ function scrollToCatalog() {
|
|
|
379
382
|
opacity: 0.5;
|
|
380
383
|
}
|
|
381
384
|
|
|
385
|
+
.v-empty {
|
|
386
|
+
writing-mode: vertical-rl;
|
|
387
|
+
text-orientation: mixed;
|
|
388
|
+
flex-shrink: 0;
|
|
389
|
+
height: 100vh;
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
justify-content: center;
|
|
393
|
+
padding: 40px 20px;
|
|
394
|
+
}
|
|
395
|
+
.v-empty-text {
|
|
396
|
+
font-size: 14px;
|
|
397
|
+
color: var(--ink-faint);
|
|
398
|
+
letter-spacing: 2px;
|
|
399
|
+
font-family: var(--sans);
|
|
400
|
+
}
|
|
401
|
+
|
|
382
402
|
@media (max-width: 768px) {
|
|
383
403
|
.h-hero { min-height: 80vh; height: auto; padding: 60px 16px; }
|
|
384
404
|
.h-ornament { font-size: 32px; letter-spacing: 12px; margin-bottom: 20px; }
|
|
@@ -312,20 +312,20 @@ function navigate(delta: number) {
|
|
|
312
312
|
if (target !== null) router.push(`/${props.bookId}/${target}`)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
const ROLE_LABELS
|
|
315
|
+
const ROLE_LABELS = computed(() => ({
|
|
316
316
|
author: t('role.author'),
|
|
317
317
|
commentator: t('role.commentator'),
|
|
318
318
|
editor: t('role.editor'),
|
|
319
319
|
translator: t('role.translator'),
|
|
320
320
|
annotator: t('role.annotator'),
|
|
321
|
-
}
|
|
321
|
+
}))
|
|
322
322
|
|
|
323
323
|
const contributorGroups = computed(() => {
|
|
324
324
|
const c = piece.value?.contributors
|
|
325
325
|
if (!c || c.length <= 1) return []
|
|
326
326
|
const groups = new Map<string, string[]>()
|
|
327
327
|
for (const x of c) {
|
|
328
|
-
const t = x.title || ROLE_LABELS[x.role] || t('role.defaultAuthor')
|
|
328
|
+
const t = x.title || ROLE_LABELS.value[x.role] || t('role.defaultAuthor')
|
|
329
329
|
if (!groups.has(t)) groups.set(t, [])
|
|
330
330
|
groups.get(t)!.push(x.name)
|
|
331
331
|
}
|
|
@@ -516,8 +516,8 @@ function tcy(n: number): string {
|
|
|
516
516
|
</div>
|
|
517
517
|
<div class="v-pane-links">
|
|
518
518
|
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="v-pane-link">CTEXT</a>
|
|
519
|
-
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
|
|
520
|
-
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
|
|
519
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">Wikipedia ZH</a>
|
|
520
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia EN</a>
|
|
521
521
|
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="v-pane-link">Wikidata</a>
|
|
522
522
|
</div>
|
|
523
523
|
<div v-if="selectedAuthorBio" class="v-pane-bio">
|
|
@@ -671,8 +671,8 @@ function tcy(n: number): string {
|
|
|
671
671
|
<span v-if="selectedAuthorWorkCount" class="h-pane-count">{{ t('piece.collected', { count: selectedAuthorWorkCount }) }}</span>
|
|
672
672
|
</div>
|
|
673
673
|
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="h-pane-alt-names">
|
|
674
|
-
<span v-if="selectedAuthorData?.courtesyName"
|
|
675
|
-
<span v-if="selectedAuthorData?.artName"
|
|
674
|
+
<span v-if="selectedAuthorData?.courtesyName">{{ t('author.courtesyName', { name: selectedAuthorData.courtesyName }) }}</span>
|
|
675
|
+
<span v-if="selectedAuthorData?.artName">{{ t('author.artName', { name: selectedAuthorData.artName }) }}</span>
|
|
676
676
|
</div>
|
|
677
677
|
</div>
|
|
678
678
|
</div>
|
|
@@ -857,7 +857,7 @@ function tcy(n: number): string {
|
|
|
857
857
|
.h-page { min-height: 100vh; }
|
|
858
858
|
.h-nav {
|
|
859
859
|
position: sticky; top: 0; z-index: 100;
|
|
860
|
-
background: var(--paper);
|
|
860
|
+
background: var(--paper);
|
|
861
861
|
backdrop-filter: blur(20px);
|
|
862
862
|
border-bottom: 1px solid var(--border-light);
|
|
863
863
|
padding: 0 40px;
|
|
@@ -1332,7 +1332,7 @@ function tcy(n: number): string {
|
|
|
1332
1332
|
|
|
1333
1333
|
@media (max-width: 768px) {
|
|
1334
1334
|
/* ─── 直排模式 ─── */
|
|
1335
|
-
.v-page { margin-right: var(--nav-width, 44px); }
|
|
1335
|
+
.v-page { margin-right: calc(var(--nav-width, 44px) + env(safe-area-inset-right, 0px)); }
|
|
1336
1336
|
.v-title-col { padding: 20px 12px; }
|
|
1337
1337
|
.v-poem-title { font-size: 28px; letter-spacing: 6px; padding-left: 12px; }
|
|
1338
1338
|
.v-poem-author { font-size: 18px; letter-spacing: 4px; }
|