@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 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":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanology/cham-browser",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "CHAM — browser-compatible parser, serializer, and site generator for Classical Han Annotated Markdown",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 .char{font-size:64px;font-weight:900;color:var(--ink,#1a1a1a);animation:pulse 1.2s ease-in-out infinite}
20
- #app-loading .text{font-size:13px;color:var(--ink-faint,#a89b8a);letter-spacing:4px;margin-top:16px;font-family:'Noto Sans TC',sans-serif}
21
- @keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
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"><div class="char">詩</div><div class="text">載入中</div></div>
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>
@@ -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
- <Suspense>
23
- <component :is="Component" :key="$route.fullPath" />
24
- </Suspense>
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 = props.vertical ? 2 : 2
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.35s cubic-bezier(0.4, 0, 0.2, 1);
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.35s ease;
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(-4px);
48
- box-shadow: 0 16px 48px rgba(var(--shadow-rgb), 0.1);
49
- border-color: var(--gold-light);
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.2s;
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.2s ease;
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 logoSvg from '../assets/hanology-logo.svg'
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="logoSvg" alt="漢流" class="sn-logo" />
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: none;
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
  }
@@ -0,0 +1,11 @@
1
+ export interface SiteConfig {
2
+ logoUrl?: string
3
+ }
4
+
5
+ const config: SiteConfig = {
6
+ logoUrl: import.meta.env.CHAM_LOGO_URL || undefined,
7
+ }
8
+
9
+ export function useSiteConfig(): SiteConfig {
10
+ return config
11
+ }
@@ -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')?.remove()
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
  }
@@ -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: 64px; font-weight: 900;
120
- color: var(--ink);
121
- animation: pulse 1.2s ease-in-out infinite;
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: 13px; color: var(--ink-faint);
126
- letter-spacing: 4px; margin-top: 16px;
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 pulse {
129
- 0%, 100% { opacity: 0.3; }
130
- 50% { opacity: 1; }
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 ===== */
@@ -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 logoSvg from '../assets/hanology-logo.svg'
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="logoSvg" alt="漢流" class="h-logo" />
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 logoSvg from '../assets/hanology-logo.svg'
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="logoSvg" alt="漢流" class="lib-logo" />
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 type { Piece, Annotation, AnnotationLayer } from '../types'
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
- <span class="v-meta-item" v-html="tcy(piece.verses.length) + ' 段'" />
236
- <span class="v-meta-item" v-html="piece.annotations.length > 0 ? tcy(piece.annotations.length) + ' ' : '無注'" />
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
- <span class="h-tag">{{ piece.verses.length }} 段</span>
374
- <span class="h-tag">{{ piece.annotations.length > 0 ? piece.annotations.length + ' 注' : '無注' }}</span>
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-poem-block">
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(--c-brand);
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(--c-brand);
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
- }