@hanology/cham-browser 0.3.9 → 0.4.2

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.
Files changed (30) hide show
  1. package/dist/cli.js +303 -32
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template/index.html +4 -8
  5. package/template/src/App.vue +101 -17
  6. package/template/src/components/AnnotationControlBar.vue +119 -49
  7. package/template/src/components/AnnotationTooltip.vue +319 -95
  8. package/template/src/components/BackToTop.vue +4 -0
  9. package/template/src/components/BookCard.vue +10 -11
  10. package/template/src/components/HorizontalDisplay.vue +56 -0
  11. package/template/src/components/PartBlock.vue +9 -0
  12. package/template/src/components/PoemCard.vue +1 -0
  13. package/template/src/components/PronunciationGroup.vue +27 -18
  14. package/template/src/components/ReadingToolbar.vue +20 -0
  15. package/template/src/components/SectionBlock.vue +91 -12
  16. package/template/src/components/SideNav.vue +5 -4
  17. package/template/src/components/VerticalScroll.vue +35 -0
  18. package/template/src/composables/useAnnotationRenderer.ts +57 -25
  19. package/template/src/composables/useData.ts +6 -1
  20. package/template/src/composables/useI18n.ts +36 -3
  21. package/template/src/composables/useReadingMode.ts +9 -4
  22. package/template/src/composables/useSiteConfig.ts +12 -1
  23. package/template/src/router.ts +0 -2
  24. package/template/src/styles/main.css +88 -0
  25. package/template/src/types.ts +12 -4
  26. package/template/src/views/AuthorView.vue +5 -5
  27. package/template/src/views/BookHome.vue +45 -21
  28. package/template/src/views/LibraryHome.vue +39 -41
  29. package/template/src/views/PieceView.vue +436 -71
  30. package/template/src/views/AboutView.vue +0 -191
@@ -1,15 +1,17 @@
1
1
  <script setup lang="ts">
2
- import { computed, watch, onMounted, onBeforeUnmount } from 'vue'
3
- import { useReadingMode } from '../composables/useReadingMode'
2
+ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
4
3
  import { annotationToPronSegment } from '../utils/annotationParser'
5
4
  import PronunciationGroup from './PronunciationGroup.vue'
6
5
  import type { Annotation } from '../types'
7
6
 
7
+ type DisplayMode = 'pane' | 'popup' | 'sheet'
8
+
8
9
  const props = defineProps<{
9
10
  visible: boolean
10
11
  annotations: Annotation[]
11
12
  layerLabels?: Record<string, string>
12
13
  style?: Record<string, string>
14
+ vertical?: boolean
13
15
  }>()
14
16
 
