@hanology/cham-browser 0.4.63 → 0.4.64

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanology/cham-browser",
3
- "version": "0.4.63",
3
+ "version": "0.4.64",
4
4
  "description": "CHAM — browser-compatible parser, serializer, and site generator for Classical Han Annotated Markdown",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -168,4 +168,11 @@ function onKey(event: KeyboardEvent) {
168
168
  color: var(--ink-faint);
169
169
  letter-spacing: 1px;
170
170
  }
171
+
172
+ .route-loading {
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ min-height: 100vh;
177
+ }
171
178
  </style>
@@ -146,7 +146,7 @@ onBeforeUnmount(() => {
146
146
  <Teleport to="body">
147
147
  <Transition name="ann-dim">
148
148
  <div
149
- v-if="visible && annotations.length && vertical && isMobile"
149
+ v-if="visible && annotations.length && vertical && ww < 768"
150
150
  class="ann-pane-dim"
151
151
  @click="emit('close')"
152
152
  />
@@ -341,18 +341,18 @@ onBeforeUnmount(() => {
341
341
  line-height: 1.5;
342
342
  }
343
343
 
344
- .ann-pane-kind.pronunciation { background: var(--jade); color: #fff; }
345
- .ann-pane-kind.semantic { background: var(--vermillion); color: #fff; }
346
- .ann-pane-kind.etymology { background: var(--ann-etymology); color: #fff; }
344
+ .ann-pane-kind.pronunciation { background: var(--jade); color: var(--paper); }
345
+ .ann-pane-kind.semantic { background: var(--vermillion); color: var(--paper); }
346
+ .ann-pane-kind.etymology { background: var(--ann-etymology); color: var(--paper); }
347
347
  .ann-pane-kind.note,
348
348
  .ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
349
- .ann-pane-kind.commentary { background: var(--ann-commentary); color: #fff; }
350
- .ann-pane-kind.translation { background: var(--ann-translation); color: #fff; }
351
- .ann-pane-kind.person { background: var(--ann-person); color: #fff; }
352
- .ann-pane-kind.place { background: var(--ann-place); color: #fff; }
353
- .ann-pane-kind.event { background: var(--ann-event); color: #fff; }
354
- .ann-pane-kind.date { background: var(--ann-date); color: #fff; }
355
- .ann-pane-kind.allusion { background: var(--ann-allusion); color: #fff; }
349
+ .ann-pane-kind.commentary { background: var(--ann-commentary); color: var(--paper); }
350
+ .ann-pane-kind.translation { background: var(--ann-translation); color: var(--paper); }
351
+ .ann-pane-kind.person { background: var(--ann-person); color: var(--paper); }
352
+ .ann-pane-kind.place { background: var(--ann-place); color: var(--paper); }
353
+ .ann-pane-kind.event { background: var(--ann-event); color: var(--paper); }
354
+ .ann-pane-kind.date { background: var(--ann-date); color: var(--paper); }
355
+ .ann-pane-kind.allusion { background: var(--ann-allusion); color: var(--paper); }
356
356
 
357
357
  .ann-pane-layer {
358
358
  font-size: 10px;
@@ -231,18 +231,18 @@ onBeforeUnmount(() => {
231
231
  letter-spacing: 1px;
232
232
  line-height: 1.5;
233
233
  }
234
- .ann-kind.pronunciation { background: var(--jade); color: #fff; }
235
- .ann-kind.semantic { background: var(--vermillion); color: #fff; }
236
- .ann-kind.etymology { background: var(--ann-etymology); color: #fff; }
234
+ .ann-kind.pronunciation { background: var(--jade); color: var(--paper); }
235
+ .ann-kind.semantic { background: var(--vermillion); color: var(--paper); }
236
+ .ann-kind.etymology { background: var(--ann-etymology); color: var(--paper); }
237
237
  .ann-kind.note,
238
238
  .ann-kind.definition { background: var(--ink); color: var(--paper); }
239
- .ann-kind.commentary { background: var(--ann-commentary); color: #fff; }
240
- .ann-kind.translation { background: var(--ann-translation); color: #fff; }
241
- .ann-kind.person { background: var(--ann-person); color: #fff; }
242
- .ann-kind.place { background: var(--ann-place); color: #fff; }
243
- .ann-kind.event { background: var(--ann-event); color: #fff; }
244
- .ann-kind.date { background: var(--ann-date); color: #fff; }
245
- .ann-kind.allusion { background: var(--ann-allusion); color: #fff; }
239
+ .ann-kind.commentary { background: var(--ann-commentary); color: var(--paper); }
240
+ .ann-kind.translation { background: var(--ann-translation); color: var(--paper); }
241
+ .ann-kind.person { background: var(--ann-person); color: var(--paper); }
242
+ .ann-kind.place { background: var(--ann-place); color: var(--paper); }
243
+ .ann-kind.event { background: var(--ann-event); color: var(--paper); }
244
+ .ann-kind.date { background: var(--ann-date); color: var(--paper); }
245
+ .ann-kind.allusion { background: var(--ann-allusion); color: var(--paper); }
246
246
 
247
247
  .ann-layer {
248
248
  font-size: 10px;
@@ -396,6 +396,34 @@ onBeforeUnmount(() => {
396
396
  background: rgba(58, 90, 140, 0.12) !important;
397
397
  box-shadow: 0 0 0 2px rgba(58, 90, 140, 0.15);
398
398
  }
399
+ :global(.ann-target.ann-active.place) {
400
+ background: rgba(139, 105, 20, 0.12) !important;
401
+ box-shadow: 0 0 0 2px rgba(139, 105, 20, 0.15);
402
+ }
403
+ :global(.ann-target.ann-active.event) {
404
+ background: rgba(107, 76, 138, 0.12) !important;
405
+ box-shadow: 0 0 0 2px rgba(107, 76, 138, 0.15);
406
+ }
407
+ :global(.ann-target.ann-active.date) {
408
+ background: rgba(42, 122, 122, 0.12) !important;
409
+ box-shadow: 0 0 0 2px rgba(42, 122, 122, 0.15);
410
+ }
411
+ :global(.ann-target.ann-active.allusion) {
412
+ background: rgba(181, 101, 29, 0.12) !important;
413
+ box-shadow: 0 0 0 2px rgba(181, 101, 29, 0.15);
414
+ }
415
+ :global(.ann-target.ann-active.etymology) {
416
+ background: rgba(107, 91, 149, 0.12) !important;
417
+ box-shadow: 0 0 0 2px rgba(107, 91, 149, 0.15);
418
+ }
419
+ :global(.ann-target.ann-active.commentary) {
420
+ background: rgba(192, 57, 43, 0.12) !important;
421
+ box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.15);
422
+ }
423
+ :global(.ann-target.ann-active.translation) {
424
+ background: rgba(44, 110, 73, 0.12) !important;
425
+ box-shadow: 0 0 0 2px rgba(44, 110, 73, 0.15);
426
+ }
399
427
 
400
428
  @media (min-width: 768px) {
401
429
  .ann-sheet { display: none; }
@@ -61,7 +61,7 @@ onUnmounted(detach)
61
61
 
62
62
  <template>
63
63
  <Transition name="btt">
64
- <button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" :aria-label="vertical ? t('action.backToStart') : t('action.backToStart')">
64
+ <button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" :aria-label="vertical ? t('action.backToStart') : t('action.backToTop')">
65
65
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
66
66
  <path d="M12 19V5M5 12l7-7 7 7"/>
67
67
  </svg>
@@ -39,7 +39,7 @@ defineProps<{
39
39
  }
40
40
  .pron-yue {
41
41
  background: var(--jade);
42
- color: #fff;
42
+ color: var(--paper);
43
43
  }
44
44
  .pron-cmn {
45
45
  background: var(--ink);
@@ -15,7 +15,7 @@ function close() { open.value = false }
15
15
  <template>
16
16
  <div class="rt" :class="{ open }">
17
17
  <button class="rt-fab" @click="toggle" :aria-label="open ? t('settings.close') : t('settings.reading')">
18
- <span v-if="!open" class="rt-icon">設</span>
18
+ <span v-if="!open" class="rt-icon">{{ t('settings.shortTitle').charAt(0) }}</span>
19
19
  <span v-else class="rt-icon">✕</span>
20
20
  </button>
21
21
  <div v-if="open" class="rt-panel" @click.stop>
@@ -104,7 +104,7 @@ const paragraphsHtml = computed(() => {
104
104
  .sb-num {
105
105
  display: inline-flex; align-items: center; justify-content: center;
106
106
  width: 28px; height: 28px; border-radius: 50%;
107
- background: var(--vermillion); color: #fff;
107
+ background: var(--vermillion); color: var(--paper);
108
108
  font-family: var(--sans); font-size: 13px; font-weight: 700;
109
109
  flex-shrink: 0;
110
110
  }
@@ -142,7 +142,7 @@ const paragraphsHtml = computed(() => {
142
142
  height: 22px;
143
143
  border-radius: 4px;
144
144
  background: var(--vermillion);
145
- color: #fff;
145
+ color: var(--paper);
146
146
  font-family: var(--sans);
147
147
  font-size: 12px;
148
148
  font-weight: 700;
@@ -78,6 +78,7 @@ const messages: Record<Locale, Record<string, string>> = {
78
78
  'annotation.noteCount': '{count}注',
79
79
  'action.close': '關閉',
80
80
  'action.backToStart': '回到起始',
81
+ 'action.backToTop': '回到頂部',
81
82
  'shortcut.toggleLayout': '直/橫',
82
83
  'shortcut.toggleTheme': '主題',
83
84
  'shortcut.goHome': '首頁',
@@ -175,6 +176,7 @@ const messages: Record<Locale, Record<string, string>> = {
175
176
  'annotation.noteCount': '{count}注',
176
177
  'action.close': '关闭',
177
178
  'action.backToStart': '回到起始',
179
+ 'action.backToTop': '回到顶部',
178
180
  'shortcut.toggleLayout': '直/横',
179
181
  'shortcut.toggleTheme': '主题',
180
182
  'shortcut.goHome': '首页',
@@ -272,6 +274,7 @@ const messages: Record<Locale, Record<string, string>> = {
272
274
  'annotation.noteCount': '{count} notes',
273
275
  'action.close': 'Close',
274
276
  'action.backToStart': 'Back to start',
277
+ 'action.backToTop': 'Back to top',
275
278
  'shortcut.toggleLayout': 'V/H',
276
279
  'shortcut.toggleTheme': 'Theme',
277
280
  'shortcut.goHome': 'Home',
@@ -5,13 +5,6 @@ export type LayoutMode = 'horizontal' | 'vertical'
5
5
 
6
6
  export const THEMES: Theme[] = ['light', 'sepia', 'dark', 'oled']
7
7
 
8
- export const THEME_LABELS: Record<Theme, string> = {
9
- light: '亮',
10
- sepia: '暖',
11
- dark: '暗',
12
- oled: '黑',
13
- }
14
-
15
8
  export const FONT_SIZES = [12, 14, 16, 18, 20, 22, 24, 28, 32] as const
16
9
  export type FontSize = typeof FONT_SIZES[number]
17
10
 
@@ -45,6 +45,18 @@
45
45
  background: rgba(181, 101, 29, 0.1);
46
46
  box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(181, 101, 29, 0.08);
47
47
  }
48
+ .ann-target.etymology:hover {
49
+ background: rgba(107, 91, 149, 0.1);
50
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(107, 91, 149, 0.08);
51
+ }
52
+ .ann-target.commentary:hover {
53
+ background: rgba(192, 57, 43, 0.1);
54
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(192, 57, 43, 0.08);
55
+ }
56
+ .ann-target.translation:hover {
57
+ background: rgba(44, 110, 73, 0.1);
58
+ box-shadow: 0 var(--ann-shadow-y, 2px) 8px rgba(44, 110, 73, 0.08);
59
+ }
48
60
 
49
61
  /* ─── Annotation number ─── */
50
62
  .ann-num {
@@ -328,3 +328,13 @@ button:focus-visible {
328
328
  [data-theme="oled"] .sb-vertical {
329
329
  border-right-color: var(--border-light);
330
330
  }
331
+
332
+ /* ===== REDUCED MOTION ===== */
333
+ @media (prefers-reduced-motion: reduce) {
334
+ *, *::before, *::after {
335
+ animation-duration: 0.01ms !important;
336
+ animation-iteration-count: 1 !important;
337
+ transition-duration: 0.01ms !important;
338
+ scroll-behavior: auto !important;
339
+ }
340
+ }
@@ -1,6 +1,8 @@
1
1
  import type { Annotation } from '../types'
2
2
  import { useI18n } from '../composables/useI18n'
3
3
 
4
+ const { t } = useI18n()
5
+
4
6
  const KIND_I18N_KEYS: Record<string, string> = {
5
7
  pronunciation: 'annotation.kind.pronunciation',
6
8
  semantic: 'annotation.kind.semantic',
@@ -18,9 +20,5 @@ const KIND_I18N_KEYS: Record<string, string> = {
18
20
 
19
21
  export function kindLabel(ann: Annotation): string {
20
22
  const key = KIND_I18N_KEYS[ann.kind]
21
- if (key) {
22
- const { t } = useI18n()
23
- return t(key)
24
- }
25
- return ann.kind
23
+ return key ? t(key) : ann.kind
26
24
  }
@@ -177,8 +177,7 @@ function goHome() { router.push('/') }
177
177
  display: flex; align-items: center; justify-content: center;
178
178
  font-size: 28px; font-weight: 900;
179
179
  color: var(--vermillion);
180
- margin-left: 24px; padding-left: 20px;
181
- border-left: 3px solid var(--vermillion);
180
+ margin-left: 24px;
182
181
  }
183
182
  .v-name {
184
183
  font-size: 48px; font-weight: 900;
@@ -251,7 +250,7 @@ function goHome() { router.push('/') }
251
250
  .h-page { min-height: 100vh; }
252
251
  .h-nav {
253
252
  position: sticky; top: 0; z-index: 100;
254
- background: var(--paper); opacity: 0.97;
253
+ background: var(--paper);
255
254
  backdrop-filter: blur(20px);
256
255
  border-bottom: 1px solid var(--border-light);
257
256
  padding: 0 40px;
@@ -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')" aria-label="search" />
81
+ <input v-model="searchQuery" class="v-search" :placeholder="t('catalog.search')" :aria-label="t('catalog.search')" />
82
82
  </span>
83
83
  </section>
84
84
 
@@ -92,6 +92,9 @@ function scrollToCatalog() {
92
92
  @click="openPiece(piece.num)"
93
93
  />
94
94
  </div>
95
+ <div v-if="searchQuery && filtered.length === 0" class="v-empty">
96
+ <span class="v-empty-text">{{ t('catalog.noResults', { query: searchQuery }) }}</span>
97
+ </div>
95
98
  </div>
96
99
  </div>
97
100
 
@@ -379,6 +382,23 @@ function scrollToCatalog() {
379
382
  opacity: 0.5;
380
383
  }
381
384
 
385
+ .v-empty {
386
+ writing-mode: vertical-rl;
387
+ text-orientation: mixed;
388
+ flex-shrink: 0;
389
+ height: 100vh;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ padding: 40px 20px;
394
+ }
395
+ .v-empty-text {
396
+ font-size: 14px;
397
+ color: var(--ink-faint);
398
+ letter-spacing: 2px;
399
+ font-family: var(--sans);
400
+ }
401
+
382
402
  @media (max-width: 768px) {
383
403
  .h-hero { min-height: 80vh; height: auto; padding: 60px 16px; }
384
404
  .h-ornament { font-size: 32px; letter-spacing: 12px; margin-bottom: 20px; }
@@ -312,20 +312,20 @@ function navigate(delta: number) {
312
312
  if (target !== null) router.push(`/${props.bookId}/${target}`)
313
313
  }
314
314
 
315
- const ROLE_LABELS: Record<string, string> = {
315
+ const ROLE_LABELS = computed(() => ({
316
316
  author: t('role.author'),
317
317
  commentator: t('role.commentator'),
318
318
  editor: t('role.editor'),
319
319
  translator: t('role.translator'),
320
320
  annotator: t('role.annotator'),
321
- }
321
+ }))
322
322
 
323
323
  const contributorGroups = computed(() => {
324
324
  const c = piece.value?.contributors
325
325
  if (!c || c.length <= 1) return []
326
326
  const groups = new Map<string, string[]>()
327
327
  for (const x of c) {
328
- const t = x.title || ROLE_LABELS[x.role] || t('role.defaultAuthor')
328
+ const t = x.title || ROLE_LABELS.value[x.role] || t('role.defaultAuthor')
329
329
  if (!groups.has(t)) groups.set(t, [])
330
330
  groups.get(t)!.push(x.name)
331
331
  }
@@ -516,8 +516,8 @@ function tcy(n: number): string {
516
516
  </div>
517
517
  <div class="v-pane-links">
518
518
  <a v-if="selectedAuthorData?.ctextId" :href="`https://ctext.org/wiki.pl?if=en&res=${selectedAuthorData.ctextId}`" target="_blank" rel="noopener" class="v-pane-link">CTEXT</a>
519
- <a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
520
- <a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia</a>
519
+ <a v-if="selectedAuthorData?.wikipediaZh" :href="selectedAuthorData.wikipediaZh" target="_blank" rel="noopener" class="v-pane-link">Wikipedia ZH</a>
520
+ <a v-if="selectedAuthorData?.wikipediaEn" :href="selectedAuthorData.wikipediaEn" target="_blank" rel="noopener" class="v-pane-link">Wikipedia EN</a>
521
521
  <a v-if="selectedAuthorData?.wikidata" :href="`https://www.wikidata.org/wiki/${selectedAuthorData.wikidata}`" target="_blank" rel="noopener" class="v-pane-link">Wikidata</a>
522
522
  </div>
523
523
  <div v-if="selectedAuthorBio" class="v-pane-bio">
@@ -671,8 +671,8 @@ function tcy(n: number): string {
671
671
  <span v-if="selectedAuthorWorkCount" class="h-pane-count">{{ t('piece.collected', { count: selectedAuthorWorkCount }) }}</span>
672
672
  </div>
673
673
  <div v-if="selectedAuthorData?.courtesyName || selectedAuthorData?.artName" class="h-pane-alt-names">
674
- <span v-if="selectedAuthorData?.courtesyName">字 {{ selectedAuthorData.courtesyName }}</span>
675
- <span v-if="selectedAuthorData?.artName">號 {{ selectedAuthorData.artName }}</span>
674
+ <span v-if="selectedAuthorData?.courtesyName">{{ t('author.courtesyName', { name: selectedAuthorData.courtesyName }) }}</span>
675
+ <span v-if="selectedAuthorData?.artName">{{ t('author.artName', { name: selectedAuthorData.artName }) }}</span>
676
676
  </div>
677
677
  </div>
678
678
  </div>
@@ -857,7 +857,7 @@ function tcy(n: number): string {
857
857
  .h-page { min-height: 100vh; }
858
858
  .h-nav {
859
859
  position: sticky; top: 0; z-index: 100;
860
- background: var(--paper); opacity: 0.97;
860
+ background: var(--paper);
861
861
  backdrop-filter: blur(20px);
862
862
  border-bottom: 1px solid var(--border-light);
863
863
  padding: 0 40px;