@hanology/cham-browser 0.4.9 → 0.4.10

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.9",
3
+ "version": "0.4.10",
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",
@@ -42,28 +42,21 @@ function toggleLayer(id: string) {
42
42
  <template>
43
43
  <div v-if="hasAnnotations" class="ann-bar">
44
44
  <button
45
- class="ann-master"
46
- :class="{ active: annotationsVisible }"
45
+ class="ann-toggle"
46
+ :class="{ on: annotationsVisible }"
47
47
  @click="toggleAnnotations"
48
- >
49
- <span class="ann-master-icon">{{ annotationsVisible ? '✓' : '注' }}</span>
50
- <span class="ann-master-text">{{ annotationsVisible ? '注釋' : '顯示注釋' }}</span>
51
- <span v-if="hasLayers() && annotationsVisible" class="ann-count">{{ activeIds.length }}/{{ layers.length }}</span>
52
- </button>
53
- <template v-if="hasLayers() && annotationsVisible">
54
- <div class="ann-chips">
55
- <button
56
- v-for="layer in layers"
57
- :key="layer.id"
58
- :class="['ann-chip', { active: activeIds.includes(layer.id) }]"
59
- :title="layer.label"
60
- @click="toggleLayer(layer.id)"
61
- >
62
- <span class="ann-chip-check">{{ activeIds.includes(layer.id) ? '✓' : '' }}</span>
63
- {{ layer.shortLabel }}
64
- </button>
65
- </div>
66
- </template>
48
+ :title="annotationsVisible ? '隱藏注釋' : '顯示注釋'"
49
+ >注</button>
50
+ <div v-if="hasLayers() && annotationsVisible" class="ann-layers">
51
+ <button
52
+ v-for="layer in layers"
53
+ :key="layer.id"
54
+ class="ann-layer-btn"
55
+ :class="{ active: activeIds.includes(layer.id) }"
56
+ @click="toggleLayer(layer.id)"
57
+ :title="layer.label"
58
+ >{{ layer.shortLabel }}</button>
59
+ </div>
67
60
  </div>
68
61
  </template>
69
62
 
@@ -71,124 +64,86 @@ function toggleLayer(id: string) {
71
64
  .ann-bar {
72
65
  display: flex;
73
66
  flex-direction: column;
74
- gap: 10px;
67
+ gap: 6px;
68
+ align-items: stretch;
75
69
  }
76
70
 
77
- .ann-master {
78
- display: inline-flex;
79
- align-items: center;
80
- gap: 8px;
81
- padding: 8px 20px;
71
+ .ann-toggle {
72
+ width: 36px;
73
+ height: 36px;
82
74
  border: 1.5px solid var(--vermillion);
83
- border-radius: 20px;
75
+ border-radius: 6px;
84
76
  background: none;
85
77
  color: var(--vermillion);
86
- font-family: var(--sans);
87
- font-size: 13px;
88
- font-weight: 600;
78
+ font-family: var(--serif);
79
+ font-size: 16px;
80
+ font-weight: 700;
81
+ letter-spacing: 0;
89
82
  cursor: pointer;
90
83
  transition: all 0.2s;
91
- letter-spacing: 1px;
92
- min-height: 44px;
93
- align-self: flex-start;
94
- }
95
-
96
- .ann-master.active {
97
- background: var(--vermillion);
98
- color: #fff;
99
- }
100
-
101
- .ann-master:active {
102
- transform: scale(0.97);
103
- }
104
-
105
- .ann-master:hover {
106
- box-shadow: 0 2px 12px rgba(194, 58, 43, 0.15);
107
- }
108
-
109
- .ann-master-icon {
110
- width: 20px;
111
- height: 20px;
112
- border-radius: 50%;
113
- border: 1.5px solid currentColor;
114
84
  display: flex;
115
85
  align-items: center;
116
86
  justify-content: center;
117
- font-size: 11px;
118
- flex-shrink: 0;
119
87
  }
120
88
 
121
- .ann-master.active .ann-master-icon {
122
- border-color: rgba(255, 255, 255, 0.5);
123
- background: rgba(255, 255, 255, 0.15);
89
+ .ann-toggle:hover {
90
+ box-shadow: 0 2px 8px rgba(194, 58, 43, 0.15);
124
91
  }
125
92
 
126
- .ann-master-text {
127
- white-space: nowrap;
93
+ .ann-toggle:active {
94
+ transform: scale(0.94);
128
95
  }
129
96
 
130
- .ann-count {
131
- font-size: 11px;
132
- opacity: 0.7;
133
- margin-left: 2px;
97
+ .ann-toggle.on {
98
+ background: var(--vermillion);
99
+ color: #fff;
134
100
  }
135
101
 
136
- .ann-chips {
102
+ .ann-layers {
137
103
  display: flex;
138
- flex-wrap: wrap;
139
- gap: 8px;
140
- padding-left: 4px;
104
+ flex-direction: column;
105
+ gap: 4px;
141
106
  }
142
107
 
143
- .ann-chip {
144
- display: inline-flex;
145
- align-items: center;
146
- gap: 4px;
147
- padding: 6px 14px;
108
+ .ann-layer-btn {
109
+ padding: 5px 10px;
148
110
  border: 1px solid var(--border);
149
- border-radius: 16px;
150
- background: var(--surface);
151
- color: var(--ink-mid);
111
+ border-radius: 4px;
112
+ background: none;
113
+ color: var(--ink-faint);
152
114
  font-family: var(--sans);
153
- font-size: 12px;
115
+ font-size: 11px;
154
116
  letter-spacing: 1px;
155
117
  cursor: pointer;
156
- transition: all 0.2s;
157
- min-height: 36px;
118
+ transition: all 0.15s;
119
+ white-space: nowrap;
120
+ text-align: center;
158
121
  }
159
122
 
160
- .ann-chip:hover {
123
+ .ann-layer-btn:hover {
161
124
  border-color: var(--gold);
162
125
  color: var(--ink);
163
126
  }
164
127
 
165
- .ann-chip.active {
128
+ .ann-layer-btn.active {
166
129
  background: var(--ink);
167
130
  color: var(--paper);
168
131
  border-color: var(--ink);
169
132
  }
170
133
 
171
- .ann-chip:active {
172
- transform: scale(0.96);
173
- }
174
-
175
- .ann-chip-check {
176
- font-size: 11px;
177
- width: 12px;
178
- text-align: center;
134
+ .ann-layer-btn:active {
135
+ transform: scale(0.95);
179
136
  }
180
137
 
181
138
  @media (max-width: 768px) {
182
- .ann-master {
183
- padding: 10px 24px;
184
- font-size: 14px;
185
- }
186
- .ann-chips {
187
- gap: 6px;
139
+ .ann-toggle {
140
+ width: 40px;
141
+ height: 40px;
142
+ font-size: 17px;
188
143
  }
189
- .ann-chip {
190
- padding: 8px 16px;
191
- font-size: 13px;
144
+ .ann-layer-btn {
145
+ padding: 6px 12px;
146
+ font-size: 12px;
192
147
  }
193
148
  }
194
149
  </style>
@@ -77,18 +77,6 @@ function kindLabel(ann: Annotation): string {
77
77
  return map[ann.kind] || ann.kind
78
78
  }
79
79
 
80
- const hasOverlap = computed(() =>
81
- props.annotations.some(a => {
82
- const seg = annotationToPronSegment(a)
83
- return !seg && props.annotations.filter(b =>
84
- b.range.scope === a.range.scope &&
85
- b.range.verseIndex === a.range.verseIndex &&
86
- (b.range.start ?? 0) !== (a.range.start ?? 0) ||
87
- (b.range.end ?? 0) !== (a.range.end ?? 0)
88
- ).length > 0
89
- })
90
- )
91
-
92
80
  function onDocClick(e: MouseEvent) {
93
81
  if (!stickyVisible.value) return
94
82
  const el = (e.target as HTMLElement)
@@ -128,18 +116,16 @@ onBeforeUnmount(() => {
128
116
  @mouseleave="emit('tooltipLeave')"
129
117
  >
130
118
  <button class="ann-pane-close" @click="dismiss" aria-label="關閉">
131
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
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>
132
120
  </button>
133
- <div class="ann-pane-inner">
134
- <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
135
- <div class="ann-detail-head">
136
- <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
137
- <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
138
- </div>
139
- <div class="ann-detail-body">
140
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
141
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
121
+ <div class="ann-pane-scroll">
122
+ <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
123
+ <div class="ann-head">
124
+ <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
125
+ <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
142
126
  </div>
127
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
128
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
143
129
  </div>
144
130
  </div>
145
131
  </div>
@@ -155,13 +141,13 @@ onBeforeUnmount(() => {
155
141
  @mouseenter="emit('tooltipEnter')"
156
142
  @mouseleave="emit('tooltipLeave')"
157
143
  >
158
- <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
159
- <div class="ann-detail-head">
160
- <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
161
- <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
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>
162
148
  </div>
163
149
  <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
164
- <span v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</span>
150
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
165
151
  </div>
166
152
  </div>
167
153
  </Transition>
@@ -175,16 +161,14 @@ onBeforeUnmount(() => {
175
161
  <button class="ann-sheet-handle" @click="dismiss">
176
162
  <span class="ann-handle-bar" />
177
163
  </button>
178
- <div class="ann-sheet-inner">
179
- <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
180
- <div class="ann-detail-head">
181
- <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
182
- <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
183
- </div>
184
- <div class="ann-detail-body">
185
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
186
- <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
164
+ <div class="ann-sheet-scroll">
165
+ <div v-for="ann in annotations" :key="ann.id" class="ann-entry" :class="ann.kind">
166
+ <div class="ann-head">
167
+ <span class="ann-kind" :class="ann.kind">{{ kindLabel(ann) }}</span>
168
+ <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
187
169
  </div>
170
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
171
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text">{{ ann.text }}</div>
188
172
  </div>
189
173
  </div>
190
174
  </div>
@@ -193,66 +177,64 @@ onBeforeUnmount(() => {
193
177
  </template>
194
178
 
195
179
  <style scoped>
196
- /* ─── Shared Entry Styles ─── */
197
- .ann-detail {
198
- margin-bottom: 16px;
199
- letter-spacing: 1px;
200
- font-size: 15px;
180
+ /* ─── Compact annotation entry ─── */
181
+ .ann-entry {
182
+ padding: 10px 0;
183
+ border-bottom: 1px solid var(--border-light);
184
+ font-size: 14px;
201
185
  color: var(--ink-mid);
186
+ letter-spacing: 0.5px;
187
+ line-height: 1.8;
202
188
  }
203
- .ann-detail:last-child { margin-bottom: 0; }
189
+ .ann-entry:last-child { border-bottom: none; padding-bottom: 0; }
190
+ .ann-entry:first-child { padding-top: 0; }
204
191
 
205
- .ann-detail-head {
206
- display: flex;
192
+ .ann-head {
193
+ display: inline-flex;
207
194
  align-items: center;
208
- gap: 8px;
209
- margin-bottom: 8px;
195
+ gap: 6px;
196
+ margin-bottom: 2px;
197
+ vertical-align: baseline;
210
198
  }
211
199
 
212
- .ann-kind-tag {
213
- display: inline-flex;
214
- align-items: center;
215
- padding: 3px 10px;
216
- border-radius: 4px;
217
- font-size: 12px;
200
+ .ann-kind {
201
+ display: inline-block;
202
+ padding: 1px 7px;
203
+ border-radius: 3px;
204
+ font-size: 10px;
218
205
  font-family: var(--sans);
219
206
  font-weight: 700;
220
207
  letter-spacing: 1px;
221
- }
222
- .ann-kind-tag.pronunciation { background: var(--jade); color: #fff; }
223
- .ann-kind-tag.semantic { background: var(--vermillion); color: #fff; }
224
- .ann-kind-tag.etymology { background: #6b5b95; color: #fff; }
225
- .ann-kind-tag.note,
226
- .ann-kind-tag.definition { background: var(--ink); color: var(--paper); }
227
- .ann-kind-tag.commentary { background: #c0392b; color: #fff; }
228
- .ann-kind-tag.translation { background: #2c6e49; color: #fff; }
229
- .ann-kind-tag.person { background: var(--ann-person); color: #fff; }
230
- .ann-kind-tag.place { background: var(--ann-place); color: #fff; }
231
- .ann-kind-tag.event { background: var(--ann-event); color: #fff; }
232
- .ann-kind-tag.date { background: var(--ann-date); color: #fff; }
233
- .ann-kind-tag.allusion { background: var(--ann-allusion); color: #fff; }
234
-
235
- .ann-layer-tag {
236
- font-size: 11px;
208
+ line-height: 1.5;
209
+ vertical-align: middle;
210
+ }
211
+ .ann-kind.pronunciation { background: var(--jade); color: #fff; }
212
+ .ann-kind.semantic { background: var(--vermillion); color: #fff; }
213
+ .ann-kind.etymology { background: #6b5b95; color: #fff; }
214
+ .ann-kind.note,
215
+ .ann-kind.definition { background: var(--ink); color: var(--paper); }
216
+ .ann-kind.commentary { background: #c0392b; color: #fff; }
217
+ .ann-kind.translation { background: #2c6e49; color: #fff; }
218
+ .ann-kind.person { background: var(--ann-person); color: #fff; }
219
+ .ann-kind.place { background: var(--ann-place); color: #fff; }
220
+ .ann-kind.event { background: var(--ann-event); color: #fff; }
221
+ .ann-kind.date { background: var(--ann-date); color: #fff; }
222
+ .ann-kind.allusion { background: var(--ann-allusion); color: #fff; }
223
+
224
+ .ann-layer {
225
+ font-size: 10px;
237
226
  font-family: var(--sans);
238
227
  color: var(--ink-faint);
239
- padding: 2px 6px;
228
+ padding: 1px 5px;
240
229
  border: 1px solid var(--border-light);
241
- border-radius: 3px;
242
- letter-spacing: 1px;
243
- }
244
-
245
- .ann-detail-body {
246
- padding-left: 4px;
247
- }
248
-
249
- .ann-pron-block {
250
- margin-bottom: 6px;
230
+ border-radius: 2px;
231
+ letter-spacing: 0.5px;
232
+ line-height: 1.4;
251
233
  }
252
234
 
253
- .ann-text-block {
254
- line-height: 1.9;
235
+ .ann-text {
255
236
  white-space: pre-line;
237
+ line-height: 1.8;
256
238
  }
257
239
 
258
240
  /* ─── Desktop Left Pane (horizontal mode) ─── */
@@ -260,15 +242,46 @@ onBeforeUnmount(() => {
260
242
  position: fixed;
261
243
  top: 72px;
262
244
  left: 20px;
263
- width: 300px;
245
+ width: 280px;
264
246
  max-height: calc(100vh - 100px);
265
247
  background: var(--surface-warm);
266
248
  border: 1px solid var(--border);
267
- border-radius: 12px;
249
+ border-radius: 10px;
268
250
  box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
269
251
  z-index: 1000;
252
+ display: flex;
253
+ flex-direction: column;
254
+ }
255
+
256
+ .ann-pane-close {
257
+ position: absolute;
258
+ top: 8px;
259
+ right: 8px;
260
+ width: 24px;
261
+ height: 24px;
262
+ border: none;
263
+ border-radius: 4px;
264
+ background: var(--surface);
265
+ color: var(--ink-faint);
266
+ cursor: pointer;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ transition: all 0.15s;
271
+ z-index: 1;
272
+ opacity: 0.6;
273
+ }
274
+ .ann-pane-close:hover {
275
+ opacity: 1;
276
+ background: var(--ink);
277
+ color: var(--paper);
278
+ }
279
+
280
+ .ann-pane-scroll {
281
+ padding: 12px 14px;
270
282
  overflow-y: auto;
271
283
  overscroll-behavior: contain;
284
+ flex: 1;
272
285
  }
273
286
 
274
287
  /* ─── Desktop Right Pane (vertical mode) ─── */
@@ -278,83 +291,63 @@ onBeforeUnmount(() => {
278
291
  right: 20px;
279
292
  height: calc(100vh - 100px);
280
293
  width: auto;
281
- max-width: 300px;
294
+ max-width: 280px;
282
295
  writing-mode: vertical-rl;
283
296
  text-orientation: mixed;
284
297
  background: var(--surface-warm);
285
298
  border: 1px solid var(--border);
286
- border-radius: 12px;
299
+ border-radius: 10px;
287
300
  box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
288
301
  z-index: 1000;
289
- overflow-x: auto;
290
- overscroll-behavior: contain;
291
- }
292
- .ann-right-pane .ann-pane-inner {
293
- padding: 14px 16px;
302
+ display: flex;
303
+ flex-direction: column;
294
304
  }
295
305
  .ann-right-pane .ann-pane-close {
296
- top: 10px;
297
- right: auto;
298
- left: 10px;
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;
299
318
  }
300
- .ann-right-pane .ann-detail {
319
+ .ann-right-pane .ann-entry {
301
320
  writing-mode: vertical-rl;
302
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;
303
326
  }
304
- .ann-right-pane .ann-detail-head {
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 {
305
330
  flex-direction: column;
306
331
  align-items: flex-start;
307
332
  margin-bottom: 0;
308
- margin-left: 8px;
333
+ margin-left: 4px;
309
334
  }
310
- .ann-right-pane .ann-detail-body {
311
- padding-left: 0;
312
- padding-top: 4px;
313
- }
314
- .ann-right-pane .ann-text-block {
335
+ .ann-right-pane .ann-text {
315
336
  writing-mode: vertical-rl;
316
337
  text-orientation: mixed;
317
338
  line-height: 2;
318
339
  }
319
340
 
320
- .ann-pane-close {
321
- position: absolute;
322
- top: 10px;
323
- right: 10px;
324
- width: 28px;
325
- height: 28px;
326
- border: 1px solid var(--border);
327
- border-radius: 6px;
328
- background: var(--surface);
329
- color: var(--ink-light);
330
- cursor: pointer;
331
- display: flex;
332
- align-items: center;
333
- justify-content: center;
334
- transition: all 0.15s;
335
- z-index: 1;
336
- }
337
- .ann-pane-close:hover {
338
- background: var(--ink);
339
- color: var(--paper);
340
- border-color: var(--ink);
341
- }
342
-
343
- .ann-pane-inner {
344
- padding: 16px;
345
- padding-top: 14px;
346
- }
347
-
348
341
  /* ─── Tablet Popup ─── */
349
342
  .ann-popup {
350
343
  position: fixed;
351
- padding: 14px 18px;
344
+ padding: 12px 14px;
352
345
  background: var(--surface-warm);
353
346
  border: 1px solid var(--border);
354
- border-radius: 10px;
347
+ border-radius: 8px;
355
348
  box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
356
- max-width: 320px;
357
- max-height: 60vh;
349
+ max-width: 300px;
350
+ max-height: 50vh;
358
351
  overflow-y: auto;
359
352
  z-index: 1000;
360
353
  }
@@ -365,41 +358,44 @@ onBeforeUnmount(() => {
365
358
  left: 0;
366
359
  right: 0;
367
360
  bottom: 0;
368
- max-height: 55vh;
361
+ max-height: 50vh;
369
362
  background: var(--surface-warm);
370
363
  border-top: 1px solid var(--border);
371
- border-radius: 16px 16px 0 0;
364
+ border-radius: 14px 14px 0 0;
372
365
  box-shadow: 0 -4px 32px rgba(var(--shadow-rgb), 0.15);
373
366
  z-index: 1000;
374
- overflow-y: auto;
375
- overscroll-behavior: contain;
367
+ display: flex;
368
+ flex-direction: column;
376
369
  }
377
370
 
378
371
  .ann-sheet-handle {
379
372
  display: flex;
380
373
  justify-content: center;
381
- padding: 12px 0 6px;
374
+ padding: 10px 0 4px;
382
375
  width: 100%;
383
376
  border: none;
384
377
  background: none;
385
378
  cursor: pointer;
379
+ flex-shrink: 0;
386
380
  }
387
381
 
388
382
  .ann-handle-bar {
389
383
  display: block;
390
- width: 40px;
384
+ width: 36px;
391
385
  height: 4px;
392
386
  border-radius: 2px;
393
387
  background: var(--border);
394
388
  }
395
389
 
396
- .ann-sheet-inner {
397
- padding: 0 20px 28px;
390
+ .ann-sheet-scroll {
391
+ padding: 4px 16px 24px;
392
+ overflow-y: auto;
393
+ overscroll-behavior: contain;
394
+ flex: 1;
398
395
  }
399
396
 
400
397
  /* ─── Transitions ─── */
401
398
 
402
- /* Left pane slide from left (horizontal mode) */
403
399
  .ann-slide-right-enter-active {
404
400
  transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
405
401
  }
@@ -408,14 +404,13 @@ onBeforeUnmount(() => {
408
404
  }
409
405
  .ann-slide-right-enter-from {
410
406
  opacity: 0;
411
- transform: translateX(-24px);
407
+ transform: translateX(-20px);
412
408
  }
413
409
  .ann-slide-right-leave-to {
414
410
  opacity: 0;
415
- transform: translateX(-16px);
411
+ transform: translateX(-12px);
416
412
  }
417
413
 
418
- /* Right pane slide from right (vertical mode) */
419
414
  .ann-slide-left-enter-active {
420
415
  transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
421
416
  }
@@ -424,14 +419,13 @@ onBeforeUnmount(() => {
424
419
  }
425
420
  .ann-slide-left-enter-from {
426
421
  opacity: 0;
427
- transform: translateX(24px);
422
+ transform: translateX(20px);
428
423
  }
429
424
  .ann-slide-left-leave-to {
430
425
  opacity: 0;
431
- transform: translateX(16px);
426
+ transform: translateX(12px);
432
427
  }
433
428
 
434
- /* Popup fade */
435
429
  .ann-fade-enter-active {
436
430
  transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
437
431
  }
@@ -440,14 +434,13 @@ onBeforeUnmount(() => {
440
434
  }
441
435
  .ann-fade-enter-from {
442
436
  opacity: 0;
443
- transform: scale(0.92) translateY(4px);
437
+ transform: scale(0.94) translateY(4px);
444
438
  }
445
439
  .ann-fade-leave-to {
446
440
  opacity: 0;
447
441
  transform: scale(0.96);
448
442
  }
449
443
 
450
- /* Bottom sheet slide up */
451
444
  .ann-sheet-enter-active {
452
445
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
453
446
  }
@@ -4,7 +4,7 @@ import { useReadingMode, THEMES, THEME_LABELS, FONT_SIZES } from '../composables
4
4
  import type { LayoutMode, FontSize } from '../composables/useReadingMode'
5
5
  import { useI18n, LOCALE_LABELS, type Locale } from '../composables/useI18n'
6
6
 
7
- const { theme, layout, mainFontSize, bodyFontSize, setTheme, setLayout, setMainFontSize, setBodyFontSize } = useReadingMode()
7
+ const { theme, layout, mainFontSize, bodyFontSize, annotationsVisible, setTheme, setLayout, setMainFontSize, setBodyFontSize, setAnnotationsVisible } = useReadingMode()
8
8
  const { t, setLocale, locale, availableLocales, localeLabels } = useI18n()
9
9
  const open = ref(false)
10
10
 
@@ -34,6 +34,21 @@ function close() { open.value = false }
34
34
  >{{ t('settings.vertical') }}</button>
35
35
  </div>
36
36
  </div>
37
+ <div class="rt-group">
38
+ <div class="rt-label">{{ t('settings.annotations') }}</div>
39
+ <div class="rt-options">
40
+ <button
41
+ class="rt-opt"
42
+ :class="{ active: annotationsVisible }"
43
+ @click="setAnnotationsVisible(true)"
44
+ >{{ t('settings.show') }}</button>
45
+ <button
46
+ class="rt-opt"
47
+ :class="{ active: !annotationsVisible }"
48
+ @click="setAnnotationsVisible(false)"
49
+ >{{ t('settings.hide') }}</button>
50
+ </div>
51
+ </div>
37
52
  <div class="rt-group">
38
53
  <div class="rt-label">{{ t('settings.theme') }}</div>
39
54
  <div class="rt-options">
@@ -24,6 +24,9 @@ const messages: Record<Locale, Record<string, string>> = {
24
24
  'settings.bodyFontSize': '內文字號',
25
25
  'settings.reading': '閱讀設定',
26
26
  'settings.close': '關閉設定',
27
+ 'settings.annotations': '注釋',
28
+ 'settings.show': '顯示',
29
+ 'settings.hide': '隱藏',
27
30
  'piece.stanzas': '段',
28
31
  'piece.notes': '注',
29
32
  'piece.noNotes': '無注',
@@ -79,6 +82,9 @@ const messages: Record<Locale, Record<string, string>> = {
79
82
  'settings.bodyFontSize': '内文字号',
80
83
  'settings.reading': '阅读设定',
81
84
  'settings.close': '关闭设定',
85
+ 'settings.annotations': '注释',
86
+ 'settings.show': '显示',
87
+ 'settings.hide': '隐藏',
82
88
  'piece.stanzas': '段',
83
89
  'piece.notes': '注',
84
90
  'piece.noNotes': '无注',
@@ -134,6 +140,9 @@ const messages: Record<Locale, Record<string, string>> = {
134
140
  'settings.bodyFontSize': 'Body Size',
135
141
  'settings.reading': 'Reading Settings',
136
142
  'settings.close': 'Close Settings',
143
+ 'settings.annotations': 'Annotations',
144
+ 'settings.show': 'Show',
145
+ 'settings.hide': 'Hide',
137
146
  'piece.stanzas': 'stanzas',
138
147
  'piece.notes': 'notes',
139
148
  'piece.noNotes': 'No notes',
@@ -19,6 +19,7 @@ const theme = ref<Theme>('light')
19
19
  const layout = ref<LayoutMode>('vertical')
20
20
  const mainFontSize = ref<FontSize>(24)
21
21
  const bodyFontSize = ref<FontSize>(16)
22
+ const annotationsVisible = ref(true)
22
23
 
23
24
  if (!import.meta.env.SSR) {
24
25
  // Theme and font sizes only affect CSS, safe to apply before hydration
@@ -31,6 +32,9 @@ if (!import.meta.env.SSR) {
31
32
  const savedBody = parseInt(localStorage.getItem('bodyFontSize') || '', 10)
32
33
  if (FONT_SIZES.includes(savedBody as any)) bodyFontSize.value = savedBody as FontSize
33
34
 
35
+ const savedAnnVis = localStorage.getItem('annotationsVisible')
36
+ if (savedAnnVis === 'false') annotationsVisible.value = false
37
+
34
38
  // Layout controls v-if/v-else DOM structure — must defer to after hydration
35
39
  // to avoid SSR/client mismatch (SSR always renders vertical)
36
40
  nextTick(() => {
@@ -57,6 +61,10 @@ if (!import.meta.env.SSR) {
57
61
  document.documentElement.style.setProperty('--body-font-size', s + 'px')
58
62
  localStorage.setItem('bodyFontSize', String(s))
59
63
  }, { immediate: true })
64
+
65
+ watch(annotationsVisible, v => {
66
+ localStorage.setItem('annotationsVisible', String(v))
67
+ })
60
68
  }
61
69
 
62
70
  export function useReadingMode() {
@@ -71,5 +79,7 @@ export function useReadingMode() {
71
79
  }
72
80
  function setMainFontSize(s: FontSize) { mainFontSize.value = s }
73
81
  function setBodyFontSize(s: FontSize) { bodyFontSize.value = s }
74
- return { theme, layout, mainFontSize, bodyFontSize, setTheme, cycleTheme, setLayout, toggleLayout, setMainFontSize, setBodyFontSize }
82
+ function setAnnotationsVisible(v: boolean) { annotationsVisible.value = v }
83
+ function toggleAnnotationsVisible() { annotationsVisible.value = !annotationsVisible.value }
84
+ return { theme, layout, mainFontSize, bodyFontSize, annotationsVisible, setTheme, cycleTheme, setLayout, toggleLayout, setMainFontSize, setBodyFontSize, setAnnotationsVisible, toggleAnnotationsVisible }
75
85
  }
@@ -24,7 +24,7 @@ const router = useRouter()
24
24
  const { getPiece, pieces, meta, load, getAdjacentNums } = useBook()
25
25
  await load(props.bookId)
26
26
 
27
- const { layout } = useReadingMode()
27
+ const { layout, annotationsVisible: prefAnnotationsVisible } = useReadingMode()
28
28
  const vPageRef = ref<HTMLElement | null>(null)
29
29
  const vScroll = useHorizontalScroll(vPageRef)
30
30
  const { t } = useI18n()
@@ -95,7 +95,7 @@ const totalAnnotationCount = computed(() => {
95
95
  const annotationLayers = computed<AnnotationLayer[]>(() => piece.value?.annotationLayers || [])
96
96
  const hasLayers = computed(() => annotationLayers.value.length > 1)
97
97
  const activeLayerIds = ref<string[]>([])
98
- const annotationsVisible = ref(true)
98
+ const annotationsVisible = prefAnnotationsVisible
99
99
 
100
100
  function initLayers() {
101
101
  if (hasLayers.value && activeLayerIds.value.length === 0) {