@hanology/cham-browser 0.4.8 → 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 +1 -1
- package/template/src/components/AnnotationControlBar.vue +54 -99
- package/template/src/components/AnnotationTooltip.vue +143 -150
- package/template/src/components/ReadingToolbar.vue +16 -1
- package/template/src/composables/useI18n.ts +9 -0
- package/template/src/composables/useReadingMode.ts +11 -1
- package/template/src/views/LibraryHome.vue +1 -50
- package/template/src/views/PieceView.vue +2 -2
package/package.json
CHANGED
|
@@ -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-
|
|
46
|
-
:class="{
|
|
45
|
+
class="ann-toggle"
|
|
46
|
+
:class="{ on: annotationsVisible }"
|
|
47
47
|
@click="toggleAnnotations"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
67
|
+
gap: 6px;
|
|
68
|
+
align-items: stretch;
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
.ann-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
75
|
+
border-radius: 6px;
|
|
84
76
|
background: none;
|
|
85
77
|
color: var(--vermillion);
|
|
86
|
-
font-family: var(--
|
|
87
|
-
font-size:
|
|
88
|
-
font-weight:
|
|
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-
|
|
122
|
-
|
|
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-
|
|
127
|
-
|
|
93
|
+
.ann-toggle:active {
|
|
94
|
+
transform: scale(0.94);
|
|
128
95
|
}
|
|
129
96
|
|
|
130
|
-
.ann-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
margin-left: 2px;
|
|
97
|
+
.ann-toggle.on {
|
|
98
|
+
background: var(--vermillion);
|
|
99
|
+
color: #fff;
|
|
134
100
|
}
|
|
135
101
|
|
|
136
|
-
.ann-
|
|
102
|
+
.ann-layers {
|
|
137
103
|
display: flex;
|
|
138
|
-
flex-
|
|
139
|
-
gap:
|
|
140
|
-
padding-left: 4px;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 4px;
|
|
141
106
|
}
|
|
142
107
|
|
|
143
|
-
.ann-
|
|
144
|
-
|
|
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:
|
|
150
|
-
background:
|
|
151
|
-
color: var(--ink-
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
background: none;
|
|
113
|
+
color: var(--ink-faint);
|
|
152
114
|
font-family: var(--sans);
|
|
153
|
-
font-size:
|
|
115
|
+
font-size: 11px;
|
|
154
116
|
letter-spacing: 1px;
|
|
155
117
|
cursor: pointer;
|
|
156
|
-
transition: all 0.
|
|
157
|
-
|
|
118
|
+
transition: all 0.15s;
|
|
119
|
+
white-space: nowrap;
|
|
120
|
+
text-align: center;
|
|
158
121
|
}
|
|
159
122
|
|
|
160
|
-
.ann-
|
|
123
|
+
.ann-layer-btn:hover {
|
|
161
124
|
border-color: var(--gold);
|
|
162
125
|
color: var(--ink);
|
|
163
126
|
}
|
|
164
127
|
|
|
165
|
-
.ann-
|
|
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-
|
|
172
|
-
transform: scale(0.
|
|
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-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.ann-chips {
|
|
187
|
-
gap: 6px;
|
|
139
|
+
.ann-toggle {
|
|
140
|
+
width: 40px;
|
|
141
|
+
height: 40px;
|
|
142
|
+
font-size: 17px;
|
|
188
143
|
}
|
|
189
|
-
.ann-
|
|
190
|
-
padding:
|
|
191
|
-
font-size:
|
|
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="
|
|
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-
|
|
134
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-
|
|
135
|
-
<div class="ann-
|
|
136
|
-
<span class="ann-kind
|
|
137
|
-
<span v-if="layerLabel(ann)" class="ann-layer
|
|
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-
|
|
159
|
-
<div class="ann-
|
|
160
|
-
<span class="ann-kind
|
|
161
|
-
<span v-if="layerLabel(ann)" class="ann-layer
|
|
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
|
-
<
|
|
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-
|
|
179
|
-
<div v-for="ann in annotations" :key="ann.id" class="ann-
|
|
180
|
-
<div class="ann-
|
|
181
|
-
<span class="ann-kind
|
|
182
|
-
<span v-if="layerLabel(ann)" class="ann-layer
|
|
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
|
-
/* ───
|
|
197
|
-
.ann-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
font-size:
|
|
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-
|
|
189
|
+
.ann-entry:last-child { border-bottom: none; padding-bottom: 0; }
|
|
190
|
+
.ann-entry:first-child { padding-top: 0; }
|
|
204
191
|
|
|
205
|
-
.ann-
|
|
206
|
-
display: flex;
|
|
192
|
+
.ann-head {
|
|
193
|
+
display: inline-flex;
|
|
207
194
|
align-items: center;
|
|
208
|
-
gap:
|
|
209
|
-
margin-bottom:
|
|
195
|
+
gap: 6px;
|
|
196
|
+
margin-bottom: 2px;
|
|
197
|
+
vertical-align: baseline;
|
|
210
198
|
}
|
|
211
199
|
|
|
212
|
-
.ann-kind
|
|
213
|
-
display: inline-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
.ann-kind
|
|
225
|
-
.ann-kind
|
|
226
|
-
.ann-kind
|
|
227
|
-
.ann-kind
|
|
228
|
-
.ann-kind
|
|
229
|
-
.ann-kind
|
|
230
|
-
.ann-kind
|
|
231
|
-
.ann-kind
|
|
232
|
-
.ann-kind
|
|
233
|
-
.ann-kind
|
|
234
|
-
|
|
235
|
-
.ann-
|
|
236
|
-
|
|
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:
|
|
228
|
+
padding: 1px 5px;
|
|
240
229
|
border: 1px solid var(--border-light);
|
|
241
|
-
border-radius:
|
|
242
|
-
letter-spacing:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
299
|
+
border-radius: 10px;
|
|
287
300
|
box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
|
|
288
301
|
z-index: 1000;
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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-
|
|
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-
|
|
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:
|
|
333
|
+
margin-left: 4px;
|
|
309
334
|
}
|
|
310
|
-
.ann-right-pane .ann-
|
|
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
|
|
344
|
+
padding: 12px 14px;
|
|
352
345
|
background: var(--surface-warm);
|
|
353
346
|
border: 1px solid var(--border);
|
|
354
|
-
border-radius:
|
|
347
|
+
border-radius: 8px;
|
|
355
348
|
box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
|
|
356
|
-
max-width:
|
|
357
|
-
max-height:
|
|
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:
|
|
361
|
+
max-height: 50vh;
|
|
369
362
|
background: var(--surface-warm);
|
|
370
363
|
border-top: 1px solid var(--border);
|
|
371
|
-
border-radius:
|
|
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
|
-
|
|
375
|
-
|
|
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:
|
|
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:
|
|
384
|
+
width: 36px;
|
|
391
385
|
height: 4px;
|
|
392
386
|
border-radius: 2px;
|
|
393
387
|
background: var(--border);
|
|
394
388
|
}
|
|
395
389
|
|
|
396
|
-
.ann-sheet-
|
|
397
|
-
padding:
|
|
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(-
|
|
407
|
+
transform: translateX(-20px);
|
|
412
408
|
}
|
|
413
409
|
.ann-slide-right-leave-to {
|
|
414
410
|
opacity: 0;
|
|
415
|
-
transform: translateX(-
|
|
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(
|
|
422
|
+
transform: translateX(20px);
|
|
428
423
|
}
|
|
429
424
|
.ann-slide-left-leave-to {
|
|
430
425
|
opacity: 0;
|
|
431
|
-
transform: translateX(
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -85,9 +85,6 @@ function openBook(bookId: string) {
|
|
|
85
85
|
<div v-if="isVertical" class="v-root">
|
|
86
86
|
<SideNav @home="router.push('/')" @back="router.push('/')" />
|
|
87
87
|
<div ref="vPageRef" class="v-page">
|
|
88
|
-
<div v-if="aboutHtml" class="v-about-col">
|
|
89
|
-
<button class="v-about-link" @click="aboutPane?.toggleAbout()">{{ t('nav.about') }}</button>
|
|
90
|
-
</div>
|
|
91
88
|
<section class="v-hero">
|
|
92
89
|
<h1 class="v-title">{{ spacedTitle }}</h1>
|
|
93
90
|
<p v-if="siteSubtitle" class="v-subtitle">{{ siteSubtitle }}</p>
|
|
@@ -117,7 +114,7 @@ function openBook(bookId: string) {
|
|
|
117
114
|
<header class="lib-hero">
|
|
118
115
|
<img v-if="logoUrl" :src="logoUrl" alt="" class="lib-logo" />
|
|
119
116
|
<div v-else class="lib-seal">{{ displayTitle.slice(0, 2) }}</div>
|
|
120
|
-
<h1>{{ displayTitle }}
|
|
117
|
+
<h1>{{ displayTitle }}</h1>
|
|
121
118
|
<p v-if="siteSubtitle" class="lib-subtitle">{{ siteSubtitle }}</p>
|
|
122
119
|
<div class="lib-stats-bar">
|
|
123
120
|
<span class="lib-stat">{{ books.length }} {{ t('stat.books') }}</span>
|
|
@@ -174,33 +171,6 @@ function openBook(bookId: string) {
|
|
|
174
171
|
justify-content: center;
|
|
175
172
|
padding: 40px 20px;
|
|
176
173
|
}
|
|
177
|
-
.v-about-col {
|
|
178
|
-
writing-mode: vertical-rl;
|
|
179
|
-
text-orientation: mixed;
|
|
180
|
-
flex-shrink: 0;
|
|
181
|
-
height: 100vh;
|
|
182
|
-
display: flex;
|
|
183
|
-
align-items: center;
|
|
184
|
-
justify-content: center;
|
|
185
|
-
padding: 0 12px;
|
|
186
|
-
border-right: 1px solid var(--border-light);
|
|
187
|
-
}
|
|
188
|
-
.v-about-link {
|
|
189
|
-
font-size: 14px;
|
|
190
|
-
color: var(--ink-faint);
|
|
191
|
-
letter-spacing: 6px;
|
|
192
|
-
font-family: var(--sans);
|
|
193
|
-
padding: 12px 8px;
|
|
194
|
-
border: 1px solid var(--border-light);
|
|
195
|
-
border-radius: 2px;
|
|
196
|
-
background: none;
|
|
197
|
-
cursor: pointer;
|
|
198
|
-
transition: all 0.2s;
|
|
199
|
-
}
|
|
200
|
-
.v-about-link:hover {
|
|
201
|
-
color: var(--ink);
|
|
202
|
-
border-color: var(--ink);
|
|
203
|
-
}
|
|
204
174
|
.v-title {
|
|
205
175
|
font-size: 48px; font-weight: 900;
|
|
206
176
|
letter-spacing: 16px; color: var(--ink);
|
|
@@ -345,25 +315,6 @@ function openBook(bookId: string) {
|
|
|
345
315
|
}
|
|
346
316
|
.lib-stat-sep { color: var(--border); }
|
|
347
317
|
|
|
348
|
-
.lib-about-link {
|
|
349
|
-
display: inline-block;
|
|
350
|
-
font-family: var(--sans);
|
|
351
|
-
font-size: 13px;
|
|
352
|
-
color: var(--ink-faint);
|
|
353
|
-
letter-spacing: 2px;
|
|
354
|
-
padding: 4px 12px;
|
|
355
|
-
border: 1px solid var(--border-light);
|
|
356
|
-
border-radius: 4px;
|
|
357
|
-
background: none;
|
|
358
|
-
cursor: pointer;
|
|
359
|
-
transition: all 0.2s;
|
|
360
|
-
vertical-align: middle;
|
|
361
|
-
}
|
|
362
|
-
.lib-about-link:hover {
|
|
363
|
-
color: var(--ink);
|
|
364
|
-
border-color: var(--ink);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
318
|
.lib-group { margin-bottom: 40px; }
|
|
368
319
|
.lib-group-title {
|
|
369
320
|
font-size: 15px;
|
|
@@ -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 =
|
|
98
|
+
const annotationsVisible = prefAnnotationsVisible
|
|
99
99
|
|
|
100
100
|
function initLayers() {
|
|
101
101
|
if (hasLayers.value && activeLayerIds.value.length === 0) {
|