@hanology/cham-browser 0.3.8 → 0.4.1

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.
@@ -1,10 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed, watch, onMounted, onBeforeUnmount } from 'vue'
3
- import { useReadingMode } from '../composables/useReadingMode'
2
+ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
4
3
  import { annotationToPronSegment } from '../utils/annotationParser'
5
4
  import PronunciationGroup from './PronunciationGroup.vue'
6
5
  import type { Annotation } from '../types'
7
6
 
7
+ type DisplayMode = 'pane' | 'popup' | 'sheet'
8
+
8
9
  const props = defineProps<{
9
10
  visible: boolean
10
11
  annotations: Annotation[]
@@ -17,8 +18,34 @@ const emit = defineEmits<{
17
18
  tooltipEnter: []
18
19
  tooltipLeave: []
19
20
  }>()
20
- const { layout } = useReadingMode()
21
- const isMobile = computed(() => typeof window !== 'undefined' && window.innerWidth < 768)
21
+
22
+ const ww = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
23
+
24
+ function onResize() { ww.value = window.innerWidth }
25
+
26
+ const mode = computed<DisplayMode>(() => {
27
+ const w = ww.value
28
+ if (w < 768) return 'sheet'
29
+ if (w >= 1024) return 'pane'
30
+ return 'popup'
31
+ })
32
+
33
+ // Pane/sheet modes: stay visible until explicitly dismissed
34
+ const stickyVisible = ref(false)
35
+
36
+ watch(() => props.visible, (v) => {
37
+ 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
+ })
44
+
45
+ function dismiss() {
46
+ stickyVisible.value = false
47
+ emit('close')
48
+ }
22
49
 
23
50
  function getSegment(ann: Annotation) {
24
51
  return annotationToPronSegment(ann)
@@ -32,26 +59,47 @@ function layerLabel(ann: Annotation): string {
32
59
  return ''
33
60
  }
34
61
 
62
+ function kindLabel(ann: Annotation): string {
63
+ const map: Record<string, string> = {
64
+ pronunciation: '讀音',
65
+ semantic: '釋義',
66
+ etymology: '詞源',
67
+ note: '備注',
68
+ definition: '釋義',
69
+ commentary: '評註',
70
+ translation: '譯文',
71
+ person: '人名',
72
+ place: '地名',
73
+ event: '事件',
74
+ date: '紀年',
75
+ allusion: '典故',
76
+ }
77
+ return map[ann.kind] || ann.kind
78
+ }
79
+
35
80
  function onDocClick(e: MouseEvent) {
36
- if (!props.visible) return
37
- const tooltip = (e.target as HTMLElement).closest('.ann-tooltip')
38
- if (tooltip) return
39
- emit('close')
81
+ if (!stickyVisible.value) return
82
+ const el = (e.target as HTMLElement)
83
+ if (el.closest('.ann-left-pane, .ann-bottom-sheet, .ann-popup')) return
84
+ if (el.closest('.ann-target')) return
85
+ dismiss()
40
86
  }
41
87
 
42
88
  function onDocTouchMove(e: TouchEvent) {
43
- if (!props.visible || !isMobile.value) return
44
- const tooltip = (e.target as HTMLElement).closest('.ann-tooltip')
45
- if (tooltip) return
46
- emit('close')
89
+ if (!stickyVisible.value || ww.value >= 768) return
90
+ const el = (e.target as HTMLElement)
91
+ if (el.closest('.ann-bottom-sheet')) return
92
+ dismiss()
47
93
  }
48
94
 
49
95
  onMounted(() => {
96
+ window.addEventListener('resize', onResize, { passive: true })
50
97
  document.addEventListener('click', onDocClick, true)
51
98
  document.addEventListener('touchmove', onDocTouchMove, { passive: true })
52
99
  })
53
100
 
54
101
  onBeforeUnmount(() => {
102
+ window.removeEventListener('resize', onResize)
55
103
  document.removeEventListener('click', onDocClick, true)
56
104
  document.removeEventListener('touchmove', onDocTouchMove)
57
105
  })
@@ -59,28 +107,72 @@ onBeforeUnmount(() => {
59
107
 
60
108
  <template>
61
109
  <Teleport to="body">
110
+ <!-- Desktop left pane (>= 1024px) -->
111
+ <Transition name="ann-slide-right">
112
+ <div
113
+ v-if="mode === 'pane' && stickyVisible && annotations.length"
114
+ class="ann-left-pane"
115
+ @mouseenter="emit('tooltipEnter')"
116
+ @mouseleave="emit('tooltipLeave')"
117
+ >
118
+ <button class="ann-pane-close" @click="dismiss" aria-label="關閉">
119
+ <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>
120
+ </button>
121
+ <div class="ann-pane-inner">
122
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
123
+ <div class="ann-detail-head">
124
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
125
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
126
+ </div>
127
+ <div class="ann-detail-body">
128
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
129
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </Transition>
135
+
136
+ <!-- Tablet popup (768–1023px) -->
62
137
  <Transition name="ann-fade">
63
138
  <div
64
- v-if="visible && annotations.length"
65
- class="ann-tooltip"
66
- :class="{ 'ann-vertical': layout === 'vertical', 'ann-mobile-bottom': isMobile }"
139
+ v-if="mode === 'popup' && visible && annotations.length"
140
+ class="ann-popup"
67
141
  :style="style"
68
142
  @mouseenter="emit('tooltipEnter')"
69
143
  @mouseleave="emit('tooltipLeave')"
70
144
  >
71
- <button v-if="isMobile" class="ann-handle" @click="emit('close')">
145
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
146
+ <div class="ann-detail-head">
147
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
148
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
149
+ </div>
150
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
151
+ <span v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</span>
152
+ </div>
153
+ </div>
154
+ </Transition>
155
+
156
+ <!-- Mobile bottom sheet (< 768px) -->
157
+ <Transition name="ann-sheet">
158
+ <div
159
+ v-if="mode === 'sheet' && stickyVisible && annotations.length"
160
+ class="ann-bottom-sheet"
161
+ >
162
+ <button class="ann-sheet-handle" @click="dismiss">
72
163
  <span class="ann-handle-bar" />
73
164
  </button>
74
- <div
75
- v-for="ann in annotations"
76
- :key="ann.id"
77
- class="ann-entry"
78
- :class="ann.kind"
79
- >
80
- <span class="ann-kind">{{ ann.kind === 'pronunciation' ? '音' : '義' }}</span>
81
- <span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
82
- <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
83
- <span v-else class="ann-body">{{ ann.text }}</span>
165
+ <div class="ann-sheet-inner">
166
+ <div v-for="ann in annotations" :key="ann.id" class="ann-detail" :class="ann.kind">
167
+ <div class="ann-detail-head">
168
+ <span class="ann-kind-tag" :class="ann.kind">{{ kindLabel(ann) }}</span>
169
+ <span v-if="layerLabel(ann)" class="ann-layer-tag">{{ layerLabel(ann) }}</span>
170
+ </div>
171
+ <div class="ann-detail-body">
172
+ <PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" class="ann-pron-block" />
173
+ <div v-if="ann.text && ann.kind !== 'pronunciation'" class="ann-text-block">{{ ann.text }}</div>
174
+ </div>
175
+ </div>
84
176
  </div>
85
177
  </div>
86
178
  </Transition>
@@ -88,121 +180,188 @@ onBeforeUnmount(() => {
88
180
  </template>
89
181
 
90
182
  <style scoped>
91
- .ann-tooltip {
183
+ /* ─── Shared Entry Styles ─── */
184
+ .ann-detail {
185
+ margin-bottom: 16px;
186
+ letter-spacing: 1px;
187
+ font-size: 15px;
188
+ color: var(--ink-mid);
189
+ }
190
+ .ann-detail:last-child { margin-bottom: 0; }
191
+
192
+ .ann-detail-head {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ margin-bottom: 8px;
197
+ }
198
+
199
+ .ann-kind-tag {
200
+ display: inline-flex;
201
+ align-items: center;
202
+ padding: 3px 10px;
203
+ border-radius: 4px;
204
+ font-size: 12px;
205
+ font-family: var(--sans);
206
+ font-weight: 700;
207
+ letter-spacing: 1px;
208
+ }
209
+ .ann-kind-tag.pronunciation { background: var(--jade); color: #fff; }
210
+ .ann-kind-tag.semantic { background: var(--vermillion); color: #fff; }
211
+ .ann-kind-tag.etymology { background: #6b5b95; color: #fff; }
212
+ .ann-kind-tag.note,
213
+ .ann-kind-tag.definition { background: var(--ink); color: var(--paper); }
214
+ .ann-kind-tag.commentary { background: #c0392b; color: #fff; }
215
+ .ann-kind-tag.translation { background: #2c6e49; color: #fff; }
216
+ .ann-kind-tag.person { background: var(--ann-person); color: #fff; }
217
+ .ann-kind-tag.place { background: var(--ann-place); color: #fff; }
218
+ .ann-kind-tag.event { background: var(--ann-event); color: #fff; }
219
+ .ann-kind-tag.date { background: var(--ann-date); color: #fff; }
220
+ .ann-kind-tag.allusion { background: var(--ann-allusion); color: #fff; }
221
+
222
+ .ann-layer-tag {
223
+ font-size: 11px;
224
+ font-family: var(--sans);
225
+ color: var(--ink-faint);
226
+ padding: 2px 6px;
227
+ border: 1px solid var(--border-light);
228
+ border-radius: 3px;
229
+ letter-spacing: 1px;
230
+ }
231
+
232
+ .ann-detail-body {
233
+ padding-left: 4px;
234
+ }
235
+
236
+ .ann-pron-block {
237
+ margin-bottom: 6px;
238
+ }
239
+
240
+ .ann-text-block {
241
+ line-height: 1.9;
242
+ white-space: pre-line;
243
+ }
244
+
245
+ /* ─── Desktop Left Pane ─── */
246
+ .ann-left-pane {
92
247
  position: fixed;
93
- padding: 12px 16px;
248
+ top: 72px;
249
+ left: 20px;
250
+ width: 300px;
251
+ max-height: calc(100vh - 100px);
94
252
  background: var(--surface-warm);
95
253
  border: 1px solid var(--border);
96
- border-radius: 8px;
97
- box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.14);
254
+ border-radius: 12px;
255
+ box-shadow: 0 8px 40px rgba(var(--shadow-rgb), 0.12);
256
+ z-index: 1000;
257
+ overflow-y: auto;
258
+ overscroll-behavior: contain;
259
+ }
260
+
261
+ .ann-pane-close {
262
+ position: absolute;
263
+ top: 10px;
264
+ right: 10px;
265
+ width: 28px;
266
+ height: 28px;
267
+ border: 1px solid var(--border);
268
+ border-radius: 6px;
269
+ background: var(--surface);
270
+ color: var(--ink-light);
271
+ cursor: pointer;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ transition: all 0.15s;
276
+ z-index: 1;
277
+ }
278
+ .ann-pane-close:hover {
279
+ background: var(--ink);
280
+ color: var(--paper);
281
+ border-color: var(--ink);
282
+ }
283
+
284
+ .ann-pane-inner {
285
+ padding: 16px;
286
+ padding-top: 14px;
287
+ }
288
+
289
+ /* ─── Tablet Popup ─── */
290
+ .ann-popup {
291
+ position: fixed;
292
+ padding: 14px 18px;
293
+ background: var(--surface-warm);
294
+ border: 1px solid var(--border);
295
+ border-radius: 10px;
296
+ box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.16);
98
297
  max-width: 320px;
99
298
  max-height: 60vh;
100
299
  overflow-y: auto;
101
300
  z-index: 1000;
102
301
  }
103
302
 
104
- /* ─── Mobile bottom sheet ─── */
105
- .ann-mobile-bottom {
303
+ /* ─── Mobile Bottom Sheet ─── */
304
+ .ann-bottom-sheet {
106
305
  position: fixed;
107
306
  left: 0;
108
307
  right: 0;
109
308
  bottom: 0;
110
- max-width: none;
111
- max-height: 50vh;
309
+ max-height: 55vh;
310
+ background: var(--surface-warm);
311
+ border-top: 1px solid var(--border);
112
312
  border-radius: 16px 16px 0 0;
113
- padding: 8px 20px 20px;
313
+ box-shadow: 0 -4px 32px rgba(var(--shadow-rgb), 0.15);
314
+ z-index: 1000;
315
+ overflow-y: auto;
114
316
  overscroll-behavior: contain;
115
317
  }
116
318
 
117
- .ann-handle {
319
+ .ann-sheet-handle {
118
320
  display: flex;
119
321
  justify-content: center;
120
- padding: 8px 0 4px;
322
+ padding: 12px 0 6px;
121
323
  width: 100%;
122
324
  border: none;
123
325
  background: none;
124
326
  cursor: pointer;
125
327
  }
328
+
126
329
  .ann-handle-bar {
127
330
  display: block;
128
- width: 36px;
331
+ width: 40px;
129
332
  height: 4px;
130
333
  border-radius: 2px;
131
334
  background: var(--border);
132
335
  }
133
336
 
134
- .ann-entry {
135
- margin-bottom: 10px;
136
- letter-spacing: 1px;
137
- font-size: 15px;
138
- color: var(--ink-mid);
139
- }
140
- .ann-entry:last-child { margin-bottom: 0; }
141
-
142
- .ann-kind {
143
- display: inline-block;
144
- font-size: 10px;
145
- font-family: var(--sans);
146
- padding: 1px 3px;
147
- border-radius: 2px;
148
- font-weight: 600;
149
- letter-spacing: 1px;
150
- margin-right: 3px;
151
- line-height: 1;
152
- vertical-align: middle;
153
- }
154
- .ann-header {
155
- display: inline-flex;
156
- align-items: center;
157
- gap: 6px;
158
- }
159
- .ann-layer {
160
- font-size: 11px;
161
- font-family: var(--sans);
162
- color: var(--ink-faint);
163
- letter-spacing: 1px;
164
- }
165
- .pronunciation .ann-kind {
166
- background: var(--jade);
167
- color: #fff;
168
- }
169
- .semantic .ann-kind {
170
- background: var(--vermillion);
171
- color: #fff;
337
+ .ann-sheet-inner {
338
+ padding: 0 20px 28px;
172
339
  }
173
340
 
174
- .ann-body {
175
- line-height: 1.8;
176
- }
341
+ /* ─── Transitions ─── */
177
342
 
178
- /* ─── 直排模式 tooltip ─── */
179
- .ann-vertical {
180
- writing-mode: vertical-rl;
181
- text-orientation: mixed;
182
- max-width: none;
183
- max-height: 60vh;
184
- padding: 16px 12px;
343
+ /* Left pane slide from left */
344
+ .ann-slide-right-enter-active {
345
+ transition: opacity 0.2s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
185
346
  }
186
- .ann-vertical .ann-entry {
187
- margin-bottom: 0;
188
- margin-left: 12px;
189
- display: inline;
347
+ .ann-slide-right-leave-active {
348
+ transition: opacity 0.15s ease, transform 0.15s ease;
190
349
  }
191
- .ann-vertical .ann-kind {
192
- margin-right: 0;
193
- text-combine-upright: all;
194
- vertical-align: baseline;
350
+ .ann-slide-right-enter-from {
351
+ opacity: 0;
352
+ transform: translateX(-24px);
195
353
  }
196
- .ann-vertical .ann-body {
197
- margin-left: 6px;
354
+ .ann-slide-right-leave-to {
355
+ opacity: 0;
356
+ transform: translateX(-16px);
198
357
  }
199
358
 
200
- /* ─── Transition ─── */
359
+ /* Popup fade */
201
360
  .ann-fade-enter-active {
202
- transition: opacity var(--dur-fast, 0.15s) ease, transform var(--dur-mid, 0.25s) cubic-bezier(0.34, 1.56, 0.64, 1);
361
+ transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
203
362
  }
204
363
  .ann-fade-leave-active {
205
- transition: opacity var(--dur-fast, 0.15s) ease, transform var(--dur-fast, 0.15s) ease;
364
+ transition: opacity 0.15s ease, transform 0.15s ease;
206
365
  }
207
366
  .ann-fade-enter-from {
208
367
  opacity: 0;
@@ -212,4 +371,24 @@ onBeforeUnmount(() => {
212
371
  opacity: 0;
213
372
  transform: scale(0.96);
214
373
  }
374
+
375
+ /* Bottom sheet slide up */
376
+ .ann-sheet-enter-active {
377
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
378
+ }
379
+ .ann-sheet-leave-active {
380
+ transition: transform 0.2s ease;
381
+ }
382
+ .ann-sheet-enter-from,
383
+ .ann-sheet-leave-to {
384
+ transform: translateY(100%);
385
+ }
386
+
387
+ @media (max-width: 1023px) {
388
+ .ann-left-pane { display: none; }
389
+ }
390
+
391
+ @media (min-width: 1024px) {
392
+ .ann-bottom-sheet { display: none; }
393
+ }
215
394
  </style>
@@ -50,6 +50,10 @@ onUnmounted(() => window.removeEventListener('scroll', onScroll))
50
50
  box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.1);
51
51
  transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
52
52
  }
53
+
54
+ @media (max-width: 768px) {
55
+ .btt { bottom: 88px; right: 16px; width: 36px; height: 36px; }
56
+ }
53
57
  .btt:hover {
54
58
  background: var(--ink);
55
59
  color: var(--paper);
@@ -1,27 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import type { BookMeta } from '../types'
3
3
  import { useRouter } from 'vue-router'
4
+ import { useI18n } from '../composables/useI18n'
4
5
 
5
- defineProps<{ book: BookMeta }>()
6
+ const props = defineProps<{ book: BookMeta }>()
6
7
  const router = useRouter()
8
+ const { t } = useI18n()
7
9
 
8
- const genreLabel: Record<string, string> = {
9
- poetry: '詩歌',
10
- prose: '散文',
11
- mixed: '綜合',
12
- drama: '戲曲',
10
+ function genreLabel(genre: string): string {
11
+ return t(`genre.${genre}` as 'genre.poetry') || genre
13
12
  }
14
13
  </script>
15
14
 
16
15
  <template>
17
- <div class="bc-root" @click="router.push(`/${book.id}`)">
16
+ <div class="bc-root" @click="router.push(`/${props.book.id}`)">
18
17
  <div class="bc-accent"></div>
19
18
  <div class="bc-body">
20
- <h2 class="bc-title">{{ book.title }}</h2>
21
- <p v-if="book.subtitle" class="bc-subtitle">{{ book.subtitle }}</p>
19
+ <h2 class="bc-title">{{ props.book.title }}</h2>
20
+ <p v-if="props.book.subtitle" class="bc-subtitle">{{ props.book.subtitle }}</p>
22
21
  <div class="bc-stats">
23
- <span class="bc-count">{{ book.count }} 篇</span>
24
- <span class="bc-genre">{{ genreLabel[book.genre] || book.genre }}</span>
22
+ <span class="bc-count">{{ t('stat.pieceCount', { count: props.book.count }) }}</span>
23
+ <span class="bc-genre">{{ genreLabel(props.book.genre) }}</span>
25
24
  </div>
26
25
  </div>
27
26
  </div>
@@ -97,4 +97,56 @@ function onTap(event: MouseEvent) {
97
97
  :deep(.ann-target.pronunciation) {
98
98
  border-bottom-color: var(--jade);
99
99
  }
100
+ :deep(.ann-target.person) {
101
+ border-bottom-color: var(--ann-person);
102
+ }
103
+ :deep(.ann-target.place) {
104
+ border-bottom-color: var(--ann-place);
105
+ }
106
+ :deep(.ann-target.event) {
107
+ border-bottom-color: var(--ann-event);
108
+ }
109
+ :deep(.ann-target.date) {
110
+ border-bottom-color: var(--ann-date);
111
+ }
112
+ :deep(.ann-target.allusion) {
113
+ border-bottom-color: var(--ann-allusion);
114
+ }
115
+ :deep(.ann-target.person:hover) {
116
+ background: rgba(58, 90, 140, 0.08);
117
+ }
118
+ :deep(.ann-target.place:hover) {
119
+ background: rgba(139, 105, 20, 0.08);
120
+ }
121
+ :deep(.ann-target.event:hover) {
122
+ background: rgba(107, 76, 138, 0.08);
123
+ }
124
+ :deep(.ann-target.date:hover) {
125
+ background: rgba(42, 122, 122, 0.08);
126
+ }
127
+ :deep(.ann-target.allusion:hover) {
128
+ background: rgba(181, 101, 29, 0.08);
129
+ }
130
+
131
+ @media (max-width: 768px) {
132
+ .h-display {
133
+ padding: 24px 20px;
134
+ border-radius: 6px;
135
+ width: 100%;
136
+ box-sizing: border-box;
137
+ }
138
+ .h-display-title {
139
+ font-size: 24px;
140
+ letter-spacing: 4px;
141
+ }
142
+ .h-display-author {
143
+ font-size: 14px;
144
+ margin-bottom: 16px;
145
+ }
146
+ .h-display-line {
147
+ font-size: var(--main-font-size, 20px);
148
+ line-height: 2.4;
149
+ letter-spacing: 2px;
150
+ }
151
+ }
100
152
  </style>
@@ -49,6 +49,7 @@ const preview = computed(() => {
49
49
  border-color: var(--gold);
50
50
  }
51
51
  .pc-root:hover .pc-accent { height: 100%; }
52
+ .pc-root:active { transform: scale(0.98); }
52
53
  .pc-body { padding: 24px; }
53
54
  .pc-num {
54
55
  font-size: 11px; color: var(--ink-faint);
@@ -8,38 +8,47 @@ defineProps<{
8
8
 
9
9
  <template>
10
10
  <span class="pron-group">
11
- <span class="ann-pron" :class="segment.lang === 'yue' ? 'ann-yue' : 'ann-cmn'">
11
+ <span class="pron-badge" :class="segment.lang === 'yue' ? 'pron-yue' : 'pron-cmn'">
12
12
  {{ segment.label }}
13
13
  </span>
14
- <span
15
- v-for="(part, i) in segment.parts"
16
- :key="i"
17
- class="ann-phonetic"
18
- >{{ part }}</span>
14
+ <span class="pron-text">{{ segment.parts.join(' ') }}</span>
19
15
  </span>
20
16
  </template>
21
17
 
22
18
  <style scoped>
23
19
  .pron-group {
24
20
  display: inline-flex;
25
- align-items: baseline;
26
- gap: 0.3em;
21
+ align-items: center;
22
+ gap: 5px;
27
23
  white-space: nowrap;
28
24
  }
29
- .ann-pron {
30
- display: inline-block;
31
- font-size: 0.75em;
25
+ .pron-badge {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ min-width: 22px;
30
+ height: 18px;
31
+ padding: 0 5px;
32
+ border-radius: 9px;
33
+ font-size: 11px;
32
34
  font-family: var(--sans);
33
- font-weight: 600;
34
- padding: 1px 3px;
35
- border-radius: 2px;
36
- vertical-align: middle;
35
+ font-weight: 700;
36
+ letter-spacing: 0.5px;
37
37
  line-height: 1;
38
+ flex-shrink: 0;
39
+ }
40
+ .pron-yue {
41
+ background: var(--jade);
42
+ color: #fff;
43
+ }
44
+ .pron-cmn {
45
+ background: var(--ink);
46
+ color: var(--paper);
38
47
  }
39
- .ann-yue { background: var(--jade); color: #fff; }
40
- .ann-cmn { background: var(--ink); color: var(--paper); }
41
- .ann-phonetic {
48
+ .pron-text {
42
49
  font-family: var(--sans);
50
+ font-size: 13px;
43
51
  color: var(--ink-light);
52
+ letter-spacing: 0.5px;
44
53
  }
45
54
  </style>
@@ -91,6 +91,26 @@ function close() { open.value = false }
91
91
  right: 24px;
92
92
  z-index: 500;
93
93
  }
94
+
95
+ @media (max-width: 768px) {
96
+ .rt { bottom: 16px; right: 16px; }
97
+ .rt-panel {
98
+ position: fixed;
99
+ bottom: 0;
100
+ right: 0;
101
+ left: 0;
102
+ width: auto;
103
+ border-radius: 16px 16px 0 0;
104
+ max-height: 60vh;
105
+ overflow-y: auto;
106
+ overscroll-behavior: contain;
107
+ animation: slideUpMobile 0.3s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
108
+ }
109
+ @keyframes slideUpMobile {
110
+ from { opacity: 0; transform: translateY(100%); }
111
+ to { opacity: 1; transform: translateY(0); }
112
+ }
113
+ }
94
114
  .rt-fab {
95
115
  width: 44px; height: 44px;
96
116
  border-radius: 50%;