@hanology/cham-browser 0.3.9 → 0.4.2

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.
Files changed (30) hide show
  1. package/dist/cli.js +303 -32
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template/index.html +4 -8
  5. package/template/src/App.vue +101 -17
  6. package/template/src/components/AnnotationControlBar.vue +119 -49
  7. package/template/src/components/AnnotationTooltip.vue +319 -95
  8. package/template/src/components/BackToTop.vue +4 -0
  9. package/template/src/components/BookCard.vue +10 -11
  10. package/template/src/components/HorizontalDisplay.vue +56 -0
  11. package/template/src/components/PartBlock.vue +9 -0
  12. package/template/src/components/PoemCard.vue +1 -0
  13. package/template/src/components/PronunciationGroup.vue +27 -18
  14. package/template/src/components/ReadingToolbar.vue +20 -0
  15. package/template/src/components/SectionBlock.vue +91 -12
  16. package/template/src/components/SideNav.vue +5 -4
  17. package/template/src/components/VerticalScroll.vue +35 -0
  18. package/template/src/composables/useAnnotationRenderer.ts +57 -25
  19. package/template/src/composables/useData.ts +6 -1
  20. package/template/src/composables/useI18n.ts +36 -3
  21. package/template/src/composables/useReadingMode.ts +9 -4
  22. package/template/src/composables/useSiteConfig.ts +12 -1
  23. package/template/src/router.ts +0 -2
  24. package/template/src/styles/main.css +88 -0
  25. package/template/src/types.ts +12 -4
  26. package/template/src/views/AuthorView.vue +5 -5
  27. package/template/src/views/BookHome.vue +45 -21
  28. package/template/src/views/LibraryHome.vue +39 -41
  29. package/template/src/views/PieceView.vue +436 -71
  30. package/template/src/views/AboutView.vue +0 -191
@@ -1,16 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import { useRouter } from 'vue-router'
3
3
  import { useReadingMode } from './composables/useReadingMode'
4
+ import { useSiteConfig } from './composables/useSiteConfig'
4
5
  import ReadingToolbar from './components/ReadingToolbar.vue'
5
- import { computed, ref } from 'vue'
6
+ import { computed, ref, provide } from 'vue'
6
7
 
7
8
  const router = useRouter()
8
9
  const { toggleLayout, cycleTheme, layout } = useReadingMode()
10
+ const { logoUrl, aboutHtml } = useSiteConfig()
9
11
  const isVertical = computed(() => layout.value === 'vertical')
10
12
 
13
+ const aboutOpen = ref(false)
14
+ function toggleAbout() { aboutOpen.value = !aboutOpen.value }
15
+ function closeAbout() { aboutOpen.value = false }
16
+ provide('aboutPane', { toggleAbout, closeAbout })
17
+
11
18
  function onKey(event: KeyboardEvent) {
12
19
  if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) return
13
- if (event.key === 'Escape') router.push('/')
20
+ if (event.key === 'Escape') {
21
+ if (aboutOpen.value) { closeAbout(); return }
22
+ router.push('/')
23
+ }
14
24
  if (event.key === 'v' || event.key === 'V') toggleLayout()
15
25
  if (event.key === 't' || event.key === 'T') cycleTheme()
16
26
  }
