@hanology/cham-browser 0.4.21 → 0.4.23

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.21",
3
+ "version": "0.4.23",
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",
@@ -21,6 +21,71 @@ const emit = defineEmits<{
21
21
 
22
22
  const bodyRef = ref<HTMLElement | null>(null)
23
23
 
24
+ // ─── Resize ───
25
+ const paneWidth = ref(320)
26
+ const MIN_W = 180
27
+ const MAX_W = 500
28
+ const RESIZE_KEY = 'ann-pane-width'
29
+
30
+ function initWidth() {
31
+ try {
32
+ const saved = localStorage.getItem(RESIZE_KEY)
33
+ if (saved) {
34
+ const w = parseInt(saved, 10)
35
+ if (w >= MIN_W && w <= MAX_W) paneWidth.value = w
36
+ return
37
+ }
38
+ } catch {}
39
+ paneWidth.value = props.vertical ? 240 : 320
40
+ }
41
+
42
+ let resizing = false
43
+ let resizeStartX = 0
44
+ let resizeStartW = 0
45
+ let hasMoved = false
46
+ let moveFn: ((e: MouseEvent | TouchEvent) => void) | null = null
47
+ let endFn: (() => void) | null = null
48
+
49
+ function onHandleStart(e: MouseEvent | TouchEvent) {
50
+ resizing = true
51
+ hasMoved = false
52
+ resizeStartX = 'touches' in e ? e.touches[0].clientX : (e as MouseEvent).clientX
53
+ resizeStartW = paneWidth.value
54
+
55
+ moveFn = (ev: MouseEvent | TouchEvent) => {
56
+ if (!resizing) return
57
+ const x = 'touches' in ev ? ev.touches[0].clientX : (ev as MouseEvent).clientX
58
+ const dx = x - resizeStartX
59
+ if (Math.abs(dx) > 2) hasMoved = true
60
+ paneWidth.value = Math.max(MIN_W, Math.min(MAX_W, resizeStartW + dx))
61
+ }
62
+
63
+ endFn = () => {
64
+ resizing = false
65
+ if (!hasMoved) {
66
+ emit('close')
67
+ } else {
68
+ try { localStorage.setItem(RESIZE_KEY, String(paneWidth.value)) } catch {}
69
+ }
70
+ if (moveFn) {
71
+ document.removeEventListener('mousemove', moveFn)
72
+ document.removeEventListener('touchmove', moveFn)
73
+ }
74
+ if (endFn) {
75
+ document.removeEventListener('mouseup', endFn)
76
+ document.removeEventListener('touchend', endFn)
77
+ }
78
+ moveFn = null
79
+ endFn = null
80
+ }
81
+
82
+ document.addEventListener('mousemove', moveFn)
83
+ document.addEventListener('mouseup', endFn)
84
+ document.addEventListener('touchmove', moveFn, { passive: false })
85
+ document.addEventListener('touchend', endFn)
86
+ e.preventDefault()
87
+ }
88
+
24
89
  function getSegment(ann: Annotation) {
25
90
  return annotationToPronSegment(ann)
26
91
  }
@@ -69,14 +134,33 @@ watch(() => props.activeId, async (id) => {
69
134
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
70
135
  })
71
136
 
72
- onMounted(() => document.addEventListener('keydown', onKeydown))
73
- onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
137
+ onMounted(() => {
138
+ initWidth()
139
+ document.addEventListener('keydown', onKeydown)
140
+ })
141
+
142
+ onBeforeUnmount(() => {
143
+ document.removeEventListener('keydown', onKeydown)
144
+ if (moveFn) {
145
+ document.removeEventListener('mousemove', moveFn)
146
+ document.removeEventListener('touchmove', moveFn)
147
+ }
148
+ if (endFn) {
149
+ document.removeEventListener('mouseup', endFn)
150
+ document.removeEventListener('touchend', endFn)
151
+ }
152
+ })
74
153
  </script>
75
154
 
76
155
  <template>
77
156
  <Teleport to="body">
78
157
  <Transition name="ann-pane">
79
- <div v-if="visible && annotations.length" class="ann-pane" :class="{ vertical }">
158
+ <div
159
+ v-if="visible && annotations.length"
160
+ class="ann-pane"
161
+ :class="{ vertical }"
162
+ :style="{ width: paneWidth + 'px' }"
163
+ >
80
164
  <div class="ann-pane-header">
81
165
  <span class="ann-pane-title">注釋</span>
82
166
  <span class="ann-pane-count">{{ annotations.length }}</span>
@@ -93,18 +177,29 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
93
177
  :class="{ active: activeId === ann.id, [ann.kind]: true }"
94
178
  @click="emit('select', ann)"
95
179
  >
96
- <div class="ann-pane-entry-head">
97
- <span class="ann-pane-idx">{{ toChineseNumber(idx + 1) }}</span>
98
- <span v-if="headword(ann)" class="ann-pane-word">{{ headword(ann) }}</span>
99
- <span class="ann-pane-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
100
- <span v-if="layerLabel(ann)" class="ann-pane-layer">{{ layerLabel(ann) }}</span>
180
+ <!-- Vertical: headword column on the right side -->
181
+ <div v-if="vertical && headword(ann)" class="ann-pane-v-word">
182
+ <span class="ann-pane-word-v">{{ headword(ann) }}</span>
183
+ <span class="ann-pane-idx-v">{{ toChineseNumber(idx + 1) }}</span>
101
184
  </div>
102
- <div class="ann-pane-entry-body">
103
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
104
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-pane-text">{{ ann.text }}</div>
185
+ <div class="ann-pane-entry-main">
186
+ <div class="ann-pane-entry-head">
187
+ <span v-if="!vertical" class="ann-pane-idx">{{ toChineseNumber(idx + 1) }}</span>
188
+ <span v-if="!vertical && headword(ann)" class="ann-pane-word">{{ headword(ann) }}</span>
189
+ <span class="ann-pane-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
190
+ <span v-if="layerLabel(ann)" class="ann-pane-layer">{{ layerLabel(ann) }}</span>
191
+ </div>
192
+ <div class="ann-pane-entry-body">
193
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
194
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-pane-text">{{ ann.text }}</div>
195
+ </div>
105
196
  </div>
106
197
  </div>
107
198
  </div>
199
+ <!-- Resize / close handle on right edge -->
200
+ <div class="ann-pane-handle" @mousedown="onHandleStart" @touchstart.prevent="onHandleStart">
201
+ <span class="ann-handle-grip" />
202
+ </div>
108
203
  </div>
109
204
  </Transition>
110
205
  </Teleport>
@@ -115,7 +210,6 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
115
210
  position: fixed;
116
211
  left: 0;
117
212
  top: 0;
118
- width: 320px;
119
213
  height: 100vh;
120
214
  background: var(--surface-warm);
121
215
  border-right: 1px solid var(--border);
@@ -276,7 +370,35 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
276
370
  white-space: pre-line;
277
371
  }
