@hanology/cham-browser 0.4.17 → 0.4.19

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.17",
3
+ "version": "0.4.19",
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",
@@ -11,6 +11,7 @@ const props = defineProps<{
11
11
  headwords: Record<string, string>
12
12
  layerLabels?: Record<string, string>
13
13
  activeId: string
14
+ vertical?: boolean
14
15
  }>()
15
16
 
16
17
  const emit = defineEmits<{
@@ -35,7 +36,7 @@ function kindLabel(ann: Annotation): string {
35
36
  etymology: '詞源',
36
37
  note: '備注',
37
38
  definition: '釋義',
38
- commentary: '評註',
39
+ commentary: '',
39
40
  translation: '譯文',
40
41
  person: '人名',
41
42
  place: '地名',
@@ -75,7 +76,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
75
76
  <template>
76
77
  <Teleport to="body">
77
78
  <Transition name="ann-pane">
78
- <div v-if="visible && annotations.length" class="ann-pane">
79
+ <div v-if="visible && annotations.length" class="ann-pane" :class="{ vertical }">
79
80
  <div class="ann-pane-header">
80
81
  <span class="ann-pane-title">注釋</span>
81
82
  <span class="ann-pane-count">{{ annotations.length }}</span>
@@ -305,4 +306,54 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
305
306
  transform: translateY(100%);
306
307
  }
307
308
  }
309
+
310
+ /* ─── Vertical mode ─── */
311
+ .ann-pane.vertical {
312
+ writing-mode: vertical-rl;
313
+ text-orientation: mixed;
314
+ width: 220px;
315
+ }
316
+
317
+ .ann-pane.vertical .ann-pane-body {
318
+ display: flex;
319
+ flex-direction: row;
320
+ overflow-y: auto;
321
+ overflow-x: hidden;
322
+ }
323
+
324
+ .ann-pane.vertical .ann-pane-entry {
325
+ padding: 10px 6px;
326
+ border-bottom: none;
327
+ border-right: 3px solid transparent;
328
+ }
329
+
330
+ .ann-pane.vertical .ann-pane-entry + .ann-pane-entry {
331
+ border-top: 1px solid var(--border-light);
332
+ }
333
+
334
+ .ann-pane.vertical .ann-pane-entry.active {
335
+ border-right-color: var(--vermillion);
336
+ }
337
+
338
+ .ann-pane.vertical .ann-pane-entry.active.pronunciation {
339
+ border-right-color: var(--jade);
340
+ }
341
+
342
+ .ann-pane.vertical .ann-pane-entry-head {
343
+ flex-direction: row;
344
+ gap: 4px;
345
+ }
346
+
347
+ .ann-pane.vertical .ann-pane-text {
348
+ line-height: 2;
349
+ letter-spacing: 1px;
350
+ }
351
+
352
+ .ann-pane.vertical .ann-pane-header {
353
+ flex-direction: row;
354
+ }
355
+
356
+ .ann-pane.vertical .ann-pane-close {
357
+ writing-mode: horizontal-tb;
358
+ }
308
359
  </style>
