@hanology/cham-browser 0.4.62 → 0.4.64
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/App.vue +7 -0
- package/template/src/components/AnnotationControlBar.vue +7 -4
- package/template/src/components/AnnotationPane.vue +12 -13
- package/template/src/components/AnnotationTooltip.vue +46 -15
- package/template/src/components/BackToTop.vue +5 -18
- package/template/src/components/BookCard.vue +2 -1
- package/template/src/components/PoemCard.vue +1 -1
- package/template/src/components/PronunciationGroup.vue +1 -1
- package/template/src/components/ReadingToolbar.vue +7 -7
- package/template/src/components/SectionBlock.vue +2 -2
- package/template/src/components/SideNav.vue +10 -10
- package/template/src/composables/useI18n.ts +99 -0
- package/template/src/composables/useReadingMode.ts +0 -7
- package/template/src/styles/annotation-targets.css +12 -0
- package/template/src/styles/main.css +10 -4
- package/template/src/utils/annotationLabels.ts +18 -14
- package/template/src/views/AuthorView.vue +6 -7
- package/template/src/views/BookHome.vue +23 -3
- package/template/src/views/LibraryHome.vue +8 -0
- package/template/src/views/PieceView.vue +23 -31
package/package.json
CHANGED
package/template/src/App.vue
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 {
|
|
@@ -25,7 +25,6 @@ const bodyRef = ref<HTMLElement | null>(null)
|
|
|
25
25
|
|
|
26
26
|
const { t } = useI18n()
|
|
27
27
|
const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
28
|
-
const isMobile = computed(() => ww.value < 768)
|
|
29
28
|
function onResize() { ww.value = window.innerWidth }
|
|
30
29
|
|
|
31
30
|
// ─── Resize ───
|
|
@@ -147,7 +146,7 @@ onBeforeUnmount(() => {
|
|
|
147
146
|
<Teleport to="body">
|
|
148
147
|
<Transition name="ann-dim">
|
|
149
148
|
<div
|
|
150
|
-
v-if="visible && annotations.length && vertical &&
|
|
149
|
+
v-if="visible && annotations.length && vertical && ww < 768"
|
|
151
150
|
class="ann-pane-dim"
|
|
152
151
|
@click="emit('close')"
|
|
153
152
|
/>
|
|
@@ -162,7 +161,7 @@ onBeforeUnmount(() => {
|
|
|
162
161
|
<div class="ann-pane-header">
|
|
163
162
|
<span class="ann-pane-title">{{ t('annotation.all') }}</span>
|
|
164
163
|
<span class="ann-pane-count">{{ annotations.length }}</span>
|
|
165
|
-
<button class="ann-pane-close" @click="emit('close')" aria-label="
|
|
164
|
+
<button class="ann-pane-close" @click="emit('close')" :aria-label="t('action.close')">
|
|
166
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>
|
|
167
166
|
</button>
|
|
168
167
|
</div>
|
|
@@ -342,18 +341,18 @@ onBeforeUnmount(() => {
|
|
|
342
341
|
line-height: 1.5;
|
|
343
342
|
}
|
|
344
343
|
|
|
345
|
-
.ann-pane-kind.pronunciation { background: var(--jade); color:
|
|
346
|
-
.ann-pane-kind.semantic { background: var(--vermillion); color:
|
|
347
|
-
.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); }
|
|
348
347
|
.ann-pane-kind.note,
|
|
349
348
|
.ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
|
|
350
|
-
.ann-pane-kind.commentary { background: var(--ann-commentary); color:
|
|
351
|
-
.ann-pane-kind.translation { background: var(--ann-translation); color:
|
|
352
|
-
.ann-pane-kind.person { background: var(--ann-person); color:
|
|
353
|
-
.ann-pane-kind.place { background: var(--ann-place); color:
|
|
354
|
-
.ann-pane-kind.event { background: var(--ann-event); color:
|
|
355
|
-
.ann-pane-kind.date { background: var(--ann-date); color:
|
|
356
|
-
.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); }
|
|
357
356
|
|
|
358
357
|
.ann-pane-layer {
|
|
359
358
|
font-size: 10px;
|
|
@@ -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>
|
|
@@ -228,18 +231,18 @@ onBeforeUnmount(() => {
|
|
|
228
231
|
letter-spacing: 1px;
|
|
229
232
|
line-height: 1.5;
|
|
230
233
|
}
|
|
231
|
-
.ann-kind.pronunciation { background: var(--jade); color:
|
|
232
|
-
.ann-kind.semantic { background: var(--vermillion); color:
|
|
233
|
-
.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); }
|
|
234
237
|
.ann-kind.note,
|
|
235
238
|
.ann-kind.definition { background: var(--ink); color: var(--paper); }
|
|
236
|
-
.ann-kind.commentary { background: var(--ann-commentary); color:
|
|
237
|
-
.ann-kind.translation { background: var(--ann-translation); color:
|
|
238
|
-
.ann-kind.person { background: var(--ann-person); color:
|
|
239
|
-
.ann-kind.place { background: var(--ann-place); color:
|
|
240
|
-
.ann-kind.event { background: var(--ann-event); color:
|
|
241
|
-
.ann-kind.date { background: var(--ann-date); color:
|
|
242
|
-
.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); }
|
|
243
246
|
|
|
244
247
|
.ann-layer {
|
|
245
248
|
font-size: 10px;
|
|
@@ -393,6 +396,34 @@ onBeforeUnmount(() => {
|
|
|
393
396
|
background: rgba(58, 90, 140, 0.12) !important;
|
|
394
397
|
box-shadow: 0 0 0 2px rgba(58, 90, 140, 0.15);
|
|
395
398
|
}
|
|
399
|
+
:global(.ann-target.ann-active.place) {
|
|
400
|
+
background: rgba(139, 105, 20, 0.12) !important;
|
|
401
|
+
box-shadow: 0 0 0 2px rgba(139, 105, 20, 0.15);
|
|
402
|
+
}
|
|
403
|
+
:global(.ann-target.ann-active.event) {
|
|
404
|
+
background: rgba(107, 76, 138, 0.12) !important;
|
|
405
|
+
box-shadow: 0 0 0 2px rgba(107, 76, 138, 0.15);
|
|
406
|
+
}
|
|
407
|
+
:global(.ann-target.ann-active.date) {
|
|
408
|
+
background: rgba(42, 122, 122, 0.12) !important;
|
|
409
|
+
box-shadow: 0 0 0 2px rgba(42, 122, 122, 0.15);
|
|
410
|
+
}
|
|
411
|
+
:global(.ann-target.ann-active.allusion) {
|
|
412
|
+
background: rgba(181, 101, 29, 0.12) !important;
|
|
413
|
+
box-shadow: 0 0 0 2px rgba(181, 101, 29, 0.15);
|
|
414
|
+
}
|
|
415
|
+
:global(.ann-target.ann-active.etymology) {
|
|
416
|
+
background: rgba(107, 91, 149, 0.12) !important;
|
|
417
|
+
box-shadow: 0 0 0 2px rgba(107, 91, 149, 0.15);
|
|
418
|
+
}
|
|
419
|
+
:global(.ann-target.ann-active.commentary) {
|
|
420
|
+
background: rgba(192, 57, 43, 0.12) !important;
|
|
421
|
+
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.15);
|
|
422
|
+
}
|
|
423
|
+
:global(.ann-target.ann-active.translation) {
|
|
424
|
+
background: rgba(44, 110, 73, 0.12) !important;
|
|
425
|
+
box-shadow: 0 0 0 2px rgba(44, 110, 73, 0.15);
|
|
426
|
+
}
|
|
396
427
|
|
|
397
428
|
@media (min-width: 768px) {
|
|
398
429
|
.ann-sheet { display: none; }
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
3
|
+
import { useI18n } from '../composables/useI18n'
|
|
4
|
+
|
|
5
|
+
const { t } = useI18n()
|
|
3
6
|
|
|
4
7
|
const props = defineProps<{
|
|
5
8
|
vertical?: boolean
|
|
@@ -58,7 +61,7 @@ onUnmounted(detach)
|
|
|
58
61
|
|
|
59
62
|
<template>
|
|
60
63
|
<Transition name="btt">
|
|
61
|
-
<button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @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.backToTop')">
|
|
62
65
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
63
66
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
64
67
|
</svg>
|
|
@@ -93,29 +96,13 @@ onUnmounted(detach)
|
|
|
93
96
|
right: auto;
|
|
94
97
|
left: calc(var(--nav-width, 56px) + 12px);
|
|
95
98
|
}
|
|
96
|
-
width: 40px;
|
|
97
|
-
height: 40px;
|
|
98
|
-
border-radius: 50%;
|
|
99
|
-
border: 1px solid var(--border);
|
|
100
|
-
background: var(--surface);
|
|
101
|
-
color: var(--ink-light);
|
|
102
|
-
cursor: pointer;
|
|
103
|
-
z-index: 400;
|
|
104
|
-
display: flex;
|
|
105
|
-
align-items: center;
|
|
106
|
-
justify-content: center;
|
|
107
|
-
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.1);
|
|
108
|
-
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
109
|
-
backdrop-filter: blur(8px);
|
|
110
|
-
-webkit-backdrop-filter: blur(8px);
|
|
111
|
-
}
|
|
112
99
|
|
|
113
100
|
@media (max-width: 768px) {
|
|
114
101
|
.btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
|
|
115
102
|
}
|
|
116
103
|
.btt:hover {
|
|
117
104
|
background: var(--vermillion);
|
|
118
|
-
color:
|
|
105
|
+
color: var(--paper);
|
|
119
106
|
border-color: var(--vermillion);
|
|
120
107
|
transform: translateY(-3px);
|
|
121
108
|
box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.15);
|
|
@@ -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>
|
|
@@ -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,8 +14,8 @@ 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 ? '
|
|
18
|
-
<span v-if="!open" class="rt-icon"
|
|
17
|
+
<button class="rt-fab" @click="toggle" :aria-label="open ? t('settings.close') : t('settings.reading')">
|
|
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>
|
|
@@ -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" />
|
|
@@ -104,7 +104,7 @@ const paragraphsHtml = computed(() => {
|
|
|
104
104
|
.sb-num {
|
|
105
105
|
display: inline-flex; align-items: center; justify-content: center;
|
|
106
106
|
width: 28px; height: 28px; border-radius: 50%;
|
|
107
|
-
background: var(--vermillion); color:
|
|
107
|
+
background: var(--vermillion); color: var(--paper);
|
|
108
108
|
font-family: var(--sans); font-size: 13px; font-weight: 700;
|
|
109
109
|
flex-shrink: 0;
|
|
110
110
|
}
|
|
@@ -142,7 +142,7 @@ const paragraphsHtml = computed(() => {
|
|
|
142
142
|
height: 22px;
|
|
143
143
|
border-radius: 4px;
|
|
144
144
|
background: var(--vermillion);
|
|
145
|
-
color:
|
|
145
|
+
color: var(--paper);
|
|
146
146
|
font-family: var(--sans);
|
|
147
147
|
font-size: 12px;
|
|
148
148
|
font-weight: 700;
|
|
@@ -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,39 @@ 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
|
+
'action.backToTop': '回到頂部',
|
|
82
|
+
'shortcut.toggleLayout': '直/橫',
|
|
83
|
+
'shortcut.toggleTheme': '主題',
|
|
84
|
+
'shortcut.goHome': '首頁',
|
|
85
|
+
'settings.shortTitle': '設定',
|
|
86
|
+
'layout.verticalShort': '直',
|
|
87
|
+
'author.courtesyName': '字{name}',
|
|
88
|
+
'author.artName': '號{name}',
|
|
89
|
+
'link.wikipedia': '維基百科',
|
|
90
|
+
'link.ctext': '文庫',
|
|
91
|
+
'role.defaultAuthor': '作者',
|
|
92
|
+
'piece.defaultAuthorInitial': '詩',
|
|
93
|
+
'theme.light': '亮',
|
|
94
|
+
'theme.sepia': '暖',
|
|
95
|
+
'theme.dark': '暗',
|
|
96
|
+
'theme.oled': '黑',
|
|
97
|
+
'annotation.kind.pronunciation': '讀音',
|
|
98
|
+
'annotation.kind.semantic': '釋義',
|
|
99
|
+
'annotation.kind.etymology': '詞源',
|
|
100
|
+
'annotation.kind.note': '備注',
|
|
101
|
+
'annotation.kind.definition': '釋義',
|
|
102
|
+
'annotation.kind.commentary': '注',
|
|
103
|
+
'annotation.kind.translation': '譯文',
|
|
104
|
+
'annotation.kind.person': '人名',
|
|
105
|
+
'annotation.kind.place': '地名',
|
|
106
|
+
'annotation.kind.event': '事件',
|
|
107
|
+
'annotation.kind.date': '紀年',
|
|
108
|
+
'annotation.kind.allusion': '典故',
|
|
76
109
|
},
|
|
77
110
|
'zh-Hans': {
|
|
78
111
|
'site.title': '古典诗文图书馆',
|
|
@@ -138,6 +171,39 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
138
171
|
'section.preparation': '预习活动',
|
|
139
172
|
'section.follow_up': '跟进活动',
|
|
140
173
|
'section.think_questions': '想一想',
|
|
174
|
+
'annotation.showAnnotations': '显示注释',
|
|
175
|
+
'annotation.hideAnnotations': '隐藏注释',
|
|
176
|
+
'annotation.noteCount': '{count}注',
|
|
177
|
+
'action.close': '关闭',
|
|
178
|
+
'action.backToStart': '回到起始',
|
|
179
|
+
'action.backToTop': '回到顶部',
|
|
180
|
+
'shortcut.toggleLayout': '直/横',
|
|
181
|
+
'shortcut.toggleTheme': '主题',
|
|
182
|
+
'shortcut.goHome': '首页',
|
|
183
|
+
'settings.shortTitle': '设定',
|
|
184
|
+
'layout.verticalShort': '直',
|
|
185
|
+
'author.courtesyName': '字{name}',
|
|
186
|
+
'author.artName': '号{name}',
|
|
187
|
+
'link.wikipedia': '维基百科',
|
|
188
|
+
'link.ctext': '文库',
|
|
189
|
+
'role.defaultAuthor': '作者',
|
|
190
|
+
'piece.defaultAuthorInitial': '诗',
|
|
191
|
+
'theme.light': '亮',
|
|
192
|
+
'theme.sepia': '暖',
|
|
193
|
+
'theme.dark': '暗',
|
|
194
|
+
'theme.oled': '黑',
|
|
195
|
+
'annotation.kind.pronunciation': '读音',
|
|
196
|
+
'annotation.kind.semantic': '释义',
|
|
197
|
+
'annotation.kind.etymology': '词源',
|
|
198
|
+
'annotation.kind.note': '备注',
|
|
199
|
+
'annotation.kind.definition': '释义',
|
|
200
|
+
'annotation.kind.commentary': '注',
|
|
201
|
+
'annotation.kind.translation': '译文',
|
|
202
|
+
'annotation.kind.person': '人名',
|
|
203
|
+
'annotation.kind.place': '地名',
|
|
204
|
+
'annotation.kind.event': '事件',
|
|
205
|
+
'annotation.kind.date': '纪年',
|
|
206
|
+
'annotation.kind.allusion': '典故',
|
|
141
207
|
},
|
|
142
208
|
'en': {
|
|
143
209
|
'site.title': 'Classical Chinese Text Library',
|
|
@@ -203,6 +269,39 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
203
269
|
'section.preparation': 'Pre-reading',
|
|
204
270
|
'section.follow_up': 'Follow-up',
|
|
205
271
|
'section.think_questions': 'Think About It',
|
|
272
|
+
'annotation.showAnnotations': 'Show annotations',
|
|
273
|
+
'annotation.hideAnnotations': 'Hide annotations',
|
|
274
|
+
'annotation.noteCount': '{count} notes',
|
|
275
|
+
'action.close': 'Close',
|
|
276
|
+
'action.backToStart': 'Back to start',
|
|
277
|
+
'action.backToTop': 'Back to top',
|
|
278
|
+
'shortcut.toggleLayout': 'V/H',
|
|
279
|
+
'shortcut.toggleTheme': 'Theme',
|
|
280
|
+
'shortcut.goHome': 'Home',
|
|
281
|
+
'settings.shortTitle': 'Settings',
|
|
282
|
+
'layout.verticalShort': 'V',
|
|
283
|
+
'author.courtesyName': 'Courtesy: {name}',
|
|
284
|
+
'author.artName': 'Art name: {name}',
|
|
285
|
+
'link.wikipedia': 'Wikipedia',
|
|
286
|
+
'link.ctext': 'CTEXT',
|
|
287
|
+
'role.defaultAuthor': 'Author',
|
|
288
|
+
'piece.defaultAuthorInitial': 'P',
|
|
289
|
+
'theme.light': 'Light',
|
|
290
|
+
'theme.sepia': 'Warm',
|
|
291
|
+
'theme.dark': 'Dark',
|
|
292
|
+
'theme.oled': 'Black',
|
|
293
|
+
'annotation.kind.pronunciation': 'Pronunciation',
|
|
294
|
+
'annotation.kind.semantic': 'Definition',
|
|
295
|
+
'annotation.kind.etymology': 'Etymology',
|
|
296
|
+
'annotation.kind.note': 'Note',
|
|
297
|
+
'annotation.kind.definition': 'Definition',
|
|
298
|
+
'annotation.kind.commentary': 'Commentary',
|
|
299
|
+
'annotation.kind.translation': 'Translation',
|
|
300
|
+
'annotation.kind.person': 'Person',
|
|
301
|
+
'annotation.kind.place': 'Place',
|
|
302
|
+
'annotation.kind.event': 'Event',
|
|
303
|
+
'annotation.kind.date': 'Date',
|
|
304
|
+
'annotation.kind.allusion': 'Allusion',
|
|
206
305
|
},
|
|
207
306
|
}
|
|
208
307
|
|
|
@@ -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
|
|
|
@@ -45,6 +45,18 @@
|
|
|
45
45
|
background: rgba(181, 101, 29, 0.1);
|
|
46
46
|
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(181, 101, 29, 0.08);
|
|
47
47
|
}
|
|
48
|
+
.ann-target.etymology:hover {
|
|
49
|
+
background: rgba(107, 91, 149, 0.1);
|
|
50
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(107, 91, 149, 0.08);
|
|
51
|
+
}
|
|
52
|
+
.ann-target.commentary:hover {
|
|
53
|
+
background: rgba(192, 57, 43, 0.1);
|
|
54
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(192, 57, 43, 0.08);
|
|
55
|
+
}
|
|
56
|
+
.ann-target.translation:hover {
|
|
57
|
+
background: rgba(44, 110, 73, 0.1);
|
|
58
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(44, 110, 73, 0.08);
|
|
59
|
+
}
|
|
48
60
|
|
|
49
61
|
/* ─── Annotation number ─── */
|
|
50
62
|
.ann-num {
|
|
@@ -234,10 +234,6 @@ button:focus-visible {
|
|
|
234
234
|
from { opacity: 0; transform: translateY(12px); }
|
|
235
235
|
to { opacity: 1; transform: translateY(0); }
|
|
236
236
|
}
|
|
237
|
-
@keyframes fadeIn {
|
|
238
|
-
from { opacity: 0; }
|
|
239
|
-
to { opacity: 1; }
|
|
240
|
-
}
|
|
241
237
|
@keyframes scaleIn {
|
|
242
238
|
from { opacity: 0; transform: scale(0.92); }
|
|
243
239
|
to { opacity: 1; transform: scale(1); }
|
|
@@ -332,3 +328,13 @@ button:focus-visible {
|
|
|
332
328
|
[data-theme="oled"] .sb-vertical {
|
|
333
329
|
border-right-color: var(--border-light);
|
|
334
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,20 +1,24 @@
|
|
|
1
1
|
import type { Annotation } from '../types'
|
|
2
|
+
import { useI18n } from '../composables/useI18n'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
|
|
6
|
+
const KIND_I18N_KEYS: Record<string, string> = {
|
|
7
|
+
pronunciation: 'annotation.kind.pronunciation',
|
|
8
|
+
semantic: 'annotation.kind.semantic',
|
|
9
|
+
etymology: 'annotation.kind.etymology',
|
|
10
|
+
note: 'annotation.kind.note',
|
|
11
|
+
definition: 'annotation.kind.definition',
|
|
12
|
+
commentary: 'annotation.kind.commentary',
|
|
13
|
+
translation: 'annotation.kind.translation',
|
|
14
|
+
person: 'annotation.kind.person',
|
|
15
|
+
place: 'annotation.kind.place',
|
|
16
|
+
event: 'annotation.kind.event',
|
|
17
|
+
date: 'annotation.kind.date',
|
|
18
|
+
allusion: 'annotation.kind.allusion',
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export function kindLabel(ann: Annotation): string {
|
|
19
|
-
|
|
22
|
+
const key = KIND_I18N_KEYS[ann.kind]
|
|
23
|
+
return key ? t(key) : ann.kind
|
|
20
24
|
}
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -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(() => {
|
|
@@ -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
|
|
|
@@ -127,7 +130,7 @@ function scrollToCatalog() {
|
|
|
127
130
|
<p v-if="meta?.publisher">{{ meta.publisher }}</p>
|
|
128
131
|
</div>
|
|
129
132
|
<div class="h-filter">
|
|
130
|
-
<input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" />
|
|
133
|
+
<input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" :aria-label="t('catalog.search')" />
|
|
131
134
|
</div>
|
|
132
135
|
<div class="h-grid">
|
|
133
136
|
<PoemCard
|
|
@@ -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; }
|
|
@@ -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">
|
|
@@ -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] || '
|
|
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
|
}
|
|
@@ -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,14 +510,14 @@ 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"
|
|
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">
|
|
@@ -537,7 +537,7 @@ function tcy(n: number): string {
|
|
|
537
537
|
<div v-else class="h-root">
|
|
538
538
|
<ReadingProgress />
|
|
539
539
|
<div class="h-page">
|
|
540
|
-
<nav class="h-nav">
|
|
540
|
+
<nav class="h-nav" aria-label="piece navigation">
|
|
541
541
|
<div class="h-nav-inner">
|
|
542
542
|
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
543
543
|
<div class="h-nav-title-row">
|
|
@@ -671,24 +671,16 @@ 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>
|
|
679
679
|
<div class="h-pane-links">
|
|
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">
|
|
681
|
-
|
|
682
|
-
</a>
|
|
683
|
-
<a v-if="selectedAuthorData?.
|
|
684
|
-
<span class="link-icon">維</span> 維基百科
|
|
685
|
-
</a>
|
|
686
|
-
<a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="h-pane-link">
|
|
687
|
-
<span class="link-icon">W</span> Wikipedia
|
|
688
|
-
</a>
|
|
689
|
-
<a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="h-pane-link">
|
|
690
|
-
<span class="link-icon">Q</span> Wikidata
|
|
691
|
-
</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>
|
|
692
684
|
</div>
|
|
693
685
|
<div v-if="selectedAuthorBio" class="h-pane-bio">
|
|
694
686
|
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="h-pane-p">
|
|
@@ -865,7 +857,7 @@ function tcy(n: number): string {
|
|
|
865
857
|
.h-page { min-height: 100vh; }
|
|
866
858
|
.h-nav {
|
|
867
859
|
position: sticky; top: 0; z-index: 100;
|
|
868
|
-
background: var(--paper);
|
|
860
|
+
background: var(--paper);
|
|
869
861
|
backdrop-filter: blur(20px);
|
|
870
862
|
border-bottom: 1px solid var(--border-light);
|
|
871
863
|
padding: 0 40px;
|
|
@@ -901,7 +893,7 @@ function tcy(n: number): string {
|
|
|
901
893
|
align-items: center;
|
|
902
894
|
padding: 2px 8px;
|
|
903
895
|
background: var(--vermillion);
|
|
904
|
-
color:
|
|
896
|
+
color: var(--paper);
|
|
905
897
|
font-family: var(--sans);
|
|
906
898
|
font-size: 11px;
|
|
907
899
|
font-weight: 700;
|
|
@@ -1055,12 +1047,16 @@ function tcy(n: number): string {
|
|
|
1055
1047
|
/* Overlay transition */
|
|
1056
1048
|
.overlay-enter-active { transition: opacity var(--dur-mid, 0.25s) ease; }
|
|
1057
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); }
|
|
1058
1051
|
.overlay-leave-active { transition: opacity var(--dur-fast, 0.15s) ease; }
|
|
1059
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; }
|
|
1060
1054
|
.overlay-enter-from { opacity: 0; }
|
|
1061
1055
|
.overlay-enter-from .h-pane { transform: translateX(100%); }
|
|
1056
|
+
.overlay-enter-from .v-author-pane { transform: translateX(-100%); }
|
|
1062
1057
|
.overlay-leave-to { opacity: 0; }
|
|
1063
1058
|
.overlay-leave-to .h-pane { transform: translateX(40px); }
|
|
1059
|
+
.overlay-leave-to .v-author-pane { transform: translateX(-40px); }
|
|
1064
1060
|
.h-pane-close {
|
|
1065
1061
|
display: block; margin-left: auto;
|
|
1066
1062
|
width: 36px; height: 36px;
|
|
@@ -1087,7 +1083,7 @@ function tcy(n: number): string {
|
|
|
1087
1083
|
display: inline-flex;
|
|
1088
1084
|
padding: 2px 8px;
|
|
1089
1085
|
background: var(--vermillion);
|
|
1090
|
-
color:
|
|
1086
|
+
color: var(--paper);
|
|
1091
1087
|
font-family: var(--sans);
|
|
1092
1088
|
font-size: 11px;
|
|
1093
1089
|
font-weight: 700;
|
|
@@ -1331,10 +1327,6 @@ function tcy(n: number): string {
|
|
|
1331
1327
|
:deep(.ann-flash) {
|
|
1332
1328
|
animation: ann-flash-anim 1.5s ease-out;
|
|
1333
1329
|
}
|
|
1334
|
-
@keyframes ann-flash-anim {
|
|
1335
|
-
0% { background: rgba(194, 58, 43, 0.25); box-shadow: 0 0 12px rgba(194, 58, 43, 0.15); }
|
|
1336
|
-
100% { background: transparent; box-shadow: none; }
|
|
1337
|
-
}
|
|
1338
1330
|
|
|
1339
1331
|
/* ═══════ 行動裝置適配 ═══════ */
|
|
1340
1332
|
|