@hanology/cham-browser 0.3.4 → 0.3.5
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/dist/cli.js +0 -0
- package/package.json +1 -1
- package/template/src/components/PartBlock.vue +27 -1
- package/template/src/components/PartGroup.vue +1 -0
- package/template/src/components/ReadingProgress.vue +83 -0
- package/template/src/components/ReadingToolbar.vue +36 -0
- package/template/src/components/SectionBlock.vue +32 -7
- package/template/src/components/SideNav.vue +36 -0
- package/template/src/types.ts +4 -2
- package/template/src/views/AboutView.vue +6 -0
- package/template/src/views/BookHome.vue +1 -0
- package/template/src/views/LibraryHome.vue +9 -2
- package/template/src/views/PieceView.vue +46 -6
package/dist/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ const props = defineProps<{
|
|
|
8
8
|
annotations: Annotation[]
|
|
9
9
|
vertical?: boolean
|
|
10
10
|
source?: PieceSource
|
|
11
|
+
annotationText?: string
|
|
11
12
|
}>()
|
|
12
13
|
|
|
13
14
|
const emit = defineEmits<{
|
|
@@ -55,6 +56,9 @@ const sourceLabel = (() => {
|
|
|
55
56
|
v-html="verseHtml(i)"
|
|
56
57
|
/>
|
|
57
58
|
</div>
|
|
59
|
+
<div v-if="annotationText" class="part-annotations">
|
|
60
|
+
<div v-for="line in annotationText.split('\n')" :key="line" class="part-ann-line">{{ line }}</div>
|
|
61
|
+
</div>
|
|
58
62
|
</div>
|
|
59
63
|
</template>
|
|
60
64
|
|
|
@@ -90,6 +94,20 @@ const sourceLabel = (() => {
|
|
|
90
94
|
line-height: 1;
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
.part-annotations {
|
|
98
|
+
margin-top: 16px;
|
|
99
|
+
padding-top: 12px;
|
|
100
|
+
border-top: 1px dashed var(--border-light);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.part-ann-line {
|
|
104
|
+
font-family: var(--sans);
|
|
105
|
+
font-size: 14px;
|
|
106
|
+
line-height: 2;
|
|
107
|
+
color: var(--ink-mid);
|
|
108
|
+
letter-spacing: 0.5px;
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
.part-line-h {
|
|
94
112
|
font-size: var(--main-font-size, 22px);
|
|
95
113
|
line-height: 2.4;
|
|
@@ -134,7 +152,6 @@ const sourceLabel = (() => {
|
|
|
134
152
|
background: rgba(58, 107, 94, 0.08);
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
/* Vertical mode overrides */
|
|
138
155
|
.part-block--vertical :deep(.ann-target) {
|
|
139
156
|
border-bottom: none;
|
|
140
157
|
border-left: 2px solid var(--vermillion);
|
|
@@ -157,4 +174,13 @@ const sourceLabel = (() => {
|
|
|
157
174
|
margin-bottom: 0;
|
|
158
175
|
margin-left: 8px;
|
|
159
176
|
}
|
|
177
|
+
|
|
178
|
+
.part-block--vertical .part-annotations {
|
|
179
|
+
margin-top: 0;
|
|
180
|
+
margin-left: 12px;
|
|
181
|
+
padding-top: 0;
|
|
182
|
+
padding-left: 12px;
|
|
183
|
+
border-top: none;
|
|
184
|
+
border-left: 1px dashed var(--border-light);
|
|
185
|
+
}
|
|
160
186
|
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
vertical?: boolean
|
|
6
|
+
scrollContainer?: HTMLElement | null
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const progress = ref(0)
|
|
10
|
+
let raf = 0
|
|
11
|
+
|
|
12
|
+
function updateProgress() {
|
|
13
|
+
if (props.vertical && props.scrollContainer) {
|
|
14
|
+
const el = props.scrollContainer
|
|
15
|
+
const max = el.scrollWidth - el.clientWidth
|
|
16
|
+
progress.value = max > 0 ? Math.min((el.scrollLeft / max) * 100, 100) : 0
|
|
17
|
+
} else if (!props.vertical) {
|
|
18
|
+
const max = document.documentElement.scrollHeight - window.innerHeight
|
|
19
|
+
progress.value = max > 0 ? Math.min((window.scrollY / max) * 100, 100) : 0
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onScroll() {
|
|
24
|
+
cancelAnimationFrame(raf)
|
|
25
|
+
raf = requestAnimationFrame(updateProgress)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function attach() {
|
|
29
|
+
if (props.vertical && props.scrollContainer) {
|
|
30
|
+
props.scrollContainer.addEventListener('scroll', onScroll, { passive: true })
|
|
31
|
+
} else if (!props.vertical) {
|
|
32
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
33
|
+
}
|
|
34
|
+
updateProgress()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detach() {
|
|
38
|
+
if (props.vertical && props.scrollContainer) {
|
|
39
|
+
props.scrollContainer.removeEventListener('scroll', onScroll)
|
|
40
|
+
} else {
|
|
41
|
+
window.removeEventListener('scroll', onScroll)
|
|
42
|
+
}
|
|
43
|
+
cancelAnimationFrame(raf)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
watch(() => props.scrollContainer, (el, old) => {
|
|
47
|
+
detach()
|
|
48
|
+
if (old) old.removeEventListener('scroll', onScroll)
|
|
49
|
+
attach()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
onMounted(attach)
|
|
53
|
+
onUnmounted(detach)
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div
|
|
58
|
+
class="rp"
|
|
59
|
+
:class="{ 'rp-v': vertical }"
|
|
60
|
+
:style="vertical
|
|
61
|
+
? { height: progress + '%' }
|
|
62
|
+
: { width: progress + '%' }"
|
|
63
|
+
/>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.rp {
|
|
68
|
+
position: fixed;
|
|
69
|
+
z-index: 1001;
|
|
70
|
+
background: linear-gradient(90deg, var(--vermillion), var(--vermillion-light));
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
will-change: width, height;
|
|
73
|
+
}
|
|
74
|
+
.rp:not(.rp-v) {
|
|
75
|
+
top: 0; left: 0;
|
|
76
|
+
height: 2px;
|
|
77
|
+
}
|
|
78
|
+
.rp-v {
|
|
79
|
+
top: 0; left: 0;
|
|
80
|
+
width: 2px;
|
|
81
|
+
background: linear-gradient(180deg, var(--vermillion), var(--vermillion-light));
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -74,6 +74,11 @@ function close() { open.value = false }
|
|
|
74
74
|
>{{ localeLabels[loc] }}</button>
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
77
|
+
<div class="rt-shortcuts">
|
|
78
|
+
<div class="rt-sc"><kbd>V</kbd> 直/橫</div>
|
|
79
|
+
<div class="rt-sc"><kbd>T</kbd> 主題</div>
|
|
80
|
+
<div class="rt-sc"><kbd>Esc</kbd> 首頁</div>
|
|
81
|
+
</div>
|
|
77
82
|
</div>
|
|
78
83
|
<div v-if="open" class="rt-backdrop" @click="close" />
|
|
79
84
|
</div>
|
|
@@ -182,4 +187,35 @@ function close() { open.value = false }
|
|
|
182
187
|
position: fixed; inset: 0;
|
|
183
188
|
z-index: -1;
|
|
184
189
|
}
|
|
190
|
+
.rt-shortcuts {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
gap: 6px;
|
|
194
|
+
padding-top: 10px;
|
|
195
|
+
border-top: 1px solid var(--border-light);
|
|
196
|
+
margin-top: 2px;
|
|
197
|
+
}
|
|
198
|
+
.rt-sc {
|
|
199
|
+
font-family: var(--sans);
|
|
200
|
+
font-size: 10px;
|
|
201
|
+
color: var(--ink-faint);
|
|
202
|
+
letter-spacing: 1px;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 3px;
|
|
206
|
+
}
|
|
207
|
+
.rt-sc kbd {
|
|
208
|
+
display: inline-flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
min-width: 18px;
|
|
212
|
+
height: 16px;
|
|
213
|
+
padding: 0 3px;
|
|
214
|
+
border: 1px solid var(--border);
|
|
215
|
+
border-radius: 2px;
|
|
216
|
+
font-family: var(--sans);
|
|
217
|
+
font-size: 9px;
|
|
218
|
+
color: var(--ink-light);
|
|
219
|
+
background: var(--surface);
|
|
220
|
+
}
|
|
185
221
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { parseAnnotationBlock } from '../utils/annotationParser'
|
|
4
4
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
5
5
|
|
|
@@ -12,6 +12,27 @@ const props = defineProps<{
|
|
|
12
12
|
vertical?: boolean
|
|
13
13
|
}>()
|
|
14
14
|
|
|
15
|
+
const rootRef = ref<HTMLElement | null>(null)
|
|
16
|
+
const visible = ref(false)
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
if (props.vertical || !rootRef.value) {
|
|
20
|
+
visible.value = true
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const observer = new IntersectionObserver(
|
|
24
|
+
([entry]) => {
|
|
25
|
+
if (entry.isIntersecting) {
|
|
26
|
+
visible.value = true
|
|
27
|
+
observer.disconnect()
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{ rootMargin: '0px 0px -40px 0px', threshold: 0 }
|
|
31
|
+
)
|
|
32
|
+
observer.observe(rootRef.value)
|
|
33
|
+
onUnmounted(() => observer.disconnect())
|
|
34
|
+
})
|
|
35
|
+
|
|
15
36
|
function esc(str: string): string {
|
|
16
37
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
17
38
|
}
|
|
@@ -40,7 +61,7 @@ const paragraphsHtml = computed(() => {
|
|
|
40
61
|
</script>
|
|
41
62
|
|
|
42
63
|
<template>
|
|
43
|
-
<div v-if="text" class="sb-root" :class="{ 'sb-vertical': vertical }">
|
|
64
|
+
<div v-if="text" ref="rootRef" class="sb-root" :class="{ 'sb-vertical': vertical, 'sb-visible': visible }">
|
|
44
65
|
<div class="sb-header">
|
|
45
66
|
<span v-if="displayNum" class="sb-num" :class="{ special }">{{ displayNum }}</span>
|
|
46
67
|
<h3>{{ special ? '【' + label + '】' : label }}</h3>
|
|
@@ -64,11 +85,13 @@ const paragraphsHtml = computed(() => {
|
|
|
64
85
|
<style scoped>
|
|
65
86
|
.sb-root {
|
|
66
87
|
margin-bottom: 40px;
|
|
67
|
-
|
|
88
|
+
opacity: 0;
|
|
89
|
+
transform: translateY(12px);
|
|
90
|
+
transition: opacity 0.5s ease, transform 0.5s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
68
91
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
.sb-root.sb-visible {
|
|
93
|
+
opacity: 1;
|
|
94
|
+
transform: translateY(0);
|
|
72
95
|
}
|
|
73
96
|
.sb-header {
|
|
74
97
|
display: flex; align-items: center; gap: 12px;
|
|
@@ -107,7 +130,9 @@ const paragraphsHtml = computed(() => {
|
|
|
107
130
|
border-right: 1px solid var(--border);
|
|
108
131
|
overflow-x: auto;
|
|
109
132
|
overflow-y: hidden;
|
|
110
|
-
|
|
133
|
+
opacity: 1;
|
|
134
|
+
transform: none;
|
|
135
|
+
transition: none;
|
|
111
136
|
}
|
|
112
137
|
.sb-vertical .sb-header {
|
|
113
138
|
flex-direction: column;
|
|
@@ -113,6 +113,11 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
113
113
|
>{{ localeLabels[loc] }}</button>
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
|
+
<div class="ss-shortcuts">
|
|
117
|
+
<span class="ss-sc"><kbd>V</kbd> 直/橫</span>
|
|
118
|
+
<span class="ss-sc"><kbd>T</kbd> 主題</span>
|
|
119
|
+
<span class="ss-sc"><kbd>Esc</kbd> 首頁</span>
|
|
120
|
+
</div>
|
|
116
121
|
</div>
|
|
117
122
|
</Transition>
|
|
118
123
|
|
|
@@ -308,6 +313,37 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
308
313
|
position: fixed; inset: 0;
|
|
309
314
|
z-index: -1;
|
|
310
315
|
}
|
|
316
|
+
.ss-shortcuts {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-wrap: wrap;
|
|
319
|
+
gap: 4px 8px;
|
|
320
|
+
padding-top: 10px;
|
|
321
|
+
border-top: 1px solid var(--border-light);
|
|
322
|
+
margin-top: 2px;
|
|
323
|
+
}
|
|
324
|
+
.ss-sc {
|
|
325
|
+
font-family: var(--sans);
|
|
326
|
+
font-size: 10px;
|
|
327
|
+
color: var(--ink-faint);
|
|
328
|
+
letter-spacing: 1px;
|
|
329
|
+
display: inline-flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: 3px;
|
|
332
|
+
}
|
|
333
|
+
.ss-sc kbd {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
justify-content: center;
|
|
337
|
+
min-width: 18px;
|
|
338
|
+
height: 16px;
|
|
339
|
+
padding: 0 3px;
|
|
340
|
+
border: 1px solid var(--border);
|
|
341
|
+
border-radius: 2px;
|
|
342
|
+
font-family: var(--sans);
|
|
343
|
+
font-size: 9px;
|
|
344
|
+
color: var(--ink-light);
|
|
345
|
+
background: var(--surface);
|
|
346
|
+
}
|
|
311
347
|
|
|
312
348
|
@media (max-width: 768px) {
|
|
313
349
|
.sidenav { width: 44px; padding: 8px 0; gap: 6px; }
|
package/template/src/types.ts
CHANGED
|
@@ -108,11 +108,12 @@ export interface PieceContributor {
|
|
|
108
108
|
title?: string
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export interface PieceSource {
|
|
111
|
+
export interface PieceSource {
|
|
112
|
+
text?: string
|
|
112
113
|
textRef?: string
|
|
113
114
|
pieceRef?: number
|
|
114
115
|
relation: 'section' | 'excerpt' | 'standalone'
|
|
115
|
-
range?: { start
|
|
116
|
+
range?: { start?: string; end?: string; chapter?: string; [key: string]: string | undefined }
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
export interface ProseSection {
|
|
@@ -130,6 +131,7 @@ export interface Part {
|
|
|
130
131
|
source?: PieceSource
|
|
131
132
|
verses: VerseLine[]
|
|
132
133
|
annotations: Annotation[]
|
|
134
|
+
annotationText?: string
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
export interface Piece {
|
|
@@ -80,6 +80,7 @@ function goHome() { router.push('/') }
|
|
|
80
80
|
background: var(--paper);
|
|
81
81
|
scrollbar-width: thin;
|
|
82
82
|
scrollbar-color: var(--gold) transparent;
|
|
83
|
+
scroll-snap-type: x proximity;
|
|
83
84
|
}
|
|
84
85
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
85
86
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -161,6 +162,11 @@ function goHome() { router.push('/') }
|
|
|
161
162
|
background: var(--surface);
|
|
162
163
|
border: 1px solid var(--border-light);
|
|
163
164
|
border-radius: 8px;
|
|
165
|
+
animation: cardEnter 0.5s var(--ease-out-expo, ease) both;
|
|
166
|
+
}
|
|
167
|
+
@keyframes cardEnter {
|
|
168
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
169
|
+
to { opacity: 1; transform: translateY(0); }
|
|
164
170
|
}
|
|
165
171
|
.h-about-block:last-child { margin-bottom: 0; }
|
|
166
172
|
.h-about-block h2 {
|
|
@@ -154,6 +154,7 @@ function scrollToCatalog() {
|
|
|
154
154
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
155
155
|
scrollbar-width: thin;
|
|
156
156
|
scrollbar-color: var(--gold) transparent;
|
|
157
|
+
scroll-snap-type: x proximity;
|
|
157
158
|
}
|
|
158
159
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
159
160
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -125,9 +125,10 @@ function openBook(bookId: string) {
|
|
|
125
125
|
<h2 class="lib-group-title">{{ group.category }}</h2>
|
|
126
126
|
<div class="lib-grid">
|
|
127
127
|
<div
|
|
128
|
-
v-for="book in group.books"
|
|
128
|
+
v-for="(book, bi) in group.books"
|
|
129
129
|
:key="book.id"
|
|
130
130
|
class="lib-card"
|
|
131
|
+
:style="{ animationDelay: bi * 0.06 + 's' }"
|
|
131
132
|
@click="openBook(book.id)"
|
|
132
133
|
>
|
|
133
134
|
<div class="lib-card-accent"></div>
|
|
@@ -163,6 +164,7 @@ function openBook(bookId: string) {
|
|
|
163
164
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
164
165
|
scrollbar-width: thin;
|
|
165
166
|
scrollbar-color: var(--gold) transparent;
|
|
167
|
+
scroll-snap-type: x proximity;
|
|
166
168
|
}
|
|
167
169
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
168
170
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -391,9 +393,14 @@ function openBook(bookId: string) {
|
|
|
391
393
|
border: 1px solid var(--border-light);
|
|
392
394
|
border-radius: 8px;
|
|
393
395
|
cursor: pointer;
|
|
394
|
-
transition: all 0.3s ease;
|
|
396
|
+
transition: all 0.3s var(--ease-out-expo, ease);
|
|
395
397
|
position: relative;
|
|
396
398
|
background: var(--surface);
|
|
399
|
+
animation: cardEnter 0.5s var(--ease-out-expo, ease) both;
|
|
400
|
+
}
|
|
401
|
+
@keyframes cardEnter {
|
|
402
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
403
|
+
to { opacity: 1; transform: translateY(0); }
|
|
397
404
|
}
|
|
398
405
|
.lib-card:hover { border-color: var(--gold); box-shadow: 0 4px 20px rgba(var(--shadow-rgb), 0.1); }
|
|
399
406
|
.lib-card-accent {
|
|
@@ -14,6 +14,7 @@ import AnnotationTooltip from '../components/AnnotationTooltip.vue'
|
|
|
14
14
|
import AnnotationControlBar from '../components/AnnotationControlBar.vue'
|
|
15
15
|
import SideNav from '../components/SideNav.vue'
|
|
16
16
|
import PartGroup from '../components/PartGroup.vue'
|
|
17
|
+
import ReadingProgress from '../components/ReadingProgress.vue'
|
|
17
18
|
import type { Piece, Annotation, AnnotationLayer, Part } from '../types'
|
|
18
19
|
|
|
19
20
|
const props = defineProps<{ bookId: string; num: string | number }>()
|
|
@@ -59,6 +60,17 @@ useTitle(pageTitle.value)
|
|
|
59
60
|
|
|
60
61
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
61
62
|
|
|
63
|
+
const totalAnnotationCount = computed(() => {
|
|
64
|
+
if (!piece.value) return 0
|
|
65
|
+
let count = piece.value.annotations.length
|
|
66
|
+
if (piece.value.annotationLayers) {
|
|
67
|
+
for (const layer of piece.value.annotationLayers) {
|
|
68
|
+
count += layer.annotations.length
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return count
|
|
72
|
+
})
|
|
73
|
+
|
|
62
74
|
const annotationLayers = computed<AnnotationLayer[]>(() => piece.value?.annotationLayers || [])
|
|
63
75
|
const hasLayers = computed(() => annotationLayers.value.length > 1)
|
|
64
76
|
const activeLayerIds = ref<string[]>([])
|
|
@@ -242,6 +254,7 @@ function tcy(n: number): string {
|
|
|
242
254
|
@back="goBack"
|
|
243
255
|
@home="goHome"
|
|
244
256
|
/>
|
|
257
|
+
<ReadingProgress vertical :scroll-container="vPageRef" />
|
|
245
258
|
<div ref="vPageRef" class="v-page">
|
|
246
259
|
<section ref="vTitleRef" class="v-title-col">
|
|
247
260
|
<h1 class="v-poem-title">{{ piece.title }}</h1>
|
|
@@ -262,7 +275,7 @@ function tcy(n: number): string {
|
|
|
262
275
|
</template>
|
|
263
276
|
<template v-else>
|
|
264
277
|
<span class="v-meta-item" v-html="tcy(piece.verses.length) + ' 段'" />
|
|
265
|
-
<span class="v-meta-item" v-html="
|
|
278
|
+
<span class="v-meta-item" v-html="totalAnnotationCount > 0 ? tcy(totalAnnotationCount) + ' 注' : '無注'" />
|
|
266
279
|
</template>
|
|
267
280
|
</div>
|
|
268
281
|
</section>
|
|
@@ -392,6 +405,7 @@ function tcy(n: number): string {
|
|
|
392
405
|
|
|
393
406
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
394
407
|
<div v-else class="h-root">
|
|
408
|
+
<ReadingProgress />
|
|
395
409
|
<div class="h-page">
|
|
396
410
|
<nav class="h-nav">
|
|
397
411
|
<div class="h-nav-inner">
|
|
@@ -419,7 +433,7 @@ function tcy(n: number): string {
|
|
|
419
433
|
</template>
|
|
420
434
|
<template v-else>
|
|
421
435
|
<span class="h-tag">{{ piece.verses.length }} 段</span>
|
|
422
|
-
<span class="h-tag">{{
|
|
436
|
+
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + ' 注' : '無注' }}</span>
|
|
423
437
|
</template>
|
|
424
438
|
</div>
|
|
425
439
|
</div>
|
|
@@ -537,8 +551,8 @@ function tcy(n: number): string {
|
|
|
537
551
|
</div>
|
|
538
552
|
</div>
|
|
539
553
|
|
|
540
|
-
<div v-else
|
|
541
|
-
<
|
|
554
|
+
<div v-else class="loading">
|
|
555
|
+
<div class="loading-seal">詩</div>
|
|
542
556
|
</div>
|
|
543
557
|
</template>
|
|
544
558
|
|
|
@@ -556,6 +570,7 @@ function tcy(n: number): string {
|
|
|
556
570
|
background: var(--paper);
|
|
557
571
|
scrollbar-width: thin;
|
|
558
572
|
scrollbar-color: var(--gold) transparent;
|
|
573
|
+
scroll-snap-type: x proximity;
|
|
559
574
|
}
|
|
560
575
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
561
576
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -571,6 +586,7 @@ function tcy(n: number): string {
|
|
|
571
586
|
gap: 16px;
|
|
572
587
|
padding: 40px 24px;
|
|
573
588
|
border-right: 1px solid var(--border);
|
|
589
|
+
scroll-snap-align: start;
|
|
574
590
|
}
|
|
575
591
|
.v-poem-title {
|
|
576
592
|
font-size: 40px; font-weight: 900;
|
|
@@ -659,6 +675,7 @@ function tcy(n: number): string {
|
|
|
659
675
|
justify-content: center;
|
|
660
676
|
padding: 24px 12px;
|
|
661
677
|
gap: 32px;
|
|
678
|
+
scroll-snap-align: start;
|
|
662
679
|
}
|
|
663
680
|
.v-nav-spacer { flex: 1; }
|
|
664
681
|
.v-nav-btn {
|
|
@@ -790,7 +807,9 @@ function tcy(n: number): string {
|
|
|
790
807
|
|
|
791
808
|
.h-overlay {
|
|
792
809
|
position: fixed; inset: 0;
|
|
793
|
-
background: rgba(var(--shadow-rgb), 0.
|
|
810
|
+
background: rgba(var(--shadow-rgb), 0.2);
|
|
811
|
+
backdrop-filter: blur(8px);
|
|
812
|
+
-webkit-backdrop-filter: blur(8px);
|
|
794
813
|
z-index: 200;
|
|
795
814
|
display: flex; justify-content: flex-end;
|
|
796
815
|
animation: fadeIn 0.2s ease;
|
|
@@ -837,7 +856,9 @@ function tcy(n: number): string {
|
|
|
837
856
|
|
|
838
857
|
.v-overlay {
|
|
839
858
|
position: fixed; inset: 0;
|
|
840
|
-
background: rgba(var(--shadow-rgb), 0.
|
|
859
|
+
background: rgba(var(--shadow-rgb), 0.2);
|
|
860
|
+
backdrop-filter: blur(8px);
|
|
861
|
+
-webkit-backdrop-filter: blur(8px);
|
|
841
862
|
z-index: 200;
|
|
842
863
|
display: flex; justify-content: flex-start;
|
|
843
864
|
animation: fadeIn 0.2s ease;
|
|
@@ -894,6 +915,25 @@ function tcy(n: number): string {
|
|
|
894
915
|
margin-left: 12px;
|
|
895
916
|
}
|
|
896
917
|
|
|
918
|
+
.loading {
|
|
919
|
+
display: flex; flex-direction: column;
|
|
920
|
+
align-items: center; justify-content: center;
|
|
921
|
+
height: 100vh;
|
|
922
|
+
}
|
|
923
|
+
.loading-seal {
|
|
924
|
+
width: 56px; height: 56px;
|
|
925
|
+
border: 2px solid var(--vermillion);
|
|
926
|
+
border-radius: 4px;
|
|
927
|
+
display: flex; align-items: center; justify-content: center;
|
|
928
|
+
font-size: 28px; font-weight: 900;
|
|
929
|
+
color: var(--vermillion);
|
|
930
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
931
|
+
}
|
|
932
|
+
@keyframes pulse {
|
|
933
|
+
0%, 100% { opacity: 0.3; }
|
|
934
|
+
50% { opacity: 1; }
|
|
935
|
+
}
|
|
936
|
+
|
|
897
937
|
@media (max-width: 768px) {
|
|
898
938
|
.h-content { padding: 30px 20px; }
|
|
899
939
|
}
|