@hanology/cham-browser 0.3.2 → 0.3.4
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/dist/cli.js +23 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/template/index.html +14 -5
- package/template/src/App.vue +21 -4
- package/template/src/components/PartBlock.vue +160 -0
- package/template/src/components/PartGroup.vue +72 -0
- package/template/src/components/PoemCard.vue +6 -6
- package/template/src/components/ReadingToolbar.vue +45 -5
- package/template/src/components/SideNav.vue +24 -3
- package/template/src/composables/useSiteConfig.ts +11 -0
- package/template/src/main.ts +5 -1
- package/template/src/router.ts +2 -0
- package/template/src/styles/main.css +45 -8
- package/template/src/types.ts +10 -0
- package/template/src/views/AboutView.vue +17 -2
- package/template/src/views/LibraryHome.vue +67 -2
- package/template/src/views/PieceView.vue +97 -10
- package/template/src/components/AnnotationLayerSelector.vue +0 -66
- package/template/src/composables/usePageLayout.ts +0 -25
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ function loadConfig(configPath) {
|
|
|
11
11
|
nameEn: raw.nameEn,
|
|
12
12
|
subtitle: raw.subtitle,
|
|
13
13
|
subtitleEn: raw.subtitleEn,
|
|
14
|
+
logo: raw.logo,
|
|
14
15
|
libraryDir: raw.libraryDir || 'library/content',
|
|
15
16
|
authorsFile: raw.authorsFile,
|
|
16
17
|
outputDir: raw.outputDir || 'dist',
|
|
@@ -125,14 +126,32 @@ function generateData(config, configDir) {
|
|
|
125
126
|
async function buildSite(config, configDir) {
|
|
126
127
|
const templateDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'template');
|
|
127
128
|
const outputDir = resolve(configDir, config.outputDir || 'dist');
|
|
129
|
+
// Copy logo to dist if configured
|
|
130
|
+
let logoUrl;
|
|
131
|
+
if (config.logo) {
|
|
132
|
+
const logoSrc = resolve(configDir, config.logo);
|
|
133
|
+
if (existsSync(logoSrc)) {
|
|
134
|
+
const assetsDir = join(outputDir, 'assets');
|
|
135
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
136
|
+
const ext = logoSrc.endsWith('.svg') ? 'svg' : logoSrc.endsWith('.png') ? 'png' : 'bin';
|
|
137
|
+
const logoName = `logo.${ext}`;
|
|
138
|
+
writeFileSync(join(assetsDir, logoName), readFileSync(logoSrc));
|
|
139
|
+
logoUrl = `/assets/${logoName}`;
|
|
140
|
+
console.log(`Logo: ${config.logo} → ${logoUrl}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.warn(`Logo not found: ${logoSrc}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
128
146
|
const { build: ssgBuild } = await import('vite-ssg/node');
|
|
129
147
|
const vue = (await import('@vitejs/plugin-vue')).default;
|
|
130
148
|
process.env.CHAM_DATA_DIR = join(outputDir, 'data');
|
|
149
|
+
process.env.CHAM_LOGO_URL = logoUrl || '';
|
|
131
150
|
await ssgBuild({
|
|
132
151
|
script: 'async',
|
|
133
152
|
formatting: 'minify',
|
|
134
153
|
includedRoutes(paths, routes) {
|
|
135
|
-
const result = ['/'];
|
|
154
|
+
const result = ['/', '/about'];
|
|
136
155
|
const library = JSON.parse(readFileSync(join(outputDir, 'data', 'library.json'), 'utf-8'));
|
|
137
156
|
const authors = JSON.parse(readFileSync(join(outputDir, 'data', 'authors.json'), 'utf-8'));
|
|
138
157
|
for (const book of library.books) {
|
|
@@ -155,6 +174,9 @@ async function buildSite(config, configDir) {
|
|
|
155
174
|
'@': resolve(templateDir, 'src'),
|
|
156
175
|
},
|
|
157
176
|
},
|
|
177
|
+
define: {
|
|
178
|
+
'import.meta.env.CHAM_LOGO_URL': JSON.stringify(logoUrl || ''),
|
|
179
|
+
},
|
|
158
180
|
build: {
|
|
159
181
|
outDir: outputDir,
|
|
160
182
|
emptyOutDir: false,
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;AAC9F,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAA;AACnC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAA;AACzC,OAAO,EACL,kBAAkB,EAAE,aAAa,EAAE,iBAAiB,EACpD,gBAAgB,EAAE,kBAAkB,GACrC,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/template/index.html
CHANGED
|
@@ -15,14 +15,23 @@
|
|
|
15
15
|
})();
|
|
16
16
|
</script>
|
|
17
17
|
<style>
|
|
18
|
-
#app-loading{position:fixed;inset:0;background:var(--paper,#faf6ee);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999;font-family:'Noto Serif TC',serif}
|
|
19
|
-
#app-loading
|
|
20
|
-
#app-loading .
|
|
21
|
-
|
|
18
|
+
#app-loading{position:fixed;inset:0;background:var(--paper,#faf6ee);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999;font-family:'Noto Serif TC',serif;transition:opacity .4s ease}
|
|
19
|
+
#app-loading.fade-out{opacity:0;pointer-events:none}
|
|
20
|
+
#app-loading .seal{width:72px;height:72px;border:2px solid var(--vermillion,#c23a2b);border-radius:4px;display:flex;align-items:center;justify-content:center;animation:sealReveal .8s cubic-bezier(.16,1,.3,1) forwards;opacity:0}
|
|
21
|
+
#app-loading .char{font-size:36px;font-weight:900;color:var(--vermillion,#c23a2b);line-height:1}
|
|
22
|
+
#app-loading .line{width:1px;height:40px;background:linear-gradient(180deg,var(--vermillion,#c23a2b),transparent);margin-top:24px;animation:lineGrow .6s .3s cubic-bezier(.16,1,.3,1) forwards;transform:scaleY(0);transform-origin:top}
|
|
23
|
+
#app-loading .text{font-size:11px;color:var(--ink-faint,#a89b8a);letter-spacing:6px;margin-top:16px;animation:fadeIn .5s .5s ease forwards;opacity:0;font-family:'Noto Sans TC',sans-serif}
|
|
24
|
+
@keyframes sealReveal{from{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}
|
|
25
|
+
@keyframes lineGrow{to{transform:scaleY(1)}}
|
|
26
|
+
@keyframes fadeIn{to{opacity:1}}
|
|
22
27
|
</style>
|
|
23
28
|
</head>
|
|
24
29
|
<body>
|
|
25
|
-
<div id="app-loading"
|
|
30
|
+
<div id="app-loading">
|
|
31
|
+
<div class="seal"><div class="char">詩</div></div>
|
|
32
|
+
<div class="line"></div>
|
|
33
|
+
<div class="text">載 入 中</div>
|
|
34
|
+
</div>
|
|
26
35
|
<div id="app"></div>
|
|
27
36
|
<script type="module" src="/src/main.ts"></script>
|
|
28
37
|
</body>
|
package/template/src/App.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useRouter } from 'vue-router'
|
|
3
3
|
import { useReadingMode } from './composables/useReadingMode'
|
|
4
4
|
import ReadingToolbar from './components/ReadingToolbar.vue'
|
|
5
|
-
import { computed } from 'vue'
|
|
5
|
+
import { computed, ref } from 'vue'
|
|
6
6
|
|
|
7
7
|
const router = useRouter()
|
|
8
8
|
const { toggleLayout, cycleTheme, layout } = useReadingMode()
|
|
@@ -19,11 +19,28 @@ function onKey(event: KeyboardEvent) {
|
|
|
19
19
|
<template>
|
|
20
20
|
<div @keydown="onKey">
|
|
21
21
|
<router-view v-slot="{ Component }">
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
22
|
+
<Transition name="page" mode="out-in">
|
|
23
|
+
<Suspense>
|
|
24
|
+
<component :is="Component" :key="$route.fullPath" />
|
|
25
|
+
</Suspense>
|
|
26
|
+
</Transition>
|
|
25
27
|
</router-view>
|
|
26
28
|
<!-- 橫排模式才顯示浮動設定鈕 -->
|
|
27
29
|
<ReadingToolbar v-if="!isVertical" />
|
|
28
30
|
</div>
|
|
29
31
|
</template>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.page-enter-active,
|
|
35
|
+
.page-leave-active {
|
|
36
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
37
|
+
}
|
|
38
|
+
.page-enter-from {
|
|
39
|
+
opacity: 0;
|
|
40
|
+
transform: translateY(8px);
|
|
41
|
+
}
|
|
42
|
+
.page-leave-to {
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transform: translateY(-4px);
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Annotation, VerseLine, PieceSource } from '../types'
|
|
3
|
+
import { buildVerseAnnotations, renderAnnotatedText, resolveHoveredAnnotations, countVerseSpans } from '../composables/useAnnotationRenderer'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
num: number
|
|
7
|
+
verses: VerseLine[]
|
|
8
|
+
annotations: Annotation[]
|
|
9
|
+
vertical?: boolean
|
|
10
|
+
source?: PieceSource
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
annotationHover: [event: MouseEvent, annotations: Annotation[]]
|
|
15
|
+
annotationLeave: []
|
|
16
|
+
annotationTap: [event: MouseEvent, annotations: Annotation[]]
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
function verseHtml(index: number): string {
|
|
20
|
+
const useRuby = props.vertical
|
|
21
|
+
let offset = 0
|
|
22
|
+
for (let i = 0; i < index; i++) offset += countVerseSpans(props.annotations, i)
|
|
23
|
+
const spans = buildVerseAnnotations(props.annotations, index)
|
|
24
|
+
return renderAnnotatedText(props.verses[index].text, spans, useRuby, offset)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onHover(event: MouseEvent) {
|
|
28
|
+
const matched = resolveHoveredAnnotations(event, props.annotations)
|
|
29
|
+
if (matched) emit('annotationHover', event, matched)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function onLeave() { emit('annotationLeave') }
|
|
33
|
+
|
|
34
|
+
function onTap(event: MouseEvent) {
|
|
35
|
+
const matched = resolveHoveredAnnotations(event, props.annotations)
|
|
36
|
+
if (matched) emit('annotationTap', event, matched)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sourceLabel = (() => {
|
|
40
|
+
const r = props.source?.range as Record<string, string> | undefined
|
|
41
|
+
return r?.chapter || ''
|
|
42
|
+
})()
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="part-block" :class="{ 'part-block--vertical': vertical }">
|
|
47
|
+
<div v-if="sourceLabel" class="part-source">
|
|
48
|
+
{{ sourceLabel }}
|
|
49
|
+
</div>
|
|
50
|
+
<div class="part-text" @mouseover="onHover" @mouseleave="onLeave" @click="onTap">
|
|
51
|
+
<span
|
|
52
|
+
v-for="(_, i) in verses"
|
|
53
|
+
:key="i"
|
|
54
|
+
:class="vertical ? 'part-line-v' : 'part-line-h'"
|
|
55
|
+
v-html="verseHtml(i)"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.part-block {
|
|
63
|
+
padding: 20px 0;
|
|
64
|
+
border-bottom: 1px solid var(--border-light);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.part-block:last-child {
|
|
68
|
+
border-bottom: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.part-block--vertical {
|
|
72
|
+
writing-mode: vertical-rl;
|
|
73
|
+
text-orientation: mixed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.part-source {
|
|
77
|
+
font-family: var(--sans);
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
letter-spacing: 1px;
|
|
80
|
+
color: var(--ink-faint);
|
|
81
|
+
background: var(--surface);
|
|
82
|
+
display: inline-block;
|
|
83
|
+
padding: 3px 10px;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
margin-bottom: 12px;
|
|
86
|
+
border: 1px solid var(--border-light);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.part-text {
|
|
90
|
+
line-height: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.part-line-h {
|
|
94
|
+
font-size: var(--main-font-size, 22px);
|
|
95
|
+
line-height: 2.4;
|
|
96
|
+
letter-spacing: 3px;
|
|
97
|
+
color: var(--ink);
|
|
98
|
+
display: block;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.part-line-v {
|
|
102
|
+
font-size: var(--main-font-size, 22px);
|
|
103
|
+
line-height: 2.4;
|
|
104
|
+
letter-spacing: 6px;
|
|
105
|
+
color: var(--ink);
|
|
106
|
+
display: block;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:deep(.ann-target) {
|
|
110
|
+
border-bottom: 2px solid var(--vermillion);
|
|
111
|
+
cursor: help;
|
|
112
|
+
transition: background 0.15s;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:deep(.ann-target:hover) {
|
|
116
|
+
background: rgba(194, 58, 43, 0.08);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:deep(.ann-num) {
|
|
120
|
+
font-size: 10px;
|
|
121
|
+
color: var(--vermillion);
|
|
122
|
+
font-family: var(--sans);
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
vertical-align: super;
|
|
125
|
+
margin-right: 1px;
|
|
126
|
+
letter-spacing: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
:deep(.ann-target.pronunciation) {
|
|
130
|
+
border-bottom-color: var(--jade);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
:deep(.ann-target.pronunciation:hover) {
|
|
134
|
+
background: rgba(58, 107, 94, 0.08);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Vertical mode overrides */
|
|
138
|
+
.part-block--vertical :deep(.ann-target) {
|
|
139
|
+
border-bottom: none;
|
|
140
|
+
border-left: 2px solid var(--vermillion);
|
|
141
|
+
padding-left: 2px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.part-block--vertical :deep(.ann-target.pronunciation) {
|
|
145
|
+
border-left-color: var(--jade);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.part-block--vertical :deep(.ann-num) {
|
|
149
|
+
font-size: 0.45em;
|
|
150
|
+
text-combine-upright: all;
|
|
151
|
+
text-align: end;
|
|
152
|
+
letter-spacing: 0;
|
|
153
|
+
vertical-align: baseline;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.part-block--vertical .part-source {
|
|
157
|
+
margin-bottom: 0;
|
|
158
|
+
margin-left: 8px;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Annotation, Part } from '../types'
|
|
3
|
+
import PartBlock from './PartBlock.vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
label: string
|
|
7
|
+
parts: Part[]
|
|
8
|
+
vertical?: boolean
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
annotationHover: [event: MouseEvent, annotations: Annotation[]]
|
|
13
|
+
annotationLeave: []
|
|
14
|
+
annotationTap: [event: MouseEvent, annotations: Annotation[]]
|
|
15
|
+
}>()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="part-group" :class="{ 'part-group--vertical': vertical }">
|
|
20
|
+
<div class="part-group-label">{{ label }}</div>
|
|
21
|
+
<PartBlock
|
|
22
|
+
v-for="part in parts"
|
|
23
|
+
:key="part.num"
|
|
24
|
+
:num="part.num"
|
|
25
|
+
:verses="part.verses"
|
|
26
|
+
:annotations="part.annotations"
|
|
27
|
+
:vertical="vertical"
|
|
28
|
+
:source="part.source"
|
|
29
|
+
@annotation-hover="(e, a) => emit('annotationHover', e, a)"
|
|
30
|
+
@annotation-leave="emit('annotationLeave')"
|
|
31
|
+
@annotation-tap="(e, a) => emit('annotationTap', e, a)"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.part-group {
|
|
38
|
+
margin-bottom: 40px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.part-group:last-child {
|
|
42
|
+
margin-bottom: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.part-group-label {
|
|
46
|
+
font-size: 28px;
|
|
47
|
+
font-weight: 900;
|
|
48
|
+
letter-spacing: 6px;
|
|
49
|
+
color: var(--ink);
|
|
50
|
+
padding-bottom: 12px;
|
|
51
|
+
margin-bottom: 8px;
|
|
52
|
+
border-bottom: 3px solid var(--vermillion);
|
|
53
|
+
display: inline-block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.part-group--vertical {
|
|
57
|
+
writing-mode: vertical-rl;
|
|
58
|
+
text-orientation: mixed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.part-group--vertical .part-group-label {
|
|
62
|
+
font-size: 28px;
|
|
63
|
+
letter-spacing: 10px;
|
|
64
|
+
border-bottom: none;
|
|
65
|
+
border-left: 3px solid var(--vermillion);
|
|
66
|
+
padding-bottom: 0;
|
|
67
|
+
padding-left: 16px;
|
|
68
|
+
margin-bottom: 0;
|
|
69
|
+
margin-left: 12px;
|
|
70
|
+
display: block;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
defineEmits<{ click: [] }>()
|
|
10
10
|
|
|
11
11
|
const preview = computed(() => {
|
|
12
|
-
const max =
|
|
12
|
+
const max = 2
|
|
13
13
|
return props.poem.verses.slice(0, max).map(v => v.text).join('\n')
|
|
14
14
|
})
|
|
15
15
|
</script>
|
|
@@ -33,7 +33,7 @@ const preview = computed(() => {
|
|
|
33
33
|
border: 1px solid var(--border-light);
|
|
34
34
|
border-radius: 8px;
|
|
35
35
|
cursor: pointer;
|
|
36
|
-
transition: all 0.
|
|
36
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
37
37
|
overflow: hidden;
|
|
38
38
|
}
|
|
39
39
|
.pc-accent {
|
|
@@ -41,12 +41,12 @@ const preview = computed(() => {
|
|
|
41
41
|
top: 0; left: 0;
|
|
42
42
|
width: 3px; height: 0;
|
|
43
43
|
background: var(--vermillion);
|
|
44
|
-
transition: height 0.
|
|
44
|
+
transition: height 0.3s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
45
45
|
}
|
|
46
46
|
.pc-root:hover {
|
|
47
|
-
transform: translateY(-
|
|
48
|
-
box-shadow: 0
|
|
49
|
-
border-color: var(--gold
|
|
47
|
+
transform: translateY(-2px);
|
|
48
|
+
box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.08);
|
|
49
|
+
border-color: var(--gold);
|
|
50
50
|
}
|
|
51
51
|
.pc-root:hover .pc-accent { height: 100%; }
|
|
52
52
|
.pc-body { padding: 24px; }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import { useReadingMode, THEMES, THEME_LABELS } from '../composables/useReadingMode'
|
|
4
|
-
import type { LayoutMode } from '../composables/useReadingMode'
|
|
3
|
+
import { useReadingMode, THEMES, THEME_LABELS, FONT_SIZES } from '../composables/useReadingMode'
|
|
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, setTheme, setLayout } = useReadingMode()
|
|
7
|
+
const { theme, layout, mainFontSize, bodyFontSize, setTheme, setLayout, setMainFontSize, setBodyFontSize } = useReadingMode()
|
|
8
8
|
const { t, setLocale, locale, availableLocales, localeLabels } = useI18n()
|
|
9
9
|
const open = ref(false)
|
|
10
10
|
|
|
@@ -46,6 +46,22 @@ function close() { open.value = false }
|
|
|
46
46
|
>{{ THEME_LABELS[t] }}</button>
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
|
+
<div class="rt-group">
|
|
50
|
+
<div class="rt-label">{{ t('settings.mainFontSize') }}</div>
|
|
51
|
+
<div class="rt-size-row">
|
|
52
|
+
<button class="rt-size-btn" @click="setMainFontSize(FONT_SIZES[Math.max(0, FONT_SIZES.indexOf(mainFontSize) - 1)] as FontSize)">−</button>
|
|
53
|
+
<span class="rt-size-val">{{ mainFontSize }}</span>
|
|
54
|
+
<button class="rt-size-btn" @click="setMainFontSize(FONT_SIZES[Math.min(FONT_SIZES.length - 1, FONT_SIZES.indexOf(mainFontSize) + 1)] as FontSize)">+</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="rt-group">
|
|
58
|
+
<div class="rt-label">{{ t('settings.bodyFontSize') }}</div>
|
|
59
|
+
<div class="rt-size-row">
|
|
60
|
+
<button class="rt-size-btn" @click="setBodyFontSize(FONT_SIZES[Math.max(0, FONT_SIZES.indexOf(bodyFontSize) - 1)] as FontSize)">−</button>
|
|
61
|
+
<span class="rt-size-val">{{ bodyFontSize }}</span>
|
|
62
|
+
<button class="rt-size-btn" @click="setBodyFontSize(FONT_SIZES[Math.min(FONT_SIZES.length - 1, FONT_SIZES.indexOf(bodyFontSize) + 1)] as FontSize)">+</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
49
65
|
<div class="rt-group">
|
|
50
66
|
<div class="rt-label">{{ t('settings.language') }}</div>
|
|
51
67
|
<div class="rt-options">
|
|
@@ -79,13 +95,14 @@ function close() { open.value = false }
|
|
|
79
95
|
font-size: 16px;
|
|
80
96
|
cursor: pointer;
|
|
81
97
|
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.12);
|
|
82
|
-
transition: all 0.
|
|
98
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
83
99
|
display: flex; align-items: center; justify-content: center;
|
|
84
100
|
}
|
|
85
101
|
.rt-fab:hover {
|
|
86
102
|
background: var(--ink);
|
|
87
103
|
color: var(--paper);
|
|
88
104
|
border-color: var(--ink);
|
|
105
|
+
transform: scale(1.05);
|
|
89
106
|
}
|
|
90
107
|
.rt-icon {
|
|
91
108
|
font-family: var(--sans);
|
|
@@ -102,7 +119,7 @@ function close() { open.value = false }
|
|
|
102
119
|
border-radius: 8px;
|
|
103
120
|
box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.16);
|
|
104
121
|
padding: 16px;
|
|
105
|
-
animation: slideUp 0.
|
|
122
|
+
animation: slideUp 0.25s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
106
123
|
}
|
|
107
124
|
@keyframes slideUp {
|
|
108
125
|
from { opacity: 0; transform: translateY(8px); }
|
|
@@ -138,6 +155,29 @@ function close() { open.value = false }
|
|
|
138
155
|
color: var(--paper);
|
|
139
156
|
border-color: var(--ink);
|
|
140
157
|
}
|
|
158
|
+
.rt-size-row {
|
|
159
|
+
display: flex; align-items: center; gap: 6px; justify-content: center;
|
|
160
|
+
}
|
|
161
|
+
.rt-size-btn {
|
|
162
|
+
width: 28px; height: 28px;
|
|
163
|
+
border: 1px solid var(--border);
|
|
164
|
+
border-radius: 4px;
|
|
165
|
+
background: none;
|
|
166
|
+
font-family: var(--sans);
|
|
167
|
+
font-size: 14px;
|
|
168
|
+
color: var(--ink-mid);
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
display: flex; align-items: center; justify-content: center;
|
|
171
|
+
transition: all 0.15s;
|
|
172
|
+
}
|
|
173
|
+
.rt-size-btn:hover { border-color: var(--ink); color: var(--ink); }
|
|
174
|
+
.rt-size-val {
|
|
175
|
+
font-family: var(--sans);
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
color: var(--ink);
|
|
178
|
+
min-width: 32px;
|
|
179
|
+
text-align: center;
|
|
180
|
+
}
|
|
141
181
|
.rt-backdrop {
|
|
142
182
|
position: fixed; inset: 0;
|
|
143
183
|
z-index: -1;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
3
4
|
import { useReadingMode, THEMES, THEME_LABELS, FONT_SIZES } from '../composables/useReadingMode'
|
|
4
5
|
import type { LayoutMode, FontSize } from '../composables/useReadingMode'
|
|
5
6
|
import { useI18n, LOCALE_LABELS, type Locale } from '../composables/useI18n'
|
|
6
|
-
import
|
|
7
|
+
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
7
8
|
|
|
8
9
|
defineProps<{
|
|
9
10
|
context?: string
|
|
@@ -19,6 +20,8 @@ const emit = defineEmits<{
|
|
|
19
20
|
|
|
20
21
|
const { theme, layout, mainFontSize, bodyFontSize, setTheme, setLayout, setMainFontSize, setBodyFontSize } = useReadingMode()
|
|
21
22
|
const { t, setLocale, locale, availableLocales, localeLabels } = useI18n()
|
|
23
|
+
const { logoUrl } = useSiteConfig()
|
|
24
|
+
const router = useRouter()
|
|
22
25
|
const settingsOpen = ref(false)
|
|
23
26
|
|
|
24
27
|
function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
@@ -27,7 +30,8 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
27
30
|
<template>
|
|
28
31
|
<nav class="sidenav">
|
|
29
32
|
<button class="sn-brand" @click="emit('home')" title="首頁">
|
|
30
|
-
<img :src="
|
|
33
|
+
<img v-if="logoUrl" :src="logoUrl" alt="" class="sn-logo" />
|
|
34
|
+
<span v-else class="sn-seal">漢流</span>
|
|
31
35
|
</button>
|
|
32
36
|
|
|
33
37
|
<button class="sn-btn" @click="emit('back')" title="返回">
|
|
@@ -45,6 +49,10 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
45
49
|
|
|
46
50
|
<div class="sn-spacer" />
|
|
47
51
|
|
|
52
|
+
<button class="sn-btn" @click="router.push('/about')" title="關於">
|
|
53
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
48
56
|
<button
|
|
49
57
|
class="sn-btn"
|
|
50
58
|
:class="{ active: settingsOpen }"
|
|
@@ -128,7 +136,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
128
136
|
|
|
129
137
|
.sn-brand {
|
|
130
138
|
width: 40px; height: 48px;
|
|
131
|
-
border:
|
|
139
|
+
border: 2px solid var(--vermillion);
|
|
132
140
|
border-radius: 3px;
|
|
133
141
|
background: none;
|
|
134
142
|
display: flex; align-items: center; justify-content: center;
|
|
@@ -143,6 +151,18 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
143
151
|
width: auto;
|
|
144
152
|
object-fit: contain;
|
|
145
153
|
}
|
|
154
|
+
.sn-brand:has(.sn-logo) { border: none; }
|
|
155
|
+
.sn-seal {
|
|
156
|
+
writing-mode: vertical-rl;
|
|
157
|
+
text-orientation: upright;
|
|
158
|
+
font-family: var(--serif);
|
|
159
|
+
font-size: 14px; font-weight: 900;
|
|
160
|
+
color: var(--vermillion);
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
letter-spacing: 2px;
|
|
164
|
+
line-height: 1;
|
|
165
|
+
}
|
|
146
166
|
|
|
147
167
|
.sn-btn {
|
|
148
168
|
width: 36px; height: 36px;
|
|
@@ -292,6 +312,7 @@ function toggleSettings() { settingsOpen.value = !settingsOpen.value }
|
|
|
292
312
|
@media (max-width: 768px) {
|
|
293
313
|
.sidenav { width: 44px; padding: 8px 0; gap: 6px; }
|
|
294
314
|
.sn-brand { width: 32px; height: 38px; }
|
|
315
|
+
.sn-seal { font-size: 15px; }
|
|
295
316
|
.sn-btn { width: 30px; height: 30px; }
|
|
296
317
|
.sn-context { font-size: 10px; max-height: 80px; }
|
|
297
318
|
}
|
package/template/src/main.ts
CHANGED
|
@@ -17,6 +17,10 @@ export async function createApp() {
|
|
|
17
17
|
if (!import.meta.env.SSR) {
|
|
18
18
|
createApp().then(({ app, router }) => {
|
|
19
19
|
app.mount('#app')
|
|
20
|
-
document.getElementById('app-loading')
|
|
20
|
+
const loader = document.getElementById('app-loading')
|
|
21
|
+
if (loader) {
|
|
22
|
+
loader.classList.add('fade-out')
|
|
23
|
+
loader.addEventListener('transitionend', () => loader.remove())
|
|
24
|
+
}
|
|
21
25
|
})
|
|
22
26
|
}
|
package/template/src/router.ts
CHANGED
|
@@ -8,9 +8,11 @@ import LibraryHome from './views/LibraryHome.vue'
|
|
|
8
8
|
import BookHome from './views/BookHome.vue'
|
|
9
9
|
import PieceView from './views/PieceView.vue'
|
|
10
10
|
import AuthorView from './views/AuthorView.vue'
|
|
11
|
+
import AboutView from './views/AboutView.vue'
|
|
11
12
|
|
|
12
13
|
export const routes: RouteRecordRaw[] = [
|
|
13
14
|
{ path: '/', component: LibraryHome },
|
|
15
|
+
{ path: '/about', component: AboutView },
|
|
14
16
|
{ path: '/author/:name', component: AuthorView, props: true },
|
|
15
17
|
{ path: '/:bookId', component: BookHome, props: true },
|
|
16
18
|
{ path: '/:bookId/:num', component: PieceView, props: true },
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
--serif: 'Noto Serif TC', '宋體-繁', serif;
|
|
90
90
|
--sans: 'Noto Sans TC', 'PingFang TC', sans-serif;
|
|
91
91
|
--nav-width: 56px;
|
|
92
|
+
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
|
92
93
|
}
|
|
93
94
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
94
95
|
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
|
|
@@ -103,6 +104,7 @@ body {
|
|
|
103
104
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
104
105
|
::-webkit-scrollbar-track { background: transparent; }
|
|
105
106
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
107
|
+
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
|
|
106
108
|
|
|
107
109
|
a { color: inherit; text-decoration: none; }
|
|
108
110
|
|
|
@@ -113,21 +115,56 @@ a { color: inherit; text-decoration: none; }
|
|
|
113
115
|
display: flex; flex-direction: column;
|
|
114
116
|
align-items: center; justify-content: center;
|
|
115
117
|
z-index: 9999;
|
|
118
|
+
transition: opacity 0.4s ease;
|
|
119
|
+
}
|
|
120
|
+
#app-loading.fade-out {
|
|
121
|
+
opacity: 0;
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
}
|
|
124
|
+
#app-loading .seal {
|
|
125
|
+
width: 72px; height: 72px;
|
|
126
|
+
border: 2px solid var(--vermillion);
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
display: flex; align-items: center; justify-content: center;
|
|
129
|
+
animation: sealReveal 0.8s var(--ease-out-expo) forwards;
|
|
130
|
+
opacity: 0;
|
|
116
131
|
}
|
|
117
132
|
#app-loading .char {
|
|
118
133
|
font-family: var(--serif);
|
|
119
|
-
font-size:
|
|
120
|
-
color: var(--
|
|
121
|
-
|
|
134
|
+
font-size: 36px; font-weight: 900;
|
|
135
|
+
color: var(--vermillion);
|
|
136
|
+
line-height: 1;
|
|
137
|
+
}
|
|
138
|
+
#app-loading .line {
|
|
139
|
+
width: 1px; height: 40px;
|
|
140
|
+
background: linear-gradient(180deg, var(--vermillion), transparent);
|
|
141
|
+
margin-top: 24px;
|
|
142
|
+
animation: lineGrow 0.6s 0.3s var(--ease-out-expo) forwards;
|
|
143
|
+
transform: scaleY(0);
|
|
144
|
+
transform-origin: top;
|
|
122
145
|
}
|
|
123
146
|
#app-loading .text {
|
|
124
147
|
font-family: var(--sans);
|
|
125
|
-
font-size:
|
|
126
|
-
letter-spacing:
|
|
148
|
+
font-size: 11px; color: var(--ink-faint);
|
|
149
|
+
letter-spacing: 6px; margin-top: 16px;
|
|
150
|
+
animation: fadeIn 0.5s 0.5s ease forwards;
|
|
151
|
+
opacity: 0;
|
|
127
152
|
}
|
|
128
|
-
@keyframes
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
@keyframes sealReveal {
|
|
154
|
+
from { opacity: 0; transform: scale(0.8); }
|
|
155
|
+
to { opacity: 1; transform: scale(1); }
|
|
156
|
+
}
|
|
157
|
+
@keyframes lineGrow {
|
|
158
|
+
to { transform: scaleY(1); }
|
|
159
|
+
}
|
|
160
|
+
@keyframes fadeIn {
|
|
161
|
+
to { opacity: 1; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ===== CONTENT ENTRANCE ANIMATION ===== */
|
|
165
|
+
@keyframes enterUp {
|
|
166
|
+
from { opacity: 0; transform: translateY(16px); }
|
|
167
|
+
to { opacity: 1; transform: translateY(0); }
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
/* ===== RESPONSIVE ===== */
|
package/template/src/types.ts
CHANGED
|
@@ -123,6 +123,15 @@ export interface ProseSection {
|
|
|
123
123
|
order: number
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export interface Part {
|
|
127
|
+
num: number
|
|
128
|
+
group?: string
|
|
129
|
+
title?: string
|
|
130
|
+
source?: PieceSource
|
|
131
|
+
verses: VerseLine[]
|
|
132
|
+
annotations: Annotation[]
|
|
133
|
+
}
|
|
134
|
+
|
|
126
135
|
export interface Piece {
|
|
127
136
|
bookId: string
|
|
128
137
|
num: number
|
|
@@ -139,6 +148,7 @@ export interface Piece {
|
|
|
139
148
|
annotationLayers?: AnnotationLayer[]
|
|
140
149
|
source?: PieceSource
|
|
141
150
|
contributors?: PieceContributor[]
|
|
151
|
+
parts?: Part[]
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
// Backward compatibility alias
|
|
@@ -5,12 +5,13 @@ import { useReadingMode } from '../composables/useReadingMode'
|
|
|
5
5
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
6
6
|
import SideNav from '../components/SideNav.vue'
|
|
7
7
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
8
|
-
import
|
|
8
|
+
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
9
9
|
import { ref, computed } from 'vue'
|
|
10
10
|
import { useRouter } from 'vue-router'
|
|
11
11
|
|
|
12
12
|
const { t, locale } = useI18n()
|
|
13
13
|
const { layout } = useReadingMode()
|
|
14
|
+
const { logoUrl } = useSiteConfig()
|
|
14
15
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
15
16
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
16
17
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
@@ -45,7 +46,8 @@ function goHome() { router.push('/') }
|
|
|
45
46
|
<h1 class="h-page-title">關於漢流 / About Hanology</h1>
|
|
46
47
|
</header>
|
|
47
48
|
<div class="h-content">
|
|
48
|
-
<img :src="
|
|
49
|
+
<img v-if="logoUrl" :src="logoUrl" alt="" class="h-logo" />
|
|
50
|
+
<div v-else class="h-seal">漢流</div>
|
|
49
51
|
<div class="h-about-block">
|
|
50
52
|
<h2>漢流 · Hanology</h2>
|
|
51
53
|
<p><strong>漢流</strong>,粵音 Han-Lou,普音 Han-Liu,意為「漢學之流」。</p>
|
|
@@ -141,6 +143,19 @@ function goHome() { router.push('/') }
|
|
|
141
143
|
margin: 0 auto 40px;
|
|
142
144
|
display: block;
|
|
143
145
|
}
|
|
146
|
+
.h-seal {
|
|
147
|
+
writing-mode: vertical-rl;
|
|
148
|
+
text-orientation: upright;
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: center; justify-content: center;
|
|
151
|
+
width: 56px; height: 72px;
|
|
152
|
+
border: 2px solid var(--vermillion);
|
|
153
|
+
color: var(--vermillion);
|
|
154
|
+
font-size: 24px; font-family: var(--serif);
|
|
155
|
+
font-weight: 900; letter-spacing: 2px;
|
|
156
|
+
margin: 0 auto 40px; border-radius: 4px;
|
|
157
|
+
line-height: 1;
|
|
158
|
+
}
|
|
144
159
|
.h-about-block {
|
|
145
160
|
margin-bottom: 40px; padding: 32px;
|
|
146
161
|
background: var(--surface);
|
|
@@ -9,7 +9,7 @@ import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
|
9
9
|
import BookCard from '../components/BookCard.vue'
|
|
10
10
|
import SideNav from '../components/SideNav.vue'
|
|
11
11
|
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
12
|
-
import
|
|
12
|
+
import { useSiteConfig } from '../composables/useSiteConfig'
|
|
13
13
|
import type { BookMeta } from '../types'
|
|
14
14
|
|
|
15
15
|
const { scale, books, singleBook, loadLibrary } = useLibrary()
|
|
@@ -36,6 +36,7 @@ if (scale.value === 'single-piece' && singleBook.value) {
|
|
|
36
36
|
|
|
37
37
|
const router = useRouter()
|
|
38
38
|
const { layout } = useReadingMode()
|
|
39
|
+
const { logoUrl } = useSiteConfig()
|
|
39
40
|
const isVertical = computed(() => layout.value === 'vertical')
|
|
40
41
|
const vPageRef = ref<HTMLElement | null>(null)
|
|
41
42
|
const vScroll = useHorizontalScroll(vPageRef)
|
|
@@ -79,6 +80,9 @@ function openBook(bookId: string) {
|
|
|
79
80
|
<div v-if="isVertical" class="v-root">
|
|
80
81
|
<SideNav @home="router.push('/')" @back="router.push('/')" />
|
|
81
82
|
<div ref="vPageRef" class="v-page">
|
|
83
|
+
<div class="v-about-col">
|
|
84
|
+
<router-link to="/about" class="v-about-link">關 於</router-link>
|
|
85
|
+
</div>
|
|
82
86
|
<section class="v-hero">
|
|
83
87
|
<h1 class="v-title">古 典 詩 文 圖 書 館</h1>
|
|
84
88
|
<p class="v-subtitle">Classical Chinese Text Library</p>
|
|
@@ -106,7 +110,8 @@ function openBook(bookId: string) {
|
|
|
106
110
|
<!-- ═══════ 橫排模式 ═══════ -->
|
|
107
111
|
<div v-else class="lib-root">
|
|
108
112
|
<header class="lib-hero">
|
|
109
|
-
<img :src="
|
|
113
|
+
<img v-if="logoUrl" :src="logoUrl" alt="" class="lib-logo" />
|
|
114
|
+
<div v-else class="lib-seal">漢流</div>
|
|
110
115
|
<h1>古典詩文圖書館</h1>
|
|
111
116
|
<p class="lib-subtitle">Classical Chinese Text Library</p>
|
|
112
117
|
<div class="lib-stats-bar">
|
|
@@ -114,6 +119,7 @@ function openBook(bookId: string) {
|
|
|
114
119
|
<span class="lib-stat-sep">·</span>
|
|
115
120
|
<span class="lib-stat">{{ totalPieces }} 篇</span>
|
|
116
121
|
</div>
|
|
122
|
+
<router-link to="/about" class="lib-about-link">關於</router-link>
|
|
117
123
|
</header>
|
|
118
124
|
<div v-for="group in groupedBooks" :key="group.category" class="lib-group">
|
|
119
125
|
<h2 class="lib-group-title">{{ group.category }}</h2>
|
|
@@ -172,6 +178,31 @@ function openBook(bookId: string) {
|
|
|
172
178
|
justify-content: center;
|
|
173
179
|
padding: 40px 20px;
|
|
174
180
|
}
|
|
181
|
+
.v-about-col {
|
|
182
|
+
writing-mode: vertical-rl;
|
|
183
|
+
text-orientation: mixed;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
height: 100vh;
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
padding: 0 12px;
|
|
190
|
+
border-right: 1px solid var(--border-light);
|
|
191
|
+
}
|
|
192
|
+
.v-about-link {
|
|
193
|
+
font-size: 14px;
|
|
194
|
+
color: var(--ink-faint);
|
|
195
|
+
letter-spacing: 6px;
|
|
196
|
+
font-family: var(--sans);
|
|
197
|
+
padding: 12px 8px;
|
|
198
|
+
border: 1px solid var(--border-light);
|
|
199
|
+
border-radius: 2px;
|
|
200
|
+
transition: all 0.2s;
|
|
201
|
+
}
|
|
202
|
+
.v-about-link:hover {
|
|
203
|
+
color: var(--ink);
|
|
204
|
+
border-color: var(--ink);
|
|
205
|
+
}
|
|
175
206
|
.v-title {
|
|
176
207
|
font-size: 48px; font-weight: 900;
|
|
177
208
|
letter-spacing: 16px; color: var(--ink);
|
|
@@ -274,6 +305,22 @@ function openBook(bookId: string) {
|
|
|
274
305
|
object-fit: contain;
|
|
275
306
|
margin-bottom: 24px;
|
|
276
307
|
}
|
|
308
|
+
.lib-seal {
|
|
309
|
+
writing-mode: vertical-rl;
|
|
310
|
+
text-orientation: upright;
|
|
311
|
+
display: inline-flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
width: 40px; height: 56px;
|
|
315
|
+
border: 2px solid var(--vermillion);
|
|
316
|
+
color: var(--vermillion);
|
|
317
|
+
font-size: 20px;
|
|
318
|
+
font-family: var(--serif);
|
|
319
|
+
letter-spacing: 2px;
|
|
320
|
+
margin-bottom: 24px;
|
|
321
|
+
border-radius: 4px;
|
|
322
|
+
line-height: 1;
|
|
323
|
+
}
|
|
277
324
|
.lib-hero h1 {
|
|
278
325
|
font-size: 36px;
|
|
279
326
|
font-weight: 700;
|
|
@@ -300,6 +347,24 @@ function openBook(bookId: string) {
|
|
|
300
347
|
}
|
|
301
348
|
.lib-stat-sep { color: var(--border); }
|
|
302
349
|
|
|
350
|
+
.lib-about-link {
|
|
351
|
+
display: inline-block;
|
|
352
|
+
margin-top: 16px;
|
|
353
|
+
font-family: var(--sans);
|
|
354
|
+
font-size: 13px;
|
|
355
|
+
color: var(--ink-faint);
|
|
356
|
+
letter-spacing: 2px;
|
|
357
|
+
text-decoration: none;
|
|
358
|
+
padding: 4px 12px;
|
|
359
|
+
border: 1px solid var(--border-light);
|
|
360
|
+
border-radius: 4px;
|
|
361
|
+
transition: all 0.2s;
|
|
362
|
+
}
|
|
363
|
+
.lib-about-link:hover {
|
|
364
|
+
color: var(--ink);
|
|
365
|
+
border-color: var(--ink);
|
|
366
|
+
}
|
|
367
|
+
|
|
303
368
|
.lib-group { margin-bottom: 40px; }
|
|
304
369
|
.lib-group-title {
|
|
305
370
|
font-size: 15px;
|
|
@@ -13,7 +13,8 @@ import SectionBlock from '../components/SectionBlock.vue'
|
|
|
13
13
|
import AnnotationTooltip from '../components/AnnotationTooltip.vue'
|
|
14
14
|
import AnnotationControlBar from '../components/AnnotationControlBar.vue'
|
|
15
15
|
import SideNav from '../components/SideNav.vue'
|
|
16
|
-
import
|
|
16
|
+
import PartGroup from '../components/PartGroup.vue'
|
|
17
|
+
import type { Piece, Annotation, AnnotationLayer, Part } from '../types'
|
|
17
18
|
|
|
18
19
|
const props = defineProps<{ bookId: string; num: string | number }>()
|
|
19
20
|
const router = useRouter()
|
|
@@ -128,6 +129,29 @@ function getHeadword(ann: Annotation): string {
|
|
|
128
129
|
// Initialize layers when piece loads
|
|
129
130
|
watch(() => piece.value, () => initLayers(), { immediate: true })
|
|
130
131
|
|
|
132
|
+
// ─── Multi-part ───────────────────────────────────────────────
|
|
133
|
+
const isMultiPart = computed(() => (piece.value?.parts?.length ?? 0) > 0)
|
|
134
|
+
|
|
135
|
+
const partGroups = computed<{ label: string; parts: Part[] }[]>(() => {
|
|
136
|
+
if (!piece.value?.parts?.length) return []
|
|
137
|
+
const groupMap = new Map<string, Part[]>()
|
|
138
|
+
for (const part of piece.value.parts) {
|
|
139
|
+
const key = part.group || ''
|
|
140
|
+
if (!groupMap.has(key)) groupMap.set(key, [])
|
|
141
|
+
groupMap.get(key)!.push(part)
|
|
142
|
+
}
|
|
143
|
+
return [...groupMap.entries()].map(([label, parts]) => ({ label, parts }))
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const allPartAnnotations = computed<Annotation[]>(() => {
|
|
147
|
+
if (!piece.value?.parts) return []
|
|
148
|
+
return piece.value.parts.flatMap(p => p.annotations)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const totalPartAnnotationCount = computed(() => {
|
|
152
|
+
return piece.value?.parts?.reduce((sum, p) => sum + p.annotations.length, 0) ?? 0
|
|
153
|
+
})
|
|
154
|
+
|
|
131
155
|
const SECTION_META: Record<string, { label: string; special: boolean }> = {
|
|
132
156
|
background: { label: '背景資料', special: false },
|
|
133
157
|
analysis: { label: '賞析重點', special: false },
|
|
@@ -232,12 +256,31 @@ function tcy(n: number): string {
|
|
|
232
256
|
← {{ meta?.title }}
|
|
233
257
|
</div>
|
|
234
258
|
<div class="v-poem-meta">
|
|
235
|
-
<
|
|
236
|
-
|
|
259
|
+
<template v-if="isMultiPart">
|
|
260
|
+
<span class="v-meta-item" v-html="tcy(piece.parts!.length) + ' 段'" />
|
|
261
|
+
<span class="v-meta-item" v-html="totalPartAnnotationCount > 0 ? tcy(totalPartAnnotationCount) + ' 注' : '無注'" />
|
|
262
|
+
</template>
|
|
263
|
+
<template v-else>
|
|
264
|
+
<span class="v-meta-item" v-html="tcy(piece.verses.length) + ' 段'" />
|
|
265
|
+
<span class="v-meta-item" v-html="piece.annotations.length > 0 ? tcy(piece.annotations.length) + ' 注' : '無注'" />
|
|
266
|
+
</template>
|
|
237
267
|
</div>
|
|
238
268
|
</section>
|
|
239
269
|
|
|
240
|
-
<section class="v-poem-col">
|
|
270
|
+
<section v-if="isMultiPart" class="v-poem-col v-multipart">
|
|
271
|
+
<PartGroup
|
|
272
|
+
v-for="group in partGroups"
|
|
273
|
+
:key="group.label"
|
|
274
|
+
:label="group.label"
|
|
275
|
+
:parts="group.parts"
|
|
276
|
+
:vertical="true"
|
|
277
|
+
@annotation-hover="interaction.onHover"
|
|
278
|
+
@annotation-leave="interaction.onLeave"
|
|
279
|
+
@annotation-tap="interaction.onTap"
|
|
280
|
+
/>
|
|
281
|
+
</section>
|
|
282
|
+
|
|
283
|
+
<section v-else class="v-poem-col">
|
|
241
284
|
<VerticalScroll
|
|
242
285
|
:title="''"
|
|
243
286
|
:author="''"
|
|
@@ -252,7 +295,7 @@ function tcy(n: number): string {
|
|
|
252
295
|
</section>
|
|
253
296
|
|
|
254
297
|
<SectionBlock
|
|
255
|
-
v-if="annotationsVisible && piece.sections.annotations"
|
|
298
|
+
v-if="!isMultiPart && annotationsVisible && piece.sections.annotations"
|
|
256
299
|
num=""
|
|
257
300
|
label="注釋"
|
|
258
301
|
:special="false"
|
|
@@ -370,14 +413,32 @@ function tcy(n: number): string {
|
|
|
370
413
|
<span v-else class="h-author-link" @click="openAuthorPane">{{ piece.author }}</span>
|
|
371
414
|
</div>
|
|
372
415
|
<div class="h-controls">
|
|
373
|
-
<
|
|
374
|
-
|
|
416
|
+
<template v-if="isMultiPart">
|
|
417
|
+
<span class="h-tag">{{ piece.parts!.length }} 段</span>
|
|
418
|
+
<span class="h-tag">{{ totalPartAnnotationCount > 0 ? totalPartAnnotationCount + ' 注' : '無注' }}</span>
|
|
419
|
+
</template>
|
|
420
|
+
<template v-else>
|
|
421
|
+
<span class="h-tag">{{ piece.verses.length }} 段</span>
|
|
422
|
+
<span class="h-tag">{{ piece.annotations.length > 0 ? piece.annotations.length + ' 注' : '無注' }}</span>
|
|
423
|
+
</template>
|
|
375
424
|
</div>
|
|
376
425
|
</div>
|
|
377
426
|
</nav>
|
|
378
427
|
|
|
379
428
|
<div class="h-content">
|
|
380
|
-
<div class="h-
|
|
429
|
+
<div v-if="isMultiPart" class="h-multipart">
|
|
430
|
+
<PartGroup
|
|
431
|
+
v-for="group in partGroups"
|
|
432
|
+
:key="group.label"
|
|
433
|
+
:label="group.label"
|
|
434
|
+
:parts="group.parts"
|
|
435
|
+
@annotation-hover="interaction.onHover"
|
|
436
|
+
@annotation-leave="interaction.onLeave"
|
|
437
|
+
@annotation-tap="interaction.onTap"
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div v-else class="h-poem-block">
|
|
381
442
|
<HorizontalDisplay
|
|
382
443
|
:title="piece.title"
|
|
383
444
|
:author="piece.author"
|
|
@@ -551,6 +612,22 @@ function tcy(n: number): string {
|
|
|
551
612
|
padding: 24px;
|
|
552
613
|
}
|
|
553
614
|
|
|
615
|
+
.v-multipart {
|
|
616
|
+
display: flex;
|
|
617
|
+
flex-direction: row-reverse;
|
|
618
|
+
align-items: flex-start;
|
|
619
|
+
gap: 0;
|
|
620
|
+
max-height: calc(100vh - 120px);
|
|
621
|
+
overflow-x: auto;
|
|
622
|
+
overflow-y: hidden;
|
|
623
|
+
padding: 24px 16px;
|
|
624
|
+
scrollbar-width: thin;
|
|
625
|
+
scrollbar-color: var(--gold) var(--paper);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.v-multipart::-webkit-scrollbar { height: 4px; }
|
|
629
|
+
.v-multipart::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
630
|
+
|
|
554
631
|
.v-section {
|
|
555
632
|
flex-shrink: 0;
|
|
556
633
|
}
|
|
@@ -564,7 +641,7 @@ function tcy(n: number): string {
|
|
|
564
641
|
|
|
565
642
|
.v-source-link {
|
|
566
643
|
font-size: 12px;
|
|
567
|
-
color: var(--
|
|
644
|
+
color: var(--vermillion);
|
|
568
645
|
cursor: pointer;
|
|
569
646
|
margin-top: 4px;
|
|
570
647
|
opacity: 0.8;
|
|
@@ -663,6 +740,16 @@ function tcy(n: number): string {
|
|
|
663
740
|
.h-poem-block {
|
|
664
741
|
margin-bottom: 60px; display: flex; justify-content: center;
|
|
665
742
|
}
|
|
743
|
+
|
|
744
|
+
.h-multipart {
|
|
745
|
+
max-width: min(680px, calc(100vw - 80px));
|
|
746
|
+
margin: 0 auto 60px;
|
|
747
|
+
background: var(--surface);
|
|
748
|
+
border: 1px solid var(--border);
|
|
749
|
+
border-radius: 8px;
|
|
750
|
+
padding: 32px 40px;
|
|
751
|
+
box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.08);
|
|
752
|
+
}
|
|
666
753
|
.h-sections {
|
|
667
754
|
max-width: min(680px, calc(100vw - 80px));
|
|
668
755
|
margin: 0 auto; padding-bottom: 80px;
|
|
@@ -678,7 +765,7 @@ function tcy(n: number): string {
|
|
|
678
765
|
}
|
|
679
766
|
|
|
680
767
|
.h-source-link {
|
|
681
|
-
color: var(--
|
|
768
|
+
color: var(--vermillion);
|
|
682
769
|
cursor: pointer;
|
|
683
770
|
font-size: 13px;
|
|
684
771
|
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { AnnotationLayer } from '../types'
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{
|
|
5
|
-
layers: AnnotationLayer[]
|
|
6
|
-
activeIds: string[]
|
|
7
|
-
}>()
|
|
8
|
-
|
|
9
|
-
const emit = defineEmits<{
|
|
10
|
-
'update:activeIds': [ids: string[]]
|
|
11
|
-
}>()
|
|
12
|
-
|
|
13
|
-
function toggle(id: string) {
|
|
14
|
-
const current = props.activeIds
|
|
15
|
-
if (current.includes(id)) {
|
|
16
|
-
emit('update:activeIds', current.filter(x => x !== id))
|
|
17
|
-
} else {
|
|
18
|
-
emit('update:activeIds', [...current, id])
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<template>
|
|
24
|
-
<div v-if="layers.length > 1" class="layer-selector">
|
|
25
|
-
<button
|
|
26
|
-
v-for="layer in layers"
|
|
27
|
-
:key="layer.id"
|
|
28
|
-
:class="['layer-btn', { active: activeIds.includes(layer.id) }]"
|
|
29
|
-
:title="layer.label"
|
|
30
|
-
@click="toggle(layer.id)"
|
|
31
|
-
>
|
|
32
|
-
{{ layer.shortLabel }}
|
|
33
|
-
</button>
|
|
34
|
-
</div>
|
|
35
|
-
</template>
|
|
36
|
-
|
|
37
|
-
<style scoped>
|
|
38
|
-
.layer-selector {
|
|
39
|
-
display: flex;
|
|
40
|
-
gap: 6px;
|
|
41
|
-
flex-wrap: wrap;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.layer-btn {
|
|
45
|
-
border: 1px solid var(--border);
|
|
46
|
-
border-radius: 4px;
|
|
47
|
-
padding: 4px 12px;
|
|
48
|
-
font-size: 13px;
|
|
49
|
-
background: var(--surface);
|
|
50
|
-
color: var(--ink-mid);
|
|
51
|
-
cursor: pointer;
|
|
52
|
-
transition: all 0.15s;
|
|
53
|
-
font-family: var(--sans);
|
|
54
|
-
letter-spacing: 1px;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.layer-btn:hover {
|
|
58
|
-
border-color: var(--gold);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.layer-btn.active {
|
|
62
|
-
background: var(--ink);
|
|
63
|
-
color: var(--paper);
|
|
64
|
-
border-color: var(--ink);
|
|
65
|
-
}
|
|
66
|
-
</style>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { computed, ref, type Ref } from 'vue'
|
|
2
|
-
import { useReadingMode } from './useReadingMode'
|
|
3
|
-
import { useHorizontalScroll } from './useHorizontalScroll'
|
|
4
|
-
|
|
5
|
-
export interface PageLayoutConfig {
|
|
6
|
-
mode: 'vertical' | 'horizontal'
|
|
7
|
-
navSide: 'top' | 'right'
|
|
8
|
-
contentDirection: 'ltr' | 'rtl'
|
|
9
|
-
isVertical: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function usePageLayout() {
|
|
13
|
-
const { layout } = useReadingMode()
|
|
14
|
-
const scrollRef = ref<HTMLElement | null>(null)
|
|
15
|
-
const scroll = useHorizontalScroll(scrollRef)
|
|
16
|
-
|
|
17
|
-
const config = computed<PageLayoutConfig>(() => ({
|
|
18
|
-
mode: layout.value,
|
|
19
|
-
navSide: layout.value === 'vertical' ? 'right' : 'top',
|
|
20
|
-
contentDirection: layout.value === 'vertical' ? 'rtl' : 'ltr',
|
|
21
|
-
isVertical: layout.value === 'vertical',
|
|
22
|
-
}))
|
|
23
|
-
|
|
24
|
-
return { config, scrollRef, ...scroll }
|
|
25
|
-
}
|