@@ -18,29 +28,103 @@ function onKey(event: KeyboardEvent) {
18
28
 
19
29
  <template>
20
30
  <div @keydown="onKey">
21
- <router-view v-slot="{ Component }">
22
- <Transition name="page" mode="out-in">
23
- <Suspense>
24
- <component :is="Component" :key="$route.fullPath" />
25
- </Suspense>
26
- </Transition>
31
+ <router-view v-slot="{ Component, route }">
32
+ <Suspense :key="route.fullPath">
33
+ <component :is="Component" />
34
+ <template #fallback>
35
+ <div class="route-loading"></div>
36
+ </template>
37
+ </Suspense>
27
38
  </router-view>
28
39
  <!-- 橫排模式才顯示浮動設定鈕 -->
29
40
  <ReadingToolbar v-if="!isVertical" />
41
+
42
+ <!-- About overlay (only if about content is configured) -->
43
+ <Teleport v-if="aboutHtml" to="body">
44
+ <div v-if="aboutOpen" class="about-overlay" @click="closeAbout">
45
+ <div v-if="isVertical" class="about-pane-v" @click.stop>
46
+ <button class="about-close" @click="closeAbout">✕</button>
47
+ <div class="about-v-body" v-html="aboutHtml" />
48
+ </div>
49
+
50
+ <div v-else class="about-pane-h" @click.stop>
51
+ <button class="about-close" @click="closeAbout">✕</button>
52
+ <img v-if="logoUrl" :src="logoUrl" alt="" class="about-logo" />
53
+ <div class="about-h-body" v-html="aboutHtml" />
54
+ </div>
55
+ </div>
56
+ </Teleport>
30
57
  </div>
31
58
  </template>
32
59
 
33
60
  <style>
34
- .page-enter-active,
35
- .page-leave-active {
36
- transition: opacity 0.25s ease, transform 0.25s ease;
61
+ .about-overlay {
62
+ position: fixed; inset: 0;
63
+ background: rgba(var(--shadow-rgb), 0.3);
64
+ z-index: 200;
65
+ display: flex; justify-content: center; align-items: center;
66
+ animation: aboutFadeIn 0.2s ease;
67
+ }
68
+ @keyframes aboutFadeIn { from { opacity: 0 } to { opacity: 1 } }
69
+
70
+ .about-close {
71
+ position: absolute; top: 16px; right: 16px;
72
+ width: 36px; height: 36px;
73
+ border: 1px solid var(--border); border-radius: 4px;
74
+ background: none; font-size: 16px;
75
+ color: var(--ink-light); cursor: pointer;
76
+ transition: all 0.15s; z-index: 1;
77
+ }
78
+ .about-close:hover { background: var(--ink); color: var(--paper); border-color: var(--ink) }
79
+
80
+ /* Horizontal pane */
81
+ .about-pane-h {
82
+ position: relative;
83
+ width: min(600px, 90vw);
84
+ max-height: 85vh;
85
+ background: var(--paper);
86
+ border-radius: 12px;
87
+ padding: 40px;
88
+ overflow-y: auto;
89
+ box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.15);
90
+ animation: aboutSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
37
91
  }
38
- .page-enter-from {
39
- opacity: 0;
40
- transform: translateY(8px);
92
+ @keyframes aboutSlideUp { from { opacity: 0; transform: translateY(16px) } to { opacity: 1; transform: translateY(0) } }
93
+
94
+ .about-logo { height: 64px; width: auto; object-fit: contain; margin: 0 auto 32px; display: block }
95
+ .about-h-body {
96
+ font-size: 15px; line-height: 2.2; color: var(--ink-mid);
97
+ }
98
+ .about-h-body :deep(h2) {
99
+ font-size: 18px; font-weight: 700; letter-spacing: 3px; color: var(--ink);
100
+ margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border);
101
+ }
102
+ .about-h-body :deep(p) {
103
+ text-align: justify; text-indent: 2em; margin-bottom: 10px;
104
+ }
105
+ .about-h-body :deep(p:last-child) { margin-bottom: 0 }
106
+ .about-h-body :deep(.about-block) {
107
+ margin-bottom: 32px; padding: 28px;
108
+ background: var(--surface); border: 1px solid var(--border-light); border-radius: 8px;
109
+ }
110
+ .about-h-body :deep(.about-block:last-child) { margin-bottom: 0 }
111
+
112
+ /* Vertical pane */
113
+ .about-pane-v {
114
+ writing-mode: vertical-rl; text-orientation: mixed;
115
+ position: relative;
116
+ height: 100vh;
117
+ background: var(--paper);
118
+ padding: 32px 28px;
119
+ overflow-x: auto;
120
+ box-shadow: 8px 0 32px rgba(var(--shadow-rgb), 0.1);
121
+ animation: aboutSlideInV 0.25s cubic-bezier(0.16, 1, 0.3, 1);
41
122
  }
42
- .page-leave-to {
43
- opacity: 0;
44
- transform: translateY(-4px);
123
+ @keyframes aboutSlideInV { from { transform: translateX(-100%) } to { transform: translateX(0) } }
124
+ .about-pane-v .about-close { position: static; margin-bottom: 20px }
125
+ .about-v-body {
126
+ font-size: 16px; line-height: 2.4; color: var(--ink-mid);
127
+ max-height: 80vh; overflow-x: auto;
45
128
  }
129
+ .about-v-body :deep(p) { margin-left: 16px; text-indent: 0 }
46
130
  </style>
@@ -5,6 +5,7 @@ const props = defineProps<{
5
5
  layers: AnnotationLayer[]
6
6
  hasAnnotations: boolean
7
7
  activeIds: string[]
8
+ annotationsVisible: boolean
8
9
  }>()
9
10
 
10
11
  const emit = defineEmits<{
@@ -12,16 +13,15 @@ const emit = defineEmits<{
12
13
  'update:annotationsVisible': [visible: boolean]
13
14
  }>()
14
15
 
15
- const allIds = () => props.layers.map(l => l.id)
16
- const noneIds = () => [] as string[]
16
+ const hasLayers = () => props.layers.length > 1
17
17
 
18
18
  function toggleAnnotations() {
19
- if (props.activeIds.length > 0) {
20
- emit('update:activeIds', noneIds())
19
+ if (props.annotationsVisible) {
21
20
  emit('update:annotationsVisible', false)
21
+ if (hasLayers()) emit('update:activeIds', [])
22
22
  } else {
23
- emit('update:activeIds', allIds())
24
23
  emit('update:annotationsVisible', true)
24
+ if (hasLayers()) emit('update:activeIds', props.layers.map(l => l.id))
25
25
  }
26
26
  }
27
27
 
@@ -40,85 +40,155 @@ function toggleLayer(id: string) {
40
40
  </script>
41
41
 
42
42
  <template>
43
- <div v-if="hasAnnotations" class="ann-control-bar">
43
+ <div v-if="hasAnnotations" class="ann-bar">
44
44
  <button
45
- class="ann-toggle"
46
- :class="{ active: activeIds.length > 0 }"
45
+ class="ann-master"
46
+ :class="{ active: annotationsVisible }"
47
47
  @click="toggleAnnotations"
48
- >注</button>
49
- <template v-if="layers.length > 1">
50
- <span class="ann-bar-sep" />
51
- <button
52
- v-for="layer in layers"
53
- :key="layer.id"
54
- :class="['ann-layer-btn', { active: activeIds.includes(layer.id) }]"
55
- :title="layer.label"
56
- @click="toggleLayer(layer.id)"
57
- >
58
- {{ layer.shortLabel }}
59
- </button>
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>
60
66
  </template>
61
67
  </div>
62
68
  </template>
63
69
 
64
70
  <style scoped>
65
- .ann-control-bar {
71
+ .ann-bar {
66
72
  display: flex;
67
- align-items: center;
68
- gap: 4px;
73
+ flex-direction: column;
74
+ gap: 10px;
69
75
  }
70
76
 
71
- .ann-toggle {
72
- width: 28px;
73
- height: 28px;
74
- border: 1px solid var(--vermillion);
75
- border-radius: 4px;
77
+ .ann-master {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ padding: 8px 20px;
82
+ border: 1.5px solid var(--vermillion);
83
+ border-radius: 20px;
76
84
  background: none;
77
85
  color: var(--vermillion);
78
- font-family: var(--serif);
79
- font-size: 14px;
80
- font-weight: 700;
86
+ font-family: var(--sans);
87
+ font-size: 13px;
88
+ font-weight: 600;
81
89
  cursor: pointer;
82
- transition: all 0.15s;
83
- letter-spacing: 0;
90
+ transition: all 0.2s;
91
+ letter-spacing: 1px;
92
+ min-height: 44px;
93
+ align-self: flex-start;
84
94
  }
85
95
 
86
- .ann-toggle.active {
96
+ .ann-master.active {
87
97
  background: var(--vermillion);
88
98
  color: #fff;
89
99
  }
90
100
 
91
- .ann-toggle:hover {
92
- border-color: var(--vermillion-light);
101
+ .ann-master:active {
102
+ transform: scale(0.97);
93
103
  }
94
104
 
95
- .ann-bar-sep {
96
- width: 1px;
97
- height: 16px;
98
- background: var(--border);
99
- margin: 0 2px;
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
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ font-size: 11px;
118
+ flex-shrink: 0;
100
119
  }
101
120
 
102
- .ann-layer-btn {
121
+ .ann-master.active .ann-master-icon {
122
+ border-color: rgba(255, 255, 255, 0.5);
123
+ background: rgba(255, 255, 255, 0.15);
124
+ }
125
+
126
+ .ann-master-text {
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .ann-count {
131
+ font-size: 11px;
132
+ opacity: 0.7;
133
+ margin-left: 2px;
134
+ }
135
+
136
+ .ann-chips {
137
+ display: flex;
138
+ flex-wrap: wrap;
139
+ gap: 8px;
140
+ padding-left: 4px;
141
+ }
142
+
143
+ .ann-chip {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ gap: 4px;
147
+ padding: 6px 14px;
103
148
  border: 1px solid var(--border);
104
- border-radius: 4px;
105
- padding: 3px 10px;
106
- font-size: 12px;
149
+ border-radius: 16px;
107
150
  background: var(--surface);
108
151
  color: var(--ink-mid);
109
- cursor: pointer;
110
- transition: all 0.15s;
111
152
  font-family: var(--sans);
153
+ font-size: 12px;
112
154
  letter-spacing: 1px;
155
+ cursor: pointer;
156
+ transition: all 0.2s;
157
+ min-height: 36px;
113
158
  }
114
159
 
115
- .ann-layer-btn:hover {
160
+ .ann-chip:hover {
116
161
  border-color: var(--gold);
162
+ color: var(--ink);
117
163
  }
118
164
 
119
- .ann-layer-btn.active {
165
+ .ann-chip.active {
120
166
  background: var(--ink);
121
167
  color: var(--paper);
122
168
  border-color: var(--ink);
123
169
  }
170
+
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;
179
+ }
180
+
181
+ @media (max-width: 768px) {
182
+ .ann-master {
183
+ padding: 10px 24px;
184
+ font-size: 14px;
185
+ }
186
+ .ann-chips {
187
+ gap: 6px;
188
+ }
189
+ .ann-chip {
190
+ padding: 8px 16px;
191
+ font-size: 13px;
192
+ }
193
+ }
124
194
  </style>