@hanology/cham-browser 0.4.14 → 0.4.15

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.14",
3
+ "version": "0.4.15",
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",
@@ -7,6 +7,7 @@ import type { Annotation } from '../types'
7
7
  const props = defineProps<{
8
8
  visible: boolean
9
9
  annotations: Annotation[]
10
+ headword?: string
10
11
  layerLabels?: Record<string, string>
11
12
  style?: Record<string, string>
12
13
  vertical?: boolean
@@ -64,6 +65,15 @@ function kindLabel(ann: Annotation): string {
64
65
  return map[ann.kind] || ann.kind
65
66
  }
66
67
 
68
+ function dominantKind(): string {
69
+ if (!props.annotations.length) return ''
70
+ const counts: Record<string, number> = {}
71
+ for (const a of props.annotations) {
72
+ counts[a.kind] = (counts[a.kind] || 0) + 1
73
+ }
74
+ return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]
75
+ }
76
+
67
77
  function onDocClick(e: MouseEvent) {
68
78
  if (!stickyVisible.value) return
69
79
  const el = (e.target as HTMLElement)
@@ -79,16 +89,25 @@ function onDocTouchMove(e: TouchEvent) {
79
89
  dismiss()
80
90
  }
81
91
 
92
+ function onKeydown(e: KeyboardEvent) {
93
+ if (e.key === 'Escape' && stickyVisible.value) {
94
+ e.preventDefault()
95
+ dismiss()
96
+ }
97
+ }
98
+
82
99
  onMounted(() => {
83
100
  window.addEventListener('resize', onResize, { passive: true })
84
101
  document.addEventListener('click', onDocClick, true)
85
102
  document.addEventListener('touchmove', onDocTouchMove, { passive: true })
103
+ document.addEventListener('keydown', onKeydown)
86
104
  })
87
105
 
88
106
  onBeforeUnmount(() => {
89
107
  window.removeEventListener('resize', onResize)
90
108
  document.removeEventListener('click', onDocClick, true)
91
109
  document.removeEventListener('touchmove', onDocTouchMove)
110
+ document.removeEventListener('keydown', onKeydown)
92
111
  })
93
112
  </script>
94
113
 
@@ -103,17 +122,23 @@ onBeforeUnmount(() => {
103
122
  @mouseenter="emit('tooltipEnter')"
104
123
  @mouseleave="emit('tooltipLeave')"
105
124
  >
125
+ <div v-if="headword" class="ann-card-head" :class="dominantKind()">
126
+ <span class="ann-headword">{{ headword }}</span>
127
+ <span class="ann-badge-count" v-if="annotations.length > 1">{{ annotations.length }}</span>
128
+ </div>
106
129
  <button class="ann-card-close" @click="dismiss" aria-label="關閉">
107
130
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M18 6L6 18M6 6l12 12"/></svg>
108
131
  </button>
109
132
  <div class="ann-card-scroll">
110
- <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
111
- <div class="ann-head">
133
+ <div v-for="ann in annotations" :key="ann.id" class="ann-entry">
134
+ <div class="ann-entry-header">
112
135
  <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
113
136
  <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
114
137
  </div>
115
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
116
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
138
+ <div class="ann-entry-body">
139
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
140
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
141
+ </div>
117
142
  </div>
118
143
  </div>
119
144
  </div>
@@ -128,14 +153,20 @@ onBeforeUnmount(() => {
128
153
  <button class="ann-sheet-handle" @click="dismiss">
129
154
  <span class="ann-handle-bar" />
130
155
  </button>
156
+ <div v-if="headword" class="ann-sheet-head" :class="dominantKind()">
157
+ <span class="ann-headword">{{ headword }}</span>
158
+ <span class="ann-badge-count" v-if="annotations.length > 1">{{ annotations.length }}</span>
159
+ </div>
131
160
  <div class="ann-sheet-scroll">
132
- <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
133
- <div class="ann-head">
161
+ <div v-for="ann in annotations" :key="ann.id" class="ann-entry">
162
+ <div class="ann-entry-header">
134
163
  <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
135
164
  <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
136
165
  </div>
137
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
138
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
166
+ <div class="ann-entry-body">
167
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
168
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
169
+ </div>
139
170
  </div>
140
171
  </div>
141
172
  </div>
@@ -144,7 +175,38 @@ onBeforeUnmount(() => {
144
175
  </template>
145
176
 
146
177
  <style scoped>
147
- /* ─── Shared annotation entry ─── */
178
+ /* ─── Headword header ─── */
179
+ .ann-card-head,
180
+ .ann-sheet-head {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ padding: 10px 14px;
185
+ border-bottom: 1px solid var(--border-light);
186
+ background: var(--surface);
187
+ }
188
+
189
+ .ann-headword {
190
+ font-family: var(--serif);
191
+ font-size: 22px;
192
+ font-weight: 900;
193
+ letter-spacing: 4px;
194
+ color: var(--ink);
195
+ }
196
+
197
+ .ann-badge-count {
198
+ font-family: var(--sans);
199
+ font-size: 11px;
200
+ font-weight: 700;
201
+ color: var(--ink-faint);
202
+ background: var(--surface-warm);
203
+ border: 1px solid var(--border-light);
204
+ border-radius: 10px;
205
+ padding: 2px 8px;
206
+ letter-spacing: 0;
207
+ }
208
+
209
+ /* ─── Annotation entry ─── */
148
210
  .ann-entry {
149
211
  padding: 10px 0;
150
212
  border-bottom: 1px solid var(--border-light);
@@ -156,11 +218,15 @@ onBeforeUnmount(() => {
156
218
  .ann-entry:last-child { border-bottom: none; padding-bottom: 0; }
157
219
  .ann-entry:first-child { padding-top: 0; }
158
220
 
159
- .ann-head {
221
+ .ann-entry-header {
160
222
  display: inline-flex;
161
223
  align-items: center;
162
224
  gap: 6px;
163
- margin-bottom: 2px;
225
+ margin-bottom: 3px;
226
+ }
227
+
228
+ .ann-entry-body {
229
+ padding-left: 2px;
164
230
  }
165
231
 
166
232
  .ann-kind {
@@ -201,23 +267,25 @@ onBeforeUnmount(() => {
201
267
  line-height: 1.8;
202
268
  }
203
269
 
204
- /* ─── Desktop/Tablet floating card ─── */
270
+ /* ─── Floating card ─── */
205
271
  .ann-card {
206
272
  position: fixed;
207
273
  background: var(--surface-warm);
208
274
  border: 1px solid var(--border);
209
275
  border-radius: 10px;
210
- box-shadow: 0 12px 40px rgba(var(--shadow-rgb), 0.18);
276
+ box-shadow: 0 12px 48px rgba(var(--shadow-rgb), 0.2), 0 2px 8px rgba(var(--shadow-rgb), 0.06);
211
277
  z-index: 1000;
212
278
  display: flex;
213
279
  flex-direction: column;
214
280
  overflow: hidden;
281
+ backdrop-filter: blur(8px);
282
+ -webkit-backdrop-filter: blur(8px);
215
283
  }
216
284
 
217
285
  .ann-card-close {
218
286
  position: absolute;
219
- top: 6px;
220
- right: 6px;
287
+ top: 8px;
288
+ right: 8px;
221
289
  width: 22px;
222
290
  height: 22px;
223
291
  border: none;
@@ -229,17 +297,21 @@ onBeforeUnmount(() => {
229
297
  align-items: center;
230
298
  justify-content: center;
231
299
  transition: all 0.15s;
232
- opacity: 0.4;
300
+ opacity: 0;
233
301
  z-index: 1;
234
302
  }
303
+ .ann-card:hover .ann-card-close,
304
+ .ann-card-close:focus-visible {
305
+ opacity: 0.5;
306
+ }
235
307
  .ann-card-close:hover {
236
- opacity: 1;
308
+ opacity: 1 !important;
237
309
  background: var(--ink);
238
310
  color: var(--paper);
239
311
  }
240
312
 
241
313
  .ann-card-scroll {
242
- padding: 12px 14px;
314
+ padding: 10px 14px;
243
315
  overflow-y: auto;
244
316
  overscroll-behavior: contain;
245
317
  flex: 1;
@@ -247,18 +319,18 @@ onBeforeUnmount(() => {
247
319
 
248
320
  /* Card transition */
249
321
  .ann-pop-enter-active {
250
- transition: opacity 0.15s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
322
+ transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
251
323
  }
252
324
  .ann-pop-leave-active {
253
- transition: opacity 0.12s ease, transform 0.12s ease;
325
+ transition: opacity 0.1s ease, transform 0.1s ease;
254
326
  }
255
327
  .ann-pop-enter-from {
256
328
  opacity: 0;
257
- transform: scale(0.95) translateY(6px);
329
+ transform: scale(0.96) translateY(4px);
258
330
  }
259
331
  .ann-pop-leave-to {
260
332
  opacity: 0;
261
- transform: scale(0.97);
333
+ transform: scale(0.98);
262
334
  }
263
335
 
264
336
  /* ─── Mobile bottom sheet ─── */
@@ -267,7 +339,7 @@ onBeforeUnmount(() => {
267
339
  left: 0;
268
340
  right: 0;
269
341
  bottom: 0;
270
- max-height: 50vh;
342
+ max-height: 60vh;
271
343
  background: var(--surface-warm);
272
344
  border-top: 1px solid var(--border);
273
345
  border-radius: 14px 14px 0 0;
@@ -315,6 +387,21 @@ onBeforeUnmount(() => {
315
387
  transform: translateY(100%);
316
388
  }
317
389
 
390
+ /* ─── Active annotation on page ─── */
391
+ :global(.ann-target.ann-active) {
392
+ background: rgba(194, 58, 43, 0.12) !important;
393
+ box-shadow: 0 0 0 2px rgba(194, 58, 43, 0.15);
394
+ border-radius: 2px;
395
+ }
396
+ :global(.ann-target.ann-active.pronunciation) {
397
+ background: rgba(58, 107, 94, 0.12) !important;
398
+ box-shadow: 0 0 0 2px rgba(58, 107, 94, 0.15);
399
+ }
400
+ :global(.ann-target.ann-active.person) {
401
+ background: rgba(58, 90, 140, 0.12) !important;
402
+ box-shadow: 0 0 0 2px rgba(58, 90, 140, 0.15);
403
+ }
404
+
318
405
  @media (min-width: 768px) {
319
406
  .ann-sheet { display: none; }
320
407
  }
@@ -8,19 +8,28 @@ function isMobile(): boolean {
8
8
  export function useAnnotationInteraction() {
9
9
  const tooltip = useAnnotationTooltip()
10
10
  let hideTimer: ReturnType<typeof setTimeout> | null = null
11
+ let activeEl: HTMLElement | null = null
11
12
 
12
13
  function cancelHide() {
13
14
  if (hideTimer) { clearTimeout(hideTimer); hideTimer = null }
14
15
  }
15
16
 
17
+ function setActive(el: HTMLElement | null) {
18
+ if (activeEl) activeEl.classList.remove('ann-active')
19
+ activeEl = el
20
+ if (activeEl) activeEl.classList.add('ann-active')
21
+ }
22
+
16
23
  function scheduleHide(delay = 300) {
17
24
  cancelHide()
18
- hideTimer = setTimeout(() => { tooltip.hide(); hideTimer = null }, delay)
25
+ hideTimer = setTimeout(() => { tooltip.hide(); setActive(null); hideTimer = null }, delay)
19
26
  }
20
27
 
21
28
  function onHover(event: MouseEvent, annotations: Annotation[]) {
22
29
  if (isMobile()) return
23
30
  cancelHide()
31
+ const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
32
+ setActive(el)
24
33
  tooltip.show(event, annotations)
25
34
  }
26
35
 
@@ -30,6 +39,8 @@ export function useAnnotationInteraction() {
30
39
 
31
40
  function onTap(event: MouseEvent, annotations: Annotation[]) {
32
41
  cancelHide()
42
+ const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
43
+ setActive(el)
33
44
  tooltip.toggle(event, annotations)
34
45
  }
35
46
 
@@ -43,12 +54,14 @@ export function useAnnotationInteraction() {
43
54
 
44
55
  function dismiss() {
45
56
  cancelHide()
57
+ setActive(null)
46
58
  tooltip.hide()
47
59
  }
48
60
 
49
61
  return {
50
62
  visible: tooltip.visible,
51
63
  items: tooltip.items,
64
+ headword: tooltip.headword,
52
65
  style: tooltip.style,
53
66
  onHover,
54
67
  onLeave,
@@ -132,12 +132,23 @@ export function useAnnotationTooltip() {
132
132
  const visible = ref(false)
133
133
  const items = ref<Annotation[]>([])
134
134
  const style = ref<Record<string, string>>({})
135
+ const headword = ref('')
135
136
  const { layout } = useReadingMode()
136
137
 
137
138
  function show(event: MouseEvent, annotations: Annotation[]) {
138
139
  items.value = annotations
139
140
  const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
140
141
  const rect = (el ?? event.target as HTMLElement).getBoundingClientRect()
142
+
143
+ // Extract headword text (strip the annotation number)
144
+ if (el) {
145
+ const clone = el.cloneNode(true) as HTMLElement
146
+ const nums = clone.querySelectorAll('.ann-num, sup')
147
+ nums.forEach(n => n.remove())
148
+ headword.value = clone.textContent?.trim() || ''
149
+ } else {
150
+ headword.value = ''
151
+ }
141
152
  const vw = window.innerWidth
142
153
  const vh = window.innerHeight
143
154
 
@@ -188,7 +199,7 @@ export function useAnnotationTooltip() {
188
199
  visible.value = true
189
200
  }
190
201
 
191
- function hide() { visible.value = false }
202
+ function hide() { visible.value = false; headword.value = '' }
192
203
  function toggle(event: MouseEvent, annotations: Annotation[]) {
193
204
  if (visible.value) {
194
205
  const currentIds = items.value.map(a => a.id).sort().join(',')
@@ -203,5 +214,5 @@ export function useAnnotationTooltip() {
203
214
  }
204
215
  }
205
216
 
206
- return { visible, items, style, show, hide, toggle }
217
+ return { visible, items, style, headword, show, hide, toggle }
207
218
  }
@@ -434,6 +434,7 @@ function tcy(n: number): string {
434
434
  <AnnotationTooltip
435
435
  :visible="interaction.visible"
436
436
  :annotations="interaction.items"
437
+ :headword="interaction.headword"
437
438
  :layer-labels="layerLabels"
438
439
  :style="interaction.style"
439
440
  :vertical="true"
@@ -441,10 +442,6 @@ function tcy(n: number): string {
441
442
  @tooltip-enter="interaction.onTooltipEnter"
442
443
  @tooltip-leave="interaction.onTooltipLeave"
443
444
  />
444
-
445
- <Teleport to="body">
446
- <Transition name="overlay">
447
- <div v-if="authorPaneOpen" class="v-overlay" @click="closeAuthorPane">
448
445
  <div class="v-author-pane" @click.stop>
449
446
  <button class="v-pane-close" @click="closeAuthorPane">✕</button>
450
447
  <div class="v-pane-header">
@@ -602,6 +599,7 @@ function tcy(n: number): string {
602
599
  <AnnotationTooltip
603
600
  :visible="interaction.visible"
604
601
  :annotations="interaction.items"
602
+ :headword="interaction.headword"
605
603
  :layer-labels="layerLabels"
606
604
  :style="interaction.style"
607
605
  :vertical="false"