@hanology/cham-browser 0.4.12 → 0.4.14

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.12",
3
+ "version": "0.4.14",
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",
@@ -67,6 +67,7 @@ function toggleLayer(id: string) {
67
67
 
68
68
  <style scoped>
69
69
  .ann-bar {
70
+ writing-mode: horizontal-tb;
70
71
  display: flex;
71
72
  flex-direction: column;
72
73
  gap: 6px;
@@ -4,8 +4,6 @@ import { annotationToPronSegment } from '../utils/annotationParser'
4
4
  import PronunciationGroup from './PronunciationGroup.vue'
5
5
  import type { Annotation } from '../types'
6
6
 
7
- type DisplayMode = 'pane' | 'popup' | 'sheet'
8
-
9
7
  const props = defineProps<{
10
8
  visible: boolean
11
9
  annotations: Annotation[]
@@ -21,25 +19,14 @@ const emit = defineEmits<{
21
19
  }>()
22
20
 
23
21
  const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
22
+ const isMobile = computed(() => ww.value < 768)
24
23
 
25
24
  function onResize() { ww.value = window.innerWidth }
26
25
 
27
- const mode = computed<DisplayMode>(() => {
28
- const w = ww.value
29
- if (w < 768) return 'sheet'
30
- if (w >= 1024) return 'pane'
31
- return 'popup'
32
- })
33
-
34
26
  const stickyVisible = ref(false)
35
27
 
36
28
  watch(() => props.visible, (v) => {
37
29
  if (v) stickyVisible.value = true
38
- else if (mode.value === 'popup') stickyVisible.value = false
39
- })
40
-
41
- watch(mode, () => {
42
- if (mode.value === 'popup' && !props.visible) stickyVisible.value = false
43
30
  })
44
31
 
45
32
  function dismiss() {
@@ -80,15 +67,15 @@ function kindLabel(ann: Annotation): string {
80
67
  function onDocClick(e: MouseEvent) {
81
68
  if (!stickyVisible.value) return
82
69
  const el = (e.target as HTMLElement)
83
- if (el.closest('.ann-left-pane, .ann-right-pane, .ann-bottom-sheet, .ann-popup')) return
70
+ if (el.closest('.ann-card, .ann-sheet')) return
84
71
  if (el.closest('.ann-target')) return
85
72
  dismiss()
86
73
  }
87
74
 
88
75
  function onDocTouchMove(e: TouchEvent) {
89
- if (!stickyVisible.value || ww.value >= 768) return
76
+ if (!stickyVisible.value || !isMobile.value) return
90
77
  const el = (e.target as HTMLElement)
91
- if (el.closest('.ann-bottom-sheet')) return
78
+ if (el.closest('.ann-sheet')) return
92
79
  dismiss()
93
80
  }
94
81
 
@@ -107,18 +94,19 @@ onBeforeUnmount(() => {
107
94
 
108
95
  <template>
109
96
  <Teleport to="body">
110
- <!-- Desktop pane: LEFT for horizontal, RIGHT for vertical -->
111
- <Transition :name="vertical ? 'ann-slide-left' : 'ann-slide-right'">
97
+ <!-- Desktop/Tablet floating card -->
98
+ <Transition name="ann-pop">
112
99
  <div
113
- v-if="mode === 'pane' && stickyVisible && annotations.length"
114
- :class="vertical ? 'ann-right-pane' : 'ann-left-pane'"
100
+ v-if="!isMobile && stickyVisible && annotations.length"
101
+ class="ann-card"
102
+ :style="style"
115
103
  @mouseenter="emit('tooltipEnter')"
116
104
  @mouseleave="emit('tooltipLeave')"
117
105
  >
118
- <button class="ann-pane-close" @click="dismiss" aria-label="關閉">
119
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
106
+ <button class="ann-card-close" @click="dismiss" aria-label="關閉">
107
+ <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>
120
108
  </button>
121
- <div class="ann-pane-scroll">
109
+ <div class="ann-card-scroll">
122
110
  <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
123
111
  <div class="ann-head">
124
112
  <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
@@ -131,32 +119,11 @@ onBeforeUnmount(() => {
131
119
  </div>
132
120
  </Transition>
133
121
 
134
- <!-- Tablet popup -->
135
- <Transition name="ann-fade">
136
- <div
137
- v-if="mode === 'popup' && visible && annotations.length"
138
- class="ann-popup"
139
- :class="{ 'ann-popup--vertical': vertical }"
140
- :style="style"
141
- @mouseenter="emit('tooltipEnter')"
142
- @mouseleave="emit('tooltipLeave')"
143
- >
144
- <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
145
- <div class="ann-head">
146
- <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
147
- <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
148
- </div>
149
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
150
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
151
- </div>
152
- </div>
153
- </Transition>
154
-
155
122
  <!-- Mobile bottom sheet -->
156
123
  <Transition name="ann-sheet">
157
124
  <div
158
- v-if="mode === 'sheet' && stickyVisible && annotations.length"
159
- class="ann-bottom-sheet"
125
+ v-if="isMobile && stickyVisible && annotations.length"
126
+ class="ann-sheet"
160
127
  >
161
128
  <button class="ann-sheet-handle" @click="dismiss">
162
129
  <span class="ann-handle-bar" />
@@ -177,7 +144,7 @@ onBeforeUnmount(() => {
177
144
  </template>
178
145
 
179
146
  <style scoped>
180
- /* ─── Compact annotation entry ─── */
147
+ /* ─── Shared annotation entry ─── */
181
148
  .ann-entry {
182
149
  padding: 10px 0;
183
150
  border-bottom: 1px solid var(--border-light);
@@ -194,7 +161,6 @@ onBeforeUnmount(() => {
194
161
  align-items: center;
195
162
  gap: 6px;
196
163
  margin-bottom: 2px;
197
- vertical-align: baseline;
198
164
  }
199
165
 
200
166
  .ann-kind {
@@ -206,7 +172,6 @@ onBeforeUnmount(() => {
206
172
  font-weight: 700;
207
173
  letter-spacing: 1px;
208
174
  line-height: 1.5;
209
- vertical-align: middle;
210
175
  }
211
176
  .ann-kind.pronunciation { background: var(--jade); color: #fff; }
212
177
  .ann-kind.semantic { background: var(--vermillion); color: #fff; }
@@ -229,7 +194,6 @@ onBeforeUnmount(() => {
229
194
  border: 1px solid var(--border-light);
230
195
  border-radius: 2px;
231
196
  letter-spacing: 0.5px;
232
- line-height: 1.4;
233
197
  }
234
198
 
235
199
  .ann-text {
@@ -237,123 +201,68 @@ onBeforeUnmount(() => {
237
201
  line-height: 1.8;
238
202
  }
239
203
 
240
- /* ─── Desktop Left Pane (horizontal mode) ─── */
241
- .ann-left-pane {
204
+ /* ─── Desktop/Tablet floating card ─── */
205
+ .ann-card {
242
206
  position: fixed;
243
- top: 72px;
244
- left: 20px;
245
- width: 280px;
246
- max-height: calc(100vh - 100px);
247
207
  background: var(--surface-warm);
248
208
  border: 1px solid var(--border);
249
209
  border-radius: 10px;
250
- box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
210
+ box-shadow: 0 12px 40px rgba(var(--shadow-rgb), 0.18);
251
211
  z-index: 1000;
252
212
  display: flex;
253
213
  flex-direction: column;
214
+ overflow: hidden;
254
215
  }
255
216
 
256
- .ann-pane-close {
217
+ .ann-card-close {
257
218
  position: absolute;
258
- top: 8px;
259
- right: 8px;
260
- width: 24px;
261
- height: 24px;
219
+ top: 6px;
220
+ right: 6px;
221
+ width: 22px;
222
+ height: 22px;
262
223
  border: none;
263
224
  border-radius: 4px;
264
- background: var(--surface);
225
+ background: transparent;
265
226
  color: var(--ink-faint);
266
227
  cursor: pointer;
267
228
  display: flex;
268
229
  align-items: center;
269
230
  justify-content: center;
270
231
  transition: all 0.15s;
232
+ opacity: 0.4;
271
233
  z-index: 1;
272
- opacity: 0.6;
273
234
  }
274
- .ann-pane-close:hover {
235
+ .ann-card-close:hover {
275
236
  opacity: 1;
276
237
  background: var(--ink);
277
238
  color: var(--paper);
278
239
  }
279
240
 
280
- .ann-pane-scroll {
241
+ .ann-card-scroll {
281
242
  padding: 12px 14px;
282
243
  overflow-y: auto;
283
244
  overscroll-behavior: contain;
284
245
  flex: 1;
285
246
  }
286
247
 
287
- /* ─── Desktop Right Pane (vertical mode) ─── */
288
- .ann-right-pane {
289
- position: fixed;
290
- top: 72px;
291
- right: 20px;
292
- height: calc(100vh - 100px);
293
- width: auto;
294
- max-width: 280px;
295
- writing-mode: vertical-rl;
296
- text-orientation: mixed;
297
- background: var(--surface-warm);
298
- border: 1px solid var(--border);
299
- border-radius: 10px;
300
- box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
301
- z-index: 1000;
302
- display: flex;
303
- flex-direction: column;
304
- }
305
- .ann-right-pane .ann-pane-close {
306
- position: static;
307
- margin: 8px 8px 0;
308
- opacity: 0.5;
309
- flex-shrink: 0;
310
- }
311
- .ann-right-pane .ann-pane-scroll {
312
- writing-mode: vertical-rl;
313
- text-orientation: mixed;
314
- overflow-x: auto;
315
- overflow-y: hidden;
316
- padding: 12px 14px;
317
- flex: 1;
248
+ /* Card transition */
249
+ .ann-pop-enter-active {
250
+ transition: opacity 0.15s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
318
251
  }
319
- .ann-right-pane .ann-entry {
320
- writing-mode: vertical-rl;
321
- text-orientation: mixed;
322
- border-bottom: none;
323
- border-left: 1px solid var(--border-light);
324
- padding: 0 0 0 12px;
325
- margin-left: 8px;
252
+ .ann-pop-leave-active {
253
+ transition: opacity 0.12s ease, transform 0.12s ease;
326
254
  }
327
- .ann-right-pane .ann-entry:first-child { padding-top: 0; }
328
- .ann-right-pane .ann-entry:last-child { border-left: none; }
329
- .ann-right-pane .ann-head {
330
- flex-direction: column;
331
- align-items: flex-start;
332
- margin-bottom: 0;
333
- margin-left: 4px;
334
- }
335
- .ann-right-pane .ann-text {
336
- writing-mode: vertical-rl;
337
- text-orientation: mixed;
338
- line-height: 2;
255
+ .ann-pop-enter-from {
256
+ opacity: 0;
257
+ transform: scale(0.95) translateY(6px);
339
258
  }
340
-
341
- /* ─── Tablet Popup ─── */
342
- .ann-popup {
343
- position: fixed;
344
- padding: 12px 14px;
345
- background: var(--surface-warm);
346
- border: 1px solid var(--border);
347
- border-radius: 8px;
348
- box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
349
- max-width: 300px;
350
- max-height: 50vh;
351
- overflow-y: auto;
352
- z-index: 1000;
259
+ .ann-pop-leave-to {
260
+ opacity: 0;
261
+ transform: scale(0.97);
353
262
  }
354
263
 
355
- /* ─── Mobile Bottom Sheet ─── */
356
- .ann-bottom-sheet {
264
+ /* ─── Mobile bottom sheet ─── */
265
+ .ann-sheet {
357
266
  position: fixed;
358
267
  left: 0;
359
268
  right: 0;
@@ -394,53 +303,7 @@ onBeforeUnmount(() => {
394
303
  flex: 1;
395
304
  }
396
305
 
397
- /* ─── Transitions ─── */
398
-
399
- .ann-slide-right-enter-active {
400
- transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
401
- }
402
- .ann-slide-right-leave-active {
403
- transition: opacity 0.15s ease, transform 0.15s ease;
404
- }
405
- .ann-slide-right-enter-from {
406
- opacity: 0;
407
- transform: translateX(-20px);
408
- }
409
- .ann-slide-right-leave-to {
410
- opacity: 0;
411
- transform: translateX(-12px);
412
- }
413
-
414
- .ann-slide-left-enter-active {
415
- transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
416
- }
417
- .ann-slide-left-leave-active {
418
- transition: opacity 0.15s ease, transform 0.15s ease;
419
- }
420
- .ann-slide-left-enter-from {
421
- opacity: 0;
422
- transform: translateX(20px);
423
- }
424
- .ann-slide-left-leave-to {
425
- opacity: 0;
426
- transform: translateX(12px);
427
- }
428
-
429
- .ann-fade-enter-active {
430
- transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
431
- }
432
- .ann-fade-leave-active {
433
- transition: opacity 0.15s ease, transform 0.15s ease;
434
- }
435
- .ann-fade-enter-from {
436
- opacity: 0;
437
- transform: scale(0.94) translateY(4px);
438
- }
439
- .ann-fade-leave-to {
440
- opacity: 0;
441
- transform: scale(0.96);
442
- }
443
-
306
+ /* Sheet transition */
444
307
  .ann-sheet-enter-active {
445
308
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
446
309
  }
@@ -452,11 +315,7 @@ onBeforeUnmount(() => {
452
315
  transform: translateY(100%);
453
316
  }
454
317
 
455
- @media (max-width: 1023px) {
456
- .ann-left-pane, .ann-right-pane { display: none; }
457
- }
458
-
459
- @media (min-width: 1024px) {
460
- .ann-bottom-sheet { display: none; }
318
+ @media (min-width: 768px) {
319
+ .ann-sheet { display: none; }
461
320
  }
462
321
  </style>
@@ -138,46 +138,51 @@ export function useAnnotationTooltip() {
138
138
  items.value = annotations
139
139
  const el = (event.target as HTMLElement).closest('.ann-target') as HTMLElement | null
140
140
  const rect = (el ?? event.target as HTMLElement).getBoundingClientRect()
141
+ const vw = window.innerWidth
142
+ const vh = window.innerHeight
143
+
144
+ if (vw < 768) {
145
+ // Mobile: bottom sheet
146
+ style.value = {
147
+ left: '0',
148
+ right: '0',
149
+ bottom: '0',
150
+ }
151
+ } else if (layout.value === 'vertical') {
152
+ // Vertical mode: card to the left of the annotation
153
+ const cardW = 340
154
+ const gap = 12
155
+ let left = Math.max(8, rect.left - cardW - gap)
156
+ let top = Math.max(8, Math.min(rect.top, vh - 300))
157
+
158
+ // If not enough room on left, go right
159
+ if (rect.left - cardW - gap < 8) {
160
+ left = Math.min(rect.right + gap, vw - cardW - 8)
161
+ }
141
162
 
142
- if (layout.value === 'vertical') {
143
- const isMobile = window.innerWidth < 768
144
- if (isMobile) {
145
- style.value = {
146
- left: '4vw',
147
- right: '4vw',
148
- bottom: '0',
149
- maxWidth: 'none',
150
- }
151
- } else if (window.innerWidth >= 1024) {
152
- style.value = {
153
- right: '20px',
154
- top: '72px',
155
- maxHeight: 'calc(100vh - 100px)',
156
- }
157
- } else {
158
- const right = window.innerWidth - rect.left + 8
159
- style.value = {
160
- right: Math.min(right, window.innerWidth - 40) + 'px',
161
- top: '50%',
162
- transform: 'translateY(-50%)',
163
- }
163
+ style.value = {
164
+ left: left + 'px',
165
+ top: top + 'px',
166
+ width: cardW + 'px',
167
+ maxHeight: Math.min(vh - 16, 500) + 'px',
164
168
  }
165
169
  } else {
166
- const isMobile = window.innerWidth < 768
167
- if (isMobile) {
168
- style.value = {
169
- left: '4vw',
170
- right: '4vw',
171
- bottom: '0',
172
- maxWidth: 'none',
173
- }
174
- } else {
175
- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 288))
176
- const top = Math.max(8, rect.bottom + 8)
177
- style.value = {
178
- left: left + 'px',
179
- top: Math.min(top, window.innerHeight - 200) + 'px',
180
- }
170
+ // Horizontal mode: card below/above the annotation
171
+ const cardW = 340
172
+ const gap = 8
173
+ let left = Math.max(8, Math.min(rect.left, vw - cardW - 8))
174
+ let top = rect.bottom + gap
175
+
176
+ // If overflows bottom, show above
177
+ if (top + 200 > vh) {
178
+ top = Math.max(8, rect.top - gap - 200)
179
+ }
180
+
181
+ style.value = {
182
+ left: left + 'px',
183
+ top: top + 'px',
184
+ width: cardW + 'px',
185
+ maxHeight: Math.min(vh - top - 16, 500) + 'px',
181
186
  }
182
187
  }
183
188
  visible.value = true
@@ -778,16 +778,13 @@ function tcy(n: number): string {
778
778
  .v-nav-btn {
779
779
  writing-mode: vertical-rl;
780
780
  text-orientation: mixed;
781
- display: flex;
782
- flex-direction: row;
783
- align-items: center;
784
- gap: 8px;
785
- padding: 16px 8px;
781
+ padding: 20px 12px;
786
782
  background: var(--surface);
787
783
  border: 1px solid var(--border-light);
788
784
  border-radius: 6px;
789
785
  cursor: pointer;
790
786
  transition: all 0.2s;
787
+ line-height: 1.6;
791
788
  }
792
789
  .v-nav-btn:hover {
793
790
  border-color: var(--gold);
@@ -795,10 +792,12 @@ function tcy(n: number): string {
795
792
  }
796
793
  .v-nav-dir {
797
794
  font-size: 16px; color: var(--vermillion);
795
+ margin-bottom: 0.5em;
798
796
  }
799
797
  .v-nav-label {
800
798
  font-size: 11px; color: var(--ink-faint);
801
799
  font-family: var(--sans); letter-spacing: 2px;
800
+ margin-bottom: 0.5em;
802
801
  }
803
802
  .v-nav-title {
804
803
  font-size: 18px; font-weight: 700;