15
17
  const emit = defineEmits<{
@@ -17,8 +19,33 @@ const emit = defineEmits<{
17
19
  tooltipEnter: []
18
20
  tooltipLeave: []
19
21
  }>()
20
- const { layout } = useReadingMode()
21
- const isMobile = computed(() => typeof window !== 'undefined' && window.innerWidth < 768)
22
+
23
+ const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
24
+
25
+ function onResize() { ww.value = window.innerWidth }
26
+
27
+ const mode = computed<DisplayMode>(() => {
28
+ const w = ww.value
29
+ if (w < 768) return 'sheet'
30
+ if (w >= 1024) return 'pane'
31
+ return 'popup'
32
+ })
33
+
34
+ const stickyVisible = ref(false)
35
+
36
+ watch(() => props.visible, (v) => {
37
+ if (v) stickyVisible.value = true
38
+ else if (mode.value === 'popup') stickyVisible.value = false
39
+ })
40
+
41
+ watch(mode, () => {
42
+ if (mode.value === 'popup' && !props.visible) stickyVisible.value = false
43
+ })
44
+
45
+ function dismiss() {
46
+ stickyVisible.value = false
47
+ emit('close')
48
+ }
22
49
 
23
50
  function getSegment(ann: Annotation) {
24
51
  return annotationToPronSegment(ann)
@@ -32,26 +59,59 @@ function layerLabel(ann: Annotation): string {
32
59
  return ''
33
60
  }
34
61
 
62
+ function kindLabel(ann: Annotation): string {
63
+ const map: Record<string, string> = {
64
+ pronunciation: '讀音',
65
+ semantic: '釋義',
66
+ etymology: '詞源',
67
+ note: '備注',
68
+ definition: '釋義',
69
+ commentary: '評註',
70
+ translation: '譯文',
71
+ person: '人名',
72
+ place: '地名',
73
+ event: '事件',
74
+ date: '紀年',
75
+ allusion: '典故',
76
+ }
77
+ return map[ann.kind] || ann.kind
78
+ }
79
+
80
+ const hasOverlap = computed(() =>
81
+ props.annotations.some(a => {
82
+ const seg = annotationToPronSegment(a)
83
+ return !seg && props.annotations.filter(b =>
84
+ b.range.scope === a.range.scope &&
85
+ b.range.verseIndex === a.range.verseIndex &&
86
+ (b.range.start ?? 0) !== (a.range.start ?? 0) ||
87
+ (b.range.end ?? 0) !== (a.range.end ?? 0)
88
+ ).length > 0
89
+ })
90
+ )
91
+
35
92
  function onDocClick(e: MouseEvent) {
36
- if (!props.visible) return
37
- const tooltip = (e.target as HTMLElement).closest('.ann-tooltip')
38
- if (tooltip) return
39
- emit('close')
93
+ if (!stickyVisible.value) return
94
+ const el = (e.target as HTMLElement)
95
+ if (el.closest('.ann-left-pane, .ann-right-pane, .ann-bottom-sheet, .ann-popup')) return
96
+ if (el.closest('.ann-target')) return
97
+ dismiss()
40
98
  }
41
99
 
42
100
  function onDocTouchMove(e: TouchEvent) {
43
- if (!props.visible || !isMobile.value) return
44
- const tooltip = (e.target as HTMLElement).closest('.ann-tooltip')
45
- if (tooltip) return
46
- emit('close')
101
+ if (!stickyVisible.value || ww.value >= 768) return
102
+ const el = (e.target as HTMLElement)
103
+ if (el.closest('.ann-bottom-sheet')) return
104
+ dismiss()
47
105
  }
48
106
 
49
107
  onMounted(() => {
108
+ window.addEventListener('resize', onResize, { passive: true })
50
109
  document.addEventListener('click', onDocClick, true)
51
110
  document.addEventListener('touchmove', onDocTouchMove, { passive: true })
52
111
  })
53
112
 
54
113
  onBeforeUnmount(() => {
114
+ window.removeEventListener('resize', onResize)
55
115
  document.removeEventListener('click', onDocClick, true)
56
116
  document.removeEventListener('touchmove', onDocTouchMove)
57
117
  })
@@ -59,28 +119,73 @@ onBeforeUnmount(() => {
59
119
 
60
120
  <template>
61
121
  <Teleport to="body">
122
+ <!-- Desktop pane: LEFT for horizontal, RIGHT for vertical -->
123
+ <Transition :name="vertical ? 'ann-slide-left' : 'ann-slide-right'">
124
+ <div
125
+ v-if="mode === 'pane' && stickyVisible && annotations.length"
126
+ :class="vertical ? 'ann-right-pane' : 'ann-left-pane'"
127
+ @mouseenter="emit('tooltipEnter')"
128
+ @mouseleave="emit('tooltipLeave')"
129
+ >
130
+ <button class="ann-pane-close" @click="dismiss" aria-label="關閉">
131
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
132
+ </button>
133
+ <div class="ann-pane-inner">
134
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
135
+ <div class="ann-detail-head">
136
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
137
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
138
+ </div>
139
+ <div class="ann-detail-body">
140
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
141
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </Transition>
147
+
148
+ <!-- Tablet popup -->
62
149
  <Transition name="ann-fade">
63
150
  <div
64
- v-if="visible && annotations.length"
65
- class="ann-tooltip"
66
- :class="{ 'ann-vertical': layout === 'vertical', 'ann-mobile-bottom': isMobile }"
151
+ v-if="mode === 'popup' && visible && annotations.length"
152
+ class="ann-popup"
153
+ :class="{ 'ann-popup--vertical': vertical }"
67
154
  :style="style"
68
155
  @mouseenter="emit('tooltipEnter')"
69
156
  @mouseleave="emit('tooltipLeave')"
70
157
  >
71
- <button v-if="isMobile" class="ann-handle" @click="emit('close')">
158
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
159
+ <div class="ann-detail-head">
160
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
161
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
162
+ </div>
163
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
164
+ <span v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</span>
165
+ </div>
166
+ </div>
167
+ </Transition>
168
+
169
+ <!-- Mobile bottom sheet -->
170
+ <Transition name="ann-sheet">
171
+ <div
172
+ v-if="mode === 'sheet' && stickyVisible && annotations.length"
173
+ class="ann-bottom-sheet"
174
+ >
175
+ <button class="ann-sheet-handle" @click="dismiss">
72
176
  <span class="ann-handle-bar" />
73
177
  </button>
74
- <div
75
- v-for="ann in annotations"
76
- :key="ann.id"
77
- class="ann-entry"
78
- :class="ann.kind"
79
- >
80
- <span class="ann-kind">{{ ann.kind === 'pronunciation' ? '音' : '義' }}</span>
81
- <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
82
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
83
- <span v-else class="ann-body">{{ ann.text }}</span>
178
+ <div class="ann-sheet-inner">
179
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
180
+ <div class="ann-detail-head">
181
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
182
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
183
+ </div>
184
+ <div class="ann-detail-body">
185
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
186
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
187
+ </div>
188
+ </div>
84
189
  </div>
85
190
  </div>
86
191
  </Transition>
@@ -88,121 +193,220 @@ onBeforeUnmount(() => {
88
193
  </template>
89
194
 
90
195
  <style scoped>
91
- .ann-tooltip {
196
+ /* ─── Shared Entry Styles ─── */
197
+ .ann-detail {
198
+ margin-bottom: 16px;
199
+ letter-spacing: 1px;
200
+ font-size: 15px;
201
+ color: var(--ink-mid);
202
+ }
203
+ .ann-detail:last-child { margin-bottom: 0; }
204
+
205
+ .ann-detail-head {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: 8px;
209
+ margin-bottom: 8px;
210
+ }
211
+
212
+ .ann-kind-tag {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ padding: 3px 10px;
216
+ border-radius: 4px;
217
+ font-size: 12px;
218
+ font-family: var(--sans);
219
+ font-weight: 700;
220
+ letter-spacing: 1px;
221
+ }
222
+ .ann-kind-tag.pronunciation { background: var(--jade); color: #fff; }
223
+ .ann-kind-tag.semantic { background: var(--vermillion); color: #fff; }
224
+ .ann-kind-tag.etymology { background: #6b5b95; color: #fff; }
225
+ .ann-kind-tag.note,
226
+ .ann-kind-tag.definition { background: var(--ink); color: var(--paper); }
227
+ .ann-kind-tag.commentary { background: #c0392b; color: #fff; }
228
+ .ann-kind-tag.translation { background: #2c6e49; color: #fff; }
229
+ .ann-kind-tag.person { background: var(--ann-person); color: #fff; }
230
+ .ann-kind-tag.place { background: var(--ann-place); color: #fff; }
231
+ .ann-kind-tag.event { background: var(--ann-event); color: #fff; }
232
+ .ann-kind-tag.date { background: var(--ann-date); color: #fff; }
233
+ .ann-kind-tag.allusion { background: var(--ann-allusion); color: #fff; }
234
+
235
+ .ann-layer-tag {
236
+ font-size: 11px;
237
+ font-family: var(--sans);
238
+ color: var(--ink-faint);
239
+ padding: 2px 6px;
240
+ border: 1px solid var(--border-light);
241
+ border-radius: 3px;
242
+ letter-spacing: 1px;
243
+ }
244
+
245
+ .ann-detail-body {
246
+ padding-left: 4px;
247
+ }
248
+
249
+ .ann-pron-block {
250
+ margin-bottom: 6px;
251
+ }
252
+
253
+ .ann-text-block {
254
+ line-height: 1.9;
255
+ white-space: pre-line;
256
+ }
257
+
258
+ /* ─── Desktop Left Pane (horizontal mode) ─── */
259
+ .ann-left-pane {
260
+ position: fixed;
261
+ top: 72px;
262
+ left: 20px;
263
+ width: 300px;
264
+ max-height: calc(100vh - 100px);
265
+ background: var(--surface-warm);
266
+ border: 1px solid var(--border);
267
+ border-radius: 12px;
268
+ box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
269
+ z-index: 1000;
270
+ overflow-y: auto;
271
+ overscroll-behavior: contain;
272
+ }
273
+
274
+ /* ─── Desktop Right Pane (vertical mode) ─── */
275
+ .ann-right-pane {
92
276
  position: fixed;
93
- padding: 12px 16px;
277
+ top: 72px;
278
+ right: 20px;
279
+ width: 300px;
280
+ max-height: calc(100vh - 100px);
94
281
  background: var(--surface-warm);
95
282
  border: 1px solid var(--border);
96
- border-radius: 8px;
97
- box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.14);
283
+ border-radius: 12px;
284
+ box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
285
+ z-index: 1000;
286
+ overflow-y: auto;
287
+ overscroll-behavior: contain;
288
+ }
289
+
290
+ .ann-pane-close {
291
+ position: absolute;
292
+ top: 10px;
293
+ right: 10px;
294
+ width: 28px;
295
+ height: 28px;
296
+ border: 1px solid var(--border);
297
+ border-radius: 6px;
298
+ background: var(--surface);
299
+ color: var(--ink-light);
300
+ cursor: pointer;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ transition: all 0.15s;
305
+ z-index: 1;
306
+ }
307
+ .ann-pane-close:hover {
308
+ background: var(--ink);
309
+ color: var(--paper);
310
+ border-color: var(--ink);
311
+ }
312
+
313
+ .ann-pane-inner {
314
+ padding: 16px;
315
+ padding-top: 14px;
316
+ }
317
+
318
+ /* ─── Tablet Popup ─── */
319
+ .ann-popup {
320
+ position: fixed;
321
+ padding: 14px 18px;
322
+ background: var(--surface-warm);
323
+ border: 1px solid var(--border);
324
+ border-radius: 10px;
325
+ box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
98
326
  max-width: 320px;
99
327
  max-height: 60vh;
100
328
  overflow-y: auto;
101
329
  z-index: 1000;
102
330
  }
103
331
 
104
- /* ─── Mobile bottom sheet ─── */
105
- .ann-mobile-bottom {
332
+ /* ─── Mobile Bottom Sheet ─── */
333
+ .ann-bottom-sheet {
106
334
  position: fixed;
107
335
  left: 0;
108
336
  right: 0;
109
337
  bottom: 0;
110
- max-width: none;
111
- max-height: 50vh;
338
+ max-height: 55vh;
339
+ background: var(--surface-warm);
340
+ border-top: 1px solid var(--border);
112
341
  border-radius: 16px 16px 0 0;
113
- padding: 8px 20px 20px;
342
+ box-shadow: 0 -4px 32px rgba(var(--shadow-rgb), 0.15);
343
+ z-index: 1000;
344
+ overflow-y: auto;
114
345
  overscroll-behavior: contain;
115
346
  }
116
347
 
117
- .ann-handle {
348
+ .ann-sheet-handle {
118
349
  display: flex;
119
350
  justify-content: center;
120
- padding: 8px 0 4px;
351
+ padding: 12px 0 6px;
121
352
  width: 100%;
122
353
  border: none;
123
354
  background: none;
124
355
  cursor: pointer;
125
356
  }
357
+
126
358
  .ann-handle-bar {
127
359
  display: block;
128
- width: 36px;
360
+ width: 40px;
129
361
  height: 4px;
130
362
  border-radius: 2px;
131
363
  background: var(--border);
132
364
  }
133
365
 
134
- .ann-entry {
135
- margin-bottom: 10px;
136
- letter-spacing: 1px;
137
- font-size: 15px;
138
- color: var(--ink-mid);
366
+ .ann-sheet-inner {
367
+ padding: 0 20px 28px;
139
368
  }
140
- .ann-entry:last-child { margin-bottom: 0; }
141
369
 
142
- .ann-kind {
143
- display: inline-block;
144
- font-size: 10px;
145
- font-family: var(--sans);
146
- padding: 1px 3px;
147
- border-radius: 2px;
148
- font-weight: 600;
149
- letter-spacing: 1px;
150
- margin-right: 3px;
151
- line-height: 1;
152
- vertical-align: middle;
153
- }
154
- .ann-header {
155
- display: inline-flex;
156
- align-items: center;
157
- gap: 6px;
158
- }
159
- .ann-layer {
160
- font-size: 11px;
161
- font-family: var(--sans);
162
- color: var(--ink-faint);
163
- letter-spacing: 1px;
370
+ /* ─── Transitions ─── */
371
+
372
+ /* Left pane slide from left (horizontal mode) */
373
+ .ann-slide-right-enter-active {
374
+ transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
164
375
  }
165
- .pronunciation .ann-kind {
166
- background: var(--jade);
167
- color: #fff;
376
+ .ann-slide-right-leave-active {
377
+ transition: opacity 0.15s ease, transform 0.15s ease;
168
378
  }
169
- .semantic .ann-kind {
170
- background: var(--vermillion);
171
- color: #fff;
379
+ .ann-slide-right-enter-from {
380
+ opacity: 0;
381
+ transform: translateX(-24px);
172
382
  }
173
-
174
- .ann-body {
175
- line-height: 1.8;
383
+ .ann-slide-right-leave-to {
384
+ opacity: 0;
385
+ transform: translateX(-16px);
176
386
  }
177
387
 
178
- /* ─── 直排模式 tooltip ─── */
179
- .ann-vertical {
180
- writing-mode: vertical-rl;
181
- text-orientation: mixed;
182
- max-width: none;
183
- max-height: 60vh;
184
- padding: 16px 12px;
388
+ /* Right pane slide from right (vertical mode) */
389
+ .ann-slide-left-enter-active {
390
+ transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
185
391
  }
186
- .ann-vertical .ann-entry {
187
- margin-bottom: 0;
188
- margin-left: 12px;
189
- display: inline;
392
+ .ann-slide-left-leave-active {
393
+ transition: opacity 0.15s ease, transform 0.15s ease;
190
394
  }
191
- .ann-vertical .ann-kind {
192
- margin-right: 0;
193
- text-combine-upright: all;
194
- vertical-align: baseline;
395
+ .ann-slide-left-enter-from {
396
+ opacity: 0;
397
+ transform: translateX(24px);
195
398
  }
196
- .ann-vertical .ann-body {
197
- margin-left: 6px;
399
+ .ann-slide-left-leave-to {
400
+ opacity: 0;
401
+ transform: translateX(16px);
198
402
  }
199
403
 
200
- /* ─── Transition ─── */
404
+ /* Popup fade */
201
405
  .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);
406
+ transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
203
407
  }
204
408
  .ann-fade-leave-active {
205
- transition: opacity var(--dur-fast, 0.15s) ease, transform var(--dur-fast, 0.15s) ease;
409
+ transition: opacity 0.15s ease, transform 0.15s ease;
206
410
  }
207
411
  .ann-fade-enter-from {
208
412
  opacity: 0;
@@ -212,4 +416,24 @@ onBeforeUnmount(() => {
212
416
  opacity: 0;
213
417
  transform: scale(0.96);
214
418
  }
419
+
420
+ /* Bottom sheet slide up */
421
+ .ann-sheet-enter-active {
422
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
423
+ }
424
+ .ann-sheet-leave-active {
425
+ transition: transform 0.2s ease;
426
+ }
427
+ .ann-sheet-enter-from,
428
+ .ann-sheet-leave-to {
429
+ transform: translateY(100%);
430
+ }
431
+
432
+ @media (max-width: 1023px) {
433
+ .ann-left-pane, .ann-right-pane { display: none; }
434
+ }
435
+
436
+ @media (min-width: 1024px) {
437
+ .ann-bottom-sheet { display: none; }
438
+ }
215
439
  </style>
@@ -50,6 +50,10 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
50
50
  box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.1);
51
51
  transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
52
52
  }
53
+
54
+ @media (max-width: 768px) {
55
+ .btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
56
+ }
53
57
  .btt:hover {
54
58
  background: var(--ink);
55
59
  color: var(--paper);
@@ -1,27 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import type { BookMeta } from '../types'
3
3
  import { useRouter } from 'vue-router'
4
+ import { useI18n } from '../composables/useI18n'
4
5
 
5
- defineProps<{ book: BookMeta }>()
6
+ const props = defineProps<{ book: BookMeta }>()
6
7
  const router = useRouter()
8
+ const { t } = useI18n()
7
9
 
8
- const genreLabel: Record<string, string> = {
9
- poetry: '詩歌',
10
- prose: '散文',
11
- mixed: '綜合',
12
- drama: '戲曲',
10
+ function genreLabel(genre: string): string {
11
+ return t(`genre.${genre}` as 'genre.poetry') || genre
13
12
  }
14
13
  </script>
15
14
 
16
15
  <template>
17
- <div class="bc-root" @click="router.push(`/${book.id}`)">
16
+ <div class="bc-root" @click="router.push(`/${props.book.id}`)">
18
17
  <div class="bc-accent"></div>
19
18
  <div class="bc-body">
20
- <h2 class="bc-title">{{ book.title }}</h2>
21
- <p v-if="book.subtitle" class="bc-subtitle">{{ book.subtitle }}</p>
19
+ <h2 class="bc-title">{{ props.book.title }}</h2>
20
+ <p v-if="props.book.subtitle" class="bc-subtitle">{{ props.book.subtitle }}</p>
22
21
  <div class="bc-stats">
23
- <span class="bc-count">{{ book.count }} 篇</span>
24
- <span class="bc-genre">{{ genreLabel[book.genre] || book.genre }}</span>
22
+ <span class="bc-count">{{ t('stat.pieceCount', { count: props.book.count }) }}</span>
23
+ <span class="bc-genre">{{ genreLabel(props.book.genre) }}</span>
25
24
  </div>
26
25
  </div>
27
26
  </div>
@@ -79,6 +79,10 @@ function onTap(event: MouseEvent) {
79
79
  cursor: help;
80
80
  transition: background 0.15s;
81
81
  }
82
+ :deep(.ann-target.ann-overlap) {
83
+ border-bottom-width: 3px;
84
+ border-bottom-style: double;
85
+ }
82
86
  :deep(.ann-target:hover) {
83
87
  background: rgba(194, 58, 43, 0.08);
84
88
  }
@@ -97,4 +101,56 @@ function onTap(event: MouseEvent) {
97
101
  :deep(.ann-target.pronunciation) {
98
102
  border-bottom-color: var(--jade);
99
103
  }
104
+ :deep(.ann-target.person) {
105
+ border-bottom-color: var(--ann-person);
106
+ }
107
+ :deep(.ann-target.place) {
108
+ border-bottom-color: var(--ann-place);
109
+ }
110
+ :deep(.ann-target.event) {
111
+ border-bottom-color: var(--ann-event);
112
+ }
113
+ :deep(.ann-target.date) {
114
+ border-bottom-color: var(--ann-date);
115
+ }
116
+ :deep(.ann-target.allusion) {
117
+ border-bottom-color: var(--ann-allusion);
118
+ }
119
+ :deep(.ann-target.person:hover) {
120
+ background: rgba(58, 90, 140, 0.08);
121
+ }
122
+ :deep(.ann-target.place:hover) {
123
+ background: rgba(139, 105, 20, 0.08);
124
+ }
125
+ :deep(.ann-target.event:hover) {
126
+ background: rgba(107, 76, 138, 0.08);
127
+ }
128
+ :deep(.ann-target.date:hover) {
129
+ background: rgba(42, 122, 122, 0.08);
130
+ }
131
+ :deep(.ann-target.allusion:hover) {
132
+ background: rgba(181, 101, 29, 0.08);
133
+ }
134
+
135
+ @media (max-width: 768px) {
136
+ .h-display {
137
+ padding: 24px 20px;
138
+ border-radius: 6px;
139
+ width: 100%;
140
+ box-sizing: border-box;
141
+ }
142
+ .h-display-title {
143
+ font-size: 24px;
144
+ letter-spacing: 4px;
145
+ }
146
+ .h-display-author {
147
+ font-size: 14px;
148
+ margin-bottom: 16px;
149
+ }
150
+ .h-display-line {
151
+ font-size: var(--main-font-size, 20px);
152
+ line-height: 2.4;
153
+ letter-spacing: 2px;
154
+ }
155
+ }
100
156
  </style>
@@ -129,6 +129,10 @@ const sourceLabel = (() => {
129
129
  cursor: help;
130
130
  transition: background 0.15s;
131
131
  }
132
+ :deep(.ann-target.ann-overlap) {
133
+ border-bottom-width: 3px;
134
+ border-bottom-style: double;
135
+ }
132
136
 
133
137
  :deep(.ann-target:hover) {
134
138
  background: rgba(194, 58, 43, 0.08);
@@ -157,6 +161,11 @@ const sourceLabel = (() => {
157
161
  border-left: 2px solid var(--vermillion);
158
162
  padding-left: 2px;
159
163
  }
164
+ .part-block--vertical :deep(.ann-target.ann-overlap) {
165
+ border-left-width: 3px;
166
+ border-left-style: double;
167
+ padding-left: 3px;
168
+ }
160
169
 
161
170
  .part-block--vertical :deep(.ann-target.pronunciation) {
162
171
  border-left-color: var(--jade);
@@ -49,6 +49,7 @@ const preview = computed(() => {
49
49
  border-color: var(--gold);
50
50
  }
51
51
  .pc-root:hover .pc-accent { height: 100%; }
52
+ .pc-root:active { transform: scale(0.98); }
52
53
  .pc-body { padding: 24px; }
53
54
  .pc-num {
54
55
  font-size: 11px; color: var(--ink-faint);