278
372
 
279
- /* Transition */
373
+ /* ─── Resize / close handle ─── */
374
+ .ann-pane-handle {
375
+ position: absolute;
376
+ top: 0;
377
+ right: -6px;
378
+ width: 14px;
379
+ height: 100%;
380
+ cursor: col-resize;
381
+ z-index: 2;
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ }
386
+
387
+ .ann-handle-grip {
388
+ display: block;
389
+ width: 3px;
390
+ height: 32px;
391
+ border-radius: 2px;
392
+ background: var(--border);
393
+ transition: all 0.2s;
394
+ }
395
+
396
+ .ann-pane-handle:hover .ann-handle-grip {
397
+ background: var(--vermillion);
398
+ height: 48px;
399
+ }
400
+
401
+ /* ─── Transition ─── */
280
402
  .ann-pane-enter-active {
281
403
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
282
404
  }
@@ -290,17 +412,19 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
290
412
 
291
413
  @media (max-width: 768px) {
292
414
  .ann-pane {
293
- width: 100%;
415
+ width: 100% !important;
294
416
  height: auto;
295
417
  max-height: 55vh;
296
418
  top: auto;
297
419
  bottom: 0;
298
- left: 0;
299
420
  border-right: none;
300
421
  border-top: 1px solid var(--border);
301
422
  border-radius: 14px 14px 0 0;
302
423
  box-shadow: 0 -4px 24px rgba(var(--shadow-rgb), 0.08);
303
424
  }
425
+ .ann-pane-handle {
426
+ display: none;
427
+ }
304
428
  .ann-pane-enter-from,
305
429
  .ann-pane-leave-to {
306
430
  transform: translateY(100%);
@@ -311,7 +435,6 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
311
435
  .ann-pane.vertical {
312
436
  writing-mode: vertical-rl;
313
437
  text-orientation: mixed;
314
- width: 220px;
315
438
  }
316
439
 
317
440
  .ann-pane.vertical .ann-pane-body {
@@ -322,6 +445,8 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
322
445
  }
323
446
 
324
447
  .ann-pane.vertical .ann-pane-entry {
448
+ display: flex;
449
+ flex-direction: column;
325
450
  padding: 10px 6px;
326
451
  border-bottom: none;
327
452
  border-right: 3px solid transparent;
@@ -339,6 +464,37 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
339
464
  border-right-color: var(--jade);
340
465
  }
341
466
 
467
+ /* Vertical headword on the right (first in vertical-rl reading order) */
468
+ .ann-pane-v-word {
469
+ writing-mode: vertical-rl;
470
+ text-orientation: upright;
471
+ display: flex;
472
+ flex-direction: column;
473
+ align-items: center;
474
+ gap: 6px;
475
+ padding: 6px 8px;
476
+ border-left: 1px solid var(--border-light);
477
+ }
478
+
479
+ .ann-pane-word-v {
480
+ font-family: var(--serif);
481
+ font-size: 20px;
482
+ font-weight: 900;
483
+ letter-spacing: 6px;
484
+ color: var(--ink);
485
+ }
486
+
487
+ .ann-pane-idx-v {
488
+ font-family: var(--serif);
489
+ font-size: 12px;
490
+ font-weight: 700;
491
+ color: var(--vermillion);
492
+ }
493
+
494
+ .ann-pane-entry.active.pronunciation .ann-pane-idx-v {
495
+ color: var(--jade);
496
+ }
497
+
342
498
  .ann-pane.vertical .ann-pane-entry-head {
343
499
  flex-direction: row;
344
500
  gap: 4px;
@@ -356,4 +512,14 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
356
512
  .ann-pane.vertical .ann-pane-close {
357
513
  writing-mode: horizontal-tb;
358
514
  }
515
+
516
+ .ann-pane.vertical .ann-pane-handle {
517
+ writing-mode: horizontal-tb;
518
+ }
519
+
520
+ @media (max-width: 768px) {
521
+ .ann-pane.vertical .ann-pane-handle {
522
+ display: none;
523
+ }
524
+ }
359
525
  </style>
@@ -415,6 +415,8 @@ onBeforeUnmount(() => {
415
415
  writing-mode: vertical-rl;
416
416
  text-orientation: mixed;
417
417
  flex-direction: column;
418
+ overflow-y: hidden;
419
+ overflow-x: hidden;
418
420
  }
419
421
 
420
422
  .ann-card.vertical .ann-card-head {
@@ -436,6 +438,8 @@ onBeforeUnmount(() => {
436
438
  border-bottom: none;
437
439
  padding: 0 10px;
438
440
  border-right: 1px solid var(--border-light);
441
+ max-height: inherit;
442
+ overflow-y: hidden;
439
443
  }
440
444
 
441
445
  .ann-card.vertical .ann-entry:first-child {
@@ -200,7 +200,7 @@ export function useAnnotationTooltip() {
200
200
  visible.value = true
201
201
  }
202
202
 
203
- function hide() { visible.value = false; headword.value = '' }
203
+ function hide() { visible.value = false }
204
204
  function toggle(event: MouseEvent, annotations: Annotation[]) {
205
205
  if (visible.value) {
206
206
  const currentIds = items.value.map(a => a.id).sort().join(',')
@@ -386,11 +386,11 @@ function tcy(n: number): string {
386
386
 
387
387
  <div class="v-inline-nav">
388
388
  <button v-if="adjacent.next !== null" class="v-inav" @click="navigate(1)" :title="t('piece.next')">
389
-
389
+
390
390
  </button>
391
391
  <span v-else class="v-inav-spacer" />
392
392
  <button v-if="adjacent.prev !== null" class="v-inav" @click="navigate(-1)" :title="t('piece.previous')">
393
-
393
+
394
394
  </button>
395
395
  </div>
396
396
 
@@ -475,13 +475,13 @@ function tcy(n: number): string {
475
475
 
476
476
  <nav class="v-nav">
477
477
  <button v-if="adjacent.prev !== null" class="v-nav-btn" @click="navigate(-1)">
478
- <span class="v-nav-dir">▲</span>
478
+ <span class="v-nav-dir">▶</span>
479
479
  <span class="v-nav-label">{{ t('piece.previous') }}</span>
480
480
  <span class="v-nav-title">{{ getPiece(adjacent.prev)?.title }}</span>
481
481
  </button>
482
482
  <div v-else class="v-nav-spacer" />
483
483
  <button v-if="adjacent.next !== null" class="v-nav-btn" @click="navigate(1)">
484
- <span class="v-nav-dir">▼</span>
484
+ <span class="v-nav-dir">◀</span>
485
485
  <span class="v-nav-label">{{ t('piece.next') }}</span>
486
486
  <span class="v-nav-title">{{ getPiece(adjacent.next)?.title }}</span>
487
487
  </button>