ippon-slides 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tanahiro2010
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,149 @@
1
- # Temporary Holding Version
1
+ # ippon-slides
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **1 枚 1 メッセージ** のスライドを Markdown から作る CLI。
4
+
5
+ - 黒背景・大きな文字・切り替え時のフェードイン
6
+ - 出力は CSS / JS まで全部入った **HTML 1 ファイル**(会場がオフラインでも開ける)
7
+ - `--pdf` で 16:9 の **PDF** も出せる
8
+ - 依存パッケージなし
9
+
10
+ ## 使い方
11
+
12
+ ```sh
13
+ npx ippon-slides init # サンプルの slide.md を作る
14
+ npx ippon-slides build slide.md --html -w # 保存するたびに slide.html を作り直す
15
+ npx ippon-slides build slide.md --pdf # slide.pdf
16
+ ```
17
+
18
+ グローバルに入れると `ippon` コマンドとして使えます。
19
+
20
+ ```sh
21
+ npm i -g ippon-slides
22
+ ippon build slide.md --html --pdf -o dist/
23
+ ```
24
+
25
+ ### オプション
26
+
27
+ | オプション | |
28
+ |---|---|
29
+ | `--html` | HTML を出力する(`--pdf` がなければデフォルト) |
30
+ | `--pdf` | PDF を出力する(Chrome / Chromium / Edge が必要) |
31
+ | `-o, --output <path>` | 出力先。ディレクトリを指定するとその中に `<input>.html` / `.pdf` を作る。`--html --pdf` を両方指定したときは拡張子だけ差し替える |
32
+ | `-w, --watch` | Markdown と `--css` の変更を監視して再ビルドする |
33
+ | `--css <file>` | 追加の CSS(色やフォントの上書きなど) |
34
+ | `--browser-path <path>` | PDF に使うブラウザ。環境変数 `CHROME_PATH` でも指定できる |
35
+
36
+ `ippon slide.md` は `ippon build slide.md` の省略形です。
37
+
38
+ ## 書き方
39
+
40
+ ````md
41
+ ---
42
+ title: タイトル
43
+ author: 名前
44
+ ---
45
+
46
+ ::title::
47
+
48
+ # タイトル
49
+
50
+ サブタイトル
51
+
52
+ 名前 ・ 日付
53
+
54
+ ---
55
+
56
+ ::center::
57
+
58
+ 1 枚に
59
+
60
+ 1 メッセージ
61
+
62
+ ---
63
+
64
+ ::center size=2 bg=blue::
65
+
66
+ **大きく**
67
+
68
+ -# 小さい注釈
69
+
70
+ ---
71
+
72
+ ## 見出しと箇条書き
73
+
74
+ - **太字** はアクセントカラー
75
+ - ネストもできる
76
+
77
+ ::right::
78
+
79
+ ```ts
80
+ const app = new Hono()
81
+ !! app.get('/', (c) => c.text('Hello!'))
82
+ ```
83
+ ````
84
+
85
+ | 記法 | |
86
+ |---|---|
87
+ | `---` | スライドの区切り |
88
+ | `::center::` | 1 枚 1 メッセージ(中央寄せの大きな文字) |
89
+ | `::title::` | 表紙。最後の段落はページ下に寄る |
90
+ | `::wide::` | 左右の余白を詰める(スクリーンショット向け) |
91
+ | `::bg=blue::` | 背景色。`blue` / `orange` / `purple` / `red` / `green` / `black` / `white`、または `#rrggbb` |
92
+ | `::size=1.5::` | 文字サイズの倍率 |
93
+ | `::right::` | ここから右カラム。先頭の見出しはカラムの上に出る |
94
+ | `-# 注釈` | 小さい注釈 |
95
+ | ` ```hcl ` | コード。`hcl` / `console` / `sh` / `json` / `js` / `ts` をハイライト |
96
+ | `!! ` | コードの行頭に付けるとその行を強調 |
97
+
98
+ 指示は `::center bg=blue size=2::` のようにまとめて書けます。
99
+ `console` のコードは `$ ` で始まる行をコマンド、`Error` を含む行をエラーとして色分けします。
100
+
101
+ ## 発表中の操作
102
+
103
+ | キー | |
104
+ |---|---|
105
+ | `→` / `Space` / 画面右側クリック | 次へ |
106
+ | `←` / 画面左側クリック | 前へ |
107
+ | `Home` / `End` | 最初 / 最後 |
108
+ | `f` | フルスクリーン |
109
+ | `o` / `Esc` | スライド一覧 |
110
+
111
+ 表紙から進むと右下でタイマーが動き、表紙に戻るとリセットされます。
112
+ URL の `#12` で 12 枚目から開けます。
113
+
114
+ ## 見た目を変える
115
+
116
+ `--css` で CSS 変数を上書きできます。
117
+
118
+ ```css
119
+ :root {
120
+ --slide-accent: #7b42bc; /* 進捗バー */
121
+ --slide-accent-2: #ff6a00; /* 太字・強調 */
122
+ --slide-display-font: 'Hiragino Sans', sans-serif;
123
+ }
124
+ ```
125
+
126
+ ## ライブラリとして使う
127
+
128
+ ```js
129
+ import { render, toPdf } from 'ippon-slides'
130
+
131
+ const { html } = render(markdown, { baseDir: '.', outDir: 'dist' })
132
+ ```
133
+
134
+ 型定義(`.d.ts`)も同梱しています。
135
+
136
+ ## 開発
137
+
138
+ TypeScript で書いています。`src/` が Node 側、`src/client/deck.ts` がブラウザで動くナビゲーションです。
139
+
140
+ ```sh
141
+ npm install
142
+ npm run build # dist/ と assets/deck.js を作る
143
+ npm run typecheck
144
+ npm test # ビルドしてからテスト(テストの実行には Node 22.18+ が必要)
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT
package/assets/deck.js ADDED
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ // スライドのナビゲーション。ippon が HTML に埋め込む。
3
+ // → / Space / PageDown / クリック右側 次へ
4
+ // ← / PageUp / クリック左側 前へ
5
+ // Home / End 最初 / 最後
6
+ // f フルスクリーン
7
+ // o / Esc 一覧
8
+ (() => {
9
+ const slides = Array.from(document.querySelectorAll('#deck .slide'));
10
+ if (!slides.length)
11
+ return;
12
+ const $ = (id) => document.getElementById(id);
13
+ let idx = 0;
14
+ const progress = $('progress');
15
+ const counter = $('counter');
16
+ const timerEl = $('timer');
17
+ const clamp = (n) => Math.max(0, Math.min(slides.length - 1, n));
18
+ // ---------- タイマー(表紙から進んだら開始、表紙に戻ったらリセット) ----------
19
+ let startedAt = null;
20
+ let ticking;
21
+ function fmt(ms) {
22
+ const total = Math.floor(ms / 1000);
23
+ const m = Math.floor(total / 60);
24
+ const s = total % 60;
25
+ return m + ':' + (s < 10 ? '0' : '') + s;
26
+ }
27
+ function syncTimer() {
28
+ if (idx === 0) {
29
+ startedAt = null;
30
+ clearInterval(ticking);
31
+ ticking = undefined;
32
+ timerEl.textContent = '';
33
+ return;
34
+ }
35
+ const t0 = (startedAt ?? (startedAt = Date.now()));
36
+ ticking ?? (ticking = window.setInterval(() => {
37
+ timerEl.textContent = fmt(Date.now() - t0);
38
+ }, 500));
39
+ timerEl.textContent = fmt(Date.now() - t0);
40
+ }
41
+ // ---------- 描画 ----------
42
+ function render() {
43
+ slides.forEach((s, i) => s.classList.toggle('active', i === idx));
44
+ slides[idx].scrollTop = 0;
45
+ counter.textContent = idx + 1 + ' / ' + slides.length;
46
+ progress.style.width = ((idx + 1) / slides.length) * 100 + '%';
47
+ syncTimer();
48
+ }
49
+ function go(n) {
50
+ n = clamp(n);
51
+ if (n === idx)
52
+ return;
53
+ idx = n;
54
+ render();
55
+ history.replaceState(null, '', '#' + (idx + 1));
56
+ }
57
+ window.addEventListener('hashchange', () => {
58
+ const n = parseInt(location.hash.slice(1), 10);
59
+ if (n)
60
+ go(n - 1);
61
+ });
62
+ const overview = $('overview');
63
+ let ovItems = null;
64
+ let ovOpen = false;
65
+ const THUMB = 240;
66
+ const div = (className) => {
67
+ const el = document.createElement('div');
68
+ el.className = className;
69
+ return el;
70
+ };
71
+ function buildOverview() {
72
+ return slides.map((s, i) => {
73
+ const item = div('ov-item');
74
+ const frame = div('ov-frame');
75
+ const scale = div('ov-scale');
76
+ const clone = s.cloneNode(true);
77
+ clone.classList.remove('active');
78
+ scale.appendChild(clone);
79
+ frame.appendChild(scale);
80
+ const num = div('ov-num');
81
+ num.textContent = String(i + 1);
82
+ item.appendChild(frame);
83
+ item.appendChild(num);
84
+ item.addEventListener('click', () => {
85
+ idx = i;
86
+ closeOverview();
87
+ render();
88
+ history.replaceState(null, '', '#' + (idx + 1));
89
+ });
90
+ overview.appendChild(item);
91
+ return { item, frame, scale };
92
+ });
93
+ }
94
+ function layoutOverview(items) {
95
+ const w = window.innerWidth;
96
+ const h = window.innerHeight;
97
+ const k = THUMB / w;
98
+ for (const t of items) {
99
+ t.frame.style.width = THUMB + 'px';
100
+ t.frame.style.height = h * k + 'px';
101
+ t.scale.style.width = w + 'px';
102
+ t.scale.style.height = h + 'px';
103
+ t.scale.style.transform = 'scale(' + k + ')';
104
+ }
105
+ }
106
+ function openOverview() {
107
+ const items = (ovItems ?? (ovItems = buildOverview()));
108
+ layoutOverview(items);
109
+ items.forEach((t, i) => t.item.classList.toggle('current', i === idx));
110
+ ovOpen = true;
111
+ document.body.classList.add('overview');
112
+ items[idx].item.scrollIntoView({ block: 'center' });
113
+ }
114
+ function closeOverview() {
115
+ ovOpen = false;
116
+ document.body.classList.remove('overview');
117
+ }
118
+ window.addEventListener('resize', () => {
119
+ if (ovOpen && ovItems)
120
+ layoutOverview(ovItems);
121
+ });
122
+ // ---------- 入力 ----------
123
+ document.addEventListener('keydown', (e) => {
124
+ if (e.metaKey || e.ctrlKey || e.altKey)
125
+ return;
126
+ if (ovOpen) {
127
+ if (e.key === 'Escape' || e.key === 'o')
128
+ closeOverview();
129
+ return;
130
+ }
131
+ switch (e.key) {
132
+ case 'ArrowRight':
133
+ case 'PageDown':
134
+ case ' ':
135
+ e.preventDefault();
136
+ go(idx + 1);
137
+ break;
138
+ case 'ArrowLeft':
139
+ case 'PageUp':
140
+ e.preventDefault();
141
+ go(idx - 1);
142
+ break;
143
+ case 'Home':
144
+ go(0);
145
+ break;
146
+ case 'End':
147
+ go(slides.length - 1);
148
+ break;
149
+ case 'f':
150
+ if (document.fullscreenElement)
151
+ document.exitFullscreen();
152
+ else
153
+ document.documentElement.requestFullscreen();
154
+ break;
155
+ case 'o':
156
+ openOverview();
157
+ break;
158
+ case 'Escape':
159
+ if (!document.fullscreenElement)
160
+ openOverview();
161
+ break;
162
+ }
163
+ });
164
+ // 画面の右 3 割で次へ、左 3 割で前へ(リンクと文字選択は邪魔しない)
165
+ const deck = $('deck');
166
+ deck.addEventListener('click', (e) => {
167
+ if (e.target instanceof Element && e.target.closest('a, button'))
168
+ return;
169
+ if (String(window.getSelection() || ''))
170
+ return;
171
+ const x = e.clientX / window.innerWidth;
172
+ if (x > 0.7)
173
+ go(idx + 1);
174
+ else if (x < 0.3)
175
+ go(idx - 1);
176
+ });
177
+ // 横スワイプ
178
+ let touch = null;
179
+ deck.addEventListener('touchstart', (e) => {
180
+ const t = e.touches[0];
181
+ touch = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;
182
+ }, { passive: true });
183
+ deck.addEventListener('touchend', (e) => {
184
+ const t = e.changedTouches[0];
185
+ if (!touch || !t)
186
+ return;
187
+ const dx = t.clientX - touch.x;
188
+ const dy = t.clientY - touch.y;
189
+ touch = null;
190
+ if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5)
191
+ go(dx < 0 ? idx + 1 : idx - 1);
192
+ }, { passive: true });
193
+ const start = parseInt(location.hash.slice(1), 10);
194
+ if (start)
195
+ idx = clamp(start - 1);
196
+ render();
197
+ })();