@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
@@ -15,6 +15,11 @@
15
15
  --gold: #9a7d3a;
16
16
  --gold-light: #c9a84c;
17
17
  --jade: #3a6b5e;
18
+ --ann-person: #3a5a8c;
19
+ --ann-place: #8b6914;
20
+ --ann-event: #6b4c8a;
21
+ --ann-date: #2a7a7a;
22
+ --ann-allusion: #b5651d;
18
23
  --border: #d8cdb8;
19
24
  --border-light: #e8e0d0;
20
25
  --shadow-rgb: 26,26,26;
@@ -36,6 +41,11 @@
36
41
  --gold: #8a6d2a;
37
42
  --gold-light: #b89540;
38
43
  --jade: #2d5a4e;
44
+ --ann-person: #3a5a8c;
45
+ --ann-place: #8b6914;
46
+ --ann-event: #6b4c8a;
47
+ --ann-date: #2a7a7a;
48
+ --ann-allusion: #b5651d;
39
49
  --border: #c9b896;
40
50
  --border-light: #d8cab0;
41
51
  --shadow-rgb: 74,63,46;
@@ -57,6 +67,11 @@
57
67
  --gold: #c9a84c;
58
68
  --gold-light: #d8b860;
59
69
  --jade: #5aaa98;
70
+ --ann-person: #6a8ab4;
71
+ --ann-place: #b8943a;
72
+ --ann-event: #9a7cb4;
73
+ --ann-date: #5ab4b4;
74
+ --ann-allusion: #d4843a;
60
75
  --border: #48484a;
61
76
  --border-light: #555557;
62
77
  --shadow-rgb: 0,0,0;
@@ -78,6 +93,11 @@
78
93
  --gold: #e8c840;
79
94
  --gold-light: #f0d860;
80
95
  --jade: #40c8a8;
96
+ --ann-person: #6a8ab4;
97
+ --ann-place: #b8943a;
98
+ --ann-event: #9a7cb4;
99
+ --ann-date: #5ab4b4;
100
+ --ann-allusion: #d4843a;
81
101
  --border: #333333;
82
102
  --border-light: #444444;
83
103
  --shadow-rgb: 0,0,0;
@@ -196,3 +216,71 @@ button { font-family: inherit; }
196
216
  @media (max-width: 768px) {
197
217
  :root { --nav-width: 44px; }
198
218
  }
