slidev-theme-watabegg 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.ja.md +381 -112
  2. package/README.md +197 -115
  3. package/components/AgendaList.vue +49 -0
  4. package/components/Cite.vue +28 -0
  5. package/components/DiagramFrame.vue +92 -0
  6. package/components/FigureGrid.vue +108 -0
  7. package/components/KaTexReveal.vue +25 -24
  8. package/components/QuestionList.vue +256 -125
  9. package/components/SectionNumber.vue +34 -0
  10. package/components/SlideReferences.vue +82 -0
  11. package/components/SmallText.vue +25 -0
  12. package/components/TextBox.vue +6 -6
  13. package/components/ThemeFooter.vue +51 -0
  14. package/components/ThemeWave.vue +44 -0
  15. package/custom-nav-controls.vue +40 -0
  16. package/global-bottom.vue +25 -65
  17. package/layouts/agenda.vue +33 -0
  18. package/layouts/cover.vue +7 -21
  19. package/layouts/end.vue +13 -9
  20. package/layouts/image-scroll.vue +4 -2
  21. package/layouts/image.vue +9 -3
  22. package/layouts/section.vue +65 -0
  23. package/package.json +45 -12
  24. package/setup/mermaid.ts +28 -0
  25. package/setup/shiki.ts +3 -4
  26. package/setup/shortcuts.ts +3 -4
  27. package/slide-bottom.vue +56 -0
  28. package/styles/globals.css +99 -87
  29. package/styles/globals.css.d.ts +3 -0
  30. package/styles/layout.css +10 -8
  31. package/styles/layout.css.d.ts +3 -0
  32. package/utils/agenda.ts +89 -0
  33. package/utils/colors.ts +1 -3
  34. package/utils/figure.ts +11 -0
  35. package/utils/presentation.ts +57 -0
  36. package/utils/references.ts +72 -0
  37. package/utils/render.ts +42 -4
  38. package/utils/safeUrl.ts +27 -0
  39. package/utils/themeConfig.ts +20 -0
  40. package/utils/useAvailableHeight.ts +75 -0
  41. package/utils/useThemePalette.ts +21 -25
  42. package/example.md +0 -197
  43. package/utils/link.ts +0 -17
