@hanology/cham-browser 0.4.61 → 0.4.62

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.61",
3
+ "version": "0.4.62",
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",
@@ -3,6 +3,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
3
3
  import { annotationToPronSegment } from '../utils/annotationParser'
4
4
  import { kindLabel } from '../utils/annotationLabels'
5
5
  import { toChineseNumber } from '../utils/chineseNumber'
6
+ import { useI18n } from '../composables/useI18n'
6
7
  import PronunciationGroup from './PronunciationGroup.vue'
7
8
  import type { Annotation } from '../types'
8
9
 
@@ -22,6 +23,7 @@ const emit = defineEmits<{
22
23
 
23
24
  const bodyRef = ref<HTMLElement | null>(null)
24
25
 
26
+ const { t } = useI18n()
25
27
  const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
26
28
  const isMobile = computed(() => ww.value < 768)
27
29
  function onResize() { ww.value = window.innerWidth }
@@ -158,7 +160,7 @@ onBeforeUnmount(() => {
158
160
  :style="{ width: paneWidth + 'px' }"
159
161
  >
160
162
  <div class="ann-pane-header">
161
- <span class="ann-pane-title">注釋</span>
163
+ <span class="ann-pane-title">{{ t('annotation.all') }}</span>
162
164
  <span class="ann-pane-count">{{ annotations.length }}</span>
163
165
  <button class="ann-pane-close" @click="emit('close')" aria-label="關閉">
164
166
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
@@ -171,7 +173,10 @@ onBeforeUnmount(() => {
171
173
  :data-ann-id="ann.id"
172
174
  class="ann-pane-entry"
173
175
  :class="{ active: activeId === ann.id, [ann.kind]: true }"
176
+ role="button"
177
+ tabindex="0"
174
178
  @click="emit('select', ann)"
179
+ @keydown.enter="emit('select', ann)"
175
180
  >
176
181
  <!-- Vertical: headword column on the right side -->
177
182
  <div v-if="vertical && headword(ann)" class="ann-pane-v-word">
@@ -289,6 +294,11 @@ onBeforeUnmount(() => {
289
294
  background: var(--surface);
290
295
  }
291
296
 
297
+ .ann-pane-entry:focus-visible {
298
+ outline: 2px solid var(--vermillion);
299
+ outline-offset: -2px;
300
+ }
301
+
292
302
  .ann-pane-entry.active.pronunciation {
293
303
  border-left-color: var(--jade);
294
304
  }
@@ -334,11 +344,11 @@ onBeforeUnmount(() => {
334
344
 
335
345
  .ann-pane-kind.pronunciation { background: var(--jade); color: #fff; }
336
346
  .ann-pane-kind.semantic { background: var(--vermillion); color: #fff; }
337
- .ann-pane-kind.etymology { background: #6b5b95; color: #fff; }
347
+ .ann-pane-kind.etymology { background: var(--ann-etymology); color: #fff; }
338
348
  .ann-pane-kind.note,
339
349
  .ann-pane-kind.definition { background: var(--ink); color: var(--paper); }
340
- .ann-pane-kind.commentary { background: #c0392b; color: #fff; }
341
- .ann-pane-kind.translation { background: #2c6e49; color: #fff; }
350
+ .ann-pane-kind.commentary { background: var(--ann-commentary); color: #fff; }
351
+ .ann-pane-kind.translation { background: var(--ann-translation); color: #fff; }
342
352
  .ann-pane-kind.person { background: var(--ann-person); color: #fff; }
343
353
  .ann-pane-kind.place { background: var(--ann-place); color: #fff; }
344
354
  .ann-pane-kind.event { background: var(--ann-event); color: #fff; }
@@ -199,6 +199,7 @@ onBeforeUnmount(() => {
199
199
  /* ─── Annotation entry ─── */
200
200
  .ann-entry {
201
201
  border-bottom: 1px solid var(--border-light);
202
+ padding: 8px 0;
202
203
  font-size: 14px;
203
204
  color: var(--ink-mid);
204
205
  letter-spacing: 1.5px;
@@ -229,11 +230,11 @@ onBeforeUnmount(() => {
229
230
  }
230
231
  .ann-kind.pronunciation { background: var(--jade); color: #fff; }
231
232
  .ann-kind.semantic { background: var(--vermillion); color: #fff; }
232
- .ann-kind.etymology { background: #6b5b95; color: #fff; }
233
+ .ann-kind.etymology { background: var(--ann-etymology); color: #fff; }
233
234
  .ann-kind.note,
234
235
  .ann-kind.definition { background: var(--ink); color: var(--paper); }
235
- .ann-kind.commentary { background: #c0392b; color: #fff; align-self: center; }
236
- .ann-kind.translation { background: #2c6e49; color: #fff; }
236
+ .ann-kind.commentary { background: var(--ann-commentary); color: #fff; }
237
+ .ann-kind.translation { background: var(--ann-translation); color: #fff; }
237
238
  .ann-kind.person { background: var(--ann-person); color: #fff; }
238
239
  .ann-kind.place { background: var(--ann-place); color: #fff; }
239
240
  .ann-kind.event { background: var(--ann-event); color: #fff; }
@@ -1,5 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { ref, onMounted, onUnmounted } from 'vue'
2
+ import { ref, onMounted, onUnmounted, watch } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ vertical?: boolean
6
+ scrollContainer?: HTMLElement | null
7
+ }>()
3
8
 
4
9
  const visible = ref(false)
5
10
  let ticking = false
@@ -8,22 +13,52 @@ function onScroll() {
8
13
  if (ticking) return
9
14
  ticking = true
10
15
  requestAnimationFrame(() => {
11
- visible.value = window.scrollY > 400
16
+ if (props.vertical && props.scrollContainer) {
17
+ visible.value = props.scrollContainer.scrollLeft > 400
18
+ } else {
19
+ visible.value = window.scrollY > 400
20
+ }
12
21
  ticking = false
13
22
  })
14
23
  }
15
24
 
16
25
  function scrollToTop() {
17
- window.scrollTo({ top: 0, behavior: 'smooth' })
26
+ if (props.vertical && props.scrollContainer) {
27
+ props.scrollContainer.scrollTo({ left: props.scrollContainer.scrollWidth, behavior: 'smooth' })
28
+ } else {
29
+ window.scrollTo({ top: 0, behavior: 'smooth' })
30
+ }
31
+ }
32
+
33
+ function attach() {
34
+ if (props.vertical && props.scrollContainer) {
35
+ props.scrollContainer.addEventListener('scroll', onScroll, { passive: true })
36
+ } else {
37
+ window.addEventListener('scroll', onScroll, { passive: true })
38
+ }
39
+ onScroll()
18
40
  }
19
41
 
20
- onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }))
21
- onUnmounted(() => window.removeEventListener('scroll', onScroll))
42
+ function detach() {
43
+ if (props.vertical && props.scrollContainer) {
44
+ props.scrollContainer.removeEventListener('scroll', onScroll)
45
+ } else {
46
+ window.removeEventListener('scroll', onScroll)
47
+ }
48
+ }
49
+
50
+ watch(() => props.scrollContainer, () => {
51
+ detach()
52
+ attach()
53
+ })
54
+
55
+ onMounted(attach)
56
+ onUnmounted(detach)
22
57
  </script>
23
58
 
24
59
  <template>
25
60
  <Transition name="btt">
26
- <button v-if="visible" class="btt" @click="scrollToTop" aria-label="回到頂部">
61
+ <button v-if="visible" class="btt" :class="{ 'btt-v': vertical }" @click="scrollToTop" aria-label="回到頂部">
27
62
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
28
63
  <path d="M12 19V5M5 12l7-7 7 7"/>
29
64
  </svg>
@@ -52,6 +87,28 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
52
87
  backdrop-filter: blur(8px);
53
88
  -webkit-backdrop-filter: blur(8px);
54
89
  }
90
+ .btt.btt-v {
91
+ bottom: auto;
92
+ top: 24px;
93
+ right: auto;
94
+ left: calc(var(--nav-width, 56px) + 12px);
95
+ }
96
+ width: 40px;
97
+ height: 40px;
98
+ border-radius: 50%;
99
+ border: 1px solid var(--border);
100
+ background: var(--surface);
101
+ color: var(--ink-light);
102
+ cursor: pointer;
103
+ z-index: 400;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.1);
108
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
109
+ backdrop-filter: blur(8px);
110
+ -webkit-backdrop-filter: blur(8px);
111
+ }
55
112
 
56
113
  @media (max-width: 768px) {
57
114
  .btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
@@ -61,7 +118,7 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
61
118
  color: #fff;
62
119
  border-color: var(--vermillion);
63
120
  transform: translateY(-3px);
64
- box-shadow: 0 8px 24px rgba(194, 58, 43, 0.2);
121
+ box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.15);
65
122
  }
66
123
 
67
124
  .btt-enter-active { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
@@ -128,7 +128,7 @@ const preview = computed(() => {
128
128
  top: auto; left: 0; bottom: 0;
129
129
  width: 0; height: 3px;
130
130
  background: linear-gradient(90deg, var(--gold), var(--vermillion));
131
- transition: width 0.35s ease;
131
+ transition: width 0.35s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
132
132
  }
133
133
  .pc-vertical:hover {
134
134
  transform: translateX(-4px);
@@ -75,12 +75,12 @@ onUnmounted(detach)
75
75
  top: 0; left: 0;
76
76
  height: 3px;
77
77
  background: linear-gradient(90deg, var(--vermillion), var(--gold));
78
- box-shadow: 0 0 8px rgba(194, 58, 43, 0.2);
78
+ box-shadow: 0 0 8px rgba(var(--shadow-rgb), 0.15);
79
79
  }
80
80
  .rp-v {
81
81
  top: 0; left: 0;
82
82
  width: 3px;
83
83
  background: linear-gradient(180deg, var(--vermillion), var(--gold));
84
- box-shadow: 0 0 8px rgba(194, 58, 43, 0.2);
84
+ box-shadow: 0 0 8px rgba(var(--shadow-rgb), 0.15);
85
85
  }
86
86
  </style>
@@ -22,6 +22,9 @@
22
22
  --ann-event: #6b4c8a;
23
23
  --ann-date: #2a7a7a;
24
24
  --ann-allusion: #b5651d;
25
+ --ann-etymology: #6b5b95;
26
+ --ann-commentary: #c0392b;
27
+ --ann-translation: #2c6e49;
25
28
  --border: #d8cdb8;
26
29
  --border-light: #e8e0d0;
27
30
  --shadow-rgb: 26,26,26;
@@ -48,6 +51,9 @@
48
51
  --ann-event: #6b4c8a;
49
52
  --ann-date: #2a7a7a;
50
53
  --ann-allusion: #b5651d;
54
+ --ann-etymology: #6b5b95;
55
+ --ann-commentary: #c0392b;
56
+ --ann-translation: #2c6e49;
51
57
  --border: #c9b896;
52
58
  --border-light: #d8cab0;
53
59
  --shadow-rgb: 74,63,46;
@@ -74,6 +80,9 @@
74
80
  --ann-event: #9a7cb4;
75
81
  --ann-date: #5ab4b4;
76
82
  --ann-allusion: #d4843a;
83
+ --ann-etymology: #9a8ab4;
84
+ --ann-commentary: #e06050;
85
+ --ann-translation: #5ab48a;
77
86
  --border: #48484a;
78
87
  --border-light: #555557;
79
88
  --shadow-rgb: 0,0,0;
@@ -100,6 +109,9 @@
100
109
  --ann-event: #9a7cb4;
101
110
  --ann-date: #5ab4b4;
102
111
  --ann-allusion: #d4843a;
112
+ --ann-etymology: #9a8ab4;
113
+ --ann-commentary: #ff5050;
114
+ --ann-translation: #40c890;
103
115
  --border: #333333;
104
116
  --border-light: #444444;
105
117
  --shadow-rgb: 0,0,0;
@@ -233,6 +233,9 @@ function goHome() { router.push('/') }
233
233
  border-color: var(--gold);
234
234
  box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
235
235
  }
236
+ .v-work:active {
237
+ transform: scale(0.97);
238
+ }
236
239
  .v-work-num {
237
240
  font-size: 11px; color: var(--ink-faint);
238
241
  font-family: var(--sans); letter-spacing: 2px;
@@ -332,6 +335,9 @@ function goHome() { router.push('/') }
332
335
  box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
333
336
  transform: translateY(-2px);
334
337
  }
338
+ .h-work:active {
339
+ transform: scale(0.98);
340
+ }
335
341
  .h-work-num { font-size: 11px; color: var(--ink-faint); font-family: var(--sans); letter-spacing: 2px; }
336
342
  .h-work-title { font-size: 18px; font-weight: 700; letter-spacing: 2px; margin: 6px 0 4px; }
337
343
  .h-work-preview {
@@ -336,6 +336,10 @@ function scrollToCatalog() {
336
336
  transform: translateY(-2px);
337
337
  box-shadow: 0 12px 40px rgba(var(--shadow-rgb), 0.12);
338
338
  }
339
+ .h-cta:active {
340
+ transform: scale(0.97);
341
+ box-shadow: 0 4px 12px rgba(var(--shadow-rgb), 0.08);
342
+ }
339
343
 
340
344
  .h-catalog { max-width: 1200px; margin: 0 auto; padding: 80px 40px; }
341
345
  .h-catalog-header { text-align: center; margin-bottom: 60px; }
@@ -530,6 +530,7 @@ function tcy(n: number): string {
530
530
  </div>
531
531
  </Transition>
532
532
  </Teleport>
533
+ <BackToTop vertical :scroll-container="vPageRef" />
533
534
  </div>
534
535
 
535
536
  <!-- ═══════ 橫排模式 ═══════ -->
@@ -694,7 +695,7 @@ function tcy(n: number): string {
694
695
  {{ p.trim() }}
695
696
  </div>
696
697
  </div>
697
- <div v-if="!selectedAuthorBio" class="h-pane-empty">暫無作者資料</div>
698
+ <div v-if="!selectedAuthorBio" class="h-pane-empty">{{ t('piece.noAuthorData') }}</div>
698
699
  </div>
699
700
  </div>
700
701
  </Transition>