@hanology/cham-browser 0.3.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +141 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/template/index.html +4 -8
- package/template/src/App.vue +101 -17
- package/template/src/components/AnnotationControlBar.vue +119 -49
- package/template/src/components/AnnotationTooltip.vue +278 -99
- package/template/src/components/BackToTop.vue +4 -0
- package/template/src/components/BookCard.vue +10 -11
- package/template/src/components/HorizontalDisplay.vue +52 -0
- package/template/src/components/PoemCard.vue +1 -0
- package/template/src/components/PronunciationGroup.vue +27 -18
- package/template/src/components/ReadingToolbar.vue +20 -0
- package/template/src/components/SectionBlock.vue +91 -12
- package/template/src/components/SideNav.vue +5 -4
- package/template/src/components/VerticalScroll.vue +30 -0
- package/template/src/composables/useData.ts +6 -1
- package/template/src/composables/useI18n.ts +33 -0
- package/template/src/composables/useReadingMode.ts +9 -4
- package/template/src/composables/useSiteConfig.ts +12 -1
- package/template/src/router.ts +0 -2
- package/template/src/styles/main.css +88 -0
- package/template/src/types.ts +8 -0
- package/template/src/views/BookHome.vue +45 -21
- package/template/src/views/LibraryHome.vue +39 -41
- package/template/src/views/PieceView.vue +434 -71
- package/template/src/views/AboutView.vue +0 -191
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
2
|
+
import { computed, ref, inject } from 'vue'
|
|
3
3
|
import { useRouter } from 'vue-router'
|
|
4
4
|
import { useLibrary } from '../composables/useLibrary'
|
|
5
5
|
import { useBook } from '../composables/useBook'
|
|
6
6
|
import { useTitle } from '../composables/useTitle'
|
|
7
7
|
import { useReadingMode } from '../composables/useReadingMode'
|
|
8
8
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
9
|
+
import { useI18n } from '../composables/useI18n'
|
|
9
10
|
import BookCard from '../components/BookCard.vue'
|
|
10
11
|
import SideNav from '../components/SideNav.vue'
|
|
11
12
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
@@ -13,10 +14,14 @@ import BackToTop from '../components/BackToTop.vue'
|
|
|
13
14
|
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
14
15
|
import type { BookMeta } from '../types'
|
|
15
16
|
|
|
17
|
+
const aboutPane = inject<{ toggleAbout: () => void; closeAbout: () => void }>('aboutPane')
|
|
18
|
+
|
|
16
19
|
const { scale, books, singleBook, loadLibrary } = useLibrary()
|
|
17
20
|
await loadLibrary()
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
const { siteTitle, siteSubtitle, aboutHtml, logoUrl } = useSiteConfig()
|
|
23
|
+
const displayTitle = siteTitle || 'CHAM'
|
|
24
|
+
useTitle(displayTitle)
|
|
20
25
|
|
|
21
26
|
// Single-book: redirect to book home
|
|
22
27
|
if (scale.value === 'single-book' && singleBook.value) {
|
|
@@ -37,27 +42,20 @@ if (scale.value === 'single-piece' && singleBook.value) {
|
|
|
37
42
|
|
|
38
43
|
const router = useRouter()
|
|
39
44
|
const { layout } = useReadingMode()
|
|
40
|
-
const { logoUrl } = useSiteConfig()
|
|
41
45
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
42
46
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
43
47
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
44
|
-
|
|
45
|
-
const genreLabel: Record<string, string> = {
|
|
46
|
-
poetry: '詩歌',
|
|
47
|
-
prose: '散文',
|
|
48
|
-
mixed: '綜合',
|
|
49
|
-
drama: '戲曲',
|
|
50
|
-
}
|
|
48
|
+
const { t } = useI18n()
|
|
51
49
|
|
|
52
50
|
function bookCategory(book: BookMeta): string {
|
|
53
|
-
if (book.id.startsWith('skqs-')) return '
|
|
54
|
-
if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return '
|
|
55
|
-
return '
|
|
51
|
+
if (book.id.startsWith('skqs-')) return t('genre.fourTreasuries')
|
|
52
|
+
if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return t('genre.textbooks')
|
|
53
|
+
return t('genre.classicalText')
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
const groupedBooks = computed(() => {
|
|
59
57
|
const groups = new Map<string, BookMeta[]>()
|
|
60
|
-
const order = ['
|
|
58
|
+
const order = [t('genre.textbooks'), t('genre.classicalText'), t('genre.fourTreasuries')]
|
|
61
59
|
for (const book of books.value) {
|
|
62
60
|
const cat = bookCategory(book)
|
|
63
61
|
if (!groups.has(cat)) groups.set(cat, [])
|
|
@@ -70,23 +68,29 @@ const groupedBooks = computed(() => {
|
|
|
70
68
|
|
|
71
69
|
const totalPieces = computed(() => books.value.reduce((sum, b) => sum + b.count, 0))
|
|
72
70
|
|
|
71
|
+
const spacedTitle = computed(() => displayTitle.split('').join(' '))
|
|
72
|
+
|
|
73
73
|
function openBook(bookId: string) {
|
|
74
74
|
router.push(`/${bookId}`)
|
|
75
75
|
}
|
|
76
76
|
</script>
|
|
77
77
|
|
|
78
78
|
<template>
|
|
79
|
-
<div v-if="scale
|
|
79
|
+
<div v-if="scale !== 'library'" class="page-loading">
|
|
80
|
+
<img v-if="logoUrl" :src="logoUrl" alt="" class="page-loading-logo" />
|
|
81
|
+
<div v-else class="page-loading-seal">文</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div v-else>
|
|
80
84
|
<!-- ═══════ 直排模式 ═══════ -->
|
|
81
85
|
<div v-if="isVertical" class="v-root">
|
|
82
86
|
<SideNav @home="router.push('/')" @back="router.push('/')" />
|
|
83
87
|
<div ref="vPageRef" class="v-page">
|
|
84
|
-
<div class="v-about-col">
|
|
85
|
-
<
|
|
88
|
+
<div v-if="aboutHtml" class="v-about-col">
|
|
89
|
+
<button class="v-about-link" @click="aboutPane?.toggleAbout()">{{ t('nav.about') }}</button>
|
|
86
90
|
</div>
|
|
87
91
|
<section class="v-hero">
|
|
88
|
-
<h1 class="v-title"
|
|
89
|
-
<p class="v-subtitle">
|
|
92
|
+
<h1 class="v-title">{{ spacedTitle }}</h1>
|
|
93
|
+
<p v-if="siteSubtitle" class="v-subtitle">{{ siteSubtitle }}</p>
|
|
90
94
|
<div class="v-divider"></div>
|
|
91
95
|
</section>
|
|
92
96
|
|
|
@@ -101,7 +105,7 @@ function openBook(bookId: string) {
|
|
|
101
105
|
<h2 class="v-book-title">{{ book.title }}</h2>
|
|
102
106
|
<p v-if="book.subtitle" class="v-book-sub">{{ book.subtitle }}</p>
|
|
103
107
|
<div class="v-book-stats">
|
|
104
|
-
<span class="v-book-count">{{ book.count }}
|
|
108
|
+
<span class="v-book-count">{{ t('stat.pieceCount', { count: book.count }) }}</span>
|
|
105
109
|
</div>
|
|
106
110
|
</div>
|
|
107
111
|
</section>
|
|
@@ -112,15 +116,14 @@ function openBook(bookId: string) {
|
|
|
112
116
|
<div v-else class="lib-root">
|
|
113
117
|
<header class="lib-hero">
|
|
114
118
|
<img v-if="logoUrl" :src="logoUrl" alt="" class="lib-logo" />
|
|
115
|
-
<div v-else class="lib-seal"
|
|
116
|
-
<h1
|
|
117
|
-
<p class="lib-subtitle">
|
|
119
|
+
<div v-else class="lib-seal">{{ displayTitle.slice(0, 2) }}</div>
|
|
120
|
+
<h1>{{ displayTitle }} <button v-if="aboutHtml" class="lib-about-link" @click="aboutPane?.toggleAbout()">{{ t('nav.about') }}</button></h1>
|
|
121
|
+
<p v-if="siteSubtitle" class="lib-subtitle">{{ siteSubtitle }}</p>
|
|
118
122
|
<div class="lib-stats-bar">
|
|
119
|
-
<span class="lib-stat">{{ books.length }}
|
|
123
|
+
<span class="lib-stat">{{ books.length }} {{ t('stat.books') }}</span>
|
|
120
124
|
<span class="lib-stat-sep">·</span>
|
|
121
|
-
<span class="lib-stat">{{ totalPieces }}
|
|
125
|
+
<span class="lib-stat">{{ totalPieces }} {{ t('stat.pieces') }}</span>
|
|
122
126
|
</div>
|
|
123
|
-
<router-link to="/about" class="lib-about-link">關於</router-link>
|
|
124
127
|
</header>
|
|
125
128
|
<div v-for="group in groupedBooks" :key="group.category" class="lib-group">
|
|
126
129
|
<h2 class="lib-group-title">{{ group.category }}</h2>
|
|
@@ -136,11 +139,11 @@ function openBook(bookId: string) {
|
|
|
136
139
|
<div class="lib-card-body">
|
|
137
140
|
<div class="lib-card-top">
|
|
138
141
|
<h3 class="lib-card-title">{{ book.title }}</h3>
|
|
139
|
-
<span class="lib-card-genre">{{
|
|
142
|
+
<span class="lib-card-genre">{{ bookCategory(book) }}</span>
|
|
140
143
|
</div>
|
|
141
144
|
<p v-if="book.subtitle" class="lib-card-sub">{{ book.subtitle }}</p>
|
|
142
145
|
<div class="lib-card-stats">
|
|
143
|
-
<span class="lib-card-count">{{ book.count }}
|
|
146
|
+
<span class="lib-card-count">{{ t('stat.pieceCount', { count: book.count }) }}</span>
|
|
144
147
|
</div>
|
|
145
148
|
</div>
|
|
146
149
|
</div>
|
|
@@ -156,20 +159,9 @@ function openBook(bookId: string) {
|
|
|
156
159
|
/* ═══════ 直排模式 ═══════ */
|
|
157
160
|
|
|
158
161
|
.v-page {
|
|
159
|
-
height: 100vh;
|
|
160
|
-
display: flex;
|
|
161
|
-
flex-direction: row-reverse;
|
|
162
|
-
overflow-x: auto;
|
|
163
|
-
overflow-y: hidden;
|
|
164
|
-
margin-right: var(--nav-width, 56px);
|
|
165
162
|
padding: 0 32px;
|
|
166
163
|
background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
|
|
167
|
-
scrollbar-width: thin;
|
|
168
|
-
scrollbar-color: var(--gold) transparent;
|
|
169
|
-
scroll-snap-type: x proximity;
|
|
170
164
|
}
|
|
171
|
-
.v-page::-webkit-scrollbar { height: 4px; }
|
|
172
|
-
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
173
165
|
|
|
174
166
|
.v-hero {
|
|
175
167
|
writing-mode: vertical-rl;
|
|
@@ -201,6 +193,8 @@ function openBook(bookId: string) {
|
|
|
201
193
|
padding: 12px 8px;
|
|
202
194
|
border: 1px solid var(--border-light);
|
|
203
195
|
border-radius: 2px;
|
|
196
|
+
background: none;
|
|
197
|
+
cursor: pointer;
|
|
204
198
|
transition: all 0.2s;
|
|
205
199
|
}
|
|
206
200
|
.v-about-link:hover {
|
|
@@ -353,16 +347,17 @@ function openBook(bookId: string) {
|
|
|
353
347
|
|
|
354
348
|
.lib-about-link {
|
|
355
349
|
display: inline-block;
|
|
356
|
-
margin-top: 16px;
|
|
357
350
|
font-family: var(--sans);
|
|
358
351
|
font-size: 13px;
|
|
359
352
|
color: var(--ink-faint);
|
|
360
353
|
letter-spacing: 2px;
|
|
361
|
-
text-decoration: none;
|
|
362
354
|
padding: 4px 12px;
|
|
363
355
|
border: 1px solid var(--border-light);
|
|
364
356
|
border-radius: 4px;
|
|
357
|
+
background: none;
|
|
358
|
+
cursor: pointer;
|
|
365
359
|
transition: all 0.2s;
|
|
360
|
+
vertical-align: middle;
|
|
366
361
|
}
|
|
367
362
|
.lib-about-link:hover {
|
|
368
363
|
color: var(--ink);
|
|
@@ -455,12 +450,15 @@ function openBook(bookId: string) {
|
|
|
455
450
|
.v-page { padding: 0 16px; }
|
|
456
451
|
.v-title { font-size: 36px; letter-spacing: 10px; }
|
|
457
452
|
.lib-root { padding: 40px 16px 80px; }
|
|
453
|
+
.lib-hero { margin-bottom: 32px; }
|
|
458
454
|
.lib-hero h1 { font-size: 28px; letter-spacing: 4px; }
|
|
455
|
+
.lib-logo { height: 48px; margin-bottom: 16px; }
|
|
459
456
|
.lib-grid {
|
|
460
457
|
grid-template-columns: 1fr 1fr;
|
|
461
458
|
gap: 8px;
|
|
462
459
|
}
|
|
463
460
|
.lib-card { padding: 14px; }
|
|
461
|
+
.lib-card:active { transform: scale(0.98); }
|
|
464
462
|
.lib-card-title { font-size: 18px; letter-spacing: 2px; }
|
|
465
463
|
.lib-card-genre { display: none; }
|
|
466
464
|
.lib-card-sub { font-size: 12px; margin-bottom: 8px; }
|