package/README.md CHANGED
@@ -1,179 +1,261 @@
1
1
  # slidev-theme-watabegg
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/slidev-theme-watabegg?color=3AB9D4&label=)](https://www.npmjs.com/package/slidev-theme-watabegg)
4
-
5
- English README (Japanese version: see `README.ja.md`).
6
-
7
- ## Overview
8
- Education-focused Slidev theme for exam prep & classroom lessons, optimized for Japanese typography, high readability, structured question hierarchies, and keyboard-centric navigation. Originally built for personal teaching use.
9
-
10
- ## Features
11
- - Minimal design specialized for teaching
12
- - Japanese typography (M PLUS 2) + monospace Fira Code
13
- - Automatic footer (date + current/total page) on all layouts except `cover` / `image` / `image-scroll`
14
- - Shortcuts: Enter (next) / Backspace (previous)
15
- - Layouts: `cover`, `two-cols`, `image`, `image-scroll`, `end`
16
- - Frontmatter `color` switch: `red | yellow | green | blue | purple`
17
- - Components: `QuestionList`, `TextBox`, `KaTexReveal`
18
- - Multi-level labels: circled numbers / Katakana / Hiragana / kanji numerals (1–19) / alphabet / custom
19
- - Utility classes: `.text-highlight`, `.card`
20
- - Shiki themes: `vitesse-light` / `vitesse-dark`
21
-
22
- ## Install
23
- Add to your slide frontmatter:
3
+ A personal Slidev theme for research presentations and progress reports, with Japanese typography, compact content layouts, and optional larger text.
4
+
5
+ [日本語ドキュメント](./README.ja.md)
6
+
7
+ ## Requirements and installation
8
+
9
+ Node.js **22.12+**, Slidev **53+**. This repository uses pnpm **11.1.3**.
10
+
24
11
  ```yaml
25
12
  ---
26
13
  theme: slidev-theme-watabegg
27
14
  ---
28
15
  ```
29
- Local development (cloned repository):
16
+
17
+ For local development:
18
+
19
+ ```sh
20
+ pnpm install
21
+ pnpm dev
22
+ ```
23
+
24
+ The example deck uses `theme: ./` and demonstrates the theme’s layouts and components.
25
+
26
+ ## Deck configuration
27
+
30
28
  ```yaml
31
29
  ---
32
- theme: ./
30
+ theme: slidev-theme-watabegg
31
+ title: Research progress
32
+ date: '2026/10/05'
33
+ themeConfig:
34
+ watabegg:
35
+ color: blue
36
+ density: research
37
+ footer:
38
+ text: Lab meeting · Progress report
33
39
  ---
34
40
  ```
35
41
 
36
- ## Frontmatter Example
42
+ Colors: `red | yellow | green | blue | purple` (default: green). Per-slide `color` overrides the deck setting.
43
+
44
+ ## Density
45
+
46
+ | Setting | Body | Title | Horizontal / top padding |
47
+ |:--|:--|:--|:--|
48
+ | `research` (default) | 20px | 30px | 28px / 24px |
49
+ | `comfortable` | 24px | 40px | 32px / 32px |
50
+
51
+ The bottom margin is 36px with a footer and 24px without one. Paragraphs, lists, columns, and cards also adjust their spacing. Headings stay in normal flow. Set `themeConfig.watabegg.density` once for the deck. Per-slide density values are ignored.
52
+
53
+ ## Footer
54
+
55
+ A small, transparent footer displays the date on the left, optional text in the center, and current / total pages on the right. It is hidden on `cover`, `image`, and `image-scroll` layouts.
56
+
37
57
  ```yaml
38
- title: Theme Demo
39
- subtitle: Subtitle
40
- author: Instructor Name
41
- date: '2025/08/03'
42
- color: green # red | yellow | green | blue | purple
43
- link: 'https://example.com' # optional footer link
44
- transition: fade
58
+ themeConfig:
59
+ watabegg:
60
+ footer:
61
+ text: Conference presentation
45
62
  ```
46
63
 
47
- If `color` is omitted, the default `green` palette is used.
64
+ Center text comes only from `themeConfig.watabegg.footer.text` and stays the same throughout the deck. The other slide footer fields merge with the deck settings. The date falls back to the slide's `date`, then the deck's `date`. Use `footer: false` to hide it on one slide or across the deck. A slide's `footer: true` can enable it when disabled deck-wide on a regular layout. Long footer text is truncated. The viewer footer stays outside slide transitions; overview and export pages retain their own page numbers.
48
65
 
49
- Below: `color: blue` example.
66
+ ## Agenda and section layouts
50
67
 
51
- ![Frontmatter Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/0.png)
68
+ By default, the agenda is generated from `section` slides in slide order. Agenda numbers and section badges use the same single-level outline. Titles come from `title` in frontmatter or the slide's Markdown heading:
52
69
 
53
- ## Layouts
54
- | Name | Purpose | Notes |
55
- |------|---------|-------|
56
- | cover | Title slide | Gradient wave + title/subtitle/author |
57
- | two-cols | Two columns | Slots: `::left::` / `::right::` |
58
- | image | Background image | Use `image:` + absolutely placed `TextBox` |
59
- | image-scroll | Background image (vertical scroll) | Use `image:` with tall background + `TextBox` |
60
- | end | Ending slide | Simple finish screen |
61
-
62
- ### two-cols Example
63
- ```markdown
70
+ ```yaml
64
71
  ---
65
- layout: two-cols
72
+ layout: agenda
73
+ title: Agenda
66
74
  ---
67
- ::left::
68
- Left
69
- ::right::
70
- Right
71
- ```
72
75
 
73
- ![two-cols Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/1.png)
76
+ ---
77
+ layout: section
78
+ title: Background
79
+ ---
74
80
 
75
- ### image Example
76
- ```markdown
77
81
  ---
78
- layout: image
79
- image: /path/to/bg.jpg
82
+ layout: section
83
+ title: Methods
80
84
  ---
81
- <TextBox :x="120" :y="160" :width="360">Note</TextBox>
82
85
  ```
83
86
 
84
- ### image-scroll Example
85
- ```markdown
87
+ This produces **1 Background / 2 Methods**. Reordering section slides updates both the agenda and section numbers. Ordinary content slides are excluded.
88
+
89
+ To write an outline explicitly, set `agenda` on the **first** agenda slide. All agenda and section slides then share that outline:
90
+
91
+ ```yaml
86
92
  ---
87
- layout: image-scroll
88
- image: /path/to/long-bg.jpg
89
- imageScroll:
90
- offsetY: -120
93
+ layout: agenda
94
+ title: Agenda
95
+ agenda:
96
+ - Background
97
+ - Methods
98
+ - Results
99
+ agendaActive: 2
91
100
  ---
92
101
  ```
93
102
 
94
- ![image-scroll Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/7.png)
103
+ Items may be strings or objects with a `title`. Section slides match manual agenda items by their unique title. Use `sectionNumber: 2` on a section slide when titles differ or are repeated. An unmatched section has no number badge. Later agenda slides need only `layout: agenda`; they reuse the first outline.
95
104
 
96
- image-scroll options (frontmatter `imageScroll`):
97
- - `offsetY`: initial scroll offset from image center in px
105
+ `agendaActive` selects an item by its number. When omitted, an agenda after a section automatically emphasizes that section in bold. All item titles keep the body text color. `agendaActive: 0` disables emphasis.
98
106
 
99
- Controls:
100
- - Wheel / trackpad: vertical scroll
101
- - Touch: vertical scroll
107
+ Agenda text is 30px (36px in `comfortable`). All number badges are theme-colored circles with white numbers in M PLUS 2 using tabular numerals. Longer numbers shrink to fit the circle. The outline is inset from the heading, with consistent gaps between chapters. Section slides use a white background, the same numbered badge at a larger size, and a theme-colored underline. Neither layout displays a subtitle. Both accept optional body content.
102
108
 
103
- Example with initial position:
104
- ```markdown
105
- ---
106
- layout: image-scroll
107
- image: /path/to/long-bg.jpg
108
- imageScroll:
109
- offsetY: 180
110
- ---
109
+ The shared outline follows the [USTC theme's section-derived agenda](https://github.com/luocfprime/slidev-theme-ustc/blob/main/utils/sectionModel.ts); no additional theme dependency is required.
110
+
111
+ ## Mermaid
112
+
113
+ Slidev [supports Mermaid natively](https://sli.dev/features/mermaid.html). The theme uses M PLUS 2, a classic appearance, compact flowchart spacing, and wide, short sequence actor boxes. Mermaid retains its default palette. Diagram-specific YAML configuration can override these defaults; see [Mermaid's sequence configuration](https://mermaid.js.org/syntax/sequenceDiagram.html).
114
+
115
+ Wrap a single diagram in `DiagramFrame` to fit and center its SVG automatically. It preserves the aspect ratio and leaves room for following paragraphs and references:
116
+
117
+ ````md
118
+ Description above the diagram.
119
+
120
+ <DiagramFrame>
121
+
122
+ ```mermaid
123
+ flowchart LR
124
+ A[Input] --> B[Process] --> C[Output]
111
125
  ```
112
126
 
127
+ </DiagramFrame>
128
+
129
+ Description below the diagram.
130
+ ````
131
+
132
+ `height` sets the frame's maximum logical slide height (default 260px). `max-scale` limits scale relative to the original SVG (default 1, so small diagrams are not enlarged). Use these props instead of combining automatic fitting with the fence's `scale` option. Shrinking also shrinks labels. To align flowchart node heights, use matching shapes and label line counts; the theme does not alter the graph's internal geometry.
133
+
134
+ The example includes a complex flowchart with matching rectangular nodes and a wide sequence diagram, both between paragraphs.
135
+
136
+ ## Other layouts
137
+
138
+ | Layout | Usage |
139
+ |:--|:--|
140
+ | `cover` | Wave background with `title`, `subtitle`, `author`, `date` |
141
+ | `two-cols` | Optional `title` and `::left::` / `::right::` slots |
142
+ | `image` | Background from `image:` with optional annotations |
143
+ | `image-scroll` | Wheel / touch scrolling, initial `imageScroll.offsetY` |
144
+ | `end` | Custom `title` and optional body; no return link |
145
+
146
+ Slidev's built-in layouts such as `default` and `center` remain available. Colors can vary by slide. Density and the footer caption are deck-wide settings. Overview thumbnails and exports retain the appropriate footer for each slide. Markdown tables have a bottom margin of twice the paragraph spacing (20px by default).
147
+
113
148
  ## Components
149
+
150
+ ### FigureGrid
151
+
152
+ Display **2–4 images** in a single row with one shared caption. Each image occupies a square box and preserves its aspect ratio without cropping. The default maximum height of 380 logical pixels includes the caption and before/after text; image sizes adapt to the remaining height and row width.
153
+
154
+ ```vue
155
+ <FigureGrid
156
+ :images="[{ src: '/plots/a.png', alt: 'Result A' }, { src: '/plots/b.png', alt: 'Result B' }]"
157
+ caption="Figure 1. Comparison"
158
+ >
159
+ <template #before><p>First line.<br>Second line.</p></template>
160
+ <template #after><p>First note.<br>Second note.</p></template>
161
+ </FigureGrid>
162
+ ```
163
+
164
+ The `before` and `after` slots allow 2–3 lines each. The `caption` slot replaces the caption prop and can contain `Cite`. Optional `height` further limits the total size. Use normal flow layouts; long text and references leave less space for images.
165
+
166
+ ### SmallText
167
+
168
+ Supplementary text at 16px (`research`) or 18px (`comfortable`). It renders a block by default; `inline` renders a span.
169
+
170
+ ```vue
171
+ <SmallText><p>Supplementary explanation.</p></SmallText>
172
+ <p>Body text <SmallText inline>(additional note)</SmallText></p>
173
+ ```
174
+
175
+ ### SlideReferences and Cite
176
+
177
+ One bibliography panel per slide, at 11px in muted gray above the footer. Its measured height adds bottom padding to normal flow content. Long URLs wrap. Absolute annotations need their own positioning.
178
+
179
+ ```vue
180
+ <p>Source text<Cite :number="1" /></p>
181
+ <SlideReferences :items="[
182
+ { number: 1, type: 'web', title: 'Slidev Documentation', url: 'https://sli.dev/', accessed: '2026-10-05' },
183
+ { number: 2, type: 'book', title: 'Pattern Recognition and Machine Learning', authors: ['Christopher M. Bishop'], publisher: 'Springer', year: 2006 },
184
+ { number: 3, type: 'article', title: 'Attention Is All You Need', authors: ['Ashish Vaswani et al.'], venue: 'NeurIPS', year: 2017, url: 'https://arxiv.org/abs/1706.03762' }
185
+ ]" />
186
+ ```
187
+
188
+ | Type | Required fields | Optional fields |
189
+ |:--|:--|:--|
190
+ | All | `number`: unique positive integer in the panel; `type`; `title` | — |
191
+ | `web` | `url`, `accessed` | `authors` |
192
+ | `book` | `authors`, `year`, `publisher` | `url`, `accessed` |
193
+ | `article` | `authors`, `year`, `venue` | `url`, `accessed` |
194
+
195
+ Authors are a nonempty array of names; year is a positive integer. URLs must be absolute HTTP(S) URLs. Access dates use valid `YYYY-MM-DD` dates. Invalid data or duplicate numbers produce a visible authoring error. Entries use a fixed order: authors, title, publisher/venue, year, access date, URL. Metadata is plain text.
196
+
197
+ `Cite` is optional and displays a small raised `[1]`. Clicking it or pressing Enter focuses the matching reference **on the same slide** without scrolling or changing slides. Assign numbers explicitly and reuse them consistently across the deck; numbering is not automatic. The bibliography is separate from the deck-wide footer caption.
198
+
114
199
  ### QuestionList
115
- Nested questions/answers with per-level styles and Markdown rendering.
200
+
201
+ Nested questions with Markdown, per-level labels, and KaTeX formulas:
202
+
116
203
  ```vue
