slidev-theme-gtlabo 1.1.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ChapterItem.vue +93 -0
- package/components/SectionDivider.vue +338 -12
- package/layouts/cover.vue +128 -213
- package/layouts/end.vue +76 -100
- package/package.json +1 -1
- package/components/TableOfContents.vue +0 -349
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!-- components/ChapterItem.vue -->
|
|
2
|
+
<template>
|
|
3
|
+
<div
|
|
4
|
+
class="flex flex-col"
|
|
5
|
+
:class="isNext ? 'border-sky-500 border-l-8 pl-6' : 'border-slate-200 border-l-4 pl-7'"
|
|
6
|
+
>
|
|
7
|
+
<!-- 次の章インジケーター - sky-500アクセント -->
|
|
8
|
+
<div
|
|
9
|
+
v-if="isNext"
|
|
10
|
+
:class="nextBadgeClasses"
|
|
11
|
+
class="bg-sky-500 text-white font-bold uppercase tracking-widest leading-none"
|
|
12
|
+
>
|
|
13
|
+
NEXT
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- 章番号とタイトル - 左揃え、明確なヒエラルキー -->
|
|
17
|
+
<div :class="chapterHeaderClasses">
|
|
18
|
+
<div class="flex items-end space-x-3 w-full">
|
|
19
|
+
<div :class="chapterNumberClasses" class="flex text-slate-900 font-bold tabular-nums leading-none">
|
|
20
|
+
{{ chapterNumber }}
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex-1">
|
|
23
|
+
<div class="border-b border-slate-900 mb-2">
|
|
24
|
+
<span :class="chapterTitleClasses" class="text-slate-900 font-bold leading-tight">
|
|
25
|
+
{{ chapter.title }}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- セクション一覧 - 機能的な情報構造 -->
|
|
33
|
+
<div :class="sectionListClasses">
|
|
34
|
+
<div
|
|
35
|
+
v-for="(sectionData, sectionIndex) in chapter.uniqueSections"
|
|
36
|
+
:key="`${chapter.key}-${sectionIndex}`"
|
|
37
|
+
class="flex items-start"
|
|
38
|
+
>
|
|
39
|
+
<span class="w-1 h-1 bg-slate-400 mr-2 mt-1.5 flex-shrink-0 rounded-full"></span>
|
|
40
|
+
<span :class="sectionTextClasses" class="text-slate-600 leading-snug">
|
|
41
|
+
{{ sectionData.title }}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup>
|
|
49
|
+
import { computed } from 'vue'
|
|
50
|
+
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
chapter: {
|
|
53
|
+
type: Object,
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
index: {
|
|
57
|
+
type: Number,
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
isNext: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: false
|
|
63
|
+
},
|
|
64
|
+
isAppendix: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
},
|
|
68
|
+
size: {
|
|
69
|
+
type: String,
|
|
70
|
+
default: 'md'
|
|
71
|
+
},
|
|
72
|
+
config: {
|
|
73
|
+
type: Object,
|
|
74
|
+
required: true
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// 章番号の生成(通常章は "01", "02"、付録は "A1", "A2")
|
|
79
|
+
const chapterNumber = computed(() => {
|
|
80
|
+
if (props.isAppendix) {
|
|
81
|
+
return `A${props.index + 1}`
|
|
82
|
+
}
|
|
83
|
+
return String(props.index + 1).padStart(2, '0')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// 動的クラス
|
|
87
|
+
const chapterHeaderClasses = computed(() => props.config.chapterHeader)
|
|
88
|
+
const chapterNumberClasses = computed(() => props.config.chapterNumber)
|
|
89
|
+
const chapterTitleClasses = computed(() => props.config.chapterTitle)
|
|
90
|
+
const sectionListClasses = computed(() => props.config.sectionList)
|
|
91
|
+
const sectionTextClasses = computed(() => props.config.sectionText)
|
|
92
|
+
const nextBadgeClasses = computed(() => props.config.nextBadge)
|
|
93
|
+
</script>
|
|
@@ -1,24 +1,350 @@
|
|
|
1
|
+
<!-- components/TableOfContents.vue -->
|
|
1
2
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<div class="table-of-contents-container flex flex-col justify-center bg-theme-color">
|
|
4
|
+
<!-- ヘッダー -->
|
|
5
|
+
<div
|
|
6
|
+
v-if="isAppendixSection"
|
|
7
|
+
:class="headerClasses"
|
|
8
|
+
>
|
|
9
|
+
<p :class="headerTitleClasses">
|
|
10
|
+
付録
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- メインコンテンツ -->
|
|
15
|
+
<div :class="mainContentClasses">
|
|
16
|
+
<div class="max-w-full w-full">
|
|
17
|
+
<!-- 通常の目次表示 -->
|
|
18
|
+
<div
|
|
19
|
+
v-if="!isAppendixSection"
|
|
20
|
+
:class="gridClasses"
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
v-for="(chapter, index) in regularChapters"
|
|
24
|
+
:key="chapter.key"
|
|
25
|
+
class="flex flex-col"
|
|
26
|
+
:class="{ 'relative': isNextChapter(chapter.key) }"
|
|
27
|
+
>
|
|
28
|
+
<!-- 次の章の強調表示 -->
|
|
29
|
+
<div
|
|
30
|
+
v-if="isNextChapter(chapter.key)"
|
|
31
|
+
:class="highlightBorderClasses"
|
|
32
|
+
/>
|
|
33
|
+
<div
|
|
34
|
+
v-if="isNextChapter(chapter.key)"
|
|
35
|
+
:class="nextBadgeClasses"
|
|
36
|
+
>
|
|
37
|
+
NEXT
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- 章番号とタイトル -->
|
|
41
|
+
<div :class="chapterHeaderClasses">
|
|
42
|
+
<div :class="chapterNumberClasses">
|
|
43
|
+
{{ String(index + 1).padStart(2, '0') }}
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex-1 border-b border-white pb-2">
|
|
46
|
+
<span :class="chapterTitleClasses">
|
|
47
|
+
{{ chapter.title }}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- セクション一覧(重複除去済み) -->
|
|
53
|
+
<div :class="sectionListClasses">
|
|
54
|
+
<div
|
|
55
|
+
v-for="(sectionData, sectionIndex) in chapter.uniqueSections"
|
|
56
|
+
:key="`${chapter.key}-${sectionIndex}`"
|
|
57
|
+
:class="sectionItemClasses"
|
|
58
|
+
>
|
|
59
|
+
<span :class="sectionTextClasses">
|
|
60
|
+
{{ sectionData.title }}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- 付録の目次表示 -->
|
|
68
|
+
<div
|
|
69
|
+
v-else
|
|
70
|
+
:class="gridClasses"
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
v-for="(chapter, index) in appendixChapters"
|
|
74
|
+
:key="chapter.key"
|
|
75
|
+
class="flex flex-col"
|
|
76
|
+
:class="{ 'relative': isNextChapter(chapter.key) }"
|
|
77
|
+
>
|
|
78
|
+
<!-- 次の章の強調表示 -->
|
|
79
|
+
<div
|
|
80
|
+
v-if="isNextChapter(chapter.key)"
|
|
81
|
+
:class="highlightBorderClasses"
|
|
82
|
+
/>
|
|
83
|
+
<div
|
|
84
|
+
v-if="isNextChapter(chapter.key)"
|
|
85
|
+
:class="nextBadgeClasses"
|
|
86
|
+
>
|
|
87
|
+
NEXT
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- 付録番号とタイトル -->
|
|
91
|
+
<div :class="chapterHeaderClasses">
|
|
92
|
+
<div :class="appendixNumberClasses">
|
|
93
|
+
A{{ index + 1 }}
|
|
94
|
+
</div>
|
|
95
|
+
<div class="flex-1 border-b border-white pb-2">
|
|
96
|
+
<h2 :class="chapterTitleClasses">
|
|
97
|
+
{{ chapter.title }}
|
|
98
|
+
</h2>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- セクション一覧(重複除去済み) -->
|
|
103
|
+
<div :class="sectionListClasses">
|
|
104
|
+
<div
|
|
105
|
+
v-for="(sectionData, sectionIndex) in chapter.uniqueSections"
|
|
106
|
+
:key="`${chapter.key}-${sectionIndex}`"
|
|
107
|
+
:class="sectionItemClasses"
|
|
108
|
+
>
|
|
109
|
+
<span :class="sectionTextClasses">
|
|
110
|
+
{{ sectionData.title }}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
7
119
|
</template>
|
|
8
|
-
|
|
120
|
+
|
|
9
121
|
<script setup>
|
|
10
|
-
import
|
|
122
|
+
import { computed } from 'vue'
|
|
123
|
+
import { useSlideContext } from '@slidev/client'
|
|
124
|
+
|
|
125
|
+
// Slidevコンテキストからconfigsにアクセス
|
|
126
|
+
const { $slidev } = useSlideContext()
|
|
127
|
+
const chapters = $slidev.configs.chapters || {}
|
|
11
128
|
|
|
12
129
|
const props = defineProps({
|
|
13
|
-
nextChapter: {
|
|
130
|
+
nextChapter: {
|
|
14
131
|
type: String,
|
|
15
132
|
required: true,
|
|
16
|
-
description: '
|
|
17
|
-
},
|
|
18
|
-
currentChapter: {
|
|
133
|
+
description: '次に始まる章のキー'
|
|
134
|
+
},
|
|
135
|
+
currentChapter: {
|
|
19
136
|
type: String,
|
|
20
137
|
required: false,
|
|
21
138
|
description: '現在の章のキー(進捗表示用)'
|
|
139
|
+
},
|
|
140
|
+
size: {
|
|
141
|
+
type: String,
|
|
142
|
+
default: 'md',
|
|
143
|
+
validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value),
|
|
144
|
+
description: '目次のサイズ(xs, sm, md, lg, xl)'
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// セクションの重複を除去する関数
|
|
149
|
+
const getUniqueSections = (chapterKey) => {
|
|
150
|
+
const chapter = chapters[chapterKey]
|
|
151
|
+
if (!chapter?.sections) return []
|
|
152
|
+
|
|
153
|
+
const sections = chapter.sections
|
|
154
|
+
const sectionKeys = Object.keys(sections)
|
|
155
|
+
const uniqueSections = []
|
|
156
|
+
let previousTitle = null
|
|
157
|
+
|
|
158
|
+
for (const sectionKey of sectionKeys) {
|
|
159
|
+
const sectionData = sections[sectionKey]
|
|
160
|
+
const currentTitle = sectionData?.title || `セクション ${sectionKey}`
|
|
161
|
+
|
|
162
|
+
// 前のセクションと同じタイトルでない場合のみ追加
|
|
163
|
+
if (currentTitle !== previousTitle) {
|
|
164
|
+
uniqueSections.push({
|
|
165
|
+
key: sectionKey,
|
|
166
|
+
title: currentTitle,
|
|
167
|
+
originalData: sectionData
|
|
168
|
+
})
|
|
169
|
+
previousTitle = currentTitle
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return uniqueSections
|
|
22
174
|
}
|
|
175
|
+
|
|
176
|
+
// 次の章が付録かどうかを判定
|
|
177
|
+
const isAppendixSection = computed(() => {
|
|
178
|
+
return props.nextChapter.startsWith('ap')
|
|
23
179
|
})
|
|
24
|
-
|
|
180
|
+
|
|
181
|
+
// 通常の章リスト(refと付録を除外)
|
|
182
|
+
const regularChapters = computed(() => {
|
|
183
|
+
if (!chapters || typeof chapters !== 'object') {
|
|
184
|
+
return []
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Object.keys(chapters)
|
|
188
|
+
.filter(chapterKey => chapterKey !== 'ref' && !chapterKey.startsWith('ap'))
|
|
189
|
+
.map(chapterKey => {
|
|
190
|
+
const chapter = chapters[chapterKey]
|
|
191
|
+
const sections = chapter?.sections || {}
|
|
192
|
+
const sectionKeys = Object.keys(sections)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
key: chapterKey,
|
|
196
|
+
title: chapter?.title || `章 ${chapterKey}`,
|
|
197
|
+
sections: sectionKeys,
|
|
198
|
+
sectionData: sections,
|
|
199
|
+
uniqueSections: getUniqueSections(chapterKey) // 重複除去済みセクション
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// 付録の章リスト
|
|
205
|
+
const appendixChapters = computed(() => {
|
|
206
|
+
if (!chapters || typeof chapters !== 'object') {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Object.keys(chapters)
|
|
211
|
+
.filter(chapterKey => chapterKey.startsWith('ap'))
|
|
212
|
+
.map(chapterKey => {
|
|
213
|
+
const chapter = chapters[chapterKey]
|
|
214
|
+
const sections = chapter?.sections || {}
|
|
215
|
+
const sectionKeys = Object.keys(sections)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
key: chapterKey,
|
|
219
|
+
title: chapter?.title || `付録 ${chapterKey}`,
|
|
220
|
+
sections: sectionKeys,
|
|
221
|
+
sectionData: sections,
|
|
222
|
+
uniqueSections: getUniqueSections(chapterKey) // 重複除去済みセクション
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// セクションタイトルを取得
|
|
228
|
+
const getSectionTitle = (chapterKey, sectionKey) => {
|
|
229
|
+
const chapter = chapters[chapterKey]
|
|
230
|
+
const sectionData = chapter?.sections?.[sectionKey]
|
|
231
|
+
return sectionData?.title || `セクション ${sectionKey}`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 次の章かどうかを判定
|
|
235
|
+
const isNextChapter = (chapterKey) => {
|
|
236
|
+
return props.nextChapter === chapterKey
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// サイズ設定に基づく動的スタイル
|
|
240
|
+
const sizeConfigs = {
|
|
241
|
+
xs: {
|
|
242
|
+
columns: 4,
|
|
243
|
+
headerPadding: 'pt-6 pb-3 px-4 ',
|
|
244
|
+
headerTitle: 'text-2xl',
|
|
245
|
+
mainPadding: 'px-4 py-4',
|
|
246
|
+
gridGap: 'gap-x-4 gap-y-2',
|
|
247
|
+
chapterNumber: 'text-2xl',
|
|
248
|
+
chapterTitle: 'text-sm font-bold',
|
|
249
|
+
chapterHeader: 'mb-1',
|
|
250
|
+
sectionList: 'ml-8 space-y-0.5',
|
|
251
|
+
sectionText: 'text-xs',
|
|
252
|
+
appendixNumber: 'text-lg',
|
|
253
|
+
nextBadge: 'px-1 py-0.5 text-xs'
|
|
254
|
+
},
|
|
255
|
+
sm: {
|
|
256
|
+
columns: 3,
|
|
257
|
+
headerPadding: 'pt-8 pb-4 px-6',
|
|
258
|
+
headerTitle: 'text-3xl',
|
|
259
|
+
mainPadding: 'px-6 py-6',
|
|
260
|
+
gridGap: 'gap-x-6 gap-y-3',
|
|
261
|
+
chapterNumber: 'text-3xl',
|
|
262
|
+
chapterTitle: 'text-base',
|
|
263
|
+
chapterHeader: 'mb-2',
|
|
264
|
+
sectionList: 'ml-10 space-y-1',
|
|
265
|
+
sectionText: 'text-sm',
|
|
266
|
+
appendixNumber: 'text-2xl',
|
|
267
|
+
nextBadge: 'px-2 py-1 text-xs'
|
|
268
|
+
},
|
|
269
|
+
md: {
|
|
270
|
+
columns: 2,
|
|
271
|
+
headerPadding: 'pt-14 pb-8 px-10',
|
|
272
|
+
headerTitle: 'text-5xl',
|
|
273
|
+
mainPadding: 'px-6 py-40',
|
|
274
|
+
gridGap: 'gap-x-12 gap-y-2',
|
|
275
|
+
chapterNumber: 'text-6xl',
|
|
276
|
+
chapterTitle: 'text-2xl',
|
|
277
|
+
chapterHeader: 'mb-4',
|
|
278
|
+
sectionList: 'ml-20 space-y-2',
|
|
279
|
+
sectionText: 'text-lg',
|
|
280
|
+
appendixNumber: 'text-4xl',
|
|
281
|
+
nextBadge: 'px-3 py-1 text-sm'
|
|
282
|
+
},
|
|
283
|
+
lg: {
|
|
284
|
+
columns: 2,
|
|
285
|
+
headerPadding: 'pt-16 pb-10 px-12',
|
|
286
|
+
headerTitle: 'text-6xl',
|
|
287
|
+
mainPadding: 'px-16 py-16',
|
|
288
|
+
gridGap: 'gap-x-20 gap-y-10',
|
|
289
|
+
chapterNumber: 'text-7xl',
|
|
290
|
+
chapterTitle: 'text-3xl',
|
|
291
|
+
chapterHeader: 'mb-6',
|
|
292
|
+
sectionList: 'ml-24 space-y-3',
|
|
293
|
+
sectionText: 'text-xl',
|
|
294
|
+
appendixNumber: 'text-5xl',
|
|
295
|
+
nextBadge: 'px-4 py-2 text-base'
|
|
296
|
+
},
|
|
297
|
+
xl: {
|
|
298
|
+
columns: 1,
|
|
299
|
+
headerPadding: 'pt-20 pb-12 px-16',
|
|
300
|
+
headerTitle: 'text-8xl',
|
|
301
|
+
mainPadding: 'px-20 py-20',
|
|
302
|
+
gridGap: 'gap-x-24 gap-y-12',
|
|
303
|
+
chapterNumber: 'text-9xl',
|
|
304
|
+
chapterTitle: 'text-5xl',
|
|
305
|
+
chapterHeader: 'mb-8',
|
|
306
|
+
sectionList: 'ml-32 space-y-4',
|
|
307
|
+
sectionText: 'text-3xl',
|
|
308
|
+
appendixNumber: 'text-7xl',
|
|
309
|
+
nextBadge: 'px-5 py-2 text-lg'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const currentConfig = computed(() => sizeConfigs[props.size])
|
|
314
|
+
|
|
315
|
+
// 動的クラス
|
|
316
|
+
const headerClasses = computed(() => `text-left ${currentConfig.value.headerPadding}`)
|
|
317
|
+
const headerTitleClasses = computed(() => `${currentConfig.value.headerTitle} font-bold text-white`)
|
|
318
|
+
const mainContentClasses = computed(() => `flex-1 ${currentConfig.value.mainPadding}`)
|
|
319
|
+
const gridClasses = computed(() => `grid grid-cols-${currentConfig.value.columns} ${currentConfig.value.gridGap}`)
|
|
320
|
+
const chapterHeaderClasses = computed(() => `flex items-center ${currentConfig.value.chapterHeader} relative z-5`)
|
|
321
|
+
const chapterNumberClasses = computed(() => `${currentConfig.value.chapterNumber} font-bold text-white mr-2`)
|
|
322
|
+
const chapterTitleClasses = computed(() => `${currentConfig.value.chapterTitle} font-bold text-white`)
|
|
323
|
+
const sectionListClasses = computed(() => currentConfig.value.sectionList)
|
|
324
|
+
const sectionItemClasses = computed(() => 'flex items-start')
|
|
325
|
+
const sectionTextClasses = computed(() => `text-white ${currentConfig.value.sectionText} leading-relaxed`)
|
|
326
|
+
const appendixNumberClasses = computed(() => `${currentConfig.value.appendixNumber} font-bold text-white mr-6 pb-2`)
|
|
327
|
+
const highlightBorderClasses = computed(() => 'absolute -inset-2 border-4 border-yellow-400 rounded-lg opacity-80')
|
|
328
|
+
const nextBadgeClasses = computed(() => `absolute -top-2 -right-2 bg-yellow-400 text-black ${currentConfig.value.nextBadge} rounded-full font-bold z-10`)
|
|
329
|
+
</script>
|
|
330
|
+
|
|
331
|
+
<style>
|
|
332
|
+
/* このコンポーネント専用のスタイル */
|
|
333
|
+
.table-of-contents-container {
|
|
334
|
+
/* ページ全体をカバーする背景 */
|
|
335
|
+
position: absolute !important;
|
|
336
|
+
top: 0 !important;
|
|
337
|
+
left: 50% !important;
|
|
338
|
+
transform: translateX(-50%) !important;
|
|
339
|
+
width: 100% !important;
|
|
340
|
+
height: 100% !important;
|
|
341
|
+
margin: 0 !important;
|
|
342
|
+
box-sizing: border-box !important;
|
|
343
|
+
z-index: 0 !important;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* 背景色を確実に適用 */
|
|
347
|
+
.table-of-contents-container.bg-theme-color {
|
|
348
|
+
background-color: rgba(2, 132, 199, 1) !important;
|
|
349
|
+
}
|
|
350
|
+
</style>
|