@berlysia/vertical-writing-slide-system 0.1.2 → 0.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.
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/App.tsx +220 -95
- package/src/WritingModeHint.tsx +89 -0
- package/src/index.css +62 -11
- package/src/language-utils.ts +26 -0
- package/src/print.css +14 -1
- package/src/remark-slide-images.ts +22 -4
- package/src/screen.css +177 -6
- package/src/vite-plugin-slides.ts +12 -3
package/README.md
CHANGED
|
@@ -139,7 +139,26 @@ Visual regression tests ensure consistent rendering across browsers, with specia
|
|
|
139
139
|
|
|
140
140
|
### Print Mode
|
|
141
141
|
|
|
142
|
-
Print
|
|
142
|
+
Print mode works reliably only in **Chromium-based browsers** (Chrome, Edge, etc.). Firefox and Safari have fundamental limitations with CSS print specifications.
|
|
143
|
+
|
|
144
|
+
#### Firefox
|
|
145
|
+
|
|
146
|
+
- `@page size` is not fully supported
|
|
147
|
+
- [Bug 851937](https://bugzilla.mozilla.org/show_bug.cgi?id=851937) - Implementing `@page { size }` (marked as RESOLVED FIXED, but may not be available in all versions)
|
|
148
|
+
- [Bug 851441](https://bugzilla.mozilla.org/show_bug.cgi?id=851441) - Implementing `@page` size attribute
|
|
149
|
+
|
|
150
|
+
#### Safari / WebKit
|
|
151
|
+
|
|
152
|
+
- `@page size` is ignored
|
|
153
|
+
- `@page margin` does not work correctly
|
|
154
|
+
- `page-break-after` / `break-after` do not work reliably
|
|
155
|
+
- `page-break-inside: avoid` / `break-inside: avoid` are not respected
|
|
156
|
+
- [WebKit Bug 5097](https://bugs.webkit.org/show_bug.cgi?id=5097) - `page-break-after` does not work
|
|
157
|
+
- [MDN Browser Compat Issue #23178](https://github.com/mdn/browser-compat-data/issues/23178) - Safari does not handle print page margins correctly
|
|
158
|
+
|
|
159
|
+
#### Recommendation
|
|
160
|
+
|
|
161
|
+
For PDF export or printing, use a Chromium-based browser (Chrome, Edge, Arc, etc.).
|
|
143
162
|
|
|
144
163
|
### Browser-Specific Issues
|
|
145
164
|
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
2
2
|
import slidesContent, { slideScripts } from "virtual:slides.js";
|
|
3
3
|
import { globalScriptManager } from "./script-manager";
|
|
4
|
+
import { WritingModeHint } from "./WritingModeHint";
|
|
5
|
+
|
|
6
|
+
type DisplayMode = "single" | "dual";
|
|
7
|
+
|
|
8
|
+
// 動作確認用のテストスライドHTML
|
|
9
|
+
const testSlideContent = `<div class="wrapper" style="display: grid; place-items: center;">
|
|
10
|
+
|
|
11
|
+
<svg style="position: absolute; top: 0; left: 0; width: 8cqh; height: 8cqh;" viewBox="0 0 24 24" fill="currentColor">
|
|
12
|
+
<path d="M7 7L17 7L17 9L10.41 9L17 15.59L15.59 17L9 10.41L9 17L7 17Z"/>
|
|
13
|
+
</svg>
|
|
14
|
+
<svg style="position: absolute; top: 0; right: 0; width: 8cqh; height: 8cqh;" viewBox="0 0 24 24" fill="currentColor">
|
|
15
|
+
<path d="M17 7L7 7L7 9L13.59 9L7 15.59L8.41 17L15 10.41L15 17L17 17Z"/>
|
|
16
|
+
</svg>
|
|
17
|
+
<svg style="position: absolute; bottom: 0; left: 0; width: 8cqh; height: 8cqh;" viewBox="0 0 24 24" fill="currentColor">
|
|
18
|
+
<path d="M7 17L17 17L17 15L10.41 15L17 8.41L15.59 7L9 13.59L9 7L7 7Z"/>
|
|
19
|
+
</svg>
|
|
20
|
+
<svg style="position: absolute; bottom: 0; right: 0; width: 8cqh; height: 8cqh;" viewBox="0 0 24 24" fill="currentColor">
|
|
21
|
+
<path d="M17 17L7 17L7 15L13.59 15L7 8.41L8.41 7L15 13.59L15 7L17 7Z"/>
|
|
22
|
+
</svg>
|
|
23
|
+
<div class="wm-horizontal">動作確認</div>
|
|
24
|
+
|
|
25
|
+
</div>`;
|
|
4
26
|
|
|
5
27
|
function App() {
|
|
6
28
|
const [writingMode, setWritingMode] = useState(() => {
|
|
7
29
|
const saved = sessionStorage.getItem("slide-writing-mode");
|
|
8
30
|
return saved ?? "vertical-rl";
|
|
9
31
|
});
|
|
32
|
+
const [displayMode, setDisplayMode] = useState<DisplayMode>(() => {
|
|
33
|
+
const saved = sessionStorage.getItem("slide-display-mode");
|
|
34
|
+
return (saved as DisplayMode) ?? "single";
|
|
35
|
+
});
|
|
36
|
+
const [showTestSlide, setShowTestSlide] = useState(false);
|
|
37
|
+
const [controlsHidden, setControlsHidden] = useState(false);
|
|
10
38
|
const isVertical = writingMode !== "horizontal-tb";
|
|
39
|
+
const isDualMode = displayMode === "dual";
|
|
11
40
|
const slidesRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
const slidesRefHorizontal = useRef<HTMLDivElement>(null);
|
|
12
42
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
43
|
+
// 3秒後にコントロールを非表示
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
setControlsHidden(true);
|
|
47
|
+
}, 3000);
|
|
48
|
+
return () => clearTimeout(timer);
|
|
49
|
+
}, []);
|
|
21
50
|
|
|
22
51
|
// 状態変更時にsessionStorageに保存
|
|
23
52
|
useEffect(() => {
|
|
@@ -25,15 +54,8 @@ function App() {
|
|
|
25
54
|
}, [writingMode]);
|
|
26
55
|
|
|
27
56
|
useEffect(() => {
|
|
28
|
-
sessionStorage.setItem("slide-
|
|
29
|
-
}, [
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
sessionStorage.setItem(
|
|
33
|
-
"slide-with-absolute-font-size",
|
|
34
|
-
withAbsoluteFontSize.toString(),
|
|
35
|
-
);
|
|
36
|
-
}, [withAbsoluteFontSize]);
|
|
57
|
+
sessionStorage.setItem("slide-display-mode", displayMode);
|
|
58
|
+
}, [displayMode]);
|
|
37
59
|
|
|
38
60
|
// スクリプトの読み込み
|
|
39
61
|
useEffect(() => {
|
|
@@ -54,12 +76,23 @@ function App() {
|
|
|
54
76
|
useEffect(() => {
|
|
55
77
|
const hash = location.hash;
|
|
56
78
|
if (hash) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
// ハッシュからページ番号を抽出(#page-2 -> 2)
|
|
80
|
+
const match = hash.match(/^#page-(\d+)/);
|
|
81
|
+
if (match) {
|
|
82
|
+
const pageIndex = match[1];
|
|
83
|
+
if (isDualMode) {
|
|
84
|
+
// デュアルモードでは両方のスライドをスクロール
|
|
85
|
+
const verticalTarget = document.getElementById(`page-${pageIndex}-v`);
|
|
86
|
+
const horizontalTarget = document.getElementById(`page-${pageIndex}-h`);
|
|
87
|
+
verticalTarget?.scrollIntoView();
|
|
88
|
+
horizontalTarget?.scrollIntoView();
|
|
89
|
+
} else {
|
|
90
|
+
const target = document.getElementById(`page-${pageIndex}`);
|
|
91
|
+
target?.scrollIntoView();
|
|
92
|
+
}
|
|
60
93
|
}
|
|
61
94
|
}
|
|
62
|
-
}, [writingMode]);
|
|
95
|
+
}, [writingMode, isDualMode]);
|
|
63
96
|
|
|
64
97
|
// IntersectionObserverでスクロール位置に応じてページ番号を変更
|
|
65
98
|
useEffect(() => {
|
|
@@ -68,7 +101,7 @@ function App() {
|
|
|
68
101
|
entries.forEach((entry) => {
|
|
69
102
|
if (entry.isIntersecting) {
|
|
70
103
|
const id = entry.target.id;
|
|
71
|
-
const index = parseInt(id.replace("page-", ""));
|
|
104
|
+
const index = parseInt(id.replace("page-", "").replace("-v", "").replace("-h", ""));
|
|
72
105
|
history.replaceState(null, "", `#page-${index}`);
|
|
73
106
|
}
|
|
74
107
|
});
|
|
@@ -85,15 +118,82 @@ function App() {
|
|
|
85
118
|
};
|
|
86
119
|
}, []);
|
|
87
120
|
|
|
121
|
+
// デュアルモード時の同期スクロール
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!isDualMode) return;
|
|
124
|
+
|
|
125
|
+
const verticalSlides = slidesRef.current;
|
|
126
|
+
const horizontalSlides = slidesRefHorizontal.current;
|
|
127
|
+
if (!verticalSlides || !horizontalSlides) return;
|
|
128
|
+
|
|
129
|
+
let isSyncing = false;
|
|
130
|
+
|
|
131
|
+
const syncScroll = (source: HTMLElement, target: HTMLElement) => {
|
|
132
|
+
if (isSyncing) return;
|
|
133
|
+
isSyncing = true;
|
|
134
|
+
|
|
135
|
+
// 現在表示中のスライドを特定
|
|
136
|
+
const sourceSlides = source.querySelectorAll(".slide");
|
|
137
|
+
let visibleIndex = 0;
|
|
138
|
+
sourceSlides.forEach((slide, index) => {
|
|
139
|
+
const rect = slide.getBoundingClientRect();
|
|
140
|
+
const sourceRect = source.getBoundingClientRect();
|
|
141
|
+
// スライドの中心がコンテナ内にあるかチェック
|
|
142
|
+
const slideCenterX = rect.left + rect.width / 2;
|
|
143
|
+
const slideCenterY = rect.top + rect.height / 2;
|
|
144
|
+
if (
|
|
145
|
+
slideCenterX >= sourceRect.left &&
|
|
146
|
+
slideCenterX <= sourceRect.right &&
|
|
147
|
+
slideCenterY >= sourceRect.top &&
|
|
148
|
+
slideCenterY <= sourceRect.bottom
|
|
149
|
+
) {
|
|
150
|
+
visibleIndex = index;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ターゲット側の同じインデックスのスライドにスクロール
|
|
155
|
+
const targetSlide = target.querySelector(`[id$="-${visibleIndex === 0 ? "0" : visibleIndex}"]`) ||
|
|
156
|
+
target.querySelectorAll(".slide")[visibleIndex];
|
|
157
|
+
if (targetSlide) {
|
|
158
|
+
targetSlide.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
isSyncing = false;
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleVerticalScroll = () => syncScroll(verticalSlides, horizontalSlides);
|
|
167
|
+
const handleHorizontalScroll = () => syncScroll(horizontalSlides, verticalSlides);
|
|
168
|
+
|
|
169
|
+
verticalSlides.addEventListener("scroll", handleVerticalScroll);
|
|
170
|
+
horizontalSlides.addEventListener("scroll", handleHorizontalScroll);
|
|
171
|
+
|
|
172
|
+
return () => {
|
|
173
|
+
verticalSlides.removeEventListener("scroll", handleVerticalScroll);
|
|
174
|
+
horizontalSlides.removeEventListener("scroll", handleHorizontalScroll);
|
|
175
|
+
};
|
|
176
|
+
}, [isDualMode]);
|
|
177
|
+
|
|
88
178
|
const gotoNextSlide = useCallback((forward = true) => {
|
|
89
179
|
const currentHash = location.hash;
|
|
90
|
-
const currentIndex = parseInt(currentHash.replace("#page-", ""));
|
|
180
|
+
const currentIndex = parseInt(currentHash.replace("#page-", "").replace("-v", "").replace("-h", "")) || 0;
|
|
91
181
|
const nextIndex = forward ? currentIndex + 1 : currentIndex - 1;
|
|
92
182
|
if (nextIndex < 0 || nextIndex >= slidesContent.length) {
|
|
93
183
|
return;
|
|
94
184
|
}
|
|
95
|
-
|
|
96
|
-
|
|
185
|
+
|
|
186
|
+
if (isDualMode) {
|
|
187
|
+
// デュアルモードでは両方のスライドをスクロール
|
|
188
|
+
const verticalSlide = document.getElementById(`page-${nextIndex}-v`);
|
|
189
|
+
const horizontalSlide = document.getElementById(`page-${nextIndex}-h`);
|
|
190
|
+
verticalSlide?.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
191
|
+
horizontalSlide?.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
192
|
+
history.replaceState(null, "", `#page-${nextIndex}`);
|
|
193
|
+
} else {
|
|
194
|
+
location.hash = `#page-${nextIndex}`;
|
|
195
|
+
}
|
|
196
|
+
}, [isDualMode]);
|
|
97
197
|
|
|
98
198
|
// keydownイベントでページ送り
|
|
99
199
|
useEffect(() => {
|
|
@@ -123,93 +223,118 @@ function App() {
|
|
|
123
223
|
};
|
|
124
224
|
}, [gotoNextSlide]);
|
|
125
225
|
|
|
226
|
+
const renderSlide = (content: (typeof slidesContent)[number], index: number, suffix: string = "") => {
|
|
227
|
+
if (typeof content === "string") {
|
|
228
|
+
return (
|
|
229
|
+
<div className="slide" id={`page-${index}${suffix}`} key={`${index}${suffix}`}>
|
|
230
|
+
<div
|
|
231
|
+
className="slide-content"
|
|
232
|
+
dangerouslySetInnerHTML={{
|
|
233
|
+
__html: content,
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
const SlideComponent = content;
|
|
240
|
+
return (
|
|
241
|
+
<div className="slide" id={`page-${index}${suffix}`} key={`${index}${suffix}`}>
|
|
242
|
+
<div className="slide-content">
|
|
243
|
+
<SlideComponent />
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
126
250
|
return (
|
|
127
251
|
<div
|
|
128
|
-
className=
|
|
252
|
+
className={`slides-container ${isDualMode ? "dual-mode" : ""}`}
|
|
129
253
|
style={{ "--slide-writing-mode": writingMode }}
|
|
130
254
|
>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
</div>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
})}
|
|
163
|
-
</div>
|
|
164
|
-
<div className="controls">
|
|
165
|
-
<div>
|
|
255
|
+
{isDualMode ? (
|
|
256
|
+
<div className="dual-slides-wrapper">
|
|
257
|
+
<div className="dual-pane">
|
|
258
|
+
<div className="slides" ref={slidesRef} style={{ "--slide-writing-mode": "vertical-rl" } as React.CSSProperties}>
|
|
259
|
+
{slidesContent.map((content, index) => renderSlide(content, index, "-v"))}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="dual-pane">
|
|
263
|
+
<div className="slides" ref={slidesRefHorizontal} style={{ "--slide-writing-mode": "horizontal-tb" } as React.CSSProperties}>
|
|
264
|
+
{slidesContent.map((content, index) => renderSlide(content, index, "-h"))}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
) : (
|
|
269
|
+
<div className="slides" ref={slidesRef}>
|
|
270
|
+
{slidesContent.map((content, index) => renderSlide(content, index))}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
<div className={`controls ${controlsHidden ? "hidden" : ""}`}>
|
|
274
|
+
<div className="controls-group">
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
onClick={() =>
|
|
278
|
+
(location.hash = `#page-${slidesContent.length - 1}`)
|
|
279
|
+
}
|
|
280
|
+
>
|
|
281
|
+
末尾
|
|
282
|
+
</button>
|
|
166
283
|
<button type="button" onClick={() => gotoNextSlide()}>
|
|
167
284
|
次
|
|
168
285
|
</button>
|
|
286
|
+
<button type="button" onClick={() => gotoNextSlide(false)}>
|
|
287
|
+
前
|
|
288
|
+
</button>
|
|
289
|
+
<button type="button" onClick={() => (location.hash = "#page-0")}>
|
|
290
|
+
先頭
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="controls-group">
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={() =>
|
|
297
|
+
setDisplayMode(isDualMode ? "single" : "dual")
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
{isDualMode ? "単一" : "並列"}
|
|
301
|
+
</button>
|
|
169
302
|
<button
|
|
170
303
|
type="button"
|
|
171
304
|
onClick={() =>
|
|
172
305
|
setWritingMode(isVertical ? "horizontal-tb" : "vertical-rl")
|
|
173
306
|
}
|
|
307
|
+
disabled={isDualMode}
|
|
174
308
|
>
|
|
175
|
-
{isVertical ? "
|
|
309
|
+
{isVertical ? "横書き" : "縦書き"}
|
|
176
310
|
</button>
|
|
177
|
-
<button type="button" onClick={() =>
|
|
178
|
-
|
|
311
|
+
<button type="button" onClick={() => setShowTestSlide(true)}>
|
|
312
|
+
確認
|
|
179
313
|
</button>
|
|
180
314
|
</div>
|
|
181
|
-
<div>
|
|
182
|
-
<label>
|
|
183
|
-
フォントサイズを指定する
|
|
184
|
-
<input
|
|
185
|
-
type="checkbox"
|
|
186
|
-
checked={withAbsoluteFontSize}
|
|
187
|
-
onChange={(e) => setWithAbsoluteFontSize(e.target.checked)}
|
|
188
|
-
/>
|
|
189
|
-
</label>
|
|
190
|
-
<label>
|
|
191
|
-
<input
|
|
192
|
-
type="number"
|
|
193
|
-
min="10"
|
|
194
|
-
step="1"
|
|
195
|
-
value={fontSize}
|
|
196
|
-
onChange={(e) => {
|
|
197
|
-
const t = Number(e.target.value);
|
|
198
|
-
setFontSize(Number.isNaN(t) || t < 4 ? 4 : t);
|
|
199
|
-
}}
|
|
200
|
-
onKeyDown={(e) => {
|
|
201
|
-
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
202
|
-
e.stopPropagation();
|
|
203
|
-
}
|
|
204
|
-
}}
|
|
205
|
-
style={{
|
|
206
|
-
inlineSize: `${fontSize.toString(10).length / 2 + 2}em`,
|
|
207
|
-
}}
|
|
208
|
-
/>
|
|
209
|
-
px
|
|
210
|
-
</label>
|
|
211
|
-
</div>
|
|
212
315
|
</div>
|
|
316
|
+
<WritingModeHint
|
|
317
|
+
onSwitchToHorizontal={() => setWritingMode("horizontal-tb")}
|
|
318
|
+
isHorizontal={!isVertical}
|
|
319
|
+
/>
|
|
320
|
+
{showTestSlide && (
|
|
321
|
+
<div
|
|
322
|
+
className="test-slide-overlay"
|
|
323
|
+
onClick={() => setShowTestSlide(false)}
|
|
324
|
+
>
|
|
325
|
+
<div
|
|
326
|
+
className="test-slide-modal"
|
|
327
|
+
style={{ "--slide-writing-mode": writingMode } as React.CSSProperties}
|
|
328
|
+
>
|
|
329
|
+
<div className="slide">
|
|
330
|
+
<div
|
|
331
|
+
className="slide-content"
|
|
332
|
+
dangerouslySetInnerHTML={{ __html: testSlideContent }}
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
213
338
|
</div>
|
|
214
339
|
);
|
|
215
340
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { isUserInCJKLocale } from "./language-utils";
|
|
3
|
+
|
|
4
|
+
interface WritingModeHintProps {
|
|
5
|
+
onSwitchToHorizontal?: () => void;
|
|
6
|
+
isHorizontal: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 自動翻訳検出とCJK圏外ユーザー向けの横書きモードヒント
|
|
11
|
+
*/
|
|
12
|
+
export function WritingModeHint({
|
|
13
|
+
onSwitchToHorizontal,
|
|
14
|
+
isHorizontal,
|
|
15
|
+
}: WritingModeHintProps) {
|
|
16
|
+
const [visible, setVisible] = useState(false);
|
|
17
|
+
|
|
18
|
+
const dismiss = useCallback(() => {
|
|
19
|
+
setVisible(false);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const handleSwitchClick = useCallback(() => {
|
|
23
|
+
onSwitchToHorizontal?.();
|
|
24
|
+
dismiss();
|
|
25
|
+
}, [onSwitchToHorizontal, dismiss]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const STORAGE_KEY = "writingModeHintShown";
|
|
29
|
+
|
|
30
|
+
// 初回チェック: CJK圏外ユーザーかつセッション内で未表示なら表示
|
|
31
|
+
if (!isUserInCJKLocale() && !sessionStorage.getItem(STORAGE_KEY)) {
|
|
32
|
+
setVisible(true);
|
|
33
|
+
sessionStorage.setItem(STORAGE_KEY, "true");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 自動翻訳検出: Google翻訳等はhtml要素のclass/langを変更する
|
|
38
|
+
// 翻訳時は毎回表示(sessionStorageは使わない)
|
|
39
|
+
const checkTranslation = () => {
|
|
40
|
+
const html = document.documentElement;
|
|
41
|
+
|
|
42
|
+
// Google翻訳の検出
|
|
43
|
+
const hasTranslatedClass =
|
|
44
|
+
html.classList.contains("translated-ltr") ||
|
|
45
|
+
html.classList.contains("translated-rtl");
|
|
46
|
+
|
|
47
|
+
// lang属性の変更検出(元が'ja'から変わった場合)
|
|
48
|
+
const langChanged = html.lang !== "" && html.lang !== "ja";
|
|
49
|
+
|
|
50
|
+
if (hasTranslatedClass || langChanged) {
|
|
51
|
+
setVisible(true);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const observer = new MutationObserver(checkTranslation);
|
|
56
|
+
|
|
57
|
+
observer.observe(document.documentElement, {
|
|
58
|
+
attributes: true,
|
|
59
|
+
attributeFilter: ["class", "lang"],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// 横書きの場合は表示しない
|
|
68
|
+
if (!visible || isHorizontal) return null;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="writing-mode-hint">
|
|
72
|
+
<div className="writing-mode-hint-content">
|
|
73
|
+
<p>
|
|
74
|
+
💡 This presentation uses vertical writing.
|
|
75
|
+
<br />
|
|
76
|
+
For translated content, horizontal mode may be easier to read.
|
|
77
|
+
</p>
|
|
78
|
+
<div className="writing-mode-hint-actions">
|
|
79
|
+
<button type="button" onClick={handleSwitchClick} translate="no">
|
|
80
|
+
Switch to Horizontal
|
|
81
|
+
</button>
|
|
82
|
+
<button type="button" onClick={dismiss} translate="no">
|
|
83
|
+
Keep Vertical
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
package/src/index.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
@import "screen.css" screen;
|
|
2
2
|
@import "print.css" print;
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
*,
|
|
5
|
+
::before,
|
|
6
|
+
::after {
|
|
5
7
|
box-sizing: border-box;
|
|
6
8
|
}
|
|
7
9
|
|
|
@@ -11,6 +13,7 @@ html {
|
|
|
11
13
|
sans-serif;
|
|
12
14
|
font-weight: 600;
|
|
13
15
|
font-style: normal;
|
|
16
|
+
text-spacing-trim: trim-start;
|
|
14
17
|
|
|
15
18
|
/* Prevent text size adjustments */
|
|
16
19
|
-webkit-text-size-adjust: 100%;
|
|
@@ -19,6 +22,12 @@ html {
|
|
|
19
22
|
text-rendering: geometricPrecision;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
body {
|
|
26
|
+
overflow-wrap: anywhere;
|
|
27
|
+
word-break: normal;
|
|
28
|
+
line-break: strict;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
code {
|
|
23
32
|
font-family: "Noto Sans Mono", monospace;
|
|
24
33
|
font-optical-sizing: auto;
|
|
@@ -32,6 +41,24 @@ code {
|
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
.slide-content {
|
|
44
|
+
/* セマンティックフォントサイズ (cqhベース) */
|
|
45
|
+
--fs-caption: 4cqh;
|
|
46
|
+
--fs-small: 4cqh;
|
|
47
|
+
--fs-body: 5cqh;
|
|
48
|
+
--fs-heading-3: 5cqh;
|
|
49
|
+
--fs-heading-2: 6cqh;
|
|
50
|
+
--fs-heading-1: 7cqh;
|
|
51
|
+
--fs-display: 8cqh;
|
|
52
|
+
|
|
53
|
+
/* セマンティックスペーシング */
|
|
54
|
+
--space-heading-gap: 1cqh;
|
|
55
|
+
--space-paragraph-gap: 3cqh;
|
|
56
|
+
--space-section-gap: 4cqh;
|
|
57
|
+
--space-list-gap: 5cqh;
|
|
58
|
+
--space-wrapper: 4cqh;
|
|
59
|
+
|
|
60
|
+
font-size: var(--fs-body);
|
|
61
|
+
|
|
35
62
|
width: 100%;
|
|
36
63
|
height: 100%;
|
|
37
64
|
|
|
@@ -40,7 +67,7 @@ code {
|
|
|
40
67
|
.wrapper {
|
|
41
68
|
width: 100%;
|
|
42
69
|
height: 100%;
|
|
43
|
-
padding:
|
|
70
|
+
padding: var(--space-wrapper);
|
|
44
71
|
|
|
45
72
|
position: relative;
|
|
46
73
|
|
|
@@ -51,6 +78,7 @@ code {
|
|
|
51
78
|
align-items: center;
|
|
52
79
|
}
|
|
53
80
|
}
|
|
81
|
+
|
|
54
82
|
.header-and-content {
|
|
55
83
|
display: block flex;
|
|
56
84
|
flex-direction: column;
|
|
@@ -70,35 +98,40 @@ code {
|
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
h1 {
|
|
73
|
-
font-size: 1
|
|
101
|
+
font-size: var(--fs-heading-1);
|
|
74
102
|
font-weight: bold;
|
|
75
103
|
margin: 0;
|
|
76
|
-
margin-block-end:
|
|
104
|
+
margin-block-end: var(--space-heading-gap);
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
h2 {
|
|
80
|
-
font-size:
|
|
108
|
+
font-size: var(--fs-heading-2);
|
|
81
109
|
font-weight: bold;
|
|
82
110
|
margin: 0;
|
|
83
|
-
margin-block-end:
|
|
111
|
+
margin-block-end: var(--space-heading-gap);
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
h3 {
|
|
87
|
-
font-size:
|
|
115
|
+
font-size: var(--fs-heading-3);
|
|
88
116
|
font-weight: bold;
|
|
89
117
|
margin: 0;
|
|
90
|
-
margin-block-end:
|
|
118
|
+
margin-block-end: var(--space-heading-gap);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
h1, h2, h3, h4, h5, h6 {
|
|
122
|
+
text-wrap: pretty;
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
ul,
|
|
94
126
|
ol {
|
|
95
127
|
list-style-position: outside;
|
|
96
128
|
margin: 0;
|
|
97
|
-
margin-block-end:
|
|
129
|
+
margin-block-end: var(--space-section-gap);
|
|
130
|
+
padding-inline-start: var(--space-list-gap);
|
|
98
131
|
}
|
|
99
132
|
ul ul,
|
|
100
133
|
ol ol {
|
|
101
|
-
margin-block-end:
|
|
134
|
+
margin-block-end: var(--space-section-gap);
|
|
102
135
|
}
|
|
103
136
|
ul ul ul,
|
|
104
137
|
ol ol ol {
|
|
@@ -106,7 +139,25 @@ code {
|
|
|
106
139
|
}
|
|
107
140
|
p {
|
|
108
141
|
margin: 0;
|
|
109
|
-
margin-block-end:
|
|
142
|
+
margin-block-end: var(--space-paragraph-gap);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
figure {
|
|
146
|
+
writing-mode: horizontal-tb;
|
|
147
|
+
margin: auto;
|
|
148
|
+
padding: var(--space-section-gap);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
figcaption {
|
|
152
|
+
text-align: center;
|
|
153
|
+
font-size: var(--fs-caption);
|
|
154
|
+
color: #888;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
a:any-link {
|
|
158
|
+
text-decoration: underline;
|
|
159
|
+
text-underline-position: left;
|
|
160
|
+
color: #22c;
|
|
110
161
|
}
|
|
111
162
|
|
|
112
163
|
.wm-toggle {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJK(中国語・日本語・韓国語)言語圏の判定ユーティリティ
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CJK_LANGUAGE_CODES = [
|
|
6
|
+
"ja", // 日本語
|
|
7
|
+
"zh", // 中国語
|
|
8
|
+
"ko", // 韓国語
|
|
9
|
+
"yue", // 広東語
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 言語コードがCJK言語かどうかを判定
|
|
14
|
+
*/
|
|
15
|
+
export function isCJKLanguage(lang: string): boolean {
|
|
16
|
+
const primaryCode = lang.split("-")[0].toLowerCase();
|
|
17
|
+
return CJK_LANGUAGE_CODES.includes(primaryCode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ユーザーの優先言語がCJK圏かどうかを判定
|
|
22
|
+
*/
|
|
23
|
+
export function isUserInCJKLocale(): boolean {
|
|
24
|
+
return isCJKLanguage(navigator.language);
|
|
25
|
+
}
|
|
26
|
+
|
package/src/print.css
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
--print-page-height: 1080px;
|
|
16
16
|
--print-slide-width: calc(var(--print-page-width) - 0px);
|
|
17
17
|
--print-slide-height: calc(var(--print-page-height) - 0px);
|
|
18
|
-
--print-slide-font-size:
|
|
18
|
+
--print-slide-font-size: 5cqh; /* 既存との互換性 */
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
@page {
|
|
@@ -49,12 +49,24 @@ body {
|
|
|
49
49
|
display: none !important;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* Container definitions for cqh units */
|
|
53
|
+
.slides-container {
|
|
54
|
+
container-name: slide-container;
|
|
55
|
+
container-type: size;
|
|
56
|
+
width: var(--print-slide-width);
|
|
57
|
+
height: var(--print-slide-height);
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
/* Slides wrapper adjustments */
|
|
53
61
|
.slides {
|
|
54
62
|
position: relative;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
.slide {
|
|
66
|
+
/* Container for cqh units */
|
|
67
|
+
container-name: slide-container;
|
|
68
|
+
container-type: size;
|
|
69
|
+
|
|
58
70
|
/* Size and position */
|
|
59
71
|
width: 100%;
|
|
60
72
|
height: 100%;
|
|
@@ -82,6 +94,7 @@ body {
|
|
|
82
94
|
writing-mode: inherit !important;
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
|
|
85
98
|
.debug {
|
|
86
99
|
background-color: orange;
|
|
87
100
|
|
|
@@ -4,12 +4,30 @@ import type { Root, Image } from "mdast";
|
|
|
4
4
|
|
|
5
5
|
interface RemarkSlideImagesOptions {
|
|
6
6
|
base: string;
|
|
7
|
+
collection?: string; // フルURL時のみ使用(複数スライドホスティング対応)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* baseとcollectionから画像パスのプレフィックスを生成
|
|
12
|
+
* - フルURL(http/https)の場合: collection名を含める
|
|
13
|
+
* - 相対パスの場合: collection名なし(ローカル開発用)
|
|
14
|
+
*/
|
|
15
|
+
function buildImagePathPrefix(base: string, collection?: string): string {
|
|
16
|
+
if (
|
|
17
|
+
collection &&
|
|
18
|
+
(base.startsWith("http://") || base.startsWith("https://"))
|
|
19
|
+
) {
|
|
20
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
21
|
+
return `${normalizedBase}${collection}/slide-assets/images/`;
|
|
22
|
+
}
|
|
23
|
+
return `${base}slide-assets/images/`;
|
|
7
24
|
}
|
|
8
25
|
|
|
9
26
|
const remarkSlideImages: Plugin<[RemarkSlideImagesOptions], Root> = (
|
|
10
27
|
options,
|
|
11
28
|
) => {
|
|
12
|
-
const { base } = options;
|
|
29
|
+
const { base, collection } = options;
|
|
30
|
+
const imagePathPrefix = buildImagePathPrefix(base, collection);
|
|
13
31
|
|
|
14
32
|
return (tree) => {
|
|
15
33
|
visit(tree, (node) => {
|
|
@@ -17,7 +35,7 @@ const remarkSlideImages: Plugin<[RemarkSlideImagesOptions], Root> = (
|
|
|
17
35
|
if (node.type === "image") {
|
|
18
36
|
const imageNode = node as Image;
|
|
19
37
|
if (imageNode.url.startsWith("@slide/")) {
|
|
20
|
-
imageNode.url = `${
|
|
38
|
+
imageNode.url = `${imagePathPrefix}${imageNode.url.slice(7)}`;
|
|
21
39
|
}
|
|
22
40
|
}
|
|
23
41
|
|
|
@@ -35,7 +53,7 @@ const remarkSlideImages: Plugin<[RemarkSlideImagesOptions], Root> = (
|
|
|
35
53
|
typeof src.value === "string" &&
|
|
36
54
|
src.value.startsWith("@slide/")
|
|
37
55
|
) {
|
|
38
|
-
src.value = `${
|
|
56
|
+
src.value = `${imagePathPrefix}${src.value.slice(7)}`;
|
|
39
57
|
}
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -50,7 +68,7 @@ const remarkSlideImages: Plugin<[RemarkSlideImagesOptions], Root> = (
|
|
|
50
68
|
(_, attributes, src) => {
|
|
51
69
|
return `<img ${attributes.replace(
|
|
52
70
|
src,
|
|
53
|
-
`${
|
|
71
|
+
`${imagePathPrefix}${src.slice(7)}`,
|
|
54
72
|
)}>`;
|
|
55
73
|
},
|
|
56
74
|
);
|
package/src/screen.css
CHANGED
|
@@ -25,7 +25,7 @@ body,
|
|
|
25
25
|
align-items: center;
|
|
26
26
|
|
|
27
27
|
border: 1px solid #ccc;
|
|
28
|
-
background-color: #
|
|
28
|
+
background-color: #666;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
.slides {
|
|
@@ -80,15 +80,68 @@ body,
|
|
|
80
80
|
display: none;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/* デュアルモード(縦書き・横書き並列表示) */
|
|
84
|
+
.slides-container.dual-mode {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.dual-slides-wrapper {
|
|
90
|
+
display: flex;
|
|
91
|
+
width: 100%;
|
|
92
|
+
height: 100%;
|
|
93
|
+
gap: 4px;
|
|
94
|
+
background-color: #666;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* 横長画面: 左右に並べる */
|
|
98
|
+
@container slide-container (aspect-ratio > 1) {
|
|
99
|
+
.dual-slides-wrapper {
|
|
100
|
+
flex-direction: row;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* 縦長画面: 上下に並べる */
|
|
105
|
+
@container slide-container (aspect-ratio <= 1) {
|
|
106
|
+
.dual-slides-wrapper {
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* デュアルモード時の各ペイン */
|
|
112
|
+
.dual-pane {
|
|
113
|
+
/* flexアイテムとして半分のスペースを取る */
|
|
114
|
+
flex: 1;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
min-height: 0;
|
|
117
|
+
/* container queryの基準にする */
|
|
118
|
+
container-type: size;
|
|
119
|
+
container-name: slide-container;
|
|
120
|
+
/* 中の.slidesを中央配置 */
|
|
121
|
+
display: flex;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
align-items: center;
|
|
124
|
+
/* 背景色で区別 */
|
|
125
|
+
background-color: #666;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* デュアルモード時の.slidesは元のスタイルをそのまま使う(cqw/cqhが.dual-paneを参照) */
|
|
129
|
+
|
|
83
130
|
.controls {
|
|
84
131
|
position: fixed;
|
|
85
132
|
left: 16px;
|
|
86
133
|
bottom: 16px;
|
|
87
134
|
background-color: rgba(255, 255, 255, 0.9);
|
|
135
|
+
padding: 12px;
|
|
136
|
+
border-radius: 12px;
|
|
137
|
+
transition: opacity 0.3s ease-out;
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.controls.hidden {
|
|
88
144
|
opacity: 0;
|
|
89
|
-
padding: 16px;
|
|
90
|
-
border-radius: 8px;
|
|
91
|
-
transition: opacity 0.2s ease-out;
|
|
92
145
|
}
|
|
93
146
|
|
|
94
147
|
.controls:hover,
|
|
@@ -96,6 +149,124 @@ body,
|
|
|
96
149
|
opacity: 1;
|
|
97
150
|
}
|
|
98
151
|
|
|
99
|
-
.
|
|
100
|
-
|
|
152
|
+
.controls-group {
|
|
153
|
+
display: flex;
|
|
154
|
+
gap: 6px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.controls button {
|
|
158
|
+
padding: 12px 16px;
|
|
159
|
+
font-size: 14px;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
border: none;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
background-color: #e8e8e8;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition: background-color 0.15s;
|
|
166
|
+
min-width: 48px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.controls button:hover {
|
|
170
|
+
background-color: #d0d0d0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.controls button:active {
|
|
174
|
+
background-color: #c0c0c0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.controls button:disabled {
|
|
178
|
+
opacity: 0.4;
|
|
179
|
+
cursor: not-allowed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* 動作確認モーダル */
|
|
183
|
+
.test-slide-overlay {
|
|
184
|
+
position: fixed;
|
|
185
|
+
inset: 0;
|
|
186
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
187
|
+
display: flex;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
align-items: center;
|
|
190
|
+
z-index: 1000;
|
|
191
|
+
/* container queryの基準にする */
|
|
192
|
+
container-type: size;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.test-slide-modal {
|
|
196
|
+
/* .slidesと同じ寸法計算 */
|
|
197
|
+
width: 100cqw;
|
|
198
|
+
height: 100cqh;
|
|
199
|
+
max-width: calc(100cqh * 16 / 9 - 0.01px);
|
|
200
|
+
max-height: calc(100cqw * 9 / 16 - 0.01px);
|
|
201
|
+
/* container queryの基準にする */
|
|
202
|
+
container-type: size;
|
|
203
|
+
container-name: slide-container;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* 横書きモードヒント(翻訳ユーザー向け) */
|
|
207
|
+
.writing-mode-hint {
|
|
208
|
+
position: fixed;
|
|
209
|
+
bottom: 80px;
|
|
210
|
+
left: 16px;
|
|
211
|
+
max-width: 360px;
|
|
212
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
215
|
+
z-index: 100;
|
|
216
|
+
animation: hint-slide-in 0.3s ease-out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@keyframes hint-slide-in {
|
|
220
|
+
from {
|
|
221
|
+
opacity: 0;
|
|
222
|
+
transform: translateY(20px);
|
|
223
|
+
}
|
|
224
|
+
to {
|
|
225
|
+
opacity: 1;
|
|
226
|
+
transform: translateY(0);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.writing-mode-hint-content {
|
|
231
|
+
padding: 16px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.writing-mode-hint-content p {
|
|
235
|
+
margin: 0 0 12px 0;
|
|
236
|
+
font-size: 14px;
|
|
237
|
+
line-height: 1.5;
|
|
238
|
+
color: #333;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.writing-mode-hint-actions {
|
|
242
|
+
display: flex;
|
|
243
|
+
gap: 8px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.writing-mode-hint-actions button {
|
|
247
|
+
flex: 1;
|
|
248
|
+
padding: 8px 12px;
|
|
249
|
+
border: none;
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
font-size: 13px;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
transition: background-color 0.15s;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.writing-mode-hint-actions button:first-child {
|
|
257
|
+
background-color: #0066cc;
|
|
258
|
+
color: white;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.writing-mode-hint-actions button:first-child:hover {
|
|
262
|
+
background-color: #0055aa;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.writing-mode-hint-actions button:last-child {
|
|
266
|
+
background-color: #e0e0e0;
|
|
267
|
+
color: #333;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.writing-mode-hint-actions button:last-child:hover {
|
|
271
|
+
background-color: #d0d0d0;
|
|
101
272
|
}
|
|
@@ -54,6 +54,7 @@ function rehypeExtractStyles(extractedStyles: string[]) {
|
|
|
54
54
|
async function processMarkdown(
|
|
55
55
|
markdown: string,
|
|
56
56
|
base: string,
|
|
57
|
+
collection: string,
|
|
57
58
|
extractedStyles: string[],
|
|
58
59
|
extractedScripts: ParsedScript,
|
|
59
60
|
) {
|
|
@@ -65,7 +66,7 @@ async function processMarkdown(
|
|
|
65
66
|
|
|
66
67
|
return await unified()
|
|
67
68
|
.use(remarkParse)
|
|
68
|
-
.use(remarkSlideImages, { base })
|
|
69
|
+
.use(remarkSlideImages, { base, collection })
|
|
69
70
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
70
71
|
.use(rehypeExtractStyles(extractedStyles))
|
|
71
72
|
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
@@ -388,7 +389,13 @@ export default async function slidesPlugin(
|
|
|
388
389
|
const extractedScripts: ParsedScript = { external: [], inline: [] };
|
|
389
390
|
const processedSlides = await Promise.all(
|
|
390
391
|
slides.map((slide) =>
|
|
391
|
-
processMarkdown(
|
|
392
|
+
processMarkdown(
|
|
393
|
+
slide,
|
|
394
|
+
base,
|
|
395
|
+
config.collection,
|
|
396
|
+
extractedStyles,
|
|
397
|
+
extractedScripts,
|
|
398
|
+
),
|
|
392
399
|
),
|
|
393
400
|
);
|
|
394
401
|
|
|
@@ -457,7 +464,9 @@ export default async function slidesPlugin(
|
|
|
457
464
|
development: false,
|
|
458
465
|
jsxImportSource: "react",
|
|
459
466
|
jsxRuntime: "automatic",
|
|
460
|
-
remarkPlugins: [
|
|
467
|
+
remarkPlugins: [
|
|
468
|
+
[remarkSlideImages, { base, collection: config.collection }],
|
|
469
|
+
],
|
|
461
470
|
});
|
|
462
471
|
return result.value as string;
|
|
463
472
|
}),
|