117
204
  <QuestionList
118
- :items="['First **OK**', { text: 'Second', items: ['Child A','Child B'] }]"
119
- :styles="['decimal-circle','katakana-paren','loweralpha-dot']"
120
- :start="[1,1,'c']"
205
+ :items="['First **item**', { text: 'Second', items: ['A', 'B'] }]"
206
+ :styles="['decimal-circle', 'katakana-paren', 'loweralpha-dot']"
207
+ :start="[1, 1, 'c']"
121
208
  />
122
209
  ```
123
- Counter types: `decimal | hiragana | katakana | kanji | upperalpha | loweralpha | none`
124
- Decorators: `circle | square | paren | dot | q | big-q | none`
125
- If an item includes a `label`, it overrides the computed label.
126
210
 
127
- ![QuestionList Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/3.png)
211
+ Counters: `decimal | hiragana | katakana | kanji | upperalpha | loweralpha | none`. Decorators: `circle | square | paren | dot | q | big-q | none`. An item's `label` overrides the generated label. Formula items support `formula` and `block`. Raw HTML is escaped; links accept HTTP(S), mailto, tel, and supported relative URLs.
128
212
 
129
213
  ### TextBox
130
- Absolutely positioned overlay (useful on `image` / `image-scroll` or for callouts).
214
+
215
+ Absolute annotations on image slides:
216
+
131
217
  ```vue