@@ -54,7 +54,7 @@ function kindLabel(ann: Annotation): string {
54
54
  etymology: '詞源',
55
55
  note: '備注',
56
56
  definition: '釋義',
57
- commentary: '評註',
57
+ commentary: '',
58
58
  translation: '譯文',
59
59
  person: '人名',
60
60
  place: '地名',
@@ -118,6 +118,7 @@ onBeforeUnmount(() => {
118
118
  <div
119
119
  v-if="!isMobile && stickyVisible && annotations.length"
120
120
  class="ann-card"
121
+ :class="{ vertical }"
121
122
  :style="style"
122
123
  @mouseenter="emit('tooltipEnter')"
123
124
  @mouseleave="emit('tooltipLeave')"
@@ -136,7 +137,7 @@ onBeforeUnmount(() => {
136
137
  <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
137
138
  </div>
138
139
  <div class="ann-entry-body">
139
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
140
+ <div v-if="getSegment(ann)" class="ann-pron-h"><PronunciationGroup :segment="getSegment(ann)!" /></div>
140
141
  <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
141
142
  </div>
142
143
  </div>
@@ -149,6 +150,7 @@ onBeforeUnmount(() => {
149
150
  <div
150
151
  v-if="isMobile && stickyVisible && annotations.length"
151
152
  class="ann-sheet"
153
+ :class="{ vertical }"
152
154
  >
153
155
  <button class="ann-sheet-handle" @click="dismiss">
154
156
  <span class="ann-handle-bar" />
@@ -164,7 +166,7 @@ onBeforeUnmount(() => {
164
166
  <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
165
167
  </div>
166
168
  <div class="ann-entry-body">
167
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
169
+ <div v-if="getSegment(ann)" class="ann-pron-h"><PronunciationGroup :segment="getSegment(ann)!" /></div>
168
170
  <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
169
171
  </div>
170
172
  </div>
@@ -407,4 +409,86 @@ onBeforeUnmount(() => {
407
409
  @media (min-width: 768px) {
408
410
  .ann-sheet { display: none; }
409
411
  }
412
+
413
+ /* ─── Vertical mode ─── */
414
+ .ann-card.vertical {
415
+ writing-mode: vertical-rl;
416
+ text-orientation: mixed;
417
+ flex-direction: row;
418
+ }
419
+
420
+ .ann-card.vertical .ann-card-head {
421
+ padding: 10px 6px;
422
+ }
423
+
424
+ .ann-card.vertical .ann-card-scroll {
425
+ display: flex;
426
+ flex-direction: row;
427
+ padding: 8px 6px;
428
+ overflow-y: auto;
429
+ }
430
+
431
+ .ann-card.vertical .ann-entry {
432
+ border-bottom: none;
433
+ padding: 8px 0;
434
+ }
435
+
436
+ .ann-card.vertical .ann-entry + .ann-entry {
437
+ border-top: 1px solid var(--border-light);
438
+ }
439
+
440
+ .ann-card.vertical .ann-card-close {
441
+ writing-mode: horizontal-tb;
442
+ top: 4px;
443
+ right: 4px;
444
+ }
445
+
446
+ .ann-card.vertical .ann-pron-h {
447
+ writing-mode: horizontal-tb;
448
+ }
449
+
450
+ .ann-card.vertical .ann-entry-body {
451
+ padding-left: 0;
452
+ }
453
+
454
+ .ann-card.vertical .ann-text {
455
+ white-space: pre-line;
456
+ line-height: 2;
457
+ }
458
+
459
+ .ann-sheet.vertical {
460
+ writing-mode: vertical-rl;
461
+ text-orientation: mixed;
462
+ flex-direction: row;
463
+ }
464
+
465
+ .ann-sheet.vertical .ann-sheet-handle {
466
+ writing-mode: horizontal-tb;
467
+ }
468
+
469
+ .ann-sheet.vertical .ann-sheet-scroll {
470
+ display: flex;
471
+ flex-direction: row;
472
+ overflow-y: auto;
473
+ overflow-x: hidden;
474
+ padding: 4px 16px 24px;
475
+ }
476
+
477
+ .ann-sheet.vertical .ann-entry {
478
+ border-bottom: none;
479
+ padding: 8px 0;
480
+ }
481
+
482
+ .ann-sheet.vertical .ann-entry + .ann-entry {
483
+ border-top: 1px solid var(--border-light);
484
+ }
485
+
486
+ .ann-sheet.vertical .ann-pron-h {
487
+ writing-mode: horizontal-tb;
488
+ }
489
+
490
+ .ann-sheet.vertical .ann-text {
491
+ white-space: pre-line;
492
+ line-height: 2;
493
+ }
410
494
  </style>
@@ -67,17 +67,20 @@ onUnmounted(detach)
67
67
  .rp {
68
68
  position: fixed;
69
69
  z-index: 1001;
70
- background: linear-gradient(90deg, var(--vermillion), var(--vermillion-light));
71
70
  pointer-events: none;
72
71
  will-change: width, height;
72
+ transition: width 0.1s ease, height 0.1s ease;
73
73
  }
74
74
  .rp:not(.rp-v) {
75
75
  top: 0; left: 0;
76
76
  height: 2px;
77
+ background: var(--vermillion);
78
+ box-shadow: 0 0 8px rgba(194, 58, 43, 0.3);
77
79
  }
78
80
  .rp-v {
79
81
  top: 0; left: 0;
80
82
  width: 2px;
81
- background: linear-gradient(180deg, var(--vermillion), var(--vermillion-light));
83
+ background: var(--vermillion);
84
+ box-shadow: 0 0 8px rgba(194, 58, 43, 0.3);
82
85
  }
83
86
  </style>
@@ -172,39 +172,51 @@ const paragraphsHtml = computed(() => {
172
172
  text-orientation: mixed;
173
173
  height: 100vh;
174
174
  flex-shrink: 0;
175
- padding: 32px 20px;
175
+ padding: 32px 24px;
176
176
  border-right: 1px solid var(--border);
177
177
  overflow-x: auto;
178
178
  overflow-y: hidden;
179
179
  opacity: 1;
180
180
  transform: none;
181
181
  transition: none;
182
+ scrollbar-width: thin;
183
+ scrollbar-color: var(--gold) transparent;
182
184
  }
185
+ .sb-vertical::-webkit-scrollbar { height: 3px; }
186
+ .sb-vertical::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
183
187
  .sb-vertical .sb-header {
184
188
  flex-direction: column;
185
189
  align-items: flex-start;
186
190
  margin-bottom: 0;
187
- margin-left: 16px;
191
+ margin-left: 20px;
188
192
  padding-bottom: 0;
189
193
  border-bottom: none;
190
194
  padding-left: 16px;
191
- border-left: 1px solid var(--border);
195
+ border-left: 2px solid var(--vermillion);
192
196
  }
193
197
  .sb-vertical .sb-num {
194
198
  width: auto; height: auto;
195
199
  border-radius: 0;
196
200
  background: none;
197
201
  color: var(--vermillion);
198
- font-size: 18px;
202
+ font-size: 16px;
203
+ }
204
+ .sb-vertical .sb-header h3 {
205
+ font-size: 20px;
206
+ letter-spacing: 6px;
199
207
  }
200
208
  .sb-vertical .sb-text {
201
209
  margin-left: 16px;
202
210
  text-align: start;
211
+ font-size: var(--body-font-size, 16px);
212
+ line-height: 2.2;
213
+ letter-spacing: 1px;
203
214
  }
204
215
  .sb-vertical .sb-text :deep(p) {
205
216
  margin-bottom: 0;
206
217
  margin-left: 12px;
207
218
  text-indent: 0;
219
+ line-height: 2.4;
208
220
  }
209
221
  .sb-vertical .sb-ann-entry {
210
222
  margin-bottom: 0;
@@ -347,10 +347,13 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
347
347
  }
348
348
 
349
349
  @media (max-width: 768px) {
350
- .sidenav { width: 44px; padding: 8px 0; gap: 6px; }
351
- .sn-brand { width: 32px; height: 38px; }
352
- .sn-seal { font-size: 15px; }
353
- .sn-btn { width: 30px; height: 30px; }
354
- .sn-context { font-size: 10px; max-height: 80px; }
350
+ .sidenav { width: 44px; padding: 8px 0; gap: 5px; }
351
+ .sn-brand { width: 30px; height: 36px; margin-bottom: 2px; }
352
+ .sn-seal { font-size: 14px; }
353
+ .sn-btn { width: 28px; height: 28px; }
354
+ .sn-btn svg { width: 15px; height: 15px; }
355
+ .sn-context { font-size: 10px; max-height: 70px; }
356
+ .sn-settings { width: 180px; right: 52px; padding: 12px; }
357
+ .sn-layout-tag { width: 20px; height: 20px; font-size: 10px; }
355
358
  }
356
359
  </style>
@@ -58,7 +58,7 @@ function onTap(event: MouseEvent) {
58
58
  .v-scroll {
59
59
  writing-mode: vertical-rl;
60
60
  text-orientation: mixed;
61
- height: calc(100vh - 120px);
61
+ height: calc(100vh - 100px);
62
62
  overflow-x: auto; overflow-y: hidden;
63
63
  padding: 32px 24px;
64
64
  background: var(--surface);
@@ -67,15 +67,15 @@ function onTap(event: MouseEvent) {
67
67
  box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
68
68
  position: relative;
69
69
  scrollbar-width: thin;
70
- scrollbar-color: var(--gold) var(--paper);
70
+ scrollbar-color: var(--gold) transparent;
71
71
  }
72
- .v-scroll::-webkit-scrollbar { height: 4px; }
72
+ .v-scroll::-webkit-scrollbar { height: 3px; }
73
73
  .v-scroll::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
74
74
 
75
75
  .v-scroll-title {
76
76
  font-size: 36px; font-weight: 900; color: var(--ink);
77
- letter-spacing: 12px; margin-left: 24px;
78
- padding-left: 24px; border-left: 3px solid var(--vermillion);
77
+ letter-spacing: 10px; margin-left: 24px;
78
+ padding-left: 20px; border-left: 3px solid var(--vermillion);
79
79
  line-height: 1.6;
80
80
  }
81
81
  .v-scroll-author {
@@ -84,9 +84,9 @@ function onTap(event: MouseEvent) {
84
84
  }
85
85
  .v-scroll-clickable { cursor: pointer; transition: color 0.15s; }
86
86
  .v-scroll-clickable:hover { color: var(--vermillion); }
87
- .v-scroll-body { margin-left: 28px; }
87
+ .v-scroll-body { margin-left: 24px; }
88
88
  .v-scroll-line {
89
- font-size: var(--main-font-size, 24px); line-height: 2.4; letter-spacing: 8px;
89
+ font-size: var(--main-font-size, 24px); line-height: 2.4; letter-spacing: 6px;
90
90
  color: var(--ink); display: block;
91
91
  }
92
92
 
@@ -160,11 +160,12 @@ export function useAnnotationTooltip() {
160
160
  bottom: '0',
161
161
  }
162
162
  } else if (layout.value === 'vertical') {
163
- // Vertical mode: card to the left of the annotation
164
- const cardW = 340
163
+ // Vertical mode: tall narrow card to the left of the annotation
164
+ const cardW = 180
165
+ const cardH = Math.min(vh - 16, 480)
165
166
  const gap = 12
166
167
  let left = Math.max(8, rect.left - cardW - gap)
167
- let top = Math.max(8, Math.min(rect.top, vh - 300))
168
+ let top = Math.max(8, Math.min(rect.top, vh - cardH))
168
169
 
169
170
  // If not enough room on left, go right
170
171
  if (rect.left - cardW - gap < 8) {
@@ -175,7 +176,7 @@ export function useAnnotationTooltip() {
175
176
  left: left + 'px',
176
177
  top: top + 'px',
177
178
  width: cardW + 'px',
178
- maxHeight: Math.min(vh - 16, 500) + 'px',
179
+ maxHeight: cardH + 'px',
179
180
  }
180
181
  } else {
181
182
  // Horizontal mode: card below/above the annotation
@@ -228,8 +228,9 @@ button { font-family: inherit; }
228
228
  scrollbar-width: thin;
229
229
  scrollbar-color: var(--gold) transparent;
230
230
  scroll-snap-type: x proximity;
231
+ scroll-padding: 0 48px;
231
232
  }
232
- .v-page::-webkit-scrollbar { height: 4px; }
233
+ .v-page::-webkit-scrollbar { height: 3px; }
233
234
  .v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
234
235
 
235
236
  /* ===== VIEW LOADING STATE ===== */
@@ -284,3 +285,19 @@ button { font-family: inherit; }
284
285
  object-fit: contain;
285
286
  animation: pulse 1.2s ease-in-out infinite;
286
287
  }
288
+
289
+ /* ===== LAYOUT TRANSITION ===== */
290
+ .v-root, .h-root {
291
+ transition: opacity 0.2s ease;
292
+ }
293
+
294
+ /* ===== DARK MODE VERTICAL WARMTH ===== */
295
+ [data-theme="dark"] .v-scroll,
296
+ [data-theme="oled"] .v-scroll {
297
+ box-shadow: 0 0 40px rgba(194, 58, 43, 0.03), 0 4px 16px rgba(0, 0, 0, 0.2);
298
+ }
299
+
300
+ [data-theme="dark"] .sb-vertical,
301
+ [data-theme="oled"] .sb-vertical {
302
+ border-right-color: var(--border-light);
303
+ }
@@ -495,6 +495,7 @@ function tcy(n: number): string {
495
495
  :headwords="annotationHeadwords"
496
496
  :layer-labels="layerLabels"
497
497
  :active-id="paneActiveId"
498
+ :vertical="isVertical"
498
499
  @close="paneVisible = false"
499
500
  @select="onPaneSelect"
500
501
  />
@@ -711,6 +712,7 @@ function tcy(n: number): string {
711
712
  :headword="interaction.headword"
712
713
  :layer-labels="layerLabels"
713
714
  :style="interaction.style"
715
+ :vertical="isVertical"
714
716
  @close="interaction.dismiss"
715
717
  @tooltip-enter="interaction.onTooltipEnter"
716
718
  @tooltip-leave="interaction.onTooltipLeave"
@@ -740,13 +742,13 @@ function tcy(n: number): string {
740
742
  align-items: flex-start;
741
743
  justify-content: center;
742
744
  gap: 16px;
743
- padding: 40px 24px;
745
+ padding: 40px 20px;
744
746
  border-right: 1px solid var(--border);
745
747
  scroll-snap-align: start;
746
748
  }
747
749
  .v-poem-title {
748
750
  font-size: 40px; font-weight: 900;
749
- letter-spacing: 12px; color: var(--ink);
751
+ letter-spacing: 10px; color: var(--ink);
750
752
  padding-left: 20px;
751
753
  border-left: 4px solid var(--vermillion);
752
754
  line-height: 1.6;
@@ -781,7 +783,7 @@ function tcy(n: number): string {
781
783
  flex-shrink: 0;
782
784
  display: flex;
783
785
  align-items: center;
784
- padding: 24px;
786
+ padding: 20px 16px;
785
787
  }
786
788
 
787
789
  .v-multipart {
@@ -789,15 +791,15 @@ function tcy(n: number): string {
789
791
  flex-direction: row-reverse;
790
792
  align-items: flex-start;
791
793
  gap: 0;
792
- max-height: calc(100vh - 120px);
794
+ max-height: calc(100vh - 100px);
793
795
  overflow-x: auto;
794
796
  overflow-y: hidden;
795
- padding: 24px 16px;
797
+ padding: 20px 16px;
796
798
  scrollbar-width: thin;
797
- scrollbar-color: var(--gold) var(--paper);
799
+ scrollbar-color: var(--gold) transparent;
798
800
  }
799
801
 
800
- .v-multipart::-webkit-scrollbar { height: 4px; }
802
+ .v-multipart::-webkit-scrollbar { height: 3px; }
801
803
  .v-multipart::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
802
804
 
803
805
  .v-section {
@@ -829,15 +831,17 @@ function tcy(n: number): string {
829
831
  flex-direction: row;
830
832
  align-items: center;
831
833
  justify-content: center;
832
- padding: 24px 12px;
834
+ padding: 24px 16px;
833
835
  gap: 32px;
834
836
  scroll-snap-align: start;
837
+ background: var(--surface);
838
+ border-right: 1px solid var(--border-light);
835
839
  }
836
840
  .v-nav-spacer { flex: 1; }
837
841
  .v-nav-btn {
838
842
  writing-mode: vertical-rl;
839
843
  text-orientation: mixed;
840
- padding: 20px 12px;
844
+ padding: 20px 14px;
841
845
  background: var(--surface);
842
846
  border: 1px solid var(--border-light);
843
847
  border-radius: 6px;
@@ -1343,10 +1347,16 @@ function tcy(n: number): string {
1343
1347
  @media (max-width: 768px) {
1344
1348
  /* ─── 直排模式 ─── */
1345
1349
  .v-page { margin-right: var(--nav-width, 44px); }
1346
- .v-title-col { padding: 24px 16px; }
1347
- .v-poem-title { font-size: 32px; letter-spacing: 8px; }
1348
- .v-poem-author { font-size: 20px; }
1349
- .v-poem-col { padding: 16px; }
1350
+ .v-title-col { padding: 20px 12px; }
1351
+ .v-poem-title { font-size: 28px; letter-spacing: 6px; padding-left: 12px; }
1352
+ .v-poem-author { font-size: 18px; letter-spacing: 4px; }
1353
+ .v-poem-col { padding: 12px 8px; }
1354
+ .v-inline-nav { padding: 0 4px; }
1355
+ .v-inav { width: 26px; height: 36px; font-size: 12px; }
1356
+ .v-inav-spacer { width: 26px; height: 36px; }
1357
+ .v-nav { padding: 16px 8px; gap: 20px; }
1358
+ .v-nav-btn { padding: 14px 10px; }
1359
+ .v-nav-title { font-size: 16px; letter-spacing: 2px; }
1350
1360
 
1351
1361
  /* ─── 橫排模式導航 ─── */
1352
1362
  .h-nav { padding: 0 16px; }