@hanology/cham-browser 0.4.61 → 0.4.62
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/AnnotationPane.vue +14 -4
- package/template/src/components/AnnotationTooltip.vue +4 -3
- package/template/src/components/BackToTop.vue +64 -7
- package/template/src/components/PoemCard.vue +1 -1
- package/template/src/components/ReadingProgress.vue +2 -2
- package/template/src/styles/main.css +12 -0
- package/template/src/views/AuthorView.vue +6 -0
- package/template/src/views/BookHome.vue +4 -0
- package/template/src/views/PieceView.vue +2 -1
package/package.json
CHANGED
|
@@ -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,6 +23,7 @@ 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
28
|
const isMobile = computed(() => ww.value < 768)
|
|
27
29
|
function onResize() { ww.value = window.innerWidth }
|
|
@@ -158,7 +160,7 @@ onBeforeUnmount(() => {
|
|
|
158
160
|
:style="{ width: paneWidth + 'px' }"
|
|
159
161
|
>
|
|
160
162
|
<div class="ann-pane-header">
|
|
161
|
-
<span class="ann-pane-title"
|
|
163
|
+
<span class="ann-pane-title">{{ t('annotation.all') }}</span>
|
|
162
164
|
<span class="ann-pane-count">{{ annotations.length }}</span>
|
|
163
165
|
<button class="ann-pane-close" @click="emit('close')" aria-label="關閉">
|
|
164
166
|
<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>
|
|
@@ -171,7 +173,10 @@ onBeforeUnmount(() => {
|
|
|
171
173
|
:data-ann-id="ann.id"
|
|
172
174
|
class="ann-pane-entry"
|
|
173
175
|
:class="{ active: activeId === ann.id, [ann.kind]: true }"
|
|
176
|
+
role="button"
|
|
177
|
+
tabindex="0"
|
|
174
178
|
@click="emit('select', ann)"
|
|
179
|
+
@keydown.enter="emit('select', ann)"
|
|
175
180
|
>
|
|
176
181
|
<!-- Vertical: headword column on the right side -->
|
|
177
182
|
<div v-if="vertical && headword(ann)" class="ann-pane-v-word">
|
|
@@ -289,6 +294,11 @@ onBeforeUnmount(() => {
|
|
|
289
294
|
background: var(--surface);
|
|
290
295
|
}
|
|
291
296
|
|
|
297
|
+
.ann-pane-entry:focus-visible {
|
|
298
|
+
outline: 2px solid var(--vermillion);
|
|
299
|
+
outline-offset: -2px;
|
|
300
|
+
}
|
|
301
|
+
|
|
292
302
|
.ann-pane-entry.active.pronunciation {
|
|
293
303
|
border-left-color: var(--jade);
|
|
294
304
|
}
|
|
@@ -334,11 +344,11 @@ onBeforeUnmount(() => {
|
|
|
334
344
|
|
|
335
345
|
.ann-pane-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
336
346
|
.ann-pane-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
337
|
-
.ann-pane-kind.etymology { background:
|
|
347
|
+
.ann-pane-kind.etymology { background: var(--ann-etymology); color: #fff; }
|
|
338
348
|
.ann-pane-kind.note,
|
|
339
349
|
.ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
|
|
340
|
-
.ann-pane-kind.commentary { background:
|
|
341
|
-
.ann-pane-kind.translation { background:
|
|
350
|
+
.ann-pane-kind.commentary { background: var(--ann-commentary); color: #fff; }
|
|
351
|
+
.ann-pane-kind.translation { background: var(--ann-translation); color: #fff; }
|
|
342
352
|
.ann-pane-kind.person { background: var(--ann-person); color: #fff; }
|
|
343
353
|
.ann-pane-kind.place { background: var(--ann-place); color: #fff; }
|
|
344
354
|
.ann-pane-kind.event { background: var(--ann-event); color: #fff; }
|
|
@@ -199,6 +199,7 @@ onBeforeUnmount(() => {
|
|
|
199
199
|
/* ─── Annotation entry ─── */
|
|
200
200
|
.ann-entry {
|
|
201
201
|
border-bottom: 1px solid var(--border-light);
|
|
202
|
+
padding: 8px 0;
|
|
202
203
|
font-size: 14px;
|
|
203
204
|
color: var(--ink-mid);
|
|
204
205
|
letter-spacing: 1.5px;
|
|
@@ -229,11 +230,11 @@ onBeforeUnmount(() => {
|
|
|
229
230
|
}
|
|
230
231
|
.ann-kind.pronunciation { background: var(--jade); color: #fff; }
|
|
231
232
|
.ann-kind.semantic { background: var(--vermillion); color: #fff; }
|
|
232
|
-
.ann-kind.etymology { background:
|
|
233
|
+
.ann-kind.etymology { background: var(--ann-etymology); color: #fff; }
|
|
233
234
|
.ann-kind.note,
|
|
234
235
|
.ann-kind.definition { background: var(--ink); color: var(--paper); }
|
|
235
|
-
.ann-kind.commentary { background:
|
|
236
|
-
.ann-kind.translation { background:
|
|
236
|
+
.ann-kind.commentary { background: var(--ann-commentary); color: #fff; }
|
|
237
|
+
.ann-kind.translation { background: var(--ann-translation); color: #fff; }
|
|
237
238
|
.ann-kind.person { background: var(--ann-person); color: #fff; }
|
|
238
239
|
.ann-kind.place { background: var(--ann-place); color: #fff; }
|
|
239
240
|
.ann-kind.event { background: var(--ann-event); color: #fff; }
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
vertical?: boolean
|
|
6
|
+
scrollContainer?: HTMLElement | null
|
|
7
|
+
}>()
|
|
3
8
|
|
|
4
9
|
const visible = ref(false)
|
|
5
10
|
let ticking = false
|
|
@@ -8,22 +13,52 @@ function onScroll() {
|
|
|
8
13
|
if (ticking) return
|
|
9
14
|
ticking = true
|
|
10
15
|
requestAnimationFrame(() => {
|
|
11
|
-
|
|
16
|
+
if (props.vertical && props.scrollContainer) {
|
|
17
|
+
visible.value = props.scrollContainer.scrollLeft > 400
|
|
18
|
+
} else {
|
|
19
|
+
visible.value = window.scrollY > 400
|
|
20
|
+
}
|
|
12
21
|
ticking = false
|
|
13
22
|
})
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
function scrollToTop() {
|
|
17
|
-
|
|
26
|
+
if (props.vertical && props.scrollContainer) {
|
|
27
|
+
props.scrollContainer.scrollTo({ left: props.scrollContainer.scrollWidth, behavior: 'smooth' })
|
|
28
|
+
} else {
|
|
29
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function attach() {
|
|
34
|
+
if (props.vertical && props.scrollContainer) {
|
|
35
|
+
props.scrollContainer.addEventListener('scroll', onScroll, { passive: true })
|
|
36
|
+
} else {
|
|
37
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
38
|
+
}
|
|
39
|
+
onScroll()
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
function detach() {
|
|
43
|
+
if (props.vertical && props.scrollContainer) {
|
|
44
|
+
props.scrollContainer.removeEventListener('scroll', onScroll)
|
|
45
|
+
} else {
|
|
46
|
+
window.removeEventListener('scroll', onScroll)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
watch(() => props.scrollContainer, () => {
|
|
51
|
+
detach()
|
|
52
|
+
attach()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
onMounted(attach)
|
|
56
|
+
onUnmounted(detach)
|
|
22
57
|
</script>
|
|
23
58
|
|
|
24
59
|
<template>
|
|
25
60
|
<Transition name="btt">
|
|
26
|
-
<button v-if="visible" class="btt" @click="scrollToTop" aria-label="回到頂部">
|
|
61
|
+
<button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" aria-label="回到頂部">
|
|
27
62
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
63
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
29
64
|
</svg>
|
|
@@ -52,6 +87,28 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
|
|
52
87
|
backdrop-filter: blur(8px);
|
|
53
88
|
-webkit-backdrop-filter: blur(8px);
|
|
54
89
|
}
|
|
90
|
+
.btt.btt-v {
|
|
91
|
+
bottom: auto;
|
|
92
|
+
top: 24px;
|
|
93
|
+
right: auto;
|
|
94
|
+
left: calc(var(--nav-width, 56px) + 12px);
|
|
95
|
+
}
|
|
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
|
+
}
|
|
55
112
|
|
|
56
113
|
@media (max-width: 768px) {
|
|
57
114
|
.btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
|
|
@@ -61,7 +118,7 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
|
|
61
118
|
color: #fff;
|
|
62
119
|
border-color: var(--vermillion);
|
|
63
120
|
transform: translateY(-3px);
|
|
64
|
-
box-shadow: 0 8px 24px rgba(
|
|
121
|
+
box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.15);
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
.btt-enter-active { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -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 {
|
|
@@ -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; }
|
|
@@ -530,6 +530,7 @@ 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
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
@@ -694,7 +695,7 @@ function tcy(n: number): string {
|
|
|
694
695
|
{{ p.trim() }}
|
|
695
696
|
</div>
|
|
696
697
|
</div>
|
|
697
|
-
<div v-if="!selectedAuthorBio" class="h-pane-empty"
|
|
698
|
+
<div v-if="!selectedAuthorBio" class="h-pane-empty">{{ t('piece.noAuthorData') }}</div>
|
|
698
699
|
</div>
|
|
699
700
|
</div>
|
|
700
701
|
</Transition>
|