219
+
220
+ /* ===== VERTICAL PAGE LAYOUT ===== */
221
+ .v-page {
222
+ height: 100vh;
223
+ display: flex;
224
+ flex-direction: row-reverse;
225
+ overflow-x: auto;
226
+ overflow-y: hidden;
227
+ margin-right: var(--nav-width, 56px);
228
+ scrollbar-width: thin;
229
+ scrollbar-color: var(--gold) transparent;
230
+ scroll-snap-type: x proximity;
231
+ }
232
+ .v-page::-webkit-scrollbar { height: 4px; }
233
+ .v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
234
+
235
+ /* ===== VIEW LOADING STATE ===== */
236
+ .v-loading {
237
+ display: flex;
238
+ flex-direction: column;
239
+ align-items: center;
240
+ justify-content: center;
241
+ min-height: 60vh;
242
+ gap: 16px;
243
+ }
244
+ .v-loading .seal {
245
+ width: 72px; height: 72px;
246
+ border: 2px solid var(--vermillion);
247
+ border-radius: 4px;
248
+ display: flex; align-items: center; justify-content: center;
249
+ animation: pulse 2s ease-in-out infinite;
250
+ }
251
+ .v-loading .char {
252
+ font-family: var(--serif);
253
+ font-size: 36px; font-weight: 900;
254
+ color: var(--vermillion);
255
+ line-height: 1;
256
+ }
257
+ .v-loading .label {
258
+ font-size: 13px;
259
+ color: var(--ink-faint);
260
+ letter-spacing: 4px;
261
+ }
262
+ @keyframes pulse {
263
+ 0%, 100% { opacity: 0.6; transform: scale(0.96); }
264
+ 50% { opacity: 1; transform: scale(1); }
265
+ }
266
+
267
+ /* ===== FULL-PAGE LOADING (LibraryHome, PieceView redirect) ===== */
268
+ .page-loading {
269
+ display: flex; flex-direction: column;
270
+ align-items: center; justify-content: center;
271
+ height: 100vh;
272
+ }
273
+ .page-loading-seal {
274
+ width: 56px; height: 56px;
275
+ border: 2px solid var(--vermillion);
276
+ border-radius: 4px;
277
+ display: flex; align-items: center; justify-content: center;
278
+ font-size: 28px; font-weight: 900;
279
+ color: var(--vermillion);
280
+ animation: pulse 1.2s ease-in-out infinite;
281
+ }
282
+ .page-loading-logo {
283
+ width: 56px; height: auto;
284
+ object-fit: contain;
285
+ animation: pulse 1.2s ease-in-out infinite;
286
+ }
@@ -140,7 +140,7 @@ export interface Piece {
140
140
  title: string
141
141
  author: string
142
142
  authorId: string
143
- dynasty: string
143
+ era: string
144
144
  genre: BookGenre
145
145
  verses: VerseLine[]
146
146
  sections: Record<string, string>
@@ -162,9 +162,17 @@ export interface Author {
162
162
  '@id': string
163
163
  '@type': string
164
164
  name: string
165
- dynasty: string
166
- poemCount: number
165
+ era: string
166
+ workCount: number
167
167
  bio?: string
168
+ born?: string
169
+ died?: string
170
+ courtesyName?: string
171
+ artName?: string
172
+ wikidata?: string
173
+ ctextId?: string
174
+ wikipediaZh?: string
175
+ wikipediaEn?: string
168
176
  }
169
177
 
170
178
  export interface Dynasty {
@@ -172,5 +180,5 @@ export interface Dynasty {
172
180
  '@type': string
173
181
  name: string
174
182
  authors: string[]
175
- poemCount: number
183
+ workCount: number
176
184
  }
@@ -50,7 +50,7 @@ function goHome() { router.push('/') }
50
50
  <section class="v-author-info">
51
51
  <div class="v-seal">{{ authorName.charAt(0) }}</div>
52
52
  <h1 class="v-name">{{ authorName }}</h1>
53
- <span v-if="author.dynasty" class="v-dynasty">{{ author.dynasty }}</span>
53
+ <span v-if="author.era" class="v-era">{{ author.era }}</span>
54
54
  <span class="v-count">{{ authorPieces.length }} 篇收錄作品</span>
55
55
  </section>
56
56
 
@@ -83,7 +83,7 @@ function goHome() { router.push('/') }
83
83
  <span class="h-author-name">{{ authorName }}</span>
84
84
  </div>
85
85
  <div class="h-controls">
86
- <span class="h-tag">{{ author.dynasty || '未知朝代' }}</span>
86
+ <span class="h-tag">{{ author.era || '未知朝代' }}</span>
87
87
  <span class="h-tag">{{ authorPieces.length }} 篇</span>
88
88
  </div>
89
89
  </div>
@@ -95,7 +95,7 @@ function goHome() { router.push('/') }
95
95
  <div class="h-info">
96
96
  <h1 class="h-name">{{ authorName }}</h1>
97
97
  <div class="h-meta">
98
- <span v-if="author.dynasty" class="h-dynasty">{{ author.dynasty }}</span>
98
+ <span v-if="author.era" class="h-era">{{ author.era }}</span>
99
99
  <span class="h-count">{{ authorPieces.length }} 篇收錄作品</span>
100
100
  </div>
101
101
  </div>
@@ -174,7 +174,7 @@ function goHome() { router.push('/') }
174
174
  letter-spacing: 10px; color: var(--ink);
175
175
  margin-left: 20px;
176
176
  }
177
- .v-dynasty {
177
+ .v-era {
178
178
  font-size: 20px; color: var(--gold);
179
179
  font-weight: 600; letter-spacing: 4px;
180
180
  margin-left: 12px;
@@ -286,7 +286,7 @@ function goHome() { router.push('/') }
286
286
  }
287
287
  .h-name { font-size: 36px; font-weight: 900; letter-spacing: 6px; color: var(--ink); }
288
288
  .h-meta { display: flex; gap: 16px; margin-top: 8px; font-family: var(--sans); font-size: 14px; }
289
- .h-dynasty { color: var(--gold); font-weight: 600; letter-spacing: 2px; }
289
+ .h-era { color: var(--gold); font-weight: 600; letter-spacing: 2px; }
290
290
  .h-count { color: var(--ink-faint); letter-spacing: 1px; }
291
291
 
292
292
  .h-bio {
@@ -5,6 +5,7 @@ import { useBook } from '../composables/useBook'
5
5
  import { useTitle } from '../composables/useTitle'
6
6
  import { useReadingMode } from '../composables/useReadingMode'
7
7
  import { useHorizontalScroll } from '../composables/useHorizontalScroll'
8
+ import { useI18n } from '../composables/useI18n'
8
9
  import PoemCard from '../components/PoemCard.vue'
9
10
  import SideNav from '../components/SideNav.vue'
10
11
  import ReadingToolbar from '../components/ReadingToolbar.vue'
@@ -22,6 +23,7 @@ const { layout } = useReadingMode()
22
23
  const isVertical = computed(() => layout.value === 'vertical')
23
24
  const vPageRef = ref<HTMLElement | null>(null)
24
25
  const vScroll = useHorizontalScroll(vPageRef)
26
+ const { t } = useI18n()
25
27
 
26
28
  const filtered = computed(() => {
27
29
  const q = searchQuery.value.toLowerCase()
@@ -72,11 +74,11 @@ function scrollToCatalog() {
72
74
  </section>
73
75
 
74
76
  <section class="v-catalog-col">
75
- <span class="v-ch-title">篇 目 錄</span>
77
+ <span class="v-ch-title">{{ t('catalog.title') }}</span>
76
78
  <span class="v-ch-line"> </span>
77
- <span class="v-count">共 {{ filtered.length }} 篇</span>
79
+ <span class="v-count">{{ t('catalog.total', { count: filtered.length }) }}</span>
78
80
  <span class="v-search-wrap">
79
- <input v-model="searchQuery" class="v-search" placeholder="搜索詩題、作者…" />
81
+ <input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" />
80
82
  </span>
81
83
  </section>
82
84
 
@@ -104,28 +106,28 @@ function scrollToCatalog() {
104
106
  <div class="h-stats">
105
107
  <div class="h-stat-block">
106
108
  <div class="h-stat-num">{{ pieces.length }}</div>
107
- <div class="h-stat-label">篇詩文</div>
109
+ <div class="h-stat-label">{{ t('stat.piecePoems') }}</div>
108
110
  </div>
109
111
  <div class="h-stat-block">
110
112
  <div class="h-stat-num">{{ authorCount }}</div>
111
- <div class="h-stat-label">位作者</div>
113
+ <div class="h-stat-label">{{ t('stat.authorsLabel') }}</div>
112
114
  </div>
113
115
  </div>
114
116
  <p v-if="meta?.publisher" class="h-publisher">{{ meta.publisher }}</p>
115
117
  <button class="h-cta" @click="scrollToCatalog">
116
- 文 庫 ↓
118
+ {{ t('catalog.enterLibrary') }}
117
119
  </button>
118
120
  </div>
119
121
  </section>
120
122
 
121
123
  <section class="h-catalog">
122
124
  <div class="h-catalog-header">
123
- <h2>篇 目 錄</h2>
125
+ <h2>{{ t('catalog.title') }}</h2>
124
126
  <div class="h-line"></div>
125
127
  <p v-if="meta?.publisher">{{ meta.publisher }}</p>
126
128
  </div>
127
129
  <div class="h-filter">
128
- <input v-model="searchQuery" class="h-search" placeholder="搜索詩題、作者…" />
130
+ <input v-model="searchQuery" class="h-search" :placeholder="t('catalog.search')" />
129
131
  </div>
130
132
  <div class="h-grid">
131
133
  <PoemCard
@@ -137,6 +139,10 @@ function scrollToCatalog() {
137
139
  @click="openPiece(piece.num)"
138
140
  />
139
141
  </div>
142
+ <div v-if="searchQuery && filtered.length === 0" class="h-empty">
143
+ <span class="h-empty-icon">🔍</span>
144
+ <p>{{ t('catalog.noResults', { query: searchQuery }) }}</p>
145
+ </div>
140
146
  </section>
141
147
 
142
148
  <BackToTop />
@@ -148,20 +154,9 @@ function scrollToCatalog() {
148
154
  /* ═══════ 直排模式 ═══════ */
149
155
 
150
156
  .v-page {
151
- height: 100vh;
152
- display: flex;
153
- flex-direction: row-reverse;
154
- overflow-x: auto;
155
- overflow-y: hidden;
156
- margin-right: var(--nav-width, 56px);
157
157
  padding: 0 32px;
158
158
  background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
159
- scrollbar-width: thin;
160
- scrollbar-color: var(--gold) transparent;
161
- scroll-snap-type: x proximity;
162
159
  }
163
- .v-page::-webkit-scrollbar { height: 4px; }
164
- .v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
165
160
 
166
161
  .v-hero {
167
162
  writing-mode: vertical-rl;
@@ -367,10 +362,31 @@ function scrollToCatalog() {
367
362
  animation: cardEnter 0.4s var(--ease-out-expo) both;
368
363
  }
369
364
 
365
+ .h-empty {
366
+ text-align: center;
367
+ padding: 60px 20px;
368
+ color: var(--ink-faint);
369
+ font-family: var(--sans);
370
+ font-size: 15px;
371
+ letter-spacing: 1px;
372
+ }
373
+ .h-empty-icon {
374
+ display: block;
375
+ font-size: 40px;
376
+ margin-bottom: 16px;
377
+ opacity: 0.5;
378
+ }
379
+
370
380
  @media (max-width: 768px) {
371
- .h-stats { gap: 24px; }
381
+ .h-hero { min-height: 80vh; height: auto; padding: 60px 16px; }
382
+ .h-ornament { font-size: 32px; letter-spacing: 12px; margin-bottom: 20px; }
383
+ .h-subtitle { margin-bottom: 32px; }
384
+ .h-divider { margin-bottom: 32px; }
385
+ .h-stats { gap: 24px; margin-bottom: 32px; }
372
386
  .h-stat-num { font-size: 28px; }
373
- .h-catalog { padding: 40px 20px; }
387
+ .h-publisher { margin-bottom: 32px; }
388
+ .h-cta { padding: 12px 32px; font-size: 14px; letter-spacing: 2px; }
389
+ .h-catalog { padding: 40px 16px; }
374
390
  .h-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
375
391
  .h-search { width: 100%; }
376
392
  .v-page { padding: 0 16px; }
@@ -380,4 +396,12 @@ function scrollToCatalog() {
380
396
  }
381
397
  .v-search { height: 160px; }
382
398
  }
399
+
400
+ @media (max-width: 480px) {
401
+ .h-hero { min-height: 70vh; }
402
+ .h-title { letter-spacing: 6px; }
403
+ .h-cta { width: 80%; justify-content: center; }
404
+ .h-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
405
+ .h-catalog-header h2 { font-size: 22px; }
406
+ }
383
407
  </style>
@@ -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
- useTitle('古典詩文圖書館')
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 === 'library'">
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
- <router-link to="/about" class="v-about-link">關 於</router-link>
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">古 詩 文 圖 書 館</h1>
89
- <p class="v-subtitle">Classical Chinese Text Library</p>
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 }} 篇</span>
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">漢流</div>
116
- <h1>古典詩文圖書館</h1>
117
- <p class="lib-subtitle">Classical Chinese Text Library</p>
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 }} 部</span>
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 }} 篇</span>
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">{{ genreLabel[book.genre] || book.genre }}</span>
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 }} 篇</span>
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; }