@hanology/cham-browser 0.1.0 → 0.2.0

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 (43) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +191 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/pipeline.d.ts +14 -0
  8. package/dist/pipeline.js +377 -0
  9. package/dist/pipeline.js.map +1 -0
  10. package/package.json +22 -3
  11. package/template/index.html +29 -0
  12. package/template/src/App.vue +29 -0
  13. package/template/src/components/AnnotationLayerSelector.vue +66 -0
  14. package/template/src/components/AnnotationTooltip.vue +189 -0
  15. package/template/src/components/BookCard.vue +85 -0
  16. package/template/src/components/HorizontalDisplay.vue +100 -0
  17. package/template/src/components/PoemCard.vue +131 -0
  18. package/template/src/components/PronunciationGroup.vue +45 -0
  19. package/template/src/components/ReadingToolbar.vue +131 -0
  20. package/template/src/components/SectionBlock.vue +142 -0
  21. package/template/src/components/SideNav.vue +291 -0
  22. package/template/src/components/VerticalScroll.vue +120 -0
  23. package/template/src/composables/useAnnotationRenderer.ts +158 -0
  24. package/template/src/composables/useBook.ts +93 -0
  25. package/template/src/composables/useData.ts +41 -0
  26. package/template/src/composables/useHorizontalScroll.ts +60 -0
  27. package/template/src/composables/useLibrary.ts +40 -0
  28. package/template/src/composables/usePageLayout.ts +25 -0
  29. package/template/src/composables/useReadingMode.ts +70 -0
  30. package/template/src/composables/useTitle.ts +5 -0
  31. package/template/src/main.ts +22 -0
  32. package/template/src/router.ts +29 -0
  33. package/template/src/shims-vue.d.ts +7 -0
  34. package/template/src/styles/main.css +136 -0
  35. package/template/src/types.ts +164 -0
  36. package/template/src/utils/annotationParser.ts +58 -0
  37. package/template/src/utils/chineseNumber.ts +41 -0
  38. package/template/src/views/AuthorView.vue +338 -0
  39. package/template/src/views/BookHome.vue +375 -0
  40. package/template/src/views/LibraryHome.vue +419 -0
  41. package/template/src/views/PieceView.vue +793 -0
  42. package/src/index.ts +0 -20
  43. package/tsconfig.json +0 -16
