@hanology/cham-browser 0.4.59 → 0.4.61
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 +13 -6
- package/template/src/components/AnnotationPane.vue +1 -18
- package/template/src/components/AnnotationTooltip.vue +1 -18
- package/template/src/components/BookCard.vue +1 -1
- package/template/src/components/HorizontalDisplay.vue +3 -56
- package/template/src/components/PartBlock.vue +7 -106
- package/template/src/components/PoemCard.vue +6 -11
- package/template/src/components/VerticalScroll.vue +4 -65
- package/template/src/composables/useI18n.ts +15 -0
- package/template/src/styles/annotation-targets.css +58 -0
- package/template/src/styles/main.css +2 -1
- package/template/src/utils/annotationLabels.ts +20 -0
- package/template/src/views/AuthorView.vue +22 -11
- package/template/src/views/BookHome.vue +3 -5
- package/template/src/views/LibraryHome.vue +22 -12
- package/template/src/views/PieceView.vue +8 -16
package/package.json
CHANGED
package/template/src/App.vue
CHANGED
|
@@ -52,12 +52,14 @@ function onKey(event: KeyboardEvent) {
|
|
|
52
52
|
<template>
|
|
53
53
|
<div @keydown="onKey">
|
|
54
54
|
<router-view v-slot="{ Component, route }">
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
<Transition name="page-fade" mode="out-in">
|
|
56
|
+
<Suspense :key="route.fullPath">
|
|
57
|
+
<component :is="Component" />
|
|
58
|
+
<template #fallback>
|
|
59
|
+
<div class="route-loading"></div>
|
|
60
|
+
</template>
|
|
61
|
+
</Suspense>
|
|
62
|
+
</Transition>
|
|
61
63
|
</router-view>
|
|
62
64
|
<!-- 橫排模式才顯示浮動設定鈕 -->
|
|
63
65
|
<ReadingToolbar v-if="!isVertical" />
|
|
@@ -81,6 +83,11 @@ function onKey(event: KeyboardEvent) {
|
|
|
81
83
|
</template>
|
|
82
84
|
|
|
83
85
|
<style>
|
|
86
|
+
.page-fade-enter-active { transition: opacity 0.15s ease, transform 0.2s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)); }
|
|
87
|
+
.page-fade-leave-active { transition: opacity 0.1s ease; }
|
|
88
|
+
.page-fade-enter-from { opacity: 0; transform: translateY(8px); }
|
|
89
|
+
.page-fade-leave-to { opacity: 0; }
|
|
90
|
+
|
|
84
91
|
.about-overlay {
|
|
85
92
|
position: fixed; inset: 0;
|
|
86
93
|
background: rgba(var(--shadow-rgb), 0.3);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
3
3
|
import { annotationToPronSegment } from '../utils/annotationParser'
|
|
4
|
+
import { kindLabel } from '../utils/annotationLabels'
|
|
4
5
|
import { toChineseNumber } from '../utils/chineseNumber'
|
|
5
6
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
6
7
|
import type { Annotation } from '../types'
|
|
@@ -98,24 +99,6 @@ function headword(ann: Annotation): string {
|
|
|
98
99
|
return props.headwords[ann.id] || ''
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function kindLabel(ann: Annotation): string {
|
|
102
|
-
const map: Record<string, string> = {
|
|
103
|
-
pronunciation: '讀音',
|
|
104
|
-
semantic: '釋義',
|
|
105
|
-
etymology: '詞源',
|
|
106
|
-
note: '備注',
|
|
107
|
-
definition: '釋義',
|
|
108
|
-
commentary: '注',
|
|
109
|
-
translation: '譯文',
|
|
110
|
-
person: '人名',
|
|
111
|
-
place: '地名',
|
|
112
|
-
event: '事件',
|
|
113
|
-
date: '紀年',
|
|
114
|
-
allusion: '典故',
|
|
115
|
-
}
|
|
116
|
-
return map[ann.kind] || ann.kind
|
|
117
|
-
}
|
|
118
|
-
|
|
119
102
|
function layerLabel(ann: Annotation): string {
|
|
120
103
|
if (!props.layerLabels || !ann.id) return ''
|
|
121
104
|
for (const [prefix, label] of Object.entries(props.layerLabels)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
3
3
|
import { annotationToPronSegment } from '../utils/annotationParser'
|
|
4
|
+
import { kindLabel } from '../utils/annotationLabels'
|
|
4
5
|
import { toChineseNumber } from '../utils/chineseNumber'
|
|
5
6
|
import PronunciationGroup from './PronunciationGroup.vue'
|
|
6
7
|
import type { Annotation } from '../types'
|
|
@@ -48,24 +49,6 @@ function layerLabel(ann: Annotation): string {
|
|
|
48
49
|
return ''
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function kindLabel(ann: Annotation): string {
|
|
52
|
-
const map: Record<string, string> = {
|
|
53
|
-
pronunciation: '讀音',
|
|
54
|
-
semantic: '釋義',
|
|
55
|
-
etymology: '詞源',
|
|
56
|
-
note: '備注',
|
|
57
|
-
definition: '釋義',
|
|
58
|
-
commentary: '注',
|
|
59
|
-
translation: '譯文',
|
|
60
|
-
person: '人名',
|
|
61
|
-
place: '地名',
|
|
62
|
-
event: '事件',
|
|
63
|
-
date: '紀年',
|
|
64
|
-
allusion: '典故',
|
|
65
|
-
}
|
|
66
|
-
return map[ann.kind] || ann.kind
|
|
67
|
-
}
|
|
68
|
-
|
|
69
52
|
function dominantKind(): string {
|
|
70
53
|
if (!props.annotations.length) return ''
|
|
71
54
|
const counts: Record<string, number> = {}
|
|
@@ -13,7 +13,7 @@ function genreLabel(genre: string): string {
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
16
|
-
<div class="bc-root" @click="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}`)">
|
|
17
17
|
<div class="bc-accent"></div>
|
|
18
18
|
<div class="bc-body">
|
|
19
19
|
<h2 class="bc-title">{{ props.book.title }}</h2>
|
|
@@ -45,7 +45,7 @@ function onTap(event: MouseEvent) {
|
|
|
45
45
|
v-for="(_, i) in verses"
|
|
46
46
|
:key="i"
|
|
47
47
|
class="h-display-line h-verse-anim"
|
|
48
|
-
:style="{ animationDelay: (0.15 + i * 0.08) + 's' }"
|
|
48
|
+
:style="{ animationDelay: Math.min(0.15 + i * 0.08, 1.2) + 's' }"
|
|
49
49
|
v-html="verseHtml(i)"
|
|
50
50
|
/>
|
|
51
51
|
</div>
|
|
@@ -88,70 +88,17 @@ function onTap(event: MouseEvent) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
:deep(.ann-target) {
|
|
91
|
-
border-bottom: 2px
|
|
92
|
-
|
|
93
|
-
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
94
|
-
-webkit-box-decoration-break: clone;
|
|
95
|
-
box-decoration-break: clone;
|
|
91
|
+
border-bottom-width: 2px;
|
|
92
|
+
border-bottom-style: solid;
|
|
96
93
|
}
|
|
97
94
|
:deep(.ann-target.ann-overlap) {
|
|
98
95
|
border-bottom-width: 3px;
|
|
99
96
|
border-bottom-style: double;
|
|
100
97
|
}
|
|
101
|
-
:deep(.ann-target:hover) {
|
|
102
|
-
background: rgba(194, 58, 43, 0.1);
|
|
103
|
-
box-shadow: 0 2px 8px rgba(194, 58, 43, 0.08);
|
|
104
|
-
}
|
|
105
98
|
:deep(.ann-num) {
|
|
106
99
|
font-size: 10px;
|
|
107
|
-
color: var(--vermillion);
|
|
108
|
-
font-family: var(--sans);
|
|
109
|
-
font-weight: 600;
|
|
110
100
|
vertical-align: super;
|
|
111
101
|
margin-right: 1px;
|
|
112
|
-
letter-spacing: 0;
|
|
113
|
-
}
|
|
114
|
-
:deep(.ann-target.pronunciation:hover) {
|
|
115
|
-
background: rgba(58, 107, 94, 0.1);
|
|
116
|
-
box-shadow: 0 2px 8px rgba(58, 107, 94, 0.08);
|
|
117
|
-
}
|
|
118
|
-
:deep(.ann-target.pronunciation) {
|
|
119
|
-
border-bottom-color: var(--jade);
|
|
120
|
-
}
|
|
121
|
-
:deep(.ann-target.person) {
|
|
122
|
-
border-bottom-color: var(--ann-person);
|
|
123
|
-
}
|
|
124
|
-
:deep(.ann-target.place) {
|
|
125
|
-
border-bottom-color: var(--ann-place);
|
|
126
|
-
}
|
|
127
|
-
:deep(.ann-target.event) {
|
|
128
|
-
border-bottom-color: var(--ann-event);
|
|
129
|
-
}
|
|
130
|
-
:deep(.ann-target.date) {
|
|
131
|
-
border-bottom-color: var(--ann-date);
|
|
132
|
-
}
|
|
133
|
-
:deep(.ann-target.allusion) {
|
|
134
|
-
border-bottom-color: var(--ann-allusion);
|
|
135
|
-
}
|
|
136
|
-
:deep(.ann-target.person:hover) {
|
|
137
|
-
background: rgba(58, 90, 140, 0.1);
|
|
138
|
-
box-shadow: 0 2px 8px rgba(58, 90, 140, 0.08);
|
|
139
|
-
}
|
|
140
|
-
:deep(.ann-target.place:hover) {
|
|
141
|
-
background: rgba(139, 105, 20, 0.1);
|
|
142
|
-
box-shadow: 0 2px 8px rgba(139, 105, 20, 0.08);
|
|
143
|
-
}
|
|
144
|
-
:deep(.ann-target.event:hover) {
|
|
145
|
-
background: rgba(107, 76, 138, 0.1);
|
|
146
|
-
box-shadow: 0 2px 8px rgba(107, 76, 138, 0.08);
|
|
147
|
-
}
|
|
148
|
-
:deep(.ann-target.date:hover) {
|
|
149
|
-
background: rgba(42, 122, 122, 0.1);
|
|
150
|
-
box-shadow: 0 2px 8px rgba(42, 122, 122, 0.08);
|
|
151
|
-
}
|
|
152
|
-
:deep(.ann-target.allusion:hover) {
|
|
153
|
-
background: rgba(181, 101, 29, 0.1);
|
|
154
|
-
box-shadow: 0 2px 8px rgba(181, 101, 29, 0.08);
|
|
155
102
|
}
|
|
156
103
|
|
|
157
104
|
@media (max-width: 768px) {
|
|
@@ -75,6 +75,9 @@ const sourceLabel = (() => {
|
|
|
75
75
|
.part-block--vertical {
|
|
76
76
|
writing-mode: vertical-rl;
|
|
77
77
|
text-orientation: mixed;
|
|
78
|
+
margin-left: 12px;
|
|
79
|
+
padding-left: 12px;
|
|
80
|
+
border-left: 1px dashed var(--border-light);
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
.part-source {
|
|
@@ -125,75 +128,23 @@ const sourceLabel = (() => {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
:deep(.ann-target) {
|
|
128
|
-
border-bottom: 2px
|
|
129
|
-
|
|
130
|
-
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
131
|
-
-webkit-box-decoration-break: clone;
|
|
132
|
-
box-decoration-break: clone;
|
|
131
|
+
border-bottom-width: 2px;
|
|
132
|
+
border-bottom-style: solid;
|
|
133
133
|
}
|
|
134
134
|
:deep(.ann-target.ann-overlap) {
|
|
135
135
|
border-bottom-width: 3px;
|
|
136
136
|
border-bottom-style: double;
|
|
137
137
|
}
|
|
138
|
-
:deep(.ann-target:hover) {
|
|
139
|
-
background: rgba(194, 58, 43, 0.1);
|
|
140
|
-
box-shadow: 0 2px 8px rgba(194, 58, 43, 0.08);
|
|
141
|
-
}
|
|
142
138
|
:deep(.ann-num) {
|
|
143
139
|
font-size: 10px;
|
|
144
|
-
color: var(--vermillion);
|
|
145
|
-
font-family: var(--sans);
|
|
146
|
-
font-weight: 600;
|
|
147
140
|
vertical-align: super;
|
|
148
141
|
margin-right: 1px;
|
|
149
|
-
letter-spacing: 0;
|
|
150
|
-
}
|
|
151
|
-
:deep(.ann-target.pronunciation) {
|
|
152
|
-
border-bottom-color: var(--jade);
|
|
153
|
-
}
|
|
154
|
-
:deep(.ann-target.pronunciation:hover) {
|
|
155
|
-
background: rgba(58, 107, 94, 0.1);
|
|
156
|
-
box-shadow: 0 2px 8px rgba(58, 107, 94, 0.08);
|
|
157
|
-
}
|
|
158
|
-
:deep(.ann-target.person) {
|
|
159
|
-
border-bottom-color: var(--ann-person);
|
|
160
|
-
}
|
|
161
|
-
:deep(.ann-target.place) {
|
|
162
|
-
border-bottom-color: var(--ann-place);
|
|
163
|
-
}
|
|
164
|
-
:deep(.ann-target.event) {
|
|
165
|
-
border-bottom-color: var(--ann-event);
|
|
166
|
-
}
|
|
167
|
-
:deep(.ann-target.date) {
|
|
168
|
-
border-bottom-color: var(--ann-date);
|
|
169
|
-
}
|
|
170
|
-
:deep(.ann-target.allusion) {
|
|
171
|
-
border-bottom-color: var(--ann-allusion);
|
|
172
|
-
}
|
|
173
|
-
:deep(.ann-target.person:hover) {
|
|
174
|
-
background: rgba(58, 90, 140, 0.1);
|
|
175
|
-
box-shadow: 0 2px 8px rgba(58, 90, 140, 0.08);
|
|
176
|
-
}
|
|
177
|
-
:deep(.ann-target.place:hover) {
|
|
178
|
-
background: rgba(139, 105, 20, 0.1);
|
|
179
|
-
box-shadow: 0 2px 8px rgba(139, 105, 20, 0.08);
|
|
180
|
-
}
|
|
181
|
-
:deep(.ann-target.event:hover) {
|
|
182
|
-
background: rgba(107, 76, 138, 0.1);
|
|
183
|
-
box-shadow: 0 2px 8px rgba(107, 76, 138, 0.08);
|
|
184
|
-
}
|
|
185
|
-
:deep(.ann-target.date:hover) {
|
|
186
|
-
background: rgba(42, 122, 122, 0.1);
|
|
187
|
-
box-shadow: 0 2px 8px rgba(42, 122, 122, 0.08);
|
|
188
|
-
}
|
|
189
|
-
:deep(.ann-target.allusion:hover) {
|
|
190
|
-
background: rgba(181, 101, 29, 0.1);
|
|
191
|
-
box-shadow: 0 2px 8px rgba(181, 101, 29, 0.08);
|
|
192
142
|
}
|
|
193
143
|
|
|
194
144
|
.part-block--vertical :deep(.ann-target) {
|
|
195
145
|
border-bottom: none;
|
|
196
|
-
border-left: 2px
|
|
146
|
+
border-left-width: 2px;
|
|
147
|
+
border-left-style: solid;
|
|
197
148
|
padding-left: 2px;
|
|
198
149
|
}
|
|
199
150
|
.part-block--vertical :deep(.ann-target.ann-overlap) {
|
|
@@ -201,59 +152,9 @@ const sourceLabel = (() => {
|
|
|
201
152
|
border-left-style: double;
|
|
202
153
|
padding-left: 3px;
|
|
203
154
|
}
|
|
204
|
-
.part-block--vertical :deep(.ann-target.pronunciation) {
|
|
205
|
-
border-left-color: var(--jade);
|
|
206
|
-
}
|
|
207
|
-
.part-block--vertical :deep(.ann-target.pronunciation.semantic) {
|
|
208
|
-
border-left-color: var(--gold);
|
|
209
|
-
}
|
|
210
|
-
.part-block--vertical :deep(.ann-target.person) {
|
|
211
|
-
border-left-color: var(--ann-person);
|
|
212
|
-
}
|
|
213
|
-
.part-block--vertical :deep(.ann-target.place) {
|
|
214
|
-
border-left-color: var(--ann-place);
|
|
215
|
-
}
|
|
216
|
-
.part-block--vertical :deep(.ann-target.event) {
|
|
217
|
-
border-left-color: var(--ann-event);
|
|
218
|
-
}
|
|
219
|
-
.part-block--vertical :deep(.ann-target.date) {
|
|
220
|
-
border-left-color: var(--ann-date);
|
|
221
|
-
}
|
|
222
|
-
.part-block--vertical :deep(.ann-target.allusion) {
|
|
223
|
-
border-left-color: var(--ann-allusion);
|
|
224
|
-
}
|
|
225
|
-
.part-block--vertical :deep(.ann-target:hover) {
|
|
226
|
-
background: rgba(194, 58, 43, 0.1);
|
|
227
|
-
box-shadow: 0 -2px 8px rgba(194, 58, 43, 0.08);
|
|
228
|
-
}
|
|
229
|
-
.part-block--vertical :deep(.ann-target.pronunciation:hover) {
|
|
230
|
-
background: rgba(58, 107, 94, 0.1);
|
|
231
|
-
box-shadow: 0 -2px 8px rgba(58, 107, 94, 0.08);
|
|
232
|
-
}
|
|
233
|
-
.part-block--vertical :deep(.ann-target.person:hover) {
|
|
234
|
-
background: rgba(58, 90, 140, 0.1);
|
|
235
|
-
box-shadow: 0 -2px 8px rgba(58, 90, 140, 0.08);
|
|
236
|
-
}
|
|
237
|
-
.part-block--vertical :deep(.ann-target.place:hover) {
|
|
238
|
-
background: rgba(139, 105, 20, 0.1);
|
|
239
|
-
box-shadow: 0 -2px 8px rgba(139, 105, 20, 0.08);
|
|
240
|
-
}
|
|
241
|
-
.part-block--vertical :deep(.ann-target.event:hover) {
|
|
242
|
-
background: rgba(107, 76, 138, 0.1);
|
|
243
|
-
box-shadow: 0 -2px 8px rgba(107, 76, 138, 0.08);
|
|
244
|
-
}
|
|
245
|
-
.part-block--vertical :deep(.ann-target.date:hover) {
|
|
246
|
-
background: rgba(42, 122, 122, 0.1);
|
|
247
|
-
box-shadow: 0 -2px 8px rgba(42, 122, 122, 0.08);
|
|
248
|
-
}
|
|
249
|
-
.part-block--vertical :deep(.ann-target.allusion:hover) {
|
|
250
|
-
background: rgba(181, 101, 29, 0.1);
|
|
251
|
-
box-shadow: 0 -2px 8px rgba(181, 101, 29, 0.08);
|
|
252
|
-
}
|
|
253
155
|
.part-block--vertical :deep(.ann-num) {
|
|
254
156
|
font-size: 0.45em;
|
|
255
157
|
text-align: end;
|
|
256
|
-
letter-spacing: 0;
|
|
257
158
|
vertical-align: baseline;
|
|
258
159
|
}
|
|
259
160
|
|
|
@@ -15,13 +15,13 @@ const preview = computed(() => {
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<div class="pc-root" :class="{ 'pc-vertical': vertical }" @click="$emit('click')">
|
|
18
|
+
<div class="pc-root" :class="{ 'pc-vertical': vertical }" role="button" tabindex="0" @click="$emit('click')" @keydown.enter="$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>
|
|
22
22
|
<h3 class="pc-title">{{ poem.title }}</h3>
|
|
23
23
|
<div class="pc-author">{{ poem.author }}</div>
|
|
24
|
-
<p class="pc-preview"
|
|
24
|
+
<p class="pc-preview">{{ preview }}</p>
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
27
27
|
</template>
|
|
@@ -78,6 +78,7 @@ const preview = computed(() => {
|
|
|
78
78
|
font-size: 13px; color: var(--ink-faint);
|
|
79
79
|
margin-top: 14px; line-height: 1.7;
|
|
80
80
|
overflow: hidden;
|
|
81
|
+
white-space: pre-line;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/* ─── 直排卡片:固定寬度,最小高度 ─── */
|
|
@@ -85,8 +86,8 @@ const preview = computed(() => {
|
|
|
85
86
|
writing-mode: vertical-rl;
|
|
86
87
|
text-orientation: mixed;
|
|
87
88
|
direction: ltr;
|
|
88
|
-
width:
|
|
89
|
-
min-height:
|
|
89
|
+
width: 140px;
|
|
90
|
+
min-height: 180px;
|
|
90
91
|
flex-shrink: 0;
|
|
91
92
|
align-self: start;
|
|
92
93
|
justify-self: start;
|
|
@@ -121,13 +122,7 @@ const preview = computed(() => {
|
|
|
121
122
|
display: block;
|
|
122
123
|
}
|
|
123
124
|
.pc-vertical .pc-preview {
|
|
124
|
-
|
|
125
|
-
margin-left: 6px;
|
|
126
|
-
font-size: 12px;
|
|
127
|
-
letter-spacing: 1px;
|
|
128
|
-
line-height: 2;
|
|
129
|
-
display: block;
|
|
130
|
-
overflow: hidden;
|
|
125
|
+
display: none;
|
|
131
126
|
}
|
|
132
127
|
.pc-vertical .pc-accent {
|
|
133
128
|
top: auto; left: 0; bottom: 0;
|
|
@@ -48,7 +48,7 @@ function onTap(event: MouseEvent) {
|
|
|
48
48
|
v-for="(_, i) in verses"
|
|
49
49
|
:key="i"
|
|
50
50
|
class="v-scroll-line v-verse-anim"
|
|
51
|
-
:style="{ animationDelay: (0.2 + i * 0.06) + 's' }"
|
|
51
|
+
:style="{ animationDelay: Math.min(0.2 + i * 0.06, 1.0) + 's' }"
|
|
52
52
|
v-html="verseHtml(i)"
|
|
53
53
|
/>
|
|
54
54
|
</div>
|
|
@@ -57,22 +57,18 @@ function onTap(event: MouseEvent) {
|
|
|
57
57
|
|
|
58
58
|
<style scoped>
|
|
59
59
|
.v-scroll {
|
|
60
|
+
--ann-shadow-y: -2px;
|
|
60
61
|
writing-mode: vertical-rl;
|
|
61
62
|
text-orientation: mixed;
|
|
62
63
|
height: calc(100vh - 100px);
|
|
63
|
-
overflow-x: auto; overflow-y: hidden;
|
|
64
64
|
padding: 32px 24px;
|
|
65
65
|
background: var(--surface);
|
|
66
66
|
border: 1px solid var(--border);
|
|
67
67
|
border-radius: 8px;
|
|
68
68
|
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
|
|
69
69
|
position: relative;
|
|
70
|
-
scrollbar-width: thin;
|
|
71
|
-
scrollbar-color: var(--gold) transparent;
|
|
72
70
|
animation: poemRevealV 0.5s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)) both;
|
|
73
71
|
}
|
|
74
|
-
.v-scroll::-webkit-scrollbar { height: 3px; }
|
|
75
|
-
.v-scroll::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
76
72
|
|
|
77
73
|
.v-scroll-title {
|
|
78
74
|
font-size: 36px; font-weight: 900; color: var(--ink);
|
|
@@ -104,12 +100,9 @@ function onTap(event: MouseEvent) {
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
:deep(.ann-target) {
|
|
107
|
-
border-left: 2px
|
|
103
|
+
border-left-width: 2px;
|
|
104
|
+
border-left-style: solid;
|
|
108
105
|
padding-left: 2px;
|
|
109
|
-
cursor: help;
|
|
110
|
-
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
111
|
-
-webkit-box-decoration-break: clone;
|
|
112
|
-
box-decoration-break: clone;
|
|
113
106
|
}
|
|
114
107
|
:deep(.ann-target.ann-overlap) {
|
|
115
108
|
border-left-width: 3px;
|
|
@@ -118,63 +111,9 @@ function onTap(event: MouseEvent) {
|
|
|
118
111
|
}
|
|
119
112
|
:deep(.ann-num) {
|
|
120
113
|
font-size: 0.45em;
|
|
121
|
-
font-family: var(--sans);
|
|
122
|
-
font-weight: 600;
|
|
123
|
-
color: var(--vermillion);
|
|
124
114
|
text-align: end;
|
|
125
|
-
letter-spacing: 0;
|
|
126
115
|
}
|
|
127
116
|
:deep(.ann-num-long) {
|
|
128
117
|
font-size: 0.38em;
|
|
129
|
-
letter-spacing: -1px;
|
|
130
|
-
}
|
|
131
|
-
:deep(.ann-target:hover) {
|
|
132
|
-
background: rgba(194, 58, 43, 0.1);
|
|
133
|
-
box-shadow: 0 -2px 8px rgba(194, 58, 43, 0.08);
|
|
134
|
-
}
|
|
135
|
-
:deep(.ann-target.pronunciation:hover) {
|
|
136
|
-
background: rgba(58, 107, 94, 0.1);
|
|
137
|
-
box-shadow: 0 -2px 8px rgba(58, 107, 94, 0.08);
|
|
138
|
-
}
|
|
139
|
-
:deep(.ann-target.pronunciation) {
|
|
140
|
-
border-left-color: var(--jade);
|
|
141
|
-
}
|
|
142
|
-
:deep(.ann-target.pronunciation.semantic) {
|
|
143
|
-
border-left-color: var(--gold);
|
|
144
|
-
}
|
|
145
|
-
:deep(.ann-target.person) {
|
|
146
|
-
border-left-color: var(--ann-person);
|
|
147
|
-
}
|
|
148
|
-
:deep(.ann-target.place) {
|
|
149
|
-
border-left-color: var(--ann-place);
|
|
150
|
-
}
|
|
151
|
-
:deep(.ann-target.event) {
|
|
152
|
-
border-left-color: var(--ann-event);
|
|
153
|
-
}
|
|
154
|
-
:deep(.ann-target.date) {
|
|
155
|
-
border-left-color: var(--ann-date);
|
|
156
|
-
}
|
|
157
|
-
:deep(.ann-target.allusion) {
|
|
158
|
-
border-left-color: var(--ann-allusion);
|
|
159
|
-
}
|
|
160
|
-
:deep(.ann-target.person:hover) {
|
|
161
|
-
background: rgba(58, 90, 140, 0.1);
|
|
162
|
-
box-shadow: 0 -2px 8px rgba(58, 90, 140, 0.08);
|
|
163
|
-
}
|
|
164
|
-
:deep(.ann-target.place:hover) {
|
|
165
|
-
background: rgba(139, 105, 20, 0.1);
|
|
166
|
-
box-shadow: 0 -2px 8px rgba(139, 105, 20, 0.08);
|
|
167
|
-
}
|
|
168
|
-
:deep(.ann-target.event:hover) {
|
|
169
|
-
background: rgba(107, 76, 138, 0.1);
|
|
170
|
-
box-shadow: 0 -2px 8px rgba(107, 76, 138, 0.08);
|
|
171
|
-
}
|
|
172
|
-
:deep(.ann-target.date:hover) {
|
|
173
|
-
background: rgba(42, 122, 122, 0.1);
|
|
174
|
-
box-shadow: 0 -2px 8px rgba(42, 122, 122, 0.08);
|
|
175
|
-
}
|
|
176
|
-
:deep(.ann-target.allusion:hover) {
|
|
177
|
-
background: rgba(181, 101, 29, 0.1);
|
|
178
|
-
box-shadow: 0 -2px 8px rgba(181, 101, 29, 0.08);
|
|
179
118
|
}
|
|
180
119
|
</style>
|
|
@@ -68,6 +68,11 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
68
68
|
'role.editor': '編者',
|
|
69
69
|
'role.translator': '譯者',
|
|
70
70
|
'role.annotator': '注者',
|
|
71
|
+
'section.background': '背景資料',
|
|
72
|
+
'section.analysis': '賞析重點',
|
|
73
|
+
'section.preparation': '預習活動',
|
|
74
|
+
'section.follow_up': '跟進活動',
|
|
75
|
+
'section.think_questions': '想一想',
|
|
71
76
|
},
|
|
72
77
|
'zh-Hans': {
|
|
73
78
|
'site.title': '古典诗文图书馆',
|
|
@@ -128,6 +133,11 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
128
133
|
'role.editor': '编者',
|
|
129
134
|
'role.translator': '译者',
|
|
130
135
|
'role.annotator': '注者',
|
|
136
|
+
'section.background': '背景资料',
|
|
137
|
+
'section.analysis': '赏析重点',
|
|
138
|
+
'section.preparation': '预习活动',
|
|
139
|
+
'section.follow_up': '跟进活动',
|
|
140
|
+
'section.think_questions': '想一想',
|
|
131
141
|
},
|
|
132
142
|
'en': {
|
|
133
143
|
'site.title': 'Classical Chinese Text Library',
|
|
@@ -188,6 +198,11 @@ const messages: Record<Locale, Record<string, string>> = {
|
|
|
188
198
|
'role.editor': 'Editor',
|
|
189
199
|
'role.translator': 'Translator',
|
|
190
200
|
'role.annotator': 'Annotator',
|
|
201
|
+
'section.background': 'Background',
|
|
202
|
+
'section.analysis': 'Analysis',
|
|
203
|
+
'section.preparation': 'Pre-reading',
|
|
204
|
+
'section.follow_up': 'Follow-up',
|
|
205
|
+
'section.think_questions': 'Think About It',
|
|
191
206
|
},
|
|
192
207
|
}
|
|
193
208
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* ─── Annotation target base ─── */
|
|
2
|
+
.ann-target {
|
|
3
|
+
cursor: help;
|
|
4
|
+
border-color: var(--vermillion);
|
|
5
|
+
transition: background 0.2s ease, box-shadow 0.2s ease;
|
|
6
|
+
-webkit-box-decoration-break: clone;
|
|
7
|
+
box-decoration-break: clone;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* ─── Annotation kind border colors ─── */
|
|
11
|
+
.ann-target.pronunciation { border-color: var(--jade); }
|
|
12
|
+
.ann-target.pronunciation.semantic { border-color: var(--gold); }
|
|
13
|
+
.ann-target.person { border-color: var(--ann-person); }
|
|
14
|
+
.ann-target.place { border-color: var(--ann-place); }
|
|
15
|
+
.ann-target.event { border-color: var(--ann-event); }
|
|
16
|
+
.ann-target.date { border-color: var(--ann-date); }
|
|
17
|
+
.ann-target.allusion { border-color: var(--ann-allusion); }
|
|
18
|
+
|
|
19
|
+
/* ─── Annotation kind hover states ─── */
|
|
20
|
+
.ann-target:hover {
|
|
21
|
+
background: rgba(194, 58, 43, 0.1);
|
|
22
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(194, 58, 43, 0.08);
|
|
23
|
+
}
|
|
24
|
+
.ann-target.pronunciation:hover {
|
|
25
|
+
background: rgba(58, 107, 94, 0.1);
|
|
26
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(58, 107, 94, 0.08);
|
|
27
|
+
}
|
|
28
|
+
.ann-target.person:hover {
|
|
29
|
+
background: rgba(58, 90, 140, 0.1);
|
|
30
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(58, 90, 140, 0.08);
|
|
31
|
+
}
|
|
32
|
+
.ann-target.place:hover {
|
|
33
|
+
background: rgba(139, 105, 20, 0.1);
|
|
34
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(139, 105, 20, 0.08);
|
|
35
|
+
}
|
|
36
|
+
.ann-target.event:hover {
|
|
37
|
+
background: rgba(107, 76, 138, 0.1);
|
|
38
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(107, 76, 138, 0.08);
|
|
39
|
+
}
|
|
40
|
+
.ann-target.date:hover {
|
|
41
|
+
background: rgba(42, 122, 122, 0.1);
|
|
42
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(42, 122, 122, 0.08);
|
|
43
|
+
}
|
|
44
|
+
.ann-target.allusion:hover {
|
|
45
|
+
background: rgba(181, 101, 29, 0.1);
|
|
46
|
+
box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(181, 101, 29, 0.08);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ─── Annotation number ─── */
|
|
50
|
+
.ann-num {
|
|
51
|
+
color: var(--vermillion);
|
|
52
|
+
font-family: var(--sans);
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
letter-spacing: 0;
|
|
55
|
+
}
|
|
56
|
+
.ann-num-long {
|
|
57
|
+
letter-spacing: -1px;
|
|
58
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import './annotation-targets.css';
|
|
2
|
+
|
|
1
3
|
/* ===== THEME DEFINITIONS ===== */
|
|
2
4
|
:root,
|
|
3
5
|
[data-theme="light"] {
|
|
@@ -136,7 +138,6 @@ body {
|
|
|
136
138
|
::-webkit-scrollbar-track { background: transparent; }
|
|
137
139
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
138
140
|
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
|
|
139
|
-
html[dir="rtl"] ::-webkit-scrollbar-thumb { background: var(--gold); }
|
|
140
141
|
|
|
141
142
|
a { color: inherit; text-decoration: none; }
|
|
142
143
|
button { font-family: inherit; }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Annotation } from '../types'
|
|
2
|
+
|
|
3
|
+
const KIND_LABELS: Record<string, string> = {
|
|
4
|
+
pronunciation: '讀音',
|
|
5
|
+
semantic: '釋義',
|
|
6
|
+
etymology: '詞源',
|
|
7
|
+
note: '備注',
|
|
8
|
+
definition: '釋義',
|
|
9
|
+
commentary: '注',
|
|
10
|
+
translation: '譯文',
|
|
11
|
+
person: '人名',
|
|
12
|
+
place: '地名',
|
|
13
|
+
event: '事件',
|
|
14
|
+
date: '紀年',
|
|
15
|
+
allusion: '典故',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function kindLabel(ann: Annotation): string {
|
|
19
|
+
return KIND_LABELS[ann.kind] || ann.kind
|
|
20
|
+
}
|
|
@@ -7,11 +7,14 @@ import { useData } from '../composables/useData'
|
|
|
7
7
|
import { useTitle } from '../composables/useTitle'
|
|
8
8
|
import { useReadingMode } from '../composables/useReadingMode'
|
|
9
9
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
10
|
+
import { useI18n } from '../composables/useI18n'
|
|
10
11
|
import SideNav from '../components/SideNav.vue'
|
|
12
|
+
import BackToTop from '../components/BackToTop.vue'
|
|
11
13
|
import type { Piece } from '../types'
|
|
12
14
|
|
|
13
15
|
const route = useRoute()
|
|
14
16
|
const router = useRouter()
|
|
17
|
+
const { t } = useI18n()
|
|
15
18
|
|
|
16
19
|
const { loadLibrary, books, singleBook } = useLibrary()
|
|
17
20
|
await loadLibrary()
|
|
@@ -51,11 +54,11 @@ function goHome() { router.push('/') }
|
|
|
51
54
|
<div class="v-seal">{{ authorName.charAt(0) }}</div>
|
|
52
55
|
<h1 class="v-name">{{ authorName }}</h1>
|
|
53
56
|
<span v-if="author.era" class="v-era">{{ author.era }}</span>
|
|
54
|
-
<span class="v-count">{{ authorPieces.length }}
|
|
57
|
+
<span class="v-count">{{ t('author.worksCount', { count: authorPieces.length }) }}</span>
|
|
55
58
|
</section>
|
|
56
59
|
|
|
57
60
|
<section v-if="author.bio" class="v-bio">
|
|
58
|
-
<div class="v-bio-label"
|
|
61
|
+
<div class="v-bio-label">{{ t('author.biography') }}</div>
|
|
59
62
|
<div class="v-bio-text">{{ author.bio }}</div>
|
|
60
63
|
</section>
|
|
61
64
|
|
|
@@ -63,7 +66,10 @@ function goHome() { router.push('/') }
|
|
|
63
66
|
v-for="piece in authorPieces"
|
|
64
67
|
:key="`${piece.bookId}-${piece.num}`"
|
|
65
68
|
class="v-work"
|
|
69
|
+
role="button"
|
|
70
|
+
tabindex="0"
|
|
66
71
|
@click="openPiece(piece)"
|
|
72
|
+
@keydown.enter="openPiece(piece)"
|
|
67
73
|
>
|
|
68
74
|
<div class="v-work-num">{{ String(piece.num).padStart(3, '0') }}</div>
|
|
69
75
|
<div class="v-work-title">{{ piece.title }}</div>
|
|
@@ -76,15 +82,15 @@ function goHome() { router.push('/') }
|
|
|
76
82
|
<div class="h-page">
|
|
77
83
|
<nav class="h-nav">
|
|
78
84
|
<div class="h-nav-inner">
|
|
79
|
-
<button class="h-back" @click="goBack">←
|
|
85
|
+
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
80
86
|
<div class="h-breadcrumb">
|
|
81
|
-
<span class="h-sep"
|
|
87
|
+
<span class="h-sep">{{ t('role.author') }}</span>
|
|
82
88
|
<span class="h-sep">·</span>
|
|
83
89
|
<span class="h-author-name">{{ authorName }}</span>
|
|
84
90
|
</div>
|
|
85
91
|
<div class="h-controls">
|
|
86
|
-
<span class="h-tag">{{ author.era || '
|
|
87
|
-
<span class="h-tag">{{ authorPieces.length }}
|
|
92
|
+
<span class="h-tag">{{ author.era || t('author.unknownEra') }}</span>
|
|
93
|
+
<span class="h-tag">{{ t('stat.pieceCount', { count: authorPieces.length }) }}</span>
|
|
88
94
|
</div>
|
|
89
95
|
</div>
|
|
90
96
|
</nav>
|
|
@@ -96,24 +102,27 @@ function goHome() { router.push('/') }
|
|
|
96
102
|
<h1 class="h-name">{{ authorName }}</h1>
|
|
97
103
|
<div class="h-meta">
|
|
98
104
|
<span v-if="author.era" class="h-era">{{ author.era }}</span>
|
|
99
|
-
<span class="h-count">{{ authorPieces.length }}
|
|
105
|
+
<span class="h-count">{{ t('author.worksCount', { count: authorPieces.length }) }}</span>
|
|
100
106
|
</div>
|
|
101
107
|
</div>
|
|
102
108
|
</div>
|
|
103
109
|
|
|
104
110
|
<div v-if="author.bio" class="h-bio">
|
|
105
|
-
<h3
|
|
111
|
+
<h3>{{ t('author.biography') }}</h3>
|
|
106
112
|
<p>{{ author.bio }}</p>
|
|
107
113
|
</div>
|
|
108
114
|
|
|
109
115
|
<div class="h-works">
|
|
110
|
-
<h3
|
|
116
|
+
<h3>{{ t('author.collectedWorks') }}</h3>
|
|
111
117
|
<div class="h-grid">
|
|
112
118
|
<div
|
|
113
119
|
v-for="piece in authorPieces"
|
|
114
120
|
:key="`${piece.bookId}-${piece.num}`"
|
|
115
121
|
class="h-work"
|
|
122
|
+
role="button"
|
|
123
|
+
tabindex="0"
|
|
116
124
|
@click="openPiece(piece)"
|
|
125
|
+
@keydown.enter="openPiece(piece)"
|
|
117
126
|
>
|
|
118
127
|
<div class="h-work-num">{{ String(piece.num).padStart(3, '0') }}</div>
|
|
119
128
|
<div class="h-work-title">{{ piece.title }}</div>
|
|
@@ -123,11 +132,13 @@ function goHome() { router.push('/') }
|
|
|
123
132
|
</div>
|
|
124
133
|
</div>
|
|
125
134
|
</div>
|
|
135
|
+
|
|
136
|
+
<BackToTop />
|
|
126
137
|
</div>
|
|
127
138
|
</div>
|
|
128
139
|
|
|
129
|
-
<div v-else
|
|
130
|
-
<
|
|
140
|
+
<div v-else class="page-loading">
|
|
141
|
+
<div class="page-loading-seal">文</div>
|
|
131
142
|
</div>
|
|
132
143
|
</template>
|
|
133
144
|
|
|
@@ -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')" />
|
|
81
|
+
<input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" aria-label="search" />
|
|
82
82
|
</span>
|
|
83
83
|
</section>
|
|
84
84
|
|
|
@@ -259,13 +259,11 @@ function scrollToCatalog() {
|
|
|
259
259
|
flex-shrink: 0;
|
|
260
260
|
display: grid;
|
|
261
261
|
grid-auto-flow: column;
|
|
262
|
-
grid-template-rows: repeat(auto-fill,
|
|
263
|
-
gap:
|
|
262
|
+
grid-template-rows: repeat(auto-fill, 180px);
|
|
263
|
+
gap: 10px;
|
|
264
264
|
padding: 24px 16px;
|
|
265
265
|
height: 100vh;
|
|
266
266
|
box-sizing: border-box;
|
|
267
|
-
overflow-x: auto;
|
|
268
|
-
overflow-y: hidden;
|
|
269
267
|
direction: rtl;
|
|
270
268
|
align-items: start;
|
|
271
269
|
}
|
|
@@ -53,6 +53,12 @@ function bookCategory(book: BookMeta): string {
|
|
|
53
53
|
return t('genre.classicalText')
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function bookCategoryKey(book: BookMeta): string {
|
|
57
|
+
if (book.id.startsWith('skqs-')) return 'fourTreasuries'
|
|
58
|
+
if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return 'textbooks'
|
|
59
|
+
return 'classicalText'
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
const groupedBooks = computed(() => {
|
|
57
63
|
const groups = new Map<string, BookMeta[]>()
|
|
58
64
|
const order = [t('genre.textbooks'), t('genre.classicalText'), t('genre.fourTreasuries')]
|
|
@@ -97,10 +103,11 @@ function openBook(bookId: string) {
|
|
|
97
103
|
<span class="v-cat-label">{{ group.category }}</span>
|
|
98
104
|
</div>
|
|
99
105
|
<div
|
|
100
|
-
v-for="book in group.books"
|
|
106
|
+
v-for="(book, bi) in group.books"
|
|
101
107
|
:key="book.id"
|
|
102
|
-
class="v-spine"
|
|
103
|
-
:
|
|
108
|
+
class="v-spine v-spine-anim"
|
|
109
|
+
:data-cat="bookCategoryKey(book)"
|
|
110
|
+
:style="{ animationDelay: bi * 0.04 + 's' }"
|
|
104
111
|
@click="openBook(book.id)"
|
|
105
112
|
>
|
|
106
113
|
<span class="v-spine-accent"></span>
|
|
@@ -261,6 +268,14 @@ function openBook(bookId: string) {
|
|
|
261
268
|
transform: scale(0.97);
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
.v-spine-anim {
|
|
272
|
+
animation: spineEnter 0.4s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1)) both;
|
|
273
|
+
}
|
|
274
|
+
@keyframes spineEnter {
|
|
275
|
+
from { opacity: 0; transform: scaleX(0.6); }
|
|
276
|
+
to { opacity: 1; transform: scaleX(1); }
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
.v-spine-accent {
|
|
265
280
|
position: absolute;
|
|
266
281
|
top: 0;
|
|
@@ -275,8 +290,8 @@ function openBook(bookId: string) {
|
|
|
275
290
|
|
|
276
291
|
/* Accent colors per category */
|
|
277
292
|
.v-spine .v-spine-accent { background: var(--vermillion); }
|
|
278
|
-
.v-spine
|
|
279
|
-
.v-spine
|
|
293
|
+
.v-spine[data-cat="textbooks"] .v-spine-accent { background: var(--gold); }
|
|
294
|
+
.v-spine[data-cat="fourTreasuries"] .v-spine-accent { background: var(--jade); }
|
|
280
295
|
|
|
281
296
|
.v-spine-title {
|
|
282
297
|
writing-mode: vertical-rl;
|
|
@@ -293,8 +308,8 @@ function openBook(bookId: string) {
|
|
|
293
308
|
.v-spine:hover .v-spine-title {
|
|
294
309
|
color: var(--vermillion);
|
|
295
310
|
}
|
|
296
|
-
.v-spine
|
|
297
|
-
.v-spine
|
|
311
|
+
.v-spine[data-cat="textbooks"]:hover .v-spine-title { color: var(--gold); }
|
|
312
|
+
.v-spine[data-cat="fourTreasuries"]:hover .v-spine-title { color: var(--jade); }
|
|
298
313
|
|
|
299
314
|
.v-spine-badge {
|
|
300
315
|
writing-mode: horizontal-tb;
|
|
@@ -406,11 +421,6 @@ function openBook(bookId: string) {
|
|
|
406
421
|
transition: all 0.3s var(--ease-out-expo, ease);
|
|
407
422
|
position: relative;
|
|
408
423
|
background: var(--surface);
|
|
409
|
-
animation: cardEnter 0.5s var(--ease-out-expo, ease) both;
|
|
410
|
-
}
|
|
411
|
-
@keyframes cardEnter {
|
|
412
|
-
from { opacity: 0; transform: translateY(12px); }
|
|
413
|
-
to { opacity: 1; transform: translateY(0); }
|
|
414
424
|
}
|
|
415
425
|
.lib-card:hover { border-color: var(--gold); box-shadow: 0 6px 24px rgba(var(--shadow-rgb), 0.1); transform: translateY(-2px); }
|
|
416
426
|
.lib-card:active { transform: scale(0.98); }
|
|
@@ -232,12 +232,12 @@ const totalPartAnnotationCount = computed(() => {
|
|
|
232
232
|
return piece.value?.parts?.reduce((sum, p) => sum + p.annotations.length, 0) ?? 0
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
-
const SECTION_META: Record<string, {
|
|
236
|
-
background: {
|
|
237
|
-
analysis: {
|
|
238
|
-
preparation: {
|
|
239
|
-
follow_up: {
|
|
240
|
-
think_questions: {
|
|
235
|
+
const SECTION_META: Record<string, { special: boolean }> = {
|
|
236
|
+
background: { special: false },
|
|
237
|
+
analysis: { special: false },
|
|
238
|
+
preparation: { special: true },
|
|
239
|
+
follow_up: { special: true },
|
|
240
|
+
think_questions: { special: true },
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
const proseSections = computed(() => {
|
|
@@ -245,13 +245,12 @@ const proseSections = computed(() => {
|
|
|
245
245
|
if (ss && ss.length > 0) {
|
|
246
246
|
return ss.filter(s => s.key !== 'author_bio' && s.body)
|
|
247
247
|
}
|
|
248
|
-
// Fallback to legacy sections record
|
|
249
248
|
const sections = piece.value?.sections || {}
|
|
250
249
|
const result: { key: string; title: string; body: string; order: number; special: boolean }[] = []
|
|
251
|
-
for (const [key,
|
|
250
|
+
for (const [key, i18nKey] of Object.entries({ background: 'section.background', analysis: 'section.analysis', preparation: 'section.preparation', follow_up: 'section.follow_up', think_questions: 'section.think_questions' })) {
|
|
252
251
|
if (sections[key]) {
|
|
253
252
|
const meta = SECTION_META[key]
|
|
254
|
-
result.push({ key, title:
|
|
253
|
+
result.push({ key, title: t(i18nKey), body: sections[key], order: meta ? (key === 'background' ? 1 : key === 'analysis' ? 2 : 3) : 99, special: meta?.special ?? false })
|
|
255
254
|
}
|
|
256
255
|
}
|
|
257
256
|
return result
|
|
@@ -791,16 +790,9 @@ function tcy(n: number): string {
|
|
|
791
790
|
gap: 0;
|
|
792
791
|
height: 100vh;
|
|
793
792
|
box-sizing: border-box;
|
|
794
|
-
overflow-x: auto;
|
|
795
|
-
overflow-y: hidden;
|
|
796
793
|
padding: 20px 16px;
|
|
797
|
-
scrollbar-width: thin;
|
|
798
|
-
scrollbar-color: var(--gold) transparent;
|
|
799
794
|
}
|
|
800
795
|
|
|
801
|
-
.v-multipart::-webkit-scrollbar { height: 3px; }
|
|
802
|
-
.v-multipart::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
803
|
-
|
|
804
796
|
.v-section {
|
|
805
797
|
flex-shrink: 0;
|
|
806
798
|
}
|