@hanology/cham-browser 0.3.4 → 0.3.6
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/AnnotationTooltip.vue +11 -3
- package/template/src/components/BackToTop.vue +65 -0
- package/template/src/components/PartBlock.vue +27 -1
- package/template/src/components/PartGroup.vue +1 -0
- package/template/src/components/PoemCard.vue +7 -0
- package/template/src/components/ReadingProgress.vue +83 -0
- package/template/src/components/ReadingToolbar.vue +55 -0
- package/template/src/components/SectionBlock.vue +32 -7
- package/template/src/components/SideNav.vue +36 -0
- package/template/src/styles/main.css +26 -1
- package/template/src/types.ts +4 -2
- package/template/src/views/AboutView.vue +8 -0
- package/template/src/views/BookHome.vue +9 -1
- package/template/src/views/LibraryHome.vue +16 -3
- package/template/src/views/PieceView.vue +95 -29
package/package.json
CHANGED
|
@@ -198,10 +198,18 @@ onBeforeUnmount(() => {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/* ─── Transition ─── */
|
|
201
|
-
.ann-fade-enter-active
|
|
202
|
-
transition: opacity 0.15s ease;
|
|
201
|
+
.ann-fade-enter-active {
|
|
202
|
+
transition: opacity var(--dur-fast, 0.15s) ease, transform var(--dur-mid, 0.25s) cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
203
203
|
}
|
|
204
|
-
.ann-fade-
|
|
204
|
+
.ann-fade-leave-active {
|
|
205
|
+
transition: opacity var(--dur-fast, 0.15s) ease, transform var(--dur-fast, 0.15s) ease;
|
|
206
|
+
}
|
|
207
|
+
.ann-fade-enter-from {
|
|
208
|
+
opacity: 0;
|
|
209
|
+
transform: scale(0.92) translateY(4px);
|
|
210
|
+
}
|
|
211
|
+
.ann-fade-leave-to {
|
|
205
212
|
opacity: 0;
|
|
213
|
+
transform: scale(0.96);
|
|
206
214
|
}
|
|
207
215
|
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
|
|
4
|
+
const visible = ref(false)
|
|
5
|
+
let ticking = false
|
|
6
|
+
|
|
7
|
+
function onScroll() {
|
|
8
|
+
if (ticking) return
|
|
9
|
+
ticking = true
|
|
10
|
+
requestAnimationFrame(() => {
|
|
11
|
+
visible.value = window.scrollY > 400
|
|
12
|
+
ticking = false
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scrollToTop() {
|
|
17
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }))
|
|
21
|
+
onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<Transition name="btt">
|
|
26
|
+
<button v-if="visible" class="btt" @click="scrollToTop" aria-label="回到頂部">
|
|
27
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</button>
|
|
31
|
+
</Transition>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.btt {
|
|
36
|
+
position: fixed;
|
|
37
|
+
bottom: 80px;
|
|
38
|
+
right: 24px;
|
|
39
|
+
width: 40px;
|
|
40
|
+
height: 40px;
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
border: 1px solid var(--border);
|
|
43
|
+
background: var(--surface);
|
|
44
|
+
color: var(--ink-light);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
z-index: 400;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.1);
|
|
51
|
+
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
52
|
+
}
|
|
53
|
+
.btt:hover {
|
|
54
|
+
background: var(--ink);
|
|
55
|
+
color: var(--paper);
|
|
56
|
+
border-color: var(--ink);
|
|
57
|
+
transform: translateY(-2px);
|
|
58
|
+
box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.16);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.btt-enter-active { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
62
|
+
.btt-leave-active { transition: all 0.15s ease; }
|
|
63
|
+
.btt-enter-from { opacity: 0; transform: translateY(12px) scale(0.8); }
|
|
64
|
+
.btt-leave-to { opacity: 0; transform: translateY(8px) scale(0.9); }
|
|
65
|
+
</style>
|
|
@@ -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>
|
|
@@ -54,6 +54,13 @@ const preview = computed(() => {
|
|
|
54
54
|
font-size: 11px; color: var(--ink-faint);
|
|
55
55
|
font-family: var(--sans); letter-spacing: 2px;
|
|
56
56
|
margin-bottom: 10px;
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
padding: 2px 6px;
|
|
61
|
+
border: 1px solid var(--border-light);
|
|
62
|
+
border-radius: 2px;
|
|
63
|
+
background: var(--surface-warm);
|
|
57
64
|
}
|
|
58
65
|
.pc-title {
|
|
59
66
|
font-size: 20px; font-weight: 700;
|
|
@@ -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>
|
|
@@ -150,6 +155,25 @@ function close() { open.value = false }
|
|
|
150
155
|
transition: all 0.15s;
|
|
151
156
|
}
|
|
152
157
|
.rt-opt:hover { border-color: var(--ink); color: var(--ink); }
|
|
158
|
+
.rt-opt.rt-theme {
|
|
159
|
+
position: relative;
|
|
160
|
+
padding-left: 22px;
|
|
161
|
+
}
|
|
162
|
+
.rt-opt.rt-theme::before {
|
|
163
|
+
content: '';
|
|
164
|
+
position: absolute;
|
|
165
|
+
left: 8px;
|
|
166
|
+
top: 50%;
|
|
167
|
+
transform: translateY(-50%);
|
|
168
|
+
width: 8px;
|
|
169
|
+
height: 8px;
|
|
170
|
+
border-radius: 50%;
|
|
171
|
+
border: 1px solid var(--border);
|
|
172
|
+
}
|
|
173
|
+
.rt-opt.theme-light::before { background: #faf6ee; }
|
|
174
|
+
.rt-opt.theme-sepia::before { background: #f0e4c8; }
|
|
175
|
+
.rt-opt.theme-dark::before { background: #1c1c1e; border-color: #48484a; }
|
|
176
|
+
.rt-opt.theme-oled::before { background: #000; border-color: #333; }
|
|
153
177
|
.rt-opt.active {
|
|
154
178
|
background: var(--ink);
|
|
155
179
|
color: var(--paper);
|
|
@@ -182,4 +206,35 @@ function close() { open.value = false }
|
|
|
182
206
|
position: fixed; inset: 0;
|
|
183
207
|
z-index: -1;
|
|
184
208
|
}
|
|
209
|
+
.rt-shortcuts {
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
gap: 6px;
|
|
213
|
+
padding-top: 10px;
|
|
214
|
+
border-top: 1px solid var(--border-light);
|
|
215
|
+
margin-top: 2px;
|
|
216
|
+
}
|
|
217
|
+
.rt-sc {
|
|
218
|
+
font-family: var(--sans);
|
|
219
|
+
font-size: 10px;
|
|
220
|
+
color: var(--ink-faint);
|
|
221
|
+
letter-spacing: 1px;
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
gap: 3px;
|
|
225
|
+
}
|
|
226
|
+
.rt-sc kbd {
|
|
227
|
+
display: inline-flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: center;
|
|
230
|
+
min-width: 18px;
|
|
231
|
+
height: 16px;
|
|
232
|
+
padding: 0 3px;
|
|
233
|
+
border: 1px solid var(--border);
|
|
234
|
+
border-radius: 2px;
|
|
235
|
+
font-family: var(--sans);
|
|
236
|
+
font-size: 9px;
|
|
237
|
+
color: var(--ink-light);
|
|
238
|
+
background: var(--surface);
|
|
239
|
+
}
|
|
185
240
|
</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; }
|
|
@@ -90,23 +90,36 @@
|
|
|
90
90
|
--sans: 'Noto Sans TC', 'PingFang TC', sans-serif;
|
|
91
91
|
--nav-width: 56px;
|
|
92
92
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
|
93
|
+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
94
|
+
--dur-fast: 0.15s;
|
|
95
|
+
--dur-mid: 0.25s;
|
|
96
|
+
--dur-slow: 0.4s;
|
|
93
97
|
}
|
|
94
98
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
95
|
-
html {
|
|
99
|
+
html {
|
|
100
|
+
scroll-behavior: smooth;
|
|
101
|
+
-webkit-font-smoothing: antialiased;
|
|
102
|
+
-moz-osx-font-smoothing: grayscale;
|
|
103
|
+
text-rendering: optimizeLegibility;
|
|
104
|
+
hanging-punctuation: first last;
|
|
105
|
+
}
|
|
96
106
|
body {
|
|
97
107
|
font-family: var(--serif);
|
|
98
108
|
background: var(--paper);
|
|
99
109
|
color: var(--ink);
|
|
100
110
|
line-height: 1.8;
|
|
101
111
|
min-height: 100vh;
|
|
112
|
+
overflow-x: hidden;
|
|
102
113
|
}
|
|
103
114
|
::selection { background: var(--vermillion); color: var(--selection-text); }
|
|
104
115
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
105
116
|
::-webkit-scrollbar-track { background: transparent; }
|
|
106
117
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
107
118
|
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
|
|
119
|
+
html[dir="rtl"] ::-webkit-scrollbar-thumb { background: var(--gold); }
|
|
108
120
|
|
|
109
121
|
a { color: inherit; text-decoration: none; }
|
|
122
|
+
button { font-family: inherit; }
|
|
110
123
|
|
|
111
124
|
/* ===== LOADING SCREEN ===== */
|
|
112
125
|
#app-loading {
|
|
@@ -166,6 +179,18 @@ a { color: inherit; text-decoration: none; }
|
|
|
166
179
|
from { opacity: 0; transform: translateY(16px); }
|
|
167
180
|
to { opacity: 1; transform: translateY(0); }
|
|
168
181
|
}
|
|
182
|
+
@keyframes cardEnter {
|
|
183
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
184
|
+
to { opacity: 1; transform: translateY(0); }
|
|
185
|
+
}
|
|
186
|
+
@keyframes fadeIn {
|
|
187
|
+
from { opacity: 0; }
|
|
188
|
+
to { opacity: 1; }
|
|
189
|
+
}
|
|
190
|
+
@keyframes scaleIn {
|
|
191
|
+
from { opacity: 0; transform: scale(0.92); }
|
|
192
|
+
to { opacity: 1; transform: scale(1); }
|
|
193
|
+
}
|
|
169
194
|
|
|
170
195
|
/* ===== RESPONSIVE ===== */
|
|
171
196
|
@media (max-width: 768px) {
|
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 {
|
|
@@ -5,6 +5,7 @@ import { useReadingMode } from '../composables/useReadingMode'
|
|
|
5
5
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
6
6
|
import SideNav from '../components/SideNav.vue'
|
|
7
7
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
8
|
+
import BackToTop from '../components/BackToTop.vue'
|
|
8
9
|
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
9
10
|
import { ref, computed } from 'vue'
|
|
10
11
|
import { useRouter } from 'vue-router'
|
|
@@ -63,6 +64,7 @@ function goHome() { router.push('/') }
|
|
|
63
64
|
<p>Hanology is a digital library for classical Chinese texts. We believe that the wisdom of antiquity should not be locked behind impenetrable editions or forgotten in dusty shelves. By combining rigorous scholarship with thoughtful design, we make the classics accessible, beautiful, and alive for every reader.</p>
|
|
64
65
|
</div>
|
|
65
66
|
</div>
|
|
67
|
+
<BackToTop />
|
|
66
68
|
<ReadingToolbar />
|
|
67
69
|
</div>
|
|
68
70
|
</template>
|
|
@@ -80,6 +82,7 @@ function goHome() { router.push('/') }
|
|
|
80
82
|
background: var(--paper);
|
|
81
83
|
scrollbar-width: thin;
|
|
82
84
|
scrollbar-color: var(--gold) transparent;
|
|
85
|
+
scroll-snap-type: x proximity;
|
|
83
86
|
}
|
|
84
87
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
85
88
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -161,6 +164,11 @@ function goHome() { router.push('/') }
|
|
|
161
164
|
background: var(--surface);
|
|
162
165
|
border: 1px solid var(--border-light);
|
|
163
166
|
border-radius: 8px;
|
|
167
|
+
animation: cardEnter 0.5s var(--ease-out-expo, ease) both;
|
|
168
|
+
}
|
|
169
|
+
@keyframes cardEnter {
|
|
170
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
171
|
+
to { opacity: 1; transform: translateY(0); }
|
|
164
172
|
}
|
|
165
173
|
.h-about-block:last-child { margin-bottom: 0; }
|
|
166
174
|
.h-about-block h2 {
|
|
@@ -8,6 +8,7 @@ import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
|
8
8
|
import PoemCard from '../components/PoemCard.vue'
|
|
9
9
|
import SideNav from '../components/SideNav.vue'
|
|
10
10
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
11
|
+
import BackToTop from '../components/BackToTop.vue'
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{ bookId: string }>()
|
|
13
14
|
const router = useRouter()
|
|
@@ -128,14 +129,17 @@ function scrollToCatalog() {
|
|
|
128
129
|
</div>
|
|
129
130
|
<div class="h-grid">
|
|
130
131
|
<PoemCard
|
|
131
|
-
v-for="piece in filtered"
|
|
132
|
+
v-for="(piece, idx) in filtered"
|
|
132
133
|
:key="piece.num"
|
|
133
134
|
:poem="piece"
|
|
135
|
+
:style="{ animationDelay: Math.min(idx * 0.04, 0.8) + 's' }"
|
|
136
|
+
class="h-card-anim"
|
|
134
137
|
@click="openPiece(piece.num)"
|
|
135
138
|
/>
|
|
136
139
|
</div>
|
|
137
140
|
</section>
|
|
138
141
|
|
|
142
|
+
<BackToTop />
|
|
139
143
|
<ReadingToolbar />
|
|
140
144
|
</div>
|
|
141
145
|
</template>
|
|
@@ -154,6 +158,7 @@ function scrollToCatalog() {
|
|
|
154
158
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
155
159
|
scrollbar-width: thin;
|
|
156
160
|
scrollbar-color: var(--gold) transparent;
|
|
161
|
+
scroll-snap-type: x proximity;
|
|
157
162
|
}
|
|
158
163
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
159
164
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -358,6 +363,9 @@ function scrollToCatalog() {
|
|
|
358
363
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
359
364
|
gap: 16px;
|
|
360
365
|
}
|
|
366
|
+
.h-card-anim {
|
|
367
|
+
animation: cardEnter 0.4s var(--ease-out-expo) both;
|
|
368
|
+
}
|
|
361
369
|
|
|
362
370
|
@media (max-width: 768px) {
|
|
363
371
|
.h-stats { gap: 24px; }
|
|
@@ -9,6 +9,7 @@ import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
|
9
9
|
import BookCard from '../components/BookCard.vue'
|
|
10
10
|
import SideNav from '../components/SideNav.vue'
|
|
11
11
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
12
|
+
import BackToTop from '../components/BackToTop.vue'
|
|
12
13
|
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
13
14
|
import type { BookMeta } from '../types'
|
|
14
15
|
|
|
@@ -125,9 +126,10 @@ function openBook(bookId: string) {
|
|
|
125
126
|
<h2 class="lib-group-title">{{ group.category }}</h2>
|
|
126
127
|
<div class="lib-grid">
|
|
127
128
|
<div
|
|
128
|
-
v-for="book in group.books"
|
|
129
|
+
v-for="(book, bi) in group.books"
|
|
129
130
|
:key="book.id"
|
|
130
|
-
class="lib-card"
|
|
131
|
+
class="lib-card lib-card-anim"
|
|
132
|
+
:style="{ animationDelay: bi * 0.06 + 's' }"
|
|
131
133
|
@click="openBook(book.id)"
|
|
132
134
|
>
|
|
133
135
|
<div class="lib-card-accent"></div>
|
|
@@ -145,6 +147,7 @@ function openBook(bookId: string) {
|
|
|
145
147
|
</div>
|
|
146
148
|
</div>
|
|
147
149
|
<ReadingToolbar />
|
|
150
|
+
<BackToTop />
|
|
148
151
|
</div>
|
|
149
152
|
</div>
|
|
150
153
|
</template>
|
|
@@ -163,6 +166,7 @@ function openBook(bookId: string) {
|
|
|
163
166
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
164
167
|
scrollbar-width: thin;
|
|
165
168
|
scrollbar-color: var(--gold) transparent;
|
|
169
|
+
scroll-snap-type: x proximity;
|
|
166
170
|
}
|
|
167
171
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
168
172
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -383,6 +387,10 @@ function openBook(bookId: string) {
|
|
|
383
387
|
gap: 12px;
|
|
384
388
|
}
|
|
385
389
|
|
|
390
|
+
.lib-card-anim {
|
|
391
|
+
animation: cardEnter 0.4s var(--ease-out-expo) both;
|
|
392
|
+
}
|
|
393
|
+
|
|
386
394
|
.lib-card {
|
|
387
395
|
display: flex;
|
|
388
396
|
flex-direction: column;
|
|
@@ -391,9 +399,14 @@ function openBook(bookId: string) {
|
|
|
391
399
|
border: 1px solid var(--border-light);
|
|
392
400
|
border-radius: 8px;
|
|
393
401
|
cursor: pointer;
|
|
394
|
-
transition: all 0.3s ease;
|
|
402
|
+
transition: all 0.3s var(--ease-out-expo, ease);
|
|
395
403
|
position: relative;
|
|
396
404
|
background: var(--surface);
|
|
405
|
+
animation: cardEnter 0.5s var(--ease-out-expo, ease) both;
|
|
406
|
+
}
|
|
407
|
+
@keyframes cardEnter {
|
|
408
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
409
|
+
to { opacity: 1; transform: translateY(0); }
|
|
397
410
|
}
|
|
398
411
|
.lib-card:hover { border-color: var(--gold); box-shadow: 0 4px 20px rgba(var(--shadow-rgb), 0.1); }
|
|
399
412
|
.lib-card-accent {
|
|
@@ -14,6 +14,8 @@ 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'
|
|
18
|
+
import BackToTop from '../components/BackToTop.vue'
|
|
17
19
|
import type { Piece, Annotation, AnnotationLayer, Part } from '../types'
|
|
18
20
|
|
|
19
21
|
const props = defineProps<{ bookId: string; num: string | number }>()
|
|
@@ -59,6 +61,17 @@ useTitle(pageTitle.value)
|
|
|
59
61
|
|
|
60
62
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
61
63
|
|
|
64
|
+
const totalAnnotationCount = computed(() => {
|
|
65
|
+
if (!piece.value) return 0
|
|
66
|
+
let count = piece.value.annotations.length
|
|
67
|
+
if (piece.value.annotationLayers) {
|
|
68
|
+
for (const layer of piece.value.annotationLayers) {
|
|
69
|
+
count += layer.annotations.length
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return count
|
|
73
|
+
})
|
|
74
|
+
|
|
62
75
|
const annotationLayers = computed<AnnotationLayer[]>(() => piece.value?.annotationLayers || [])
|
|
63
76
|
const hasLayers = computed(() => annotationLayers.value.length > 1)
|
|
64
77
|
const activeLayerIds = ref<string[]>([])
|
|
@@ -242,6 +255,7 @@ function tcy(n: number): string {
|
|
|
242
255
|
@back="goBack"
|
|
243
256
|
@home="goHome"
|
|
244
257
|
/>
|
|
258
|
+
<ReadingProgress vertical :scroll-container="vPageRef" />
|
|
245
259
|
<div ref="vPageRef" class="v-page">
|
|
246
260
|
<section ref="vTitleRef" class="v-title-col">
|
|
247
261
|
<h1 class="v-poem-title">{{ piece.title }}</h1>
|
|
@@ -262,7 +276,7 @@ function tcy(n: number): string {
|
|
|
262
276
|
</template>
|
|
263
277
|
<template v-else>
|
|
264
278
|
<span class="v-meta-item" v-html="tcy(piece.verses.length) + ' 段'" />
|
|
265
|
-
<span class="v-meta-item" v-html="
|
|
279
|
+
<span class="v-meta-item" v-html="totalAnnotationCount > 0 ? tcy(totalAnnotationCount) + ' 注' : '無注'" />
|
|
266
280
|
</template>
|
|
267
281
|
</div>
|
|
268
282
|
</section>
|
|
@@ -374,24 +388,27 @@ function tcy(n: number): string {
|
|
|
374
388
|
/>
|
|
375
389
|
|
|
376
390
|
<Teleport to="body">
|
|
377
|
-
<
|
|
378
|
-
<div class="v-
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
<div class="v-pane-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<div v-
|
|
385
|
-
|
|
391
|
+
<Transition name="overlay">
|
|
392
|
+
<div v-if="authorPaneOpen" class="v-overlay" @click="closeAuthorPane">
|
|
393
|
+
<div class="v-author-pane" @click.stop>
|
|
394
|
+
<button class="v-pane-close" @click="closeAuthorPane">✕</button>
|
|
395
|
+
<div class="v-pane-header">
|
|
396
|
+
<div class="v-pane-name">{{ selectedAuthorName }}</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div v-if="selectedAuthorBio" class="v-pane-bio">
|
|
399
|
+
<div v-for="p in selectedAuthorBio.split('\n').filter(l => l.trim())" :key="p" class="v-pane-p">
|
|
400
|
+
{{ p.trim() }}
|
|
401
|
+
</div>
|
|
386
402
|
</div>
|
|
387
403
|
</div>
|
|
388
404
|
</div>
|
|
389
|
-
</
|
|
405
|
+
</Transition>
|
|
390
406
|
</Teleport>
|
|
391
407
|
</div>
|
|
392
408
|
|
|
393
409
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
394
410
|
<div v-else class="h-root">
|
|
411
|
+
<ReadingProgress />
|
|
395
412
|
<div class="h-page">
|
|
396
413
|
<nav class="h-nav">
|
|
397
414
|
<div class="h-nav-inner">
|
|
@@ -419,7 +436,7 @@ function tcy(n: number): string {
|
|
|
419
436
|
</template>
|
|
420
437
|
<template v-else>
|
|
421
438
|
<span class="h-tag">{{ piece.verses.length }} 段</span>
|
|
422
|
-
<span class="h-tag">{{
|
|
439
|
+
<span class="h-tag">{{ totalAnnotationCount > 0 ? totalAnnotationCount + ' 注' : '無注' }}</span>
|
|
423
440
|
</template>
|
|
424
441
|
</div>
|
|
425
442
|
</div>
|
|
@@ -516,8 +533,11 @@ function tcy(n: number): string {
|
|
|
516
533
|
@tooltip-leave="interaction.onTooltipLeave"
|
|
517
534
|
/>
|
|
518
535
|
|
|
536
|
+
<BackToTop />
|
|
537
|
+
|
|
519
538
|
<Teleport to="body">
|
|
520
|
-
<
|
|
539
|
+
<Transition name="overlay">
|
|
540
|
+
<div v-if="authorPaneOpen" class="h-overlay" @click="closeAuthorPane">
|
|
521
541
|
<div class="h-pane" @click.stop>
|
|
522
542
|
<button class="h-pane-close" @click="closeAuthorPane">✕</button>
|
|
523
543
|
<div class="h-pane-header">
|
|
@@ -532,13 +552,13 @@ function tcy(n: number): string {
|
|
|
532
552
|
</div>
|
|
533
553
|
</div>
|
|
534
554
|
</div>
|
|
535
|
-
</
|
|
555
|
+
</Transition>
|
|
536
556
|
</Teleport>
|
|
537
557
|
</div>
|
|
538
558
|
</div>
|
|
539
559
|
|
|
540
|
-
<div v-else
|
|
541
|
-
<
|
|
560
|
+
<div v-else class="loading">
|
|
561
|
+
<div class="loading-seal">詩</div>
|
|
542
562
|
</div>
|
|
543
563
|
</template>
|
|
544
564
|
|
|
@@ -556,6 +576,7 @@ function tcy(n: number): string {
|
|
|
556
576
|
background: var(--paper);
|
|
557
577
|
scrollbar-width: thin;
|
|
558
578
|
scrollbar-color: var(--gold) transparent;
|
|
579
|
+
scroll-snap-type: x proximity;
|
|
559
580
|
}
|
|
560
581
|
.v-page::-webkit-scrollbar { height: 4px; }
|
|
561
582
|
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
@@ -571,6 +592,7 @@ function tcy(n: number): string {
|
|
|
571
592
|
gap: 16px;
|
|
572
593
|
padding: 40px 24px;
|
|
573
594
|
border-right: 1px solid var(--border);
|
|
595
|
+
scroll-snap-align: start;
|
|
574
596
|
}
|
|
575
597
|
.v-poem-title {
|
|
576
598
|
font-size: 40px; font-weight: 900;
|
|
@@ -659,6 +681,7 @@ function tcy(n: number): string {
|
|
|
659
681
|
justify-content: center;
|
|
660
682
|
padding: 24px 12px;
|
|
661
683
|
gap: 32px;
|
|
684
|
+
scroll-snap-align: start;
|
|
662
685
|
}
|
|
663
686
|
.v-nav-spacer { flex: 1; }
|
|
664
687
|
.v-nav-btn {
|
|
@@ -774,38 +797,63 @@ function tcy(n: number): string {
|
|
|
774
797
|
.h-nav-bottom {
|
|
775
798
|
max-width: min(680px, calc(100vw - 80px));
|
|
776
799
|
margin: 0 auto 60px;
|
|
777
|
-
display:
|
|
800
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
|
778
801
|
}
|
|
779
802
|
.h-nav-btn {
|
|
780
|
-
|
|
803
|
+
padding: 20px 24px;
|
|
781
804
|
background: var(--surface); border: 1px solid var(--border-light);
|
|
782
805
|
border-radius: 8px; cursor: pointer;
|
|
783
|
-
transition: all 0.
|
|
806
|
+
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
807
|
+
font-family: var(--serif);
|
|
784
808
|
text-align: left;
|
|
809
|
+
position: relative;
|
|
810
|
+
overflow: hidden;
|
|
811
|
+
}
|
|
812
|
+
.h-nav-btn::after {
|
|
813
|
+
content: '';
|
|
814
|
+
position: absolute;
|
|
815
|
+
bottom: 0; left: 0; right: 0;
|
|
816
|
+
height: 2px;
|
|
817
|
+
background: var(--vermillion);
|
|
818
|
+
transform: scaleX(0);
|
|
819
|
+
transition: transform 0.3s ease;
|
|
820
|
+
}
|
|
821
|
+
.h-nav-btn:hover {
|
|
822
|
+
border-color: var(--gold);
|
|
823
|
+
box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.1);
|
|
824
|
+
transform: translateY(-2px);
|
|
785
825
|
}
|
|
786
|
-
.h-nav-btn:hover {
|
|
826
|
+
.h-nav-btn:hover::after { transform: scaleX(1); }
|
|
787
827
|
.h-nav-btn.h-nav-next { text-align: right; }
|
|
788
828
|
.h-nav-label { font-size: 11px; color: var(--ink-faint); font-family: var(--sans); letter-spacing: 2px; margin-bottom: 4px; }
|
|
789
829
|
.h-nav-title { font-size: 16px; font-weight: 600; letter-spacing: 1px; color: var(--ink); }
|
|
790
830
|
|
|
791
831
|
.h-overlay {
|
|
792
832
|
position: fixed; inset: 0;
|
|
793
|
-
background: rgba(var(--shadow-rgb), 0.
|
|
833
|
+
background: rgba(var(--shadow-rgb), 0.24);
|
|
834
|
+
backdrop-filter: blur(12px);
|
|
835
|
+
-webkit-backdrop-filter: blur(12px);
|
|
794
836
|
z-index: 200;
|
|
795
837
|
display: flex; justify-content: flex-end;
|
|
796
|
-
animation: fadeIn 0.2s ease;
|
|
797
838
|
}
|
|
798
|
-
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
799
839
|
.h-pane {
|
|
800
840
|
width: min(420px, 90vw);
|
|
801
841
|
height: 100vh;
|
|
802
842
|
background: var(--paper);
|
|
803
843
|
padding: 32px;
|
|
804
844
|
overflow-y: auto;
|
|
805
|
-
animation: slideIn 0.25s ease;
|
|
806
845
|
box-shadow: -8px 0 32px rgba(var(--shadow-rgb), 0.1);
|
|
807
846
|
}
|
|
808
|
-
|
|
847
|
+
|
|
848
|
+
/* Overlay transition */
|
|
849
|
+
.overlay-enter-active { transition: opacity var(--dur-mid, 0.25s) ease; }
|
|
850
|
+
.overlay-enter-active .h-pane { transition: transform var(--dur-mid, 0.25s) cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
851
|
+
.overlay-leave-active { transition: opacity var(--dur-fast, 0.15s) ease; }
|
|
852
|
+
.overlay-leave-active .h-pane { transition: transform var(--dur-fast, 0.15s) ease; }
|
|
853
|
+
.overlay-enter-from { opacity: 0; }
|
|
854
|
+
.overlay-enter-from .h-pane { transform: translateX(100%); }
|
|
855
|
+
.overlay-leave-to { opacity: 0; }
|
|
856
|
+
.overlay-leave-to .h-pane { transform: translateX(40px); }
|
|
809
857
|
.h-pane-close {
|
|
810
858
|
display: block; margin-left: auto;
|
|
811
859
|
width: 36px; height: 36px;
|
|
@@ -837,10 +885,11 @@ function tcy(n: number): string {
|
|
|
837
885
|
|
|
838
886
|
.v-overlay {
|
|
839
887
|
position: fixed; inset: 0;
|
|
840
|
-
background: rgba(var(--shadow-rgb), 0.
|
|
888
|
+
background: rgba(var(--shadow-rgb), 0.24);
|
|
889
|
+
backdrop-filter: blur(12px);
|
|
890
|
+
-webkit-backdrop-filter: blur(12px);
|
|
841
891
|
z-index: 200;
|
|
842
892
|
display: flex; justify-content: flex-start;
|
|
843
|
-
animation: fadeIn 0.2s ease;
|
|
844
893
|
}
|
|
845
894
|
.v-author-pane {
|
|
846
895
|
writing-mode: vertical-rl;
|
|
@@ -850,9 +899,7 @@ function tcy(n: number): string {
|
|
|
850
899
|
padding: 32px 24px;
|
|
851
900
|
overflow-x: auto;
|
|
852
901
|
box-shadow: 8px 0 32px rgba(var(--shadow-rgb), 0.1);
|
|
853
|
-
animation: slideInV 0.25s ease;
|
|
854
902
|
}
|
|
855
|
-
@keyframes slideInV { from { transform: translateX(-100%); } to { transform: translateX(0); } }
|
|
856
903
|
.v-pane-close {
|
|
857
904
|
display: block;
|
|
858
905
|
width: 32px; height: 32px;
|
|
@@ -894,6 +941,25 @@ function tcy(n: number): string {
|
|
|
894
941
|
margin-left: 12px;
|
|
895
942
|
}
|
|
896
943
|
|
|
944
|
+
.loading {
|
|
945
|
+
display: flex; flex-direction: column;
|
|
946
|
+
align-items: center; justify-content: center;
|
|
947
|
+
height: 100vh;
|
|
948
|
+
}
|
|
949
|
+
.loading-seal {
|
|
950
|
+
width: 56px; height: 56px;
|
|
951
|
+
border: 2px solid var(--vermillion);
|
|
952
|
+
border-radius: 4px;
|
|
953
|
+
display: flex; align-items: center; justify-content: center;
|
|
954
|
+
font-size: 28px; font-weight: 900;
|
|
955
|
+
color: var(--vermillion);
|
|
956
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
957
|
+
}
|
|
958
|
+
@keyframes pulse {
|
|
959
|
+
0%, 100% { opacity: 0.3; }
|
|
960
|
+
50% { opacity: 1; }
|
|
961
|
+
}
|
|
962
|
+
|
|
897
963
|
@media (max-width: 768px) {
|
|
898
964
|
.h-content { padding: 30px 20px; }
|
|
899
965
|
}
|