@@ -0,0 +1,419 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { useLibrary } from '../composables/useLibrary'
5
+ import { useBook } from '../composables/useBook'
6
+ import { useTitle } from '../composables/useTitle'
7
+ import { useReadingMode } from '../composables/useReadingMode'
8
+ import { useHorizontalScroll } from '../composables/useHorizontalScroll'
9
+ import BookCard from '../components/BookCard.vue'
10
+ import SideNav from '../components/SideNav.vue'
11
+ import ReadingToolbar from '../components/ReadingToolbar.vue'
12
+ import type { BookMeta } from '../types'
13
+
14
+ const { scale, books, singleBook, loadLibrary } = useLibrary()
15
+ await loadLibrary()
16
+
17
+ useTitle('古典詩文圖書館')
18
+
19
+ // Single-book: redirect to book home
20
+ if (scale.value === 'single-book' && singleBook.value) {
21
+ const router = useRouter()
22
+ router.replace(`/${singleBook.value.id}`)
23
+ }
24
+
25
+ // Single-piece: redirect to the piece
26
+ if (scale.value === 'single-piece' && singleBook.value) {
27
+ const router = useRouter()
28
+ const { load } = useBook()
29
+ await load(singleBook.value.id)
30
+ const { pieces } = useBook()
31
+ if (pieces.value.length === 1) {
32
+ router.replace(`/${singleBook.value.id}/${pieces.value[0].num}`)
33
+ }
34
+ }
35
+
36
+ const router = useRouter()
37
+ const { layout } = useReadingMode()
38
+ const isVertical = computed(() => layout.value === 'vertical')
39
+ const vPageRef = ref<HTMLElement | null>(null)
40
+ const vScroll = useHorizontalScroll(vPageRef)
41
+
42
+ const genreLabel: Record<string, string> = {
43
+ poetry: '詩歌',
44
+ prose: '散文',
45
+ mixed: '綜合',
46
+ drama: '戲曲',
47
+ }
48
+
49
+ function bookCategory(book: BookMeta): string {
50
+ if (book.id.startsWith('skqs-')) return '四庫全書'
51
+ if (book.id === 'primary' || book.id === 'primary-culture' || book.id === 'secondary' || book.id === 'nss') return '教材'
52
+ return '古典文本'
53
+ }
54
+
55
+ const groupedBooks = computed(() => {
56
+ const groups = new Map<string, BookMeta[]>()
57
+ const order = ['教材', '古典文本', '四庫全書']
58
+ for (const book of books.value) {
59
+ const cat = bookCategory(book)
60
+ if (!groups.has(cat)) groups.set(cat, [])
61
+ groups.get(cat)!.push(book)
62
+ }
63
+ return order
64
+ .filter(cat => groups.has(cat))
65
+ .map(cat => ({ category: cat, books: groups.get(cat)! }))
66
+ })
67
+
68
+ const totalPieces = computed(() => books.value.reduce((sum, b) => sum + b.count, 0))
69
+
70
+ function openBook(bookId: string) {
71
+ router.push(`/${bookId}`)
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <div v-if="scale === 'library'">
77
+ <!-- ═══════ 直排模式 ═══════ -->
78
+ <div v-if="isVertical" class="v-root">
79
+ <SideNav @home="router.push('/')" @back="router.push('/')" />
80
+ <div ref="vPageRef" class="v-page">
81
+ <section class="v-hero">
82
+ <h1 class="v-title">古 典 詩 文 圖 書 館</h1>
83
+ <p class="v-subtitle">Classical Chinese Text Library</p>
84
+ <div class="v-divider"></div>
85
+ </section>
86
+
87
+ <section class="v-cards-col">
88
+ <div
89
+ v-for="book in books"
90
+ :key="book.id"
91
+ class="v-book-card"
92
+ @click="openBook(book.id)"
93
+ >
94
+ <div class="v-book-accent"></div>
95
+ <h2 class="v-book-title">{{ book.title }}</h2>
96
+ <p v-if="book.subtitle" class="v-book-sub">{{ book.subtitle }}</p>
97
+ <div class="v-book-stats">
98
+ <span class="v-book-count">{{ book.count }} 篇</span>
99
+ </div>
100
+ </div>
101
+ </section>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- ═══════ 橫排模式 ═══════ -->
106
+ <div v-else class="lib-root">
107
+ <header class="lib-hero">
108
+ <div class="lib-seal">漢流</div>
109
+ <h1>古典詩文圖書館</h1>
110
+ <p class="lib-subtitle">Classical Chinese Text Library</p>
111
+ <div class="lib-stats-bar">
112
+ <span class="lib-stat">{{ books.length }} 部</span>
113
+ <span class="lib-stat-sep">·</span>
114
+ <span class="lib-stat">{{ totalPieces }} 篇</span>
115
+ </div>
116
+ </header>
117
+ <div v-for="group in groupedBooks" :key="group.category" class="lib-group">
118
+ <h2 class="lib-group-title">{{ group.category }}</h2>
119
+ <div class="lib-grid">
120
+ <div
121
+ v-for="book in group.books"
122
+ :key="book.id"
123
+ class="lib-card"
124
+ @click="openBook(book.id)"
125
+ >
126
+ <div class="lib-card-accent"></div>
127
+ <div class="lib-card-body">
128
+ <div class="lib-card-top">
129
+ <h3 class="lib-card-title">{{ book.title }}</h3>
130
+ <span class="lib-card-genre">{{ genreLabel[book.genre] || book.genre }}</span>
131
+ </div>
132
+ <p v-if="book.subtitle" class="lib-card-sub">{{ book.subtitle }}</p>
133
+ <div class="lib-card-stats">
134
+ <span class="lib-card-count">{{ book.count }} 篇</span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ <ReadingToolbar />
141
+ </div>
142
+ </div>
143
+ </template>
144
+
145
+ <style scoped>
146
+ /* ═══════ 直排模式 ═══════ */
147
+
148
+ .v-page {
149
+ height: 100vh;
150
+ display: flex;
151
+ flex-direction: row-reverse;
152
+ overflow-x: auto;
153
+ overflow-y: hidden;
154
+ margin-right: var(--nav-width, 56px);
155
+ padding: 0 32px;
156
+ background: linear-gradient(90deg, var(--paper) 0%, var(--paper-warm) 100%);
157
+ scrollbar-width: thin;
158
+ scrollbar-color: var(--gold) transparent;
159
+ }
160
+ .v-page::-webkit-scrollbar { height: 4px; }
161
+ .v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
162
+
163
+ .v-hero {
164
+ writing-mode: vertical-rl;
165
+ text-orientation: mixed;
166
+ flex-shrink: 0;
167
+ height: 100vh;
168
+ display: flex;
169
+ flex-direction: column;
170
+ align-items: flex-start;
171
+ justify-content: center;
172
+ padding: 40px 20px;
173
+ }
174
+ .v-seal {
175
+ writing-mode: horizontal-tb;
176
+ display: inline-flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ width: 48px; height: 48px;
180
+ border: 2px solid var(--vermillion);
181
+ color: var(--vermillion);
182
+ font-size: 14px;
183
+ font-family: var(--serif);
184
+ font-weight: 900;
185
+ margin-bottom: 0;
186
+ margin-left: 16px;
187
+ border-radius: 4px;
188
+ letter-spacing: 0;
189
+ }
190
+ .v-title {
191
+ font-size: 48px; font-weight: 900;
192
+ letter-spacing: 16px; color: var(--ink);
193
+ margin-left: 20px; padding-left: 20px;
194
+ border-left: 4px solid var(--vermillion);
195
+ line-height: 1.6;
196
+ }
197
+ .v-subtitle {
198
+ font-size: 14px; font-weight: 300;
199
+ color: var(--ink-faint); letter-spacing: 3px;
200
+ margin-left: 16px; font-family: var(--sans);
201
+ }
202
+ .v-divider {
203
+ width: 2px; height: 80px;
204
+ background: linear-gradient(180deg, transparent, var(--gold), transparent);
205
+ margin-left: 20px;
206
+ }
207
+
208
+ .v-cards-col {
209
+ writing-mode: vertical-rl;
210
+ text-orientation: mixed;
211
+ flex-shrink: 0;
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 0;
215
+ padding: 40px 16px;
216
+ height: 100vh;
217
+ box-sizing: border-box;
218
+ overflow-x: auto;
219
+ overflow-y: hidden;
220
+ align-items: flex-start;
221
+ }
222
+
223
+ .v-book-card {
224
+ writing-mode: vertical-rl;
225
+ text-orientation: mixed;
226
+ display: flex;
227
+ flex-direction: column;
228
+ align-items: flex-start;
229
+ padding: 24px 16px;
230
+ border-left: 1px solid var(--border-light);
231
+ cursor: pointer;
232
+ transition: all 0.3s ease;
233
+ position: relative;
234
+ }
235
+ .v-book-card:hover {
236
+ background: var(--surface);
237
+ }
238
+ .v-book-accent {
239
+ position: absolute;
240
+ top: 0; right: 0;
241
+ width: 0; height: 3px;
242
+ background: var(--vermillion);
243
+ transition: width 0.35s ease;
244
+ }
245
+ .v-book-card:hover .v-book-accent { width: 100%; }
246
+ .v-book-title {
247
+ font-size: 32px; font-weight: 900;
248
+ letter-spacing: 8px; color: var(--ink);
249
+ margin-left: 16px; padding-left: 16px;
250
+ border-left: 3px solid var(--vermillion);
251
+ line-height: 1.6;
252
+ }
253
+ .v-book-sub {
254
+ font-size: 14px;
255
+ color: var(--ink-light);
256
+ letter-spacing: 3px;
257
+ margin-left: 12px;
258
+ font-family: var(--sans);
259
+ }
260
+ .v-book-stats {
261
+ margin-left: 12px;
262
+ padding-left: 12px;
263
+ border-left: 1px solid var(--border);
264
+ }
265
+ .v-book-count {
266
+ font-size: 13px;
267
+ color: var(--ink-faint);
268
+ letter-spacing: 2px;
269
+ font-family: var(--sans);
270
+ padding: 2px 8px;
271
+ background: var(--surface-warm);
272
+ border-radius: 4px;
273
+ }
274
+
275
+ /* ═══════ 橫排模式 ═══════ */
276
+
277
+ .lib-root {
278
+ max-width: 960px;
279
+ margin: 0 auto;
280
+ padding: 64px 24px 120px;
281
+ }
282
+ .lib-hero {
283
+ text-align: center;
284
+ margin-bottom: 48px;
285
+ }
286
+ .lib-seal {
287
+ writing-mode: vertical-rl;
288
+ text-orientation: upright;
289
+ display: inline-flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ width: 40px; height: 56px;
293
+ border: 2px solid var(--vermillion);
294
+ color: var(--vermillion);
295
+ font-size: 20px;
296
+ font-family: var(--serif);
297
+ letter-spacing: 2px;
298
+ margin-bottom: 24px;
299
+ border-radius: 4px;
300
+ line-height: 1;
301
+ }
302
+ .lib-hero h1 {
303
+ font-size: 36px;
304
+ font-weight: 700;
305
+ letter-spacing: 6px;
306
+ color: var(--ink);
307
+ margin-bottom: 8px;
308
+ }
309
+ .lib-subtitle {
310
+ font-size: 14px;
311
+ font-family: var(--sans);
312
+ color: var(--ink-faint);
313
+ letter-spacing: 2px;
314
+ margin-bottom: 12px;
315
+ }
316
+ .lib-stats-bar {
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ gap: 8px;
321
+ font-family: var(--sans);
322
+ font-size: 14px;
323
+ color: var(--ink-light);
324
+ letter-spacing: 2px;
325
+ }
326
+ .lib-stat-sep { color: var(--border); }
327
+
328
+ .lib-group { margin-bottom: 40px; }
329
+ .lib-group-title {
330
+ font-size: 15px;
331
+ font-family: var(--sans);
332
+ font-weight: 600;
333
+ color: var(--ink-light);
334
+ letter-spacing: 3px;
335
+ margin-bottom: 16px;
336
+ padding-bottom: 8px;
337
+ border-bottom: 1px solid var(--border-light);
338
+ }
339
+
340
+ .lib-grid {
341
+ display: grid;
342
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
343
+ gap: 12px;
344
+ }
345
+
346
+ .lib-card {
347
+ display: flex;
348
+ flex-direction: column;
349
+ justify-content: flex-start;
350
+ padding: 20px;
351
+ border: 1px solid var(--border-light);
352
+ border-radius: 8px;
353
+ cursor: pointer;
354
+ transition: all 0.3s ease;
355
+ position: relative;
356
+ background: var(--surface);
357
+ }
358
+ .lib-card:hover { border-color: var(--gold); box-shadow: 0 4px 20px rgba(var(--shadow-rgb), 0.1); }
359
+ .lib-card-accent {
360
+ position: absolute;
361
+ top: 0; left: 0;
362
+ width: 3px; height: 0;
363
+ background: var(--vermillion);
364
+ transition: height 0.35s ease;
365
+ }
366
+ .lib-card:hover .lib-card-accent { height: 100%; }
367
+ .lib-card-top {
368
+ display: flex;
369
+ align-items: baseline;
370
+ gap: 8px;
371
+ margin-bottom: 4px;
372
+ }
373
+ .lib-card-title {
374
+ font-size: 22px; font-weight: 900;
375
+ letter-spacing: 4px; color: var(--ink);
376
+ }
377
+ .lib-card-genre {
378
+ font-size: 11px;
379
+ font-family: var(--sans);
380
+ color: var(--ink-faint);
381
+ padding: 1px 6px;
382
+ border: 1px solid var(--border-light);
383
+ border-radius: 3px;
384
+ white-space: nowrap;
385
+ }
386
+ .lib-card-sub {
387
+ font-size: 13px; color: var(--ink-light);
388
+ letter-spacing: 1px; font-family: var(--sans);
389
+ margin-bottom: 12px;
390
+ }
391
+ .lib-card-stats {
392
+ font-size: 12px; color: var(--ink-faint);
393
+ font-family: var(--sans); letter-spacing: 1px;
394
+ }
395
+ .lib-card-count {
396
+ padding: 2px 8px;
397
+ background: var(--surface-warm);
398
+ border-radius: 4px;
399
+ }
400
+
401
+ @media (max-width: 768px) {
402
+ .v-page { padding: 0 16px; }
403
+ .v-title { font-size: 36px; letter-spacing: 10px; }
404
+ .lib-root { padding: 40px 16px 80px; }
405
+ .lib-hero h1 { font-size: 28px; letter-spacing: 4px; }
406
+ .lib-grid {
407
+ grid-template-columns: 1fr 1fr;
408
+ gap: 8px;
409
+ }
410
+ .lib-card { padding: 14px; }
411
+ .lib-card-title { font-size: 18px; letter-spacing: 2px; }
412
+ .lib-card-genre { display: none; }
413
+ .lib-card-sub { font-size: 12px; margin-bottom: 8px; }
414
+ }
415
+
416
+ @media (max-width: 480px) {
417
+ .lib-grid { grid-template-columns: 1fr; }
418
+ }
419
+ </style>