@hanology/cham-browser 0.4.61 → 0.4.63
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/src/components/AnnotationControlBar.vue +7 -4
- package/template/src/components/AnnotationPane.vue +15 -6
- package/template/src/components/AnnotationTooltip.vue +12 -8
- package/template/src/components/BackToTop.vue +52 -8
- package/template/src/components/BookCard.vue +2 -1
- package/template/src/components/PoemCard.vue +2 -2
- package/template/src/components/ReadingProgress.vue +2 -2
- package/template/src/components/ReadingToolbar.vue +6 -6
- package/template/src/components/SideNav.vue +10 -10
- package/template/src/composables/useI18n.ts +96 -0
- package/template/src/styles/main.css +12 -4
- package/template/src/utils/annotationLabels.ts +20 -14
- package/template/src/views/AuthorView.vue +10 -4
- package/template/src/views/BookHome.vue +6 -2
- package/template/src/views/LibraryHome.vue +8 -0
- package/template/src/views/PieceView.vue +19 -26
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import type { AnnotationLayer } from '../types'
|
|
4
|
+
import { useI18n } from '../composables/useI18n'
|
|
5
|
+
|
|
6
|
+
const { t } = useI18n()
|
|
4
7
|
|
|
5
8
|
const props = defineProps<{
|
|
6
9
|
layers: AnnotationLayer[]
|
|
@@ -51,8 +54,8 @@ function toggleLayer(id: string) {
|
|
|
51
54
|
class="ann-toggle"
|
|
52
55
|
:class="{ on: annotationsVisible }"
|
|
53
56
|
@click="toggleAnnotations"
|
|
54
|
-
:title="annotationsVisible ? '
|
|
55
|
-
|
|
57
|
+
:title="annotationsVisible ? t('annotation.hideAnnotations') : t('annotation.showAnnotations')"
|
|
58
|
+
>{{ t('annotation.notes').charAt(0) }}</button>
|
|
56
59
|
<button
|
|
57
60
|
v-for="layer in toggleableLayers"
|
|
58
61
|
:key="layer.id"
|
|
@@ -90,7 +93,7 @@ function toggleLayer(id: string) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
.ann-toggle:hover {
|
|
93
|
-
box-shadow: 0 2px 8px rgba(
|
|
96
|
+
box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.15);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
.ann-toggle:active {
|
|
@@ -99,7 +102,7 @@ function toggleLayer(id: string) {
|
|
|
99
102
|
|
|
100
103
|
.ann-toggle.on {
|
|
101
104
|
background: var(--vermillion);
|
|
102
|
-
color:
|
|
105
|
+
color: var(--paper);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
.ann-layer-btn {
|
|
@@ -3,6 +3,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
|
3
3
|
import { annotationToPronSegment } from '../utils/annotationParser'
|
|
4
4
|
import { kindLabel } from '../utils/annotationLabels'
|
|
5
5
|
import { toChineseNumber } from '../utils/chineseNumber'
|
|
6
|
+
import { useI18n } from '../composables/useI18n'
|
|
6
7
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
7
8
|
import type { Annotation } from '../types'
|
|
8
9
|
|
|
@@ -22,8 +23,8 @@ const emit = defineEmits<{
|
|
|
22
23
|
|
|
23
24
|
const bodyRef = ref<HTMLElement | null>(null)
|
|
24
25
|
|
|
26
|
+
const { t } = useI18n()
|
|
25
27
|
const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
26
|
-
const isMobile = computed(() => ww.value < 768)
|
|
27
28
|
function onResize() { ww.value = window.innerWidth }
|
|
28
29
|
|
|
29
30
|
// ─── Resize ───
|
|
@@ -158,9 +159,9 @@ onBeforeUnmount(() => {
|
|
|
158
159
|
:style="{ width: paneWidth + 'px' }"
|
|
159
160
|
>
|
|
160
161
|
<div class="ann-pane-header">
|
|
161
|
-
<span class="ann-pane-title"
|
|
162
|
+
<span class="ann-pane-title">{{ t('annotation.all') }}</span>
|
|
162
163
|
<span class="ann-pane-count">{{ annotations.length }}</span>
|
|
163
|
-
<button class="ann-pane-close" @click="emit('close')" aria-label="
|
|
164
|
+
<button class="ann-pane-close" @click="emit('close')" :aria-label="t('action.close')">
|
|
164
165
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
165
166
|
</button>
|
|
166
167
|
</div>
|
|
@@ -171,7 +172,10 @@ onBeforeUnmount(() => {
|
|
|
171
172
|
:data-ann-id="ann.id"
|
|
172
173
|
class="ann-pane-entry"
|
|
173
174
|
:class="{ active: activeId === ann.id, [ann.kind]: true }"
|
|
175
|
+
role="button"
|
|
176
|
+
tabindex="0"
|
|
174
177
|
@click="emit('select', ann)"
|
|
178
|
+
@keydown.enter="emit('select', ann)"
|
|
175
179
|
>
|
|
176
180
|
<!-- Vertical: headword column on the right side -->
|
|
177
181
|
<div v-if="vertical && headword(ann)" class="ann-pane-v-word">
|
|
@@ -289,6 +293,11 @@ onBeforeUnmount(() => {
|
|
|
289
293
|
background: var(--surface);
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
.ann-pane-entry:focus-visible {
|
|
297
|
+
outline: 2px solid var(--vermillion);
|
|
298
|
+
outline-offset: -2px;
|
|
299
|
+
}
|
|
300
|
+
|
|
292
301
|
.ann-pane-entry.active.pronunciation {
|
|
293
302
|
border-left-color: var(--jade);
|
|
294
303
|
}
|
|
@@ -334,11 +343,11 @@ onBeforeUnmount(() => {
|
|
|
334
343
|
|
|
335
344
|
.ann-pane-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
336
345
|
.ann-pane-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
337
|
-
.ann-pane-kind.etymology { background:
|
|
346
|
+
.ann-pane-kind.etymology { background: var(--ann-etymology); color: #fff; }
|
|
338
347
|
.ann-pane-kind.note,
|
|
339
348
|
.ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
|
|
340
|
-
.ann-pane-kind.commentary { background:
|
|
341
|
-
.ann-pane-kind.translation { background:
|
|
349
|
+
.ann-pane-kind.commentary { background: var(--ann-commentary); color: #fff; }
|
|
350
|
+
.ann-pane-kind.translation { background: var(--ann-translation); color: #fff; }
|
|
342
351
|
.ann-pane-kind.person { background: var(--ann-person); color: #fff; }
|
|
343
352
|
.ann-pane-kind.place { background: var(--ann-place); color: #fff; }
|
|
344
353
|
.ann-pane-kind.event { background: var(--ann-event); color: #fff; }
|
|
@@ -3,9 +3,12 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
|
3
3
|
import { annotationToPronSegment } from '../utils/annotationParser'
|
|
4
4
|
import { kindLabel } from '../utils/annotationLabels'
|
|
5
5
|
import { toChineseNumber } from '../utils/chineseNumber'
|
|
6
|
+
import { useI18n } from '../composables/useI18n'
|
|
6
7
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
7
8
|
import type { Annotation } from '../types'
|
|
8
9
|
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
|
|
9
12
|
const props = defineProps<{
|
|
10
13
|
visible: boolean
|
|
11
14
|
annotations: Annotation[]
|
|
@@ -109,9 +112,9 @@ onBeforeUnmount(() => {
|
|
|
109
112
|
>
|
|
110
113
|
<div v-if="headword" class="ann-card-head" :class="dominantKind()">
|
|
111
114
|
<div class="ann-headword">{{ headword }}</div>
|
|
112
|
-
<div class="ann-badge-count" v-if="annotations.length > 1">{{ toChineseNumber(annotations.length) }}
|
|
115
|
+
<div class="ann-badge-count" v-if="annotations.length > 1">{{ t('annotation.noteCount', { count: toChineseNumber(annotations.length) }) }}</div>
|
|
113
116
|
</div>
|
|
114
|
-
<button class="ann-card-close" @click="dismiss" aria-label="
|
|
117
|
+
<button class="ann-card-close" @click="dismiss" :aria-label="t('action.close')">
|
|
115
118
|
<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>
|
|
116
119
|
</button>
|
|
117
120
|
<div class="ann-card-scroll">
|
|
@@ -136,13 +139,13 @@ onBeforeUnmount(() => {
|
|
|
136
139
|
class="ann-sheet"
|
|
137
140
|
:class="{ vertical }"
|
|
138
141
|
>
|
|
139
|
-
<button class="ann-sheet-handle" @click="dismiss">
|
|
142
|
+
<button class="ann-sheet-handle" @click="dismiss" :aria-label="t('action.close')">
|
|
140
143
|
<span class="ann-handle-bar" />
|
|
141
144
|
</button>
|
|
142
145
|
<div class="ann-sheet-body" :class="{ vertical }">
|
|
143
146
|
<div v-if="headword" class="ann-sheet-head" :class="dominantKind()">
|
|
144
147
|
<div class="ann-headword">{{ headword }}</div>
|
|
145
|
-
<div class="ann-badge-count" v-if="annotations.length > 1">{{ toChineseNumber(annotations.length) }}
|
|
148
|
+
<div class="ann-badge-count" v-if="annotations.length > 1">{{ t('annotation.noteCount', { count: toChineseNumber(annotations.length) }) }}</div>
|
|
146
149
|
</div>
|
|
147
150
|
<div class="ann-sheet-scroll">
|
|
148
151
|
<div v-for="ann in annotations" :key="ann.id" class="ann-entry">
|
|
@@ -158,7 +161,7 @@ onBeforeUnmount(() => {
|
|
|
158
161
|
</div>
|
|
159
162
|
<div v-if="headword" class="ann-sheet-v-head" :class="dominantKind()">
|
|
160
163
|
<span class="ann-sheet-v-word">{{ headword }}</span>
|
|
161
|
-
<span v-if="annotations.length > 1" class="ann-badge-count-v">{{ toChineseNumber(annotations.length) }}
|
|
164
|
+
<span v-if="annotations.length > 1" class="ann-badge-count-v">{{ t('annotation.noteCount', { count: toChineseNumber(annotations.length) }) }}</span>
|
|
162
165
|
</div>
|
|
163
166
|
</div>
|
|
164
167
|
</div>
|
|
@@ -199,6 +202,7 @@ onBeforeUnmount(() => {
|
|
|
199
202
|
/* ─── Annotation entry ─── */
|
|
200
203
|
.ann-entry {
|
|
201
204
|
border-bottom: 1px solid var(--border-light);
|
|
205
|
+
padding: 8px 0;
|
|
202
206
|
font-size: 14px;
|
|
203
207
|
color: var(--ink-mid);
|
|
204
208
|
letter-spacing: 1.5px;
|
|
@@ -229,11 +233,11 @@ onBeforeUnmount(() => {
|
|
|
229
233
|
}
|
|
230
234
|
.ann-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
231
235
|
.ann-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
232
|
-
.ann-kind.etymology { background:
|
|
236
|
+
.ann-kind.etymology { background: var(--ann-etymology); color: #fff; }
|
|
233
237
|
.ann-kind.note,
|
|
234
238
|
.ann-kind.definition { background: var(--ink); color: var(--paper); }
|
|
235
|
-
.ann-kind.commentary { background:
|
|
236
|
-
.ann-kind.translation { background:
|
|
239
|
+
.ann-kind.commentary { background: var(--ann-commentary); color: #fff; }
|
|
240
|
+
.ann-kind.translation { background: var(--ann-translation); color: #fff; }
|
|
237
241
|
.ann-kind.person { background: var(--ann-person); color: #fff; }
|
|
238
242
|
.ann-kind.place { background: var(--ann-place); color: #fff; }
|
|
239
243
|
.ann-kind.event { background: var(--ann-event); color: #fff; }
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
3
|
+
import { useI18n } from '../composables/useI18n'
|
|
4
|
+
|
|
5
|
+
const { t } = useI18n()
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
vertical?: boolean
|
|
9
|
+
scrollContainer?: HTMLElement | null
|
|
10
|
+
}>()
|
|
3
11
|
|
|
4
12
|
const visible = ref(false)
|
|
5
13
|
let ticking = false
|
|
@@ -8,22 +16,52 @@ function onScroll() {
|
|
|
8
16
|
if (ticking) return
|
|
9
17
|
ticking = true
|
|
10
18
|
requestAnimationFrame(() => {
|
|
11
|
-
|
|
19
|
+
if (props.vertical && props.scrollContainer) {
|
|
20
|
+
visible.value = props.scrollContainer.scrollLeft > 400
|
|
21
|
+
} else {
|
|
22
|
+
visible.value = window.scrollY > 400
|
|
23
|
+
}
|
|
12
24
|
ticking = false
|
|
13
25
|
})
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
function scrollToTop() {
|
|
17
|
-
|
|
29
|
+
if (props.vertical && props.scrollContainer) {
|
|
30
|
+
props.scrollContainer.scrollTo({ left: props.scrollContainer.scrollWidth, behavior: 'smooth' })
|
|
31
|
+
} else {
|
|
32
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function attach() {
|
|
37
|
+
if (props.vertical && props.scrollContainer) {
|
|
38
|
+
props.scrollContainer.addEventListener('scroll', onScroll, { passive: true })
|
|
39
|
+
} else {
|
|
40
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
41
|
+
}
|
|
42
|
+
onScroll()
|
|
18
43
|
}
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
function detach() {
|
|
46
|
+
if (props.vertical && props.scrollContainer) {
|
|
47
|
+
props.scrollContainer.removeEventListener('scroll', onScroll)
|
|
48
|
+
} else {
|
|
49
|
+
window.removeEventListener('scroll', onScroll)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
watch(() => props.scrollContainer, () => {
|
|
54
|
+
detach()
|
|
55
|
+
attach()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
onMounted(attach)
|
|
59
|
+
onUnmounted(detach)
|
|
22
60
|
</script>
|
|
23
61
|
|
|
24
62
|
<template>
|
|
25
63
|
<Transition name="btt">
|
|
26
|
-
<button v-if="visible" class="btt" @click="scrollToTop" aria-label="
|
|
64
|
+
<button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" :aria-label="vertical ? t('action.backToStart') : t('action.backToStart')">
|
|
27
65
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
66
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
29
67
|
</svg>
|
|
@@ -52,16 +90,22 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
|
|
52
90
|
backdrop-filter: blur(8px);
|
|
53
91
|
-webkit-backdrop-filter: blur(8px);
|
|
54
92
|
}
|
|
93
|
+
.btt.btt-v {
|
|
94
|
+
bottom: auto;
|
|
95
|
+
top: 24px;
|
|
96
|
+
right: auto;
|
|
97
|
+
left: calc(var(--nav-width, 56px) + 12px);
|
|
98
|
+
}
|
|
55
99
|
|
|
56
100
|
@media (max-width: 768px) {
|
|
57
101
|
.btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
|
|
58
102
|
}
|
|
59
103
|
.btt:hover {
|
|
60
104
|
background: var(--vermillion);
|
|
61
|
-
color:
|
|
105
|
+
color: var(--paper);
|
|
62
106
|
border-color: var(--vermillion);
|
|
63
107
|
transform: translateY(-3px);
|
|
64
|
-
box-shadow: 0 8px 24px rgba(
|
|
108
|
+
box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.15);
|
|
65
109
|
}
|
|
66
110
|
|
|
67
111
|
.btt-enter-active { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
@@ -13,7 +13,7 @@ function genreLabel(genre: string): string {
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
16
|
-
<div class="bc-root" role="button" tabindex="0" @click="router.push(`/${props.book.id}`)" @keydown.enter="router.push(`/${props.book.id}`)">
|
|
16
|
+
<div class="bc-root" role="button" tabindex="0" @click="router.push(`/${props.book.id}`)" @keydown.enter="router.push(`/${props.book.id}`)" @keydown.space.prevent="router.push(`/${props.book.id}`)">
|
|
17
17
|
<div class="bc-accent"></div>
|
|
18
18
|
<div class="bc-body">
|
|
19
19
|
<h2 class="bc-title">{{ props.book.title }}</h2>
|
|
@@ -49,6 +49,7 @@ function genreLabel(genre: string): string {
|
|
|
49
49
|
border-color: var(--gold-light);
|
|
50
50
|
}
|
|
51
51
|
.bc-root:hover .bc-accent { height: 100%; }
|
|
52
|
+
.bc-root:active { transform: scale(0.98); }
|
|
52
53
|
.bc-body { padding: 28px 24px; }
|
|
53
54
|
.bc-title {
|
|
54
55
|
font-size: 22px;
|
|
@@ -15,7 +15,7 @@ const preview = computed(() => {
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<div class="pc-root" :class="{ 'pc-vertical': vertical }" role="button" tabindex="0" @click="$emit('click')" @keydown.enter="$emit('click')">
|
|
18
|
+
<div class="pc-root" :class="{ 'pc-vertical': vertical }" role="button" tabindex="0" @click="$emit('click')" @keydown.enter="$emit('click')" @keydown.space.prevent="$emit('click')">
|
|
19
19
|
<div class="pc-accent"></div>
|
|
20
20
|
<div class="pc-body">
|
|
21
21
|
<div class="pc-num">{{ String(poem.num).padStart(3, '0') }}</div>
|
|
@@ -128,7 +128,7 @@ const preview = computed(() => {
|
|
|
128
128
|
top: auto; left: 0; bottom: 0;
|
|
129
129
|
width: 0; height: 3px;
|
|
130
130
|
background: linear-gradient(90deg, var(--gold), var(--vermillion));
|
|
131
|
-
transition: width 0.35s ease;
|
|
131
|
+
transition: width 0.35s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
132
132
|
}
|
|
133
133
|
.pc-vertical:hover {
|
|
134
134
|
transform: translateX(-4px);
|
|
@@ -75,12 +75,12 @@ onUnmounted(detach)
|
|
|
75
75
|
top: 0; left: 0;
|
|
76
76
|
height: 3px;
|
|
77
77
|
background: linear-gradient(90deg, var(--vermillion), var(--gold));
|
|
78
|
-
box-shadow: 0 0 8px rgba(
|
|
78
|
+
box-shadow: 0 0 8px rgba(var(--shadow-rgb), 0.15);
|
|
79
79
|
}
|
|
80
80
|
.rp-v {
|
|
81
81
|
top: 0; left: 0;
|
|
82
82
|
width: 3px;
|
|
83
83
|
background: linear-gradient(180deg, var(--vermillion), var(--gold));
|
|
84
|
-
box-shadow: 0 0 8px rgba(
|
|
84
|
+
box-shadow: 0 0 8px rgba(var(--shadow-rgb), 0.15);
|
|
85
85
|
}
|
|
86
86
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import { useReadingMode, THEMES,
|
|
3
|
+
import { useReadingMode, THEMES, FONT_SIZES } from '../composables/useReadingMode'
|
|
4
4
|
import type { LayoutMode, FontSize } from '../composables/useReadingMode'
|
|
5
5
|
import { useI18n, LOCALE_LABELS, type Locale } from '../composables/useI18n'
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ function close() { open.value = false }
|
|
|
14
14
|
|
|
15
15
|
<template>
|
|
16
16
|
<div class="rt" :class="{ open }">
|
|
17
|
-
<button class="rt-fab" @click="toggle" :aria-label="open ? '
|
|
17
|
+
<button class="rt-fab" @click="toggle" :aria-label="open ? t('settings.close') : t('settings.reading')">
|
|
18
18
|
<span v-if="!open" class="rt-icon">設</span>
|
|
19
19
|
<span v-else class="rt-icon">✕</span>
|
|
20
20
|
</button>
|
|
@@ -73,7 +73,7 @@ function close() { open.value = false }
|
|
|
73
73
|
class="rt-opt rt-theme"
|
|
74
74
|
:class="{ active: theme === t, ['theme-' + t]: true }"
|
|
75
75
|
@click="setTheme(t)"
|
|
76
|
-
>{{
|
|
76
|
+
>{{ t('theme.' + t) }}</button>
|
|
77
77
|
</div>
|
|
78
78
|
</div>
|
|
79
79
|
<div class="rt-group">
|
|
@@ -105,9 +105,9 @@ function close() { open.value = false }
|
|
|
105
105
|
</div>
|
|
106
106
|
</div>
|
|
107
107
|
<div class="rt-shortcuts">
|
|
108
|
-
<div class="rt-sc"><kbd>V</kbd>
|
|
109
|
-
<div class="rt-sc"><kbd>T</kbd>
|
|
110
|
-
<div class="rt-sc"><kbd>Esc</kbd>
|
|
108
|
+
<div class="rt-sc"><kbd>V</kbd> {{ t('shortcut.toggleLayout') }}</div>
|
|
109
|
+
<div class="rt-sc"><kbd>T</kbd> {{ t('shortcut.toggleTheme') }}</div>
|
|
110
|
+
<div class="rt-sc"><kbd>Esc</kbd> {{ t('shortcut.goHome') }}</div>
|
|
111
111
|
</div>
|
|
112
112
|
</div>
|
|
113
113
|
<div v-if="open" class="rt-backdrop" @click="close" />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, inject } from 'vue'
|
|
3
3
|
import { useRouter } from 'vue-router'
|
|
4
|
-
import { useReadingMode, THEMES,
|
|
4
|
+
import { useReadingMode, THEMES, FONT_SIZES } from '../composables/useReadingMode'
|
|
5
5
|
import type { LayoutMode, FontSize } from '../composables/useReadingMode'
|
|
6
6
|
import { useI18n, LOCALE_LABELS, type Locale } from '../composables/useI18n'
|
|
7
7
|
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
@@ -30,12 +30,12 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
30
30
|
|
|
31
31
|
<template>
|
|
32
32
|
<nav class="sidenav">
|
|
33
|
-
<button class="sn-brand" @click="emit('home')" title="
|
|
33
|
+
<button class="sn-brand" @click="emit('home')" :title="t('nav.home')">
|
|
34
34
|
<img v-if="logoUrl" :src="logoUrl" alt="" class="sn-logo" />
|
|
35
35
|
<span v-else class="sn-seal">文</span>
|
|
36
36
|
</button>
|
|
37
37
|
|
|
38
|
-
<button class="sn-btn" @click="emit('back')" title="
|
|
38
|
+
<button class="sn-btn" @click="emit('back')" :title="t('nav.back')">
|
|
39
39
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
|
|
40
40
|
</button>
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
50
50
|
|
|
51
51
|
<div class="sn-spacer" />
|
|
52
52
|
|
|
53
|
-
<button v-if="aboutHtml" class="sn-btn" @click="aboutPane?.toggleAbout()" title="
|
|
53
|
+
<button v-if="aboutHtml" class="sn-btn" @click="aboutPane?.toggleAbout()" :title="t('nav.about')">
|
|
54
54
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
55
55
|
</button>
|
|
56
56
|
|
|
@@ -58,12 +58,12 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
58
58
|
class="sn-btn"
|
|
59
59
|
:class="{ active: settingsOpen }"
|
|
60
60
|
@click="toggleSettings"
|
|
61
|
-
title="
|
|
61
|
+
:title="t('settings.shortTitle')"
|
|
62
62
|
>
|
|
63
63
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
64
64
|
</button>
|
|
65
65
|
|
|
66
|
-
<div v-if="layout === 'vertical'" class="sn-layout-tag"
|
|
66
|
+
<div v-if="layout === 'vertical'" class="sn-layout-tag">{{ t('layout.verticalShort') }}</div>
|
|
67
67
|
|
|
68
68
|
<Transition name="slide-left">
|
|
69
69
|
<div v-if="settingsOpen" class="sn-settings" @click.stop>
|
|
@@ -83,7 +83,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
83
83
|
class="ss-opt"
|
|
84
84
|
:class="{ active: theme === t }"
|
|
85
85
|
@click="setTheme(t)"
|
|
86
|
-
>{{
|
|
86
|
+
>{{ t('theme.' + t) }}</button>
|
|
87
87
|
</div>
|
|
88
88
|
</div>
|
|
89
89
|
<div class="ss-group">
|
|
@@ -115,9 +115,9 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
117
117
|
<div class="ss-shortcuts">
|
|
118
|
-
<span class="ss-sc"><kbd>V</kbd>
|
|
119
|
-
<span class="ss-sc"><kbd>T</kbd>
|
|
120
|
-
<span class="ss-sc"><kbd>Esc</kbd>
|
|
118
|
+
<span class="ss-sc"><kbd>V</kbd> {{ t('shortcut.toggleLayout') }}</span>
|
|
119
|
+
<span class="ss-sc"><kbd>T</kbd> {{ t('shortcut.toggleTheme') }}</span>
|
|
120
|
+
<span class="ss-sc"><kbd>Esc</kbd> {{ t('shortcut.goHome') }}</span>
|
|
121
121
|
</div>
|
|
122
122
|
</div>
|
|
123
123
|
</Transition>
|
|
@@ -73,6 +73,38 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
73
73
|
'section.preparation': '預習活動',
|
|
74
74
|
'section.follow_up': '跟進活動',
|
|
75
75
|
'section.think_questions': '想一想',
|
|
76
|
+
'annotation.showAnnotations': '顯示注釋',
|
|
77
|
+
'annotation.hideAnnotations': '隱藏注釋',
|
|
78
|
+
'annotation.noteCount': '{count}注',
|
|
79
|
+
'action.close': '關閉',
|
|
80
|
+
'action.backToStart': '回到起始',
|
|
81
|
+
'shortcut.toggleLayout': '直/橫',
|
|
82
|
+
'shortcut.toggleTheme': '主題',
|
|
83
|
+
'shortcut.goHome': '首頁',
|
|
84
|
+
'settings.shortTitle': '設定',
|
|
85
|
+
'layout.verticalShort': '直',
|
|
86
|
+
'author.courtesyName': '字{name}',
|
|
87
|
+
'author.artName': '號{name}',
|
|
88
|
+
'link.wikipedia': '維基百科',
|
|
89
|
+
'link.ctext': '文庫',
|
|
90
|
+
'role.defaultAuthor': '作者',
|
|
91
|
+
'piece.defaultAuthorInitial': '詩',
|
|
92
|
+
'theme.light': '亮',
|
|
93
|
+
'theme.sepia': '暖',
|
|
94
|
+
'theme.dark': '暗',
|
|
95
|
+
'theme.oled': '黑',
|
|
96
|
+
'annotation.kind.pronunciation': '讀音',
|
|
97
|
+
'annotation.kind.semantic': '釋義',
|
|
98
|
+
'annotation.kind.etymology': '詞源',
|
|
99
|
+
'annotation.kind.note': '備注',
|
|
100
|
+
'annotation.kind.definition': '釋義',
|
|
101
|
+
'annotation.kind.commentary': '注',
|
|
102
|
+
'annotation.kind.translation': '譯文',
|
|
103
|
+
'annotation.kind.person': '人名',
|
|
104
|
+
'annotation.kind.place': '地名',
|
|
105
|
+
'annotation.kind.event': '事件',
|
|
106
|
+
'annotation.kind.date': '紀年',
|
|
107
|
+
'annotation.kind.allusion': '典故',
|
|
76
108
|
},
|
|
77
109
|
'zh-Hans': {
|
|
78
110
|
'site.title': '古典诗文图书馆',
|
|
@@ -138,6 +170,38 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
138
170
|
'section.preparation': '预习活动',
|
|
139
171
|
'section.follow_up': '跟进活动',
|
|
140
172
|
'section.think_questions': '想一想',
|
|
173
|
+
'annotation.showAnnotations': '显示注释',
|
|
174
|
+
'annotation.hideAnnotations': '隐藏注释',
|
|
175
|
+
'annotation.noteCount': '{count}注',
|
|
176
|
+
'action.close': '关闭',
|
|
177
|
+
'action.backToStart': '回到起始',
|
|
178
|
+
'shortcut.toggleLayout': '直/横',
|
|
179
|
+
'shortcut.toggleTheme': '主题',
|
|
180
|
+
'shortcut.goHome': '首页',
|
|
181
|
+
'settings.shortTitle': '设定',
|
|
182
|
+
'layout.verticalShort': '直',
|
|
183
|
+
'author.courtesyName': '字{name}',
|
|
184
|
+
'author.artName': '号{name}',
|
|
185
|
+
'link.wikipedia': '维基百科',
|
|
186
|
+
'link.ctext': '文库',
|
|
187
|
+
'role.defaultAuthor': '作者',
|
|
188
|
+
'piece.defaultAuthorInitial': '诗',
|
|
189
|
+
'theme.light': '亮',
|
|
190
|
+
'theme.sepia': '暖',
|
|
191
|
+
'theme.dark': '暗',
|
|
192
|
+
'theme.oled': '黑',
|
|
193
|
+
'annotation.kind.pronunciation': '读音',
|
|
194
|
+
'annotation.kind.semantic': '释义',
|
|
195
|
+
'annotation.kind.etymology': '词源',
|
|
196
|
+
'annotation.kind.note': '备注',
|
|
197
|
+
'annotation.kind.definition': '释义',
|
|
198
|
+
'annotation.kind.commentary': '注',
|
|
199
|
+
'annotation.kind.translation': '译文',
|
|
200
|
+
'annotation.kind.person': '人名',
|
|
201
|
+
'annotation.kind.place': '地名',
|
|
202
|
+
'annotation.kind.event': '事件',
|
|
203
|
+
'annotation.kind.date': '纪年',
|
|
204
|
+
'annotation.kind.allusion': '典故',
|
|
141
205
|
},
|
|
142
206
|
'en': {
|
|
143
207
|
'site.title': 'Classical Chinese Text Library',
|
|
@@ -203,6 +267,38 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
203
267
|
'section.preparation': 'Pre-reading',
|
|
204
268
|
'section.follow_up': 'Follow-up',
|
|
205
269
|
'section.think_questions': 'Think About It',
|
|
270
|
+
'annotation.showAnnotations': 'Show annotations',
|
|
271
|
+
'annotation.hideAnnotations': 'Hide annotations',
|
|
272
|
+
'annotation.noteCount': '{count} notes',
|
|
273
|
+
'action.close': 'Close',
|
|
274
|
+
'action.backToStart': 'Back to start',
|
|
275
|
+
'shortcut.toggleLayout': 'V/H',
|
|
276
|
+
'shortcut.toggleTheme': 'Theme',
|
|
277
|
+
'shortcut.goHome': 'Home',
|
|
278
|
+
'settings.shortTitle': 'Settings',
|
|
279
|
+
'layout.verticalShort': 'V',
|
|
280
|
+
'author.courtesyName': 'Courtesy: {name}',
|
|
281
|
+
'author.artName': 'Art name: {name}',
|
|
282
|
+
'link.wikipedia': 'Wikipedia',
|
|
283
|
+
'link.ctext': 'CTEXT',
|
|
284
|
+
'role.defaultAuthor': 'Author',
|
|
285
|
+
'piece.defaultAuthorInitial': 'P',
|
|
286
|
+
'theme.light': 'Light',
|
|
287
|
+
'theme.sepia': 'Warm',
|
|
288
|
+
'theme.dark': 'Dark',
|
|
289
|
+
'theme.oled': 'Black',
|
|
290
|
+
'annotation.kind.pronunciation': 'Pronunciation',
|
|
291
|
+
'annotation.kind.semantic': 'Definition',
|
|
292
|
+
'annotation.kind.etymology': 'Etymology',
|
|
293
|
+
'annotation.kind.note': 'Note',
|
|
294
|
+
'annotation.kind.definition': 'Definition',
|
|
295
|
+
'annotation.kind.commentary': 'Commentary',
|
|
296
|
+
'annotation.kind.translation': 'Translation',
|
|
297
|
+
'annotation.kind.person': 'Person',
|
|
298
|
+
'annotation.kind.place': 'Place',
|
|
299
|
+
'annotation.kind.event': 'Event',
|
|
300
|
+
'annotation.kind.date': 'Date',
|
|
301
|
+
'annotation.kind.allusion': 'Allusion',
|
|
206
302
|
},
|
|
207
303
|
}
|
|
208
304
|
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
--ann-event: #6b4c8a;
|
|
23
23
|
--ann-date: #2a7a7a;
|
|
24
24
|
--ann-allusion: #b5651d;
|
|
25
|
+
--ann-etymology: #6b5b95;
|
|
26
|
+
--ann-commentary: #c0392b;
|
|
27
|
+
--ann-translation: #2c6e49;
|
|
25
28
|
--border: #d8cdb8;
|
|
26
29
|
--border-light: #e8e0d0;
|
|
27
30
|
--shadow-rgb: 26,26,26;
|
|
@@ -48,6 +51,9 @@
|
|
|
48
51
|
--ann-event: #6b4c8a;
|
|
49
52
|
--ann-date: #2a7a7a;
|
|
50
53
|
--ann-allusion: #b5651d;
|
|
54
|
+
--ann-etymology: #6b5b95;
|
|
55
|
+
--ann-commentary: #c0392b;
|
|
56
|
+
--ann-translation: #2c6e49;
|
|
51
57
|
--border: #c9b896;
|
|
52
58
|
--border-light: #d8cab0;
|
|
53
59
|
--shadow-rgb: 74,63,46;
|
|
@@ -74,6 +80,9 @@
|
|
|
74
80
|
--ann-event: #9a7cb4;
|
|
75
81
|
--ann-date: #5ab4b4;
|
|
76
82
|
--ann-allusion: #d4843a;
|
|
83
|
+
--ann-etymology: #9a8ab4;
|
|
84
|
+
--ann-commentary: #e06050;
|
|
85
|
+
--ann-translation: #5ab48a;
|
|
77
86
|
--border: #48484a;
|
|
78
87
|
--border-light: #555557;
|
|
79
88
|
--shadow-rgb: 0,0,0;
|
|
@@ -100,6 +109,9 @@
|
|
|
100
109
|
--ann-event: #9a7cb4;
|
|
101
110
|
--ann-date: #5ab4b4;
|
|
102
111
|
--ann-allusion: #d4843a;
|
|
112
|
+
--ann-etymology: #9a8ab4;
|
|
113
|
+
--ann-commentary: #ff5050;
|
|
114
|
+
--ann-translation: #40c890;
|
|
103
115
|
--border: #333333;
|
|
104
116
|
--border-light: #444444;
|
|
105
117
|
--shadow-rgb: 0,0,0;
|
|
@@ -222,10 +234,6 @@ button:focus-visible {
|
|
|
222
234
|
from { opacity: 0; transform: translateY(12px); }
|
|
223
235
|
to { opacity: 1; transform: translateY(0); }
|
|
224
236
|
}
|
|
225
|
-
@keyframes fadeIn {
|
|
226
|
-
from { opacity: 0; }
|
|
227
|
-
to { opacity: 1; }
|
|
228
|
-
}
|
|
229
237
|
@keyframes scaleIn {
|
|
230
238
|
from { opacity: 0; transform: scale(0.92); }
|
|
231
239
|
to { opacity: 1; transform: scale(1); }
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import type { Annotation } from '../types'
|
|
2
|
+
import { useI18n } from '../composables/useI18n'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
pronunciation: '
|
|
5
|
-
semantic: '
|
|
6
|
-
etymology: '
|
|
7
|
-
note: '
|
|
8
|
-
definition: '
|
|
9
|
-
commentary: '
|
|
10
|
-
translation: '
|
|
11
|
-
person: '
|
|
12
|
-
place: '
|
|
13
|
-
event: '
|
|
14
|
-
date: '
|
|
15
|
-
allusion: '
|
|
4
|
+
const KIND_I18N_KEYS: Record<string, string> = {
|
|
5
|
+
pronunciation: 'annotation.kind.pronunciation',
|
|
6
|
+
semantic: 'annotation.kind.semantic',
|
|
7
|
+
etymology: 'annotation.kind.etymology',
|
|
8
|
+
note: 'annotation.kind.note',
|
|
9
|
+
definition: 'annotation.kind.definition',
|
|
10
|
+
commentary: 'annotation.kind.commentary',
|
|
11
|
+
translation: 'annotation.kind.translation',
|
|
12
|
+
person: 'annotation.kind.person',
|
|
13
|
+
place: 'annotation.kind.place',
|
|
14
|
+
event: 'annotation.kind.event',
|
|
15
|
+
date: 'annotation.kind.date',
|
|
16
|
+
allusion: 'annotation.kind.allusion',
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function kindLabel(ann: Annotation): string {
|
|
19
|
-
|
|
20
|
+
const key = KIND_I18N_KEYS[ann.kind]
|
|
21
|
+
if (key) {
|
|
22
|
+
const { t } = useI18n()
|
|
23
|
+
return t(key)
|
|
24
|
+
}
|
|
25
|
+
return ann.kind
|
|
20
26
|
}
|
|
@@ -29,7 +29,7 @@ if (bookId) await load(bookId)
|
|
|
29
29
|
const { layout } = useReadingMode()
|
|
30
30
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
31
31
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
32
|
-
|
|
32
|
+
useHorizontalScroll(vPageRef)
|
|
33
33
|
|
|
34
34
|
const authorName = computed(() => decodeURIComponent(String(route.params.name || '')))
|
|
35
35
|
const author = computed(() => getAuthor(authorName.value))
|
|
@@ -69,7 +69,7 @@ function goHome() { router.push('/') }
|
|
|
69
69
|
role="button"
|
|
70
70
|
tabindex="0"
|
|
71
71
|
@click="openPiece(piece)"
|
|
72
|
-
@keydown.enter="openPiece(piece)"
|
|
72
|
+
@keydown.enter="openPiece(piece)" @keydown.space.prevent="openPiece(piece)"
|
|
73
73
|
>
|
|
74
74
|
<div class="v-work-num">{{ String(piece.num).padStart(3, '0') }}</div>
|
|
75
75
|
<div class="v-work-title">{{ piece.title }}</div>
|
|
@@ -80,7 +80,7 @@ function goHome() { router.push('/') }
|
|
|
80
80
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
81
81
|
<div v-else class="h-root">
|
|
82
82
|
<div class="h-page">
|
|
83
|
-
<nav class="h-nav">
|
|
83
|
+
<nav class="h-nav" aria-label="piece navigation">
|
|
84
84
|
<div class="h-nav-inner">
|
|
85
85
|
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
86
86
|
<div class="h-breadcrumb">
|
|
@@ -122,7 +122,7 @@ function goHome() { router.push('/') }
|
|
|
122
122
|
role="button"
|
|
123
123
|
tabindex="0"
|
|
124
124
|
@click="openPiece(piece)"
|
|
125
|
-
@keydown.enter="openPiece(piece)"
|
|
125
|
+
@keydown.enter="openPiece(piece)" @keydown.space.prevent="openPiece(piece)"
|
|
126
126
|
>
|
|
127
127
|
<div class="h-work-num">{{ String(piece.num).padStart(3, '0') }}</div>
|
|
128
128
|
<div class="h-work-title">{{ piece.title }}</div>
|
|
@@ -233,6 +233,9 @@ function goHome() { router.push('/') }
|
|
|
233
233
|
border-color: var(--gold);
|
|
234
234
|
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
|
|
235
235
|
}
|
|
236
|
+
.v-work:active {
|
|
237
|
+
transform: scale(0.97);
|
|
238
|
+
}
|
|
236
239
|
.v-work-num {
|
|
237
240
|
font-size: 11px; color: var(--ink-faint);
|
|
238
241
|
font-family: var(--sans); letter-spacing: 2px;
|
|
@@ -332,6 +335,9 @@ function goHome() { router.push('/') }
|
|
|
332
335
|
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
|
|
333
336
|
transform: translateY(-2px);
|
|
334
337
|
}
|
|
338
|
+
.h-work:active {
|
|
339
|
+
transform: scale(0.98);
|
|
340
|
+
}
|
|
335
341
|
.h-work-num { font-size: 11px; color: var(--ink-faint); font-family: var(--sans); letter-spacing: 2px; }
|
|
336
342
|
.h-work-title { font-size: 18px; font-weight: 700; letter-spacing: 2px; margin: 6px 0 4px; }
|
|
337
343
|
.h-work-preview {
|
|
@@ -22,7 +22,7 @@ useTitle(meta.value?.title || '')
|
|
|
22
22
|
const { layout } = useReadingMode()
|
|
23
23
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
24
24
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
25
|
-
|
|
25
|
+
useHorizontalScroll(vPageRef)
|
|
26
26
|
const { t } = useI18n()
|
|
27
27
|
|
|
28
28
|
const filtered = computed(() => {
|
|
@@ -127,7 +127,7 @@ function scrollToCatalog() {
|
|
|
127
127
|
<p v-if="meta?.publisher">{{ meta.publisher }}</p>
|
|
128
128
|
</div>
|
|
129
129
|
<div class="h-filter">
|
|
130
|
-
<input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" />
|
|
130
|
+
<input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" :aria-label="t('catalog.search')" />
|
|
131
131
|
</div>
|
|
132
132
|
<div class="h-grid">
|
|
133
133
|
<PoemCard
|
|
@@ -336,6 +336,10 @@ function scrollToCatalog() {
|
|
|
336
336
|
transform: translateY(-2px);
|
|
337
337
|
box-shadow: 0 12px 40px rgba(var(--shadow-rgb), 0.12);
|
|
338
338
|
}
|
|
339
|
+
.h-cta:active {
|
|
340
|
+
transform: scale(0.97);
|
|
341
|
+
box-shadow: 0 4px 12px rgba(var(--shadow-rgb), 0.08);
|
|
342
|
+
}
|
|
339
343
|
|
|
340
344
|
.h-catalog { max-width: 1200px; margin: 0 auto; padding: 80px 40px; }
|
|
341
345
|
.h-catalog-header { text-align: center; margin-bottom: 60px; }
|
|
@@ -107,8 +107,12 @@ function openBook(bookId: string) {
|
|
|
107
107
|
:key="book.id"
|
|
108
108
|
class="v-spine v-spine-anim"
|
|
109
109
|
:data-cat="bookCategoryKey(book)"
|
|
110
|
+
role="button"
|
|
111
|
+
tabindex="0"
|
|
110
112
|
:style="{ animationDelay: bi * 0.04 + 's' }"
|
|
111
113
|
@click="openBook(book.id)"
|
|
114
|
+
@keydown.enter="openBook(book.id)"
|
|
115
|
+
@keydown.space.prevent="openBook(book.id)"
|
|
112
116
|
>
|
|
113
117
|
<span class="v-spine-accent"></span>
|
|
114
118
|
<span class="v-spine-title">{{ book.title }}</span>
|
|
@@ -139,8 +143,12 @@ function openBook(bookId: string) {
|
|
|
139
143
|
v-for="(book, bi) in group.books"
|
|
140
144
|
:key="book.id"
|
|
141
145
|
class="lib-card lib-card-anim"
|
|
146
|
+
role="button"
|
|
147
|
+
tabindex="0"
|
|
142
148
|
:style="{ animationDelay: bi * 0.06 + 's' }"
|
|
143
149
|
@click="openBook(book.id)"
|
|
150
|
+
@keydown.enter="openBook(book.id)"
|
|
151
|
+
@keydown.space.prevent="openBook(book.id)"
|
|
144
152
|
>
|
|
145
153
|
<div class="lib-card-accent"></div>
|
|
146
154
|
<div class="lib-card-body">
|
|
@@ -325,7 +325,7 @@ const contributorGroups = computed(() => {
|
|
|
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] || '
|
|
328
|
+
const t = x.title || ROLE_LABELS[x.role] || t('role.defaultAuthor')
|
|
329
329
|
if (!groups.has(t)) groups.set(t, [])
|
|
330
330
|
groups.get(t)!.push(x.name)
|
|
331
331
|
}
|
|
@@ -411,7 +411,7 @@ function tcy(n: number): string {
|
|
|
411
411
|
:title="''"
|
|
412
412
|
:author="''"
|
|
413
413
|
:verses="piece.verses"
|
|
414
|
-
:author-initial="piece.author?.charAt(0) || '
|
|
414
|
+
:author-initial="piece.author?.charAt(0) || t('piece.defaultAuthorInitial')"
|
|
415
415
|
:annotations="mergedAnnotations"
|
|
416
416
|
@annotation-hover="onAnnotationHover"
|
|
417
417
|
@annotation-leave="onAnnotationLeave"
|
|
@@ -472,7 +472,7 @@ function tcy(n: number): string {
|
|
|
472
472
|
class="v-section"
|
|
473
473
|
/>
|
|
474
474
|
|
|
475
|
-
<nav class="v-nav">
|
|
475
|
+
<nav class="v-nav" aria-label="piece navigation">
|
|
476
476
|
<button v-if="adjacent.prev !== null" class="v-nav-btn" @click="navigate(-1)">
|
|
477
477
|
<span class="v-nav-dir">▲</span>
|
|
478
478
|
<span class="v-nav-title">{{ getPiece(adjacent.prev)?.title }}</span>
|
|
@@ -510,13 +510,13 @@ function tcy(n: number): string {
|
|
|
510
510
|
<span v-if="selectedAuthorWorkCount" class="v-pane-count">{{ t('stat.pieceCount', { count: selectedAuthorWorkCount }) }}</span>
|
|
511
511
|
</div>
|
|
512
512
|
<div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="v-pane-names">
|
|
513
|
-
<span v-if="selectedAuthorData?.courtesyName"
|
|
514
|
-
<span v-if="selectedAuthorData?.artName"
|
|
513
|
+
<span v-if="selectedAuthorData?.courtesyName">{{ t('author.courtesyName', { name: selectedAuthorData.courtesyName }) }}</span>
|
|
514
|
+
<span v-if="selectedAuthorData?.artName">{{ t('author.artName', { name: selectedAuthorData.artName }) }}</span>
|
|
515
515
|
</div>
|
|
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"
|
|
519
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
|
|
520
520
|
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</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>
|
|
@@ -530,13 +530,14 @@ function tcy(n: number): string {
|
|
|
530
530
|
</div>
|
|
531
531
|
</Transition>
|
|
532
532
|
</Teleport>
|
|
533
|
+
<BackToTop vertical :scroll-container="vPageRef" />
|
|
533
534
|
</div>
|
|
534
535
|
|
|
535
536
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
536
537
|
<div v-else class="h-root">
|
|
537
538
|
<ReadingProgress />
|
|
538
539
|
<div class="h-page">
|
|
539
|
-
<nav class="h-nav">
|
|
540
|
+
<nav class="h-nav" aria-label="piece navigation">
|
|
540
541
|
<div class="h-nav-inner">
|
|
541
542
|
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
542
543
|
<div class="h-nav-title-row">
|
|
@@ -676,25 +677,17 @@ function tcy(n: number): string {
|
|
|
676
677
|
</div>
|
|
677
678
|
</div>
|
|
678
679
|
<div class="h-pane-links">
|
|
679
|
-
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
680
|
-
|
|
681
|
-
</a>
|
|
682
|
-
<a v-if="selectedAuthorData?.
|
|
683
|
-
<span class="link-icon">維</span> 維基百科
|
|
684
|
-
</a>
|
|
685
|
-
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="h-pane-link">
|
|
686
|
-
<span class="link-icon">W</span> Wikipedia
|
|
687
|
-
</a>
|
|
688
|
-
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
689
|
-
<span class="link-icon">Q</span> Wikidata
|
|
690
|
-
</a>
|
|
680
|
+
<a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="h-pane-link">CTEXT</a>
|
|
681
|
+
<a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="h-pane-link">Wikipedia</a>
|
|
682
|
+
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="h-pane-link">Wikipedia EN</a>
|
|
683
|
+
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="h-pane-link">Wikidata</a>
|
|
691
684
|
</div>
|
|
692
685
|
<div v-if="selectedAuthorBio" class="h-pane-bio">
|
|
693
686
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="h-pane-p">
|
|
694
687
|
{{ p.trim() }}
|
|
695
688
|
</div>
|
|
696
689
|
</div>
|
|
697
|
-
<div v-if="!selectedAuthorBio" class="h-pane-empty"
|
|
690
|
+
<div v-if="!selectedAuthorBio" class="h-pane-empty">{{ t('piece.noAuthorData') }}</div>
|
|
698
691
|
</div>
|
|
699
692
|
</div>
|
|
700
693
|
</Transition>
|
|
@@ -900,7 +893,7 @@ function tcy(n: number): string {
|
|
|
900
893
|
align-items: center;
|
|
901
894
|
padding: 2px 8px;
|
|
902
895
|
background: var(--vermillion);
|
|
903
|
-
color:
|
|
896
|
+
color: var(--paper);
|
|
904
897
|
font-family: var(--sans);
|
|
905
898
|
font-size: 11px;
|
|
906
899
|
font-weight: 700;
|
|
@@ -1054,12 +1047,16 @@ function tcy(n: number): string {
|
|
|
1054
1047
|
/* Overlay transition */
|
|
1055
1048
|
.overlay-enter-active { transition: opacity var(--dur-mid, 0.25s) ease; }
|
|
1056
1049
|
.overlay-enter-active .h-pane { transition: transform var(--dur-mid, 0.25s) cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
1050
|
+
.overlay-enter-active .v-author-pane { transition: transform var(--dur-mid, 0.25s) cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
1057
1051
|
.overlay-leave-active { transition: opacity var(--dur-fast, 0.15s) ease; }
|
|
1058
1052
|
.overlay-leave-active .h-pane { transition: transform var(--dur-fast, 0.15s) ease; }
|
|
1053
|
+
.overlay-leave-active .v-author-pane { transition: transform var(--dur-fast, 0.15s) ease; }
|
|
1059
1054
|
.overlay-enter-from { opacity: 0; }
|
|
1060
1055
|
.overlay-enter-from .h-pane { transform: translateX(100%); }
|
|
1056
|
+
.overlay-enter-from .v-author-pane { transform: translateX(-100%); }
|
|
1061
1057
|
.overlay-leave-to { opacity: 0; }
|
|
1062
1058
|
.overlay-leave-to .h-pane { transform: translateX(40px); }
|
|
1059
|
+
.overlay-leave-to .v-author-pane { transform: translateX(-40px); }
|
|
1063
1060
|
.h-pane-close {
|
|
1064
1061
|
display: block; margin-left: auto;
|
|
1065
1062
|
width: 36px; height: 36px;
|
|
@@ -1086,7 +1083,7 @@ function tcy(n: number): string {
|
|
|
1086
1083
|
display: inline-flex;
|
|
1087
1084
|
padding: 2px 8px;
|
|
1088
1085
|
background: var(--vermillion);
|
|
1089
|
-
color:
|
|
1086
|
+
color: var(--paper);
|
|
1090
1087
|
font-family: var(--sans);
|
|
1091
1088
|
font-size: 11px;
|
|
1092
1089
|
font-weight: 700;
|
|
@@ -1330,10 +1327,6 @@ function tcy(n: number): string {
|
|
|
1330
1327
|
:deep(.ann-flash) {
|
|
1331
1328
|
animation: ann-flash-anim 1.5s ease-out;
|
|
1332
1329
|
}
|
|
1333
|
-
@keyframes ann-flash-anim {
|
|
1334
|
-
0% { background: rgba(194, 58, 43, 0.25); box-shadow: 0 0 12px rgba(194, 58, 43, 0.15); }
|
|
1335
|
-
100% { background: transparent; box-shadow: none; }
|
|
1336
|
-
}
|
|
1337
1330
|
|
|
1338
1331
|
/* ═══════ 行動裝置適配 ═══════ */
|
|
1339
1332
|
|