132
- <TextBox :x="100" :y="220" :width="400" textBg="green" v-click="1">Memo</TextBox>
218
+ <TextBox :x="100" :y="220" :width="400" textBg="green">Annotation</TextBox>
133
219
  ```
134
- Props: `x`, `y`, `width`, `height`, `textBg`, `color`, `vClick`
220
+
221
+ Props: `x`, `y`, `width`, `height`, `textBg`, `color`, `vClick`.
135
222
 
136
223
  ### KaTexReveal
137
- KaTeX renderer to ensure math always draws. Slidev auto-registers `components/*.vue`, so drop it directly into Markdown or `QuestionList` items that contain TeX.
224
+
138
225
  ```vue
139
- <KaTexReveal formula="\\int_0^{2\\pi} \\sin x\\,dx = 0" block class="text-2xl" />
140
226
  <KaTexReveal formula="E = mc^2" :block="false" v-click="1" />
141
227
  ```
142
- Props: `formula` (required), `block` (default `false`), `tag` (auto `div`/`span`), other attrs (`class`, `v-click`, etc.) are forwarded.
143
228
 
144
- ![KaTeX Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/4.png)
229
+ Required `formula`, optional `block` (default false) and `tag`. Attributes such as `class` and `v-click` are forwarded.
145
230
 
146
- ## Footer & Shortcuts
147
- - Footer (except on `cover` / `image` / `image-scroll`): shows `date` + current/total pages.
148
- - Enter: next fragment or slide.
149
- - Backspace: previous fragment or slide.
231
+ ## Embedded viewer navigation
150
232
 
151
- ## Utility Classes
152
- - `.text-highlight` line-marker style highlight
153
- - `.card` rounded box with padding and border
233
+ The legacy `link` field no longer adds a footer or end-slide link. For embedded slides, explicitly add a return action to Slidev's viewer controls:
154
234
 
155
- ![Utility Example](https://raw.githubusercontent.com/watabegg/slidev-theme-watabegg/refs/heads/main/example/7.png)
235
+ ```yaml
236
+ themeConfig:
237
+ watabegg:
238
+ navigation:
239
+ href: 'https://example.com/research'
240
+ label: Back to presentations
241
+ ```
242
+
243
+ Move the pointer near the bottom-left to show the controls. The link targets `_top` to leave the iframe. It is outside the slide and excluded from exports. Empty or unsafe URLs produce no link.
156
244
 
157
245
  ## Development
158
- ```bash
159
- pnpm install
246
+
247
+ ```sh
160
248
  pnpm dev
249
+ pnpm dev:polling # Linux watcher-limit fallback
250
+ pnpm typecheck
251
+ pnpm run lint
252
+ pnpm format:check
253
+ pnpm check # tests, types, lint, formatting, build, package contents
161
254
  pnpm build
162
255
  pnpm export
163
256
  pnpm screenshot
164
257
  ```
165
258
 
166
- ## FAQ
167
- **Q. Do I need to configure fonts manually?**
168
- No. Google Fonts are loaded inside the theme.
169
-
170
- **Q. How can I remove the footer?**
171
- Override `global-bottom.vue` in your own custom theme.
172
-
173
- **Q. How do I change the numbering start?**
174
- Use `:start="[1,'c']"` (array per hierarchy level).
175
-
176
- ## See Also
177
- Japanese documentation: `README.ja.md`
259
+ Use `pnpm run lint` explicitly because pnpm 11 also has a built-in lint command. CI additionally audits production dependencies. TypeScript stays on 6.x for vue-tsc compatibility. Markdown-it 14 is used for Slidev 53's Comark adapter, which imports a private module absent in Markdown-it 15. FloatingVue is kept at 5.2.2 for Shiki Twoslash compatibility, and transitive DOMPurify copies are upgraded to 3.4.16. For release 1.2.0, `@slidev/types` moved to `devDependencies` by exporting setup functions directly with type-only imports and `satisfies` checks, without altering settings or shortcuts. As a result, the production dependency graph no longer includes the unfixed [braces advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm), and production audits remain enabled without exceptions. Note that development dependencies still include `braces` through Slidev's build tooling.
178
260
 
179
- Happy teaching! 🎓
261
+ Fonts: M PLUS 2 / Fira Code. Shiki: vitesse-light / vitesse-dark. Utilities: `.text-highlight`, `.card`. Shortcuts: **Enter** next slide / fragment, **Backspace** previous.
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import type { AgendaItem } from '../utils/agenda'
3
+ import SectionNumber from './SectionNumber.vue'
4
+
5
+ defineProps<{ items: AgendaItem[]; active?: string }>()
6
+ </script>
7
+
8
+ <template>
9
+ <ol class="agenda-list">
10
+ <li v-for="item in items" :key="item.number" class="agenda-item">
11
+ <div
12
+ class="agenda-row"
13
+ :class="{ 'is-active': active === item.number }"
14
+ :aria-current="active === item.number ? 'step' : undefined"
15
+ >
16
+ <SectionNumber :number="item.number" />
17
+ <span>{{ item.title }}</span>
18
+ </div>
19
+ </li>
20
+ </ol>
21
+ </template>
22
+
23
+ <style scoped>
24
+ .agenda-list {
25
+ --section-number-size: var(--watabegg-agenda-badge-size);
26
+ display: grid;
27
+ gap: 24px;
28
+ list-style: none;
29
+ padding: 0;
30
+ margin: 0;
31
+ }
32
+
33
+ .agenda-item {
34
+ margin: 0;
35
+ padding: 0;
36
+ }
37
+
38
+ .agenda-row {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 18px;
42
+ color: var(--slidev-theme-text);
43
+ line-height: 1.35;
44
+ }
45
+
46
+ .agenda-row.is-active {
47
+ font-weight: 600;
48
+ }
49
+ </style>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { ref, unref } from 'vue'
3
+ import { useSlideContext } from '@slidev/client'
4
+
5
+ defineProps<{ number: number }>()
6
+ const { $page } = useSlideContext()
7
+ const root = ref<HTMLAnchorElement | null>(null)
8
+
9
+ function focusReference(number: number) {
10
+ const page = root.value?.closest('.slidev-page')
11
+ page?.querySelector<HTMLElement>(`[data-reference-number="${number}"]`)?.focus({ preventScroll: true })
12
+ }
13
+ </script>
14
+
15
+ <template>
16
+ <a ref="root" class="citation-label" :href="`#reference-${unref($page)}-${number}`" :aria-label="`参考文献 ${number}`" @click.prevent="focusReference(number)">[{{ number }}]</a>
17
+ </template>
18
+
19
+ <style scoped>
20
+ .citation-label {
21
+ font-size: 0.75em;
22
+ line-height: 1;
23
+ vertical-align: super;
24
+ white-space: nowrap;
25
+ font-variant-numeric: tabular-nums;
26
+ margin-left: 0.15em;
27
+ }
28
+ </style>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import { fitDiagram } from '../utils/figure'
4
+ import { useAvailableHeight } from '../utils/useAvailableHeight'
5
+
6
+ const props = withDefaults(defineProps<{ height?: number; maxScale?: number }>(), {
7
+ height: 260,
8
+ maxScale: 1,
9
+ })
10
+ const root = ref<HTMLElement | null>(null)
11
+ const height = useAvailableHeight(root, computed(() => props.height))
12
+ let resize: ResizeObserver | undefined
13
+ let frame = 0
14
+ let mounted = false
15
+ const observers = new Map<Node, MutationObserver>()
16
+
17
+ function schedule() {
18
+ if (!mounted || frame) return
19
+ frame = requestAnimationFrame(() => {
20
+ frame = 0
21
+ fit()
22
+ })
23
+ }
24
+
25
+ function inspect(node: HTMLElement | ShadowRoot) {
26
+ if (!observers.has(node)) {
27
+ const observer = new MutationObserver(schedule)
28
+ observer.observe(node, { childList: true, subtree: true, attributes: true, attributeFilter: ['viewBox', 'width', 'height', 'style'] })
29
+ observers.set(node, observer)
30
+ }
31
+ const svgs = [...node.querySelectorAll<SVGSVGElement>('svg[viewBox]')]
32
+ for (const child of node.querySelectorAll('*')) {
33
+ if (child.shadowRoot) svgs.push(...inspect(child.shadowRoot))
34
+ }
35
+ return svgs
36
+ }
37
+
38
+ function fit() {
39
+ const element = root.value
40
+ if (!element) return
41
+ for (const svg of inspect(element)) {
42
+ const viewBox = svg.viewBox.baseVal
43
+ const size = fitDiagram(viewBox.width, viewBox.height, element.clientWidth, element.clientHeight, props.maxScale)
44
+ for (const [name, value] of Object.entries(size)) {
45
+ const text = String(value)
46
+ if (svg.getAttribute(name) !== text) svg.setAttribute(name, text)
47
+ }
48
+ if (svg.style.maxWidth !== 'none') svg.style.maxWidth = 'none'
49
+ if (svg.style.display !== 'block') svg.style.display = 'block'
50
+ }
51
+ }
52
+
53
+ onMounted(() => {
54
+ mounted = true
55
+ resize = new ResizeObserver(schedule)
56
+ if (root.value) resize.observe(root.value)
57
+ schedule()
58
+ })
59
+ watch(() => props.maxScale, schedule)
60
+ onUnmounted(() => {
61
+ mounted = false
62
+ cancelAnimationFrame(frame)
63
+ resize?.disconnect()
64
+ for (const observer of observers.values()) observer.disconnect()
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <div ref="root" class="diagram-frame" :style="{ height: `${height}px` }">
70
+ <slot />
71
+ </div>
72
+ </template>
73
+
74
+ <style scoped>
75
+ .diagram-frame {
76
+ display: grid;
77
+ place-items: center;
78
+ min-height: 0;
79
+ margin: 12px 0;
80
+ line-height: 0;
81
+ }
82
+
83
+ .diagram-frame :deep(.mermaid) {
84
+ line-height: 0;
85
+ }
86
+
87
+ .diagram-frame :deep(pre) {
88
+ max-height: 100%;
89
+ overflow: auto;
90
+ line-height: 1.4;
91
+ }
92
+ </style>
@@ -0,0 +1,108 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import { useAvailableHeight } from '../utils/useAvailableHeight'
4
+
5
+ interface FigureImage {
6
+ src: string
7
+ alt: string
8
+ }
9
+
10
+ const props = withDefaults(defineProps<{ images: FigureImage[]; caption?: string; height?: number }>(), {
11
+ caption: '',
12
+ height: 380,
13
+ })
14
+ const root = ref<HTMLElement | null>(null)
15
+ const plots = ref<HTMLElement | null>(null)
16
+ const height = useAvailableHeight(root, computed(() => props.height))
17
+ const side = ref(0)
18
+ const validCount = computed(() => props.images.length >= 2 && props.images.length <= 4)
19
+ let resize: ResizeObserver | undefined
20
+
21
+ function measure() {
22
+ const element = plots.value
23
+ if (!element || !validCount.value) return
24
+ const gap = Number.parseFloat(getComputedStyle(element).gap)
25
+ side.value = Math.max(0, Math.min(element.clientHeight, (element.clientWidth - gap * (props.images.length - 1)) / props.images.length))
26
+ }
27
+
28
+ onMounted(() => {
29
+ resize = new ResizeObserver(measure)
30
+ if (plots.value) resize.observe(plots.value)
31
+ measure()
32
+ })
33
+ watch(() => props.images.length, () => nextTick(measure))
34
+ onUnmounted(() => resize?.disconnect())
35
+ </script>
36
+
37
+ <template>
38
+ <div v-if="!validCount" class="figure-error">FigureGridには画像を2〜4枚指定してください。</div>
39
+ <div v-else ref="root" class="figure-grid" :style="{ height: `${height}px` }">
40
+ <div v-if="$slots.before" class="figure-copy"><slot name="before" /></div>
41
+ <figure class="figure-group">
42
+ <div ref="plots" class="figure-images" :style="{ '--figure-side': `${side}px` }">
43
+ <img
44
+ v-for="(image, index) in images"
45
+ :key="index"
46
+ :src="image.src"
47
+ :alt="image.alt"
48
+ class="figure-image"
49
+ />
50
+ </div>
51
+ <figcaption v-if="caption || $slots.caption" class="figure-caption">
52
+ <slot name="caption">{{ caption }}</slot>
53
+ </figcaption>
54
+ </figure>
55
+ <div v-if="$slots.after" class="figure-copy"><slot name="after" /></div>
56
+ </div>
57
+ </template>
58
+
59
+ <style scoped>
60
+ .figure-grid {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ min-height: 0;
65
+ }
66
+
67
+ .figure-copy {
68
+ flex: none;
69
+ }
70
+
71
+ .figure-copy :deep(p),
72
+ .figure-caption :deep(p) {
73
+ margin: 0;
74
+ }
75
+
76
+ .figure-group {
77
+ display: flex;
78
+ flex: 1;
79
+ flex-direction: column;
80
+ gap: 8px;
81
+ min-height: 0;
82
+ margin: 0;
83
+ }
84
+
85
+ .figure-images {
86
+ display: flex;
87
+ flex: 1;
88
+ align-items: center;
89
+ justify-content: center;
90
+ gap: 16px;
91
+ min-height: 0;
92
+ }
93
+
94
+ .figure-image {
95
+ flex: none;
96
+ width: var(--figure-side);
97
+ height: var(--figure-side);
98
+ object-fit: contain;
99
+ }
100
+
101
+ .figure-caption {
102
+ flex: none;
103
+ color: var(--slidev-theme-text-secondary);
104
+ font-size: var(--watabegg-small-text-size);
105
+ line-height: 1.35;
106
+ text-align: center;
107
+ }
108
+ </style>