mp 0.1.3 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/lib/mp/deck.rb +15 -3
- data/lib/mp/server.rb +7 -1
- data/lib/mp/version.rb +1 -1
- data/public/app.js +43 -0
- data/public/presenter.css +3 -0
- data/public/presenter.html +2 -0
- data/public/presenter.js +19 -5
- data/public/slides.js +34 -0
- data/public/style.css +22 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b1c5ee524e99fecc018dbcc19184f1ae96c0c6a5c6b41d7f4809e754a8b9d1e
|
|
4
|
+
data.tar.gz: ba755bb7c1dd73f7cf62ba0b14c07012811dc961750bcace65811df1cd184813
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff969b7c6e35a011c35d936cb7b1eafe00c55fcafda636bf7b3ff417b6da0b7483b9421dcbdbe73eb3155099d9a7bc6a3f6d41f632021f28e769f9168bd76964
|
|
7
|
+
data.tar.gz: 300a94ef4e03844a3b74151815d72f1e1ddc90902e0acd952954cc4b7e4ae3da5c9ea577b886377a9894f7424b0897a5ea9c31874efc9d80049b1cc6050d41e1
|
data/README.md
CHANGED
|
@@ -95,6 +95,7 @@ theme: default # default / dark / ./my-theme.css
|
|
|
95
95
|
size: 16:9 # 16:9 / 4:3
|
|
96
96
|
title: タイトル
|
|
97
97
|
paginate: true
|
|
98
|
+
duration: 20 # トークの想定時間(分)。"20:00" "90s" も可
|
|
98
99
|
header: ヘッダー
|
|
99
100
|
footer: フッター
|
|
100
101
|
style: |
|
|
@@ -105,6 +106,13 @@ style: |
|
|
|
105
106
|
<!-- _backgroundColor: black --> このスライドだけに適用(先頭に _)
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
`duration` を指定すると、スライドを表示した時点からタイマーが動き、右下に「ページ番号 / 総枚数」「このページの残り」「全体の残り」を表示します(`paginate` のページ番号はタイマーの中に移ります)。
|
|
110
|
+
|
|
111
|
+
- 1 ページの持ち時間は、全体の時間を枚数で均等に割った長さです(20 分で 10 枚なら 2 分)
|
|
112
|
+
- ページごとに使った時間を積算するので、戻って再び開くと続きから数えます
|
|
113
|
+
- 全体の残りは残り 2 分(短いトークでは全体の 2 割)、ページの残りは持ち時間の 2 割を切ると色が変わり、超過すると赤で超過時間を表示します
|
|
114
|
+
デッキを保存して再読み込みしてもタイマーは続きます。発表者ビューにも残り時間が出て、`r` で両方をリセットできます。
|
|
115
|
+
|
|
108
116
|
使えるディレクティブは `paginate` `header` `footer` `class` `color` `backgroundColor` `backgroundImage` `backgroundPosition` `backgroundRepeat` `backgroundSize` です。クラスには `lead`(中央寄せ)、`left`(左寄せ)、`right`(本文だけ右寄せ)、`invert`(配色反転)、`columns`(2 列)があります。
|
|
109
117
|
|
|
110
118
|
**画像**
|
|
@@ -149,7 +157,7 @@ bundle exec exe/mp --presenter examples/demo.md
|
|
|
149
157
|
| `Home` / `End` | 最初 / 最後 |
|
|
150
158
|
| 数字 + `Enter` | そのページへ |
|
|
151
159
|
| `Enter` | 台本のコマンドを実行 |
|
|
152
|
-
| `r` | 経過時間をリセット |
|
|
160
|
+
| `r` | 経過時間をリセット(`duration` のタイマーも戻す) |
|
|
153
161
|
| `+` / `-` | ノートの文字サイズ |
|
|
154
162
|
| `q` | 終了 |
|
|
155
163
|
|
data/lib/mp/deck.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Mp
|
|
|
15
15
|
# kramdown に渡す前に行単位で走査する。kramdown は info 文字列に属性を持つ
|
|
16
16
|
# フェンスを認識せず、`$x$` も扱えないため、独自記法はここでトークンに置換する。
|
|
17
17
|
class Deck
|
|
18
|
-
GLOBAL_DIRECTIVES = %w[theme size style title].freeze
|
|
18
|
+
GLOBAL_DIRECTIVES = %w[theme size style title duration].freeze
|
|
19
19
|
LOCAL_DIRECTIVES = %w[
|
|
20
20
|
paginate header footer class color
|
|
21
21
|
backgroundColor backgroundImage backgroundPosition backgroundRepeat backgroundSize
|
|
@@ -48,6 +48,17 @@ module Mp
|
|
|
48
48
|
SIZES.fetch(@globals["size"].to_s, SIZES["16:9"])
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# トークの想定時間(秒)。数値は分として扱い、"20:00" "90s" "20m" も受け付ける
|
|
52
|
+
def duration
|
|
53
|
+
seconds = case (value = @globals["duration"])
|
|
54
|
+
when Numeric then value * 60
|
|
55
|
+
when /\A\s*(\d+):([0-5]\d)\s*\z/ then (Regexp.last_match(1).to_i * 60) + Regexp.last_match(2).to_i
|
|
56
|
+
when /\A\s*(\d+(?:\.\d+)?)\s*(?:m|min|分)?\s*\z/ then Regexp.last_match(1).to_f * 60
|
|
57
|
+
when /\A\s*(\d+)\s*(?:s|sec|秒)\s*\z/ then Regexp.last_match(1).to_i
|
|
58
|
+
end
|
|
59
|
+
seconds.round if seconds&.positive?
|
|
60
|
+
end
|
|
61
|
+
|
|
51
62
|
def title
|
|
52
63
|
@globals["title"] || @slides.find { _1[:kind] == :cover }&.fetch(:title) || "mp"
|
|
53
64
|
end
|
|
@@ -57,6 +68,7 @@ module Mp
|
|
|
57
68
|
{
|
|
58
69
|
title: title,
|
|
59
70
|
size: size,
|
|
71
|
+
duration: duration,
|
|
60
72
|
style: @globals["style"].to_s,
|
|
61
73
|
slides: @slides.map { _1.slice(:html, :notes) },
|
|
62
74
|
embeds: @embeds.transform_values { _1.except(:cwd) }
|
|
@@ -73,9 +85,9 @@ module Mp
|
|
|
73
85
|
[match.post_match, front.is_a?(Hash) ? front : {}]
|
|
74
86
|
end
|
|
75
87
|
|
|
76
|
-
# YAML 1.1 では `4:3` が 60
|
|
88
|
+
# YAML 1.1 では `4:3` や `20:00` が 60 進数の数値になるので、文字列のまま読ませる
|
|
77
89
|
def load_yaml(text)
|
|
78
|
-
YAML.safe_load(text.gsub(/^(\s*_?size:\s*)(\d+:\d+)\s
|
|
90
|
+
YAML.safe_load(text.gsub(/^(\s*_?(?:size|duration):\s*)(\d+:\d+)\s*(#.*)?$/, '\1"\2"'))
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def scan(body)
|
data/lib/mp/server.rb
CHANGED
|
@@ -118,9 +118,12 @@ module Mp
|
|
|
118
118
|
def deck_json
|
|
119
119
|
deck = @deck.to_h
|
|
120
120
|
embeds = deck[:embeds].transform_values { |embed| embed[:type] == "web" ? embed.merge(src: embed_url(embed[:src])) : embed }
|
|
121
|
-
|
|
121
|
+
@started_at ||= Time.now # 最初にスライドを表示した時刻からタイマーを始める
|
|
122
|
+
deck.merge(embeds: embeds, themeCss: theme_css, startedAt: timer_start)
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
def timer_start = (@started_at.to_f * 1000).round
|
|
126
|
+
|
|
124
127
|
def embed_url(src)
|
|
125
128
|
return src if src.empty? || src.match?(%r{\Ahttps?://}i)
|
|
126
129
|
|
|
@@ -211,6 +214,9 @@ module Mp
|
|
|
211
214
|
case JSON.parse(message)["type"]
|
|
212
215
|
when "quit" then @on_quit&.call
|
|
213
216
|
when "open-presenter" then @on_open_presenter&.call
|
|
217
|
+
when "timer-reset"
|
|
218
|
+
@started_at = Time.now
|
|
219
|
+
broadcast(JSON.generate(type: "timer", startedAt: timer_start))
|
|
214
220
|
when "frame" then @on_frame_request&.call
|
|
215
221
|
else broadcast(message, except: socket)
|
|
216
222
|
end
|
data/lib/mp/version.rb
CHANGED
data/public/app.js
CHANGED
|
@@ -10,6 +10,12 @@ let slides = [];
|
|
|
10
10
|
let current = 0;
|
|
11
11
|
let terminals = new Map(); // embed id -> { term, socket, fit, host, slot, font }
|
|
12
12
|
let scale = 1;
|
|
13
|
+
let startedAt = null;
|
|
14
|
+
const slideSpent = new Map(); // ページ番号 -> そのページで使った時間(ミリ秒)。戻っても続きから数える
|
|
15
|
+
let enteredAt = Date.now();
|
|
16
|
+
const timer = document.createElement("div");
|
|
17
|
+
timer.id = "timer";
|
|
18
|
+
timer.innerHTML = `<span class="timer-pages"></span><span class="timer-slide"><small>ページ</small><b></b></span><span class="timer-total"><small>全体</small><b></b></span>`;
|
|
13
19
|
let activeTerminal = null;
|
|
14
20
|
let digits = "";
|
|
15
21
|
|
|
@@ -25,10 +31,39 @@ async function loadDeck() {
|
|
|
25
31
|
|
|
26
32
|
deck = await MpSlides.load(stage);
|
|
27
33
|
slides = [...stage.querySelectorAll(":scope > section.slide")];
|
|
34
|
+
startedAt = deck.startedAt;
|
|
35
|
+
stage.append(timer); // スライドと一緒に拡大縮小させる
|
|
36
|
+
stage.classList.toggle("timed", !!deck.duration);
|
|
37
|
+
renderTimer();
|
|
28
38
|
layout();
|
|
29
39
|
show(Math.min(Number(location.hash.slice(1) || 1) - 1, slides.length - 1), { fragments: "none" });
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
function spentOnCurrent(now = Date.now()) {
|
|
43
|
+
return (slideSpent.get(current) ?? 0) + (now - enteredAt);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderTimer() {
|
|
47
|
+
timer.hidden = !deck?.duration || !slides.length;
|
|
48
|
+
if (timer.hidden) return;
|
|
49
|
+
const total = MpSlides.remaining(deck, startedAt);
|
|
50
|
+
const budget = MpSlides.slideBudget(deck, slides.length);
|
|
51
|
+
const slide = MpSlides.slideRemaining(budget, spentOnCurrent());
|
|
52
|
+
timer.querySelector(".timer-pages").textContent = `${current + 1} / ${slides.length}`;
|
|
53
|
+
timer.querySelector(".timer-slide b").textContent = MpSlides.formatTime(slide);
|
|
54
|
+
timer.querySelector(".timer-slide").dataset.state = MpSlides.slideState(budget, slide);
|
|
55
|
+
timer.querySelector(".timer-total b").textContent = MpSlides.formatTime(total);
|
|
56
|
+
timer.querySelector(".timer-total").dataset.state = MpSlides.timerState(deck, total);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resetTimer(at) {
|
|
60
|
+
startedAt = at;
|
|
61
|
+
slideSpent.clear();
|
|
62
|
+
enteredAt = Date.now();
|
|
63
|
+
renderTimer();
|
|
64
|
+
sendState();
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
function layout() {
|
|
33
68
|
if (!deck) return;
|
|
34
69
|
scale = Math.min(innerWidth / deck.size[0], innerHeight / deck.size[1]);
|
|
@@ -64,6 +99,10 @@ function show(index, { fragments }) {
|
|
|
64
99
|
releaseFocus();
|
|
65
100
|
slides[current]?.classList.remove("active");
|
|
66
101
|
slides[current]?.querySelectorAll("iframe").forEach((frame) => frame.blur());
|
|
102
|
+
if (index !== current) {
|
|
103
|
+
slideSpent.set(current, spentOnCurrent());
|
|
104
|
+
enteredAt = Date.now();
|
|
105
|
+
}
|
|
67
106
|
current = index;
|
|
68
107
|
const slide = slides[current];
|
|
69
108
|
slide.classList.add("active");
|
|
@@ -71,6 +110,7 @@ function show(index, { fragments }) {
|
|
|
71
110
|
history.replaceState(null, "", `#${current + 1}`);
|
|
72
111
|
mountEmbeds(slide);
|
|
73
112
|
renderNotes();
|
|
113
|
+
renderTimer();
|
|
74
114
|
sendState();
|
|
75
115
|
}
|
|
76
116
|
|
|
@@ -81,6 +121,7 @@ function sendState() {
|
|
|
81
121
|
const terminal = currentTerminal();
|
|
82
122
|
events.send(JSON.stringify({
|
|
83
123
|
type: "state", slide: current, step, script: terminal?.script() ?? null, ready: terminal?.ready() ?? false,
|
|
124
|
+
slideSpent: spentOnCurrent(), sentAt: Date.now(), // 発表者ビューはここから経過を数える
|
|
84
125
|
}));
|
|
85
126
|
}
|
|
86
127
|
|
|
@@ -300,7 +341,9 @@ events.onmessage = (event) => {
|
|
|
300
341
|
const message = JSON.parse(event.data);
|
|
301
342
|
if (message.type === "reload") loadDeck();
|
|
302
343
|
else if (message.type === "hello") sendState();
|
|
344
|
+
else if (message.type === "timer") resetTimer(message.startedAt);
|
|
303
345
|
else if (message.type === "command") commands[message.cmd]?.(message);
|
|
304
346
|
};
|
|
305
347
|
|
|
348
|
+
setInterval(renderTimer, 250);
|
|
306
349
|
loadDeck();
|
data/public/presenter.css
CHANGED
|
@@ -29,5 +29,8 @@ body.presenter {
|
|
|
29
29
|
#speaker-notes { flex: 1; min-height: 0; overflow: auto; line-height: 1.6; white-space: pre-wrap; }
|
|
30
30
|
|
|
31
31
|
#elapsed { font: 600 34px Menlo, monospace; }
|
|
32
|
+
#remaining, #slide-remaining { font: 600 24px Menlo, "Hiragino Sans", monospace; color: #9fc4ff; }
|
|
33
|
+
:is(#remaining, #slide-remaining)[data-state="warn"] { color: #ffc861; }
|
|
34
|
+
:is(#remaining, #slide-remaining)[data-state="over"] { color: #ff7a7a; }
|
|
32
35
|
#clock { font: 20px Menlo, monospace; color: #98a1b3; }
|
|
33
36
|
.hints { margin-left: auto; font-size: 13px; color: #6f7889; }
|
data/public/presenter.html
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
</div>
|
|
32
32
|
<div class="status">
|
|
33
33
|
<span id="elapsed">00:00</span>
|
|
34
|
+
<span id="slide-remaining" hidden></span>
|
|
35
|
+
<span id="remaining" hidden></span>
|
|
34
36
|
<span id="clock"></span>
|
|
35
37
|
<span class="hints">→ 次へ ・ ← 前へ ・ Enter コマンド実行 ・ r タイマー ・ +/- ノート文字 ・ q 終了</span>
|
|
36
38
|
</div>
|
data/public/presenter.js
CHANGED
|
@@ -6,7 +6,7 @@ const FRAME_INTERVAL = 250;
|
|
|
6
6
|
let deck = null;
|
|
7
7
|
let state = { slide: 0, step: 0, script: null, ready: false };
|
|
8
8
|
let digits = "";
|
|
9
|
-
let startedAt = Date.now();
|
|
9
|
+
let startedAt = Date.now(); // サーバの開始時刻を受け取るまでの仮の値
|
|
10
10
|
let noteSize = 26;
|
|
11
11
|
let mirrorRunning = false;
|
|
12
12
|
let mirrorSlide = null;
|
|
@@ -19,6 +19,7 @@ const command = (cmd, args = {}) => send({ type: "command", cmd, ...args });
|
|
|
19
19
|
|
|
20
20
|
async function loadDeck() {
|
|
21
21
|
deck = await MpSlides.load($("current"), [$("current"), $("next")]);
|
|
22
|
+
startedAt = deck.startedAt;
|
|
22
23
|
document.title = `発表者ビュー - ${deck.title}`;
|
|
23
24
|
for (const stage of [$("current"), $("next")]) {
|
|
24
25
|
stage.querySelectorAll(".embed").forEach(describeEmbed);
|
|
@@ -110,10 +111,19 @@ function onFrame(data) {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
function tick() {
|
|
113
|
-
|
|
114
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
115
|
-
$("elapsed").textContent = `${seconds >= 3600 ? `${Math.floor(seconds / 3600)}:` : ""}${pad(Math.floor(seconds / 60) % 60)}:${pad(seconds % 60)}`;
|
|
114
|
+
$("elapsed").textContent = MpSlides.formatTime(Math.floor((Date.now() - startedAt) / 1000));
|
|
116
115
|
$("clock").textContent = new Date().toLocaleTimeString("ja-JP", { hour: "2-digit", minute: "2-digit" });
|
|
116
|
+
$("remaining").hidden = $("slide-remaining").hidden = !deck?.duration;
|
|
117
|
+
if (!deck?.duration) return;
|
|
118
|
+
const remaining = MpSlides.remaining(deck, startedAt);
|
|
119
|
+
$("remaining").textContent = remaining < 0 ? `超過 ${MpSlides.formatTime(-remaining)}` : `残り ${MpSlides.formatTime(remaining)}`;
|
|
120
|
+
$("remaining").dataset.state = MpSlides.timerState(deck, remaining);
|
|
121
|
+
|
|
122
|
+
// 聴衆画面が送った使用時間に、送ってからの経過を足す
|
|
123
|
+
const budget = MpSlides.slideBudget(deck, deck.slides.length);
|
|
124
|
+
const slide = MpSlides.slideRemaining(budget, (state.slideSpent ?? 0) + (Date.now() - (state.sentAt ?? Date.now())));
|
|
125
|
+
$("slide-remaining").textContent = `このページ ${slide < 0 ? `超過 ${MpSlides.formatTime(-slide)}` : MpSlides.formatTime(slide)}`;
|
|
126
|
+
$("slide-remaining").dataset.state = MpSlides.slideState(budget, slide);
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
function setNoteSize(size) {
|
|
@@ -128,7 +138,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
128
138
|
ArrowLeft: () => command("prev"), ArrowUp: () => command("prev"), PageUp: () => command("prev"),
|
|
129
139
|
Home: () => command("first"),
|
|
130
140
|
End: () => command("last"),
|
|
131
|
-
r: () => {
|
|
141
|
+
r: () => send({ type: "timer-reset" }), // 聴衆画面のタイマーも一緒に戻す
|
|
132
142
|
"+": () => setNoteSize(noteSize + 2),
|
|
133
143
|
"=": () => setNoteSize(noteSize + 2),
|
|
134
144
|
"-": () => setNoteSize(noteSize - 2),
|
|
@@ -154,8 +164,12 @@ socket.onmessage = (event) => {
|
|
|
154
164
|
if (message.type === "state") {
|
|
155
165
|
state = message;
|
|
156
166
|
render();
|
|
167
|
+
tick();
|
|
157
168
|
} else if (message.type === "frame") {
|
|
158
169
|
onFrame(message.data);
|
|
170
|
+
} else if (message.type === "timer") {
|
|
171
|
+
startedAt = message.startedAt;
|
|
172
|
+
tick();
|
|
159
173
|
} else if (message.type === "reload") {
|
|
160
174
|
loadDeck();
|
|
161
175
|
}
|
data/public/slides.js
CHANGED
|
@@ -20,6 +20,40 @@ const MpSlides = {
|
|
|
20
20
|
return deck;
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
+
// 残り時間。超過すると負になる
|
|
24
|
+
remaining(deck, startedAt, now = Date.now()) {
|
|
25
|
+
return deck.duration - Math.floor((now - startedAt) / 1000);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
formatTime(seconds) {
|
|
29
|
+
const sign = seconds < 0 ? "+" : "";
|
|
30
|
+
const s = Math.abs(seconds);
|
|
31
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
32
|
+
const hours = Math.floor(s / 3600);
|
|
33
|
+
return `${sign}${hours ? `${hours}:${pad(Math.floor(s / 60) % 60)}` : pad(Math.floor(s / 60))}:${pad(s % 60)}`;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// 残り 2 分、短いトークでは全体の 2 割を切ったら注意を促す
|
|
37
|
+
timerState(deck, remaining) {
|
|
38
|
+
if (remaining < 0) return "over";
|
|
39
|
+
return remaining <= Math.min(120, deck.duration * 0.2) ? "warn" : "normal";
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// 1 ページの持ち時間(秒)。全体の時間を枚数で均等に割る
|
|
43
|
+
slideBudget(deck, count) {
|
|
44
|
+
return deck.duration / count;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// そのページで使った時間を引いた残り。持ち時間の 2 割を切ったら注意を促す
|
|
48
|
+
slideRemaining(budget, spentMs) {
|
|
49
|
+
return Math.floor(budget - Math.floor(spentMs / 1000)); // 全体の残りと同じく経過秒で刻む
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
slideState(budget, remaining) {
|
|
53
|
+
if (remaining < 0) return "over";
|
|
54
|
+
return remaining <= budget * 0.2 ? "warn" : "normal";
|
|
55
|
+
},
|
|
56
|
+
|
|
23
57
|
// `# <!-- fit --> 見出し` を横幅いっぱいにする
|
|
24
58
|
fitHeadings(stage) {
|
|
25
59
|
stage.querySelectorAll(".fit").forEach((heading) => {
|
data/public/style.css
CHANGED
|
@@ -83,6 +83,28 @@ section.slide > header { top: 20px; }
|
|
|
83
83
|
section.slide > footer { bottom: 20px; }
|
|
84
84
|
section.slide > .page-number { bottom: 20px; left: auto; right: 32px; }
|
|
85
85
|
|
|
86
|
+
/* duration を指定したときの「ページ番号 / このページの残り / 全体の残り」 */
|
|
87
|
+
#timer {
|
|
88
|
+
position: absolute;
|
|
89
|
+
right: 24px;
|
|
90
|
+
bottom: 14px;
|
|
91
|
+
z-index: 3;
|
|
92
|
+
display: flex;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
background: rgba(20, 22, 27, .62);
|
|
96
|
+
color: #fff;
|
|
97
|
+
font: 600 18px/1.3 Menlo, monospace;
|
|
98
|
+
}
|
|
99
|
+
#timer[hidden] { display: none; }
|
|
100
|
+
#timer > span { padding: 3px 10px; }
|
|
101
|
+
#timer > span + span { border-left: 1px solid rgba(255, 255, 255, .25); }
|
|
102
|
+
#timer small { margin-right: 6px; font: 11px "Hiragino Sans", system-ui, sans-serif; opacity: .8; }
|
|
103
|
+
#timer [data-state="warn"] { background: rgba(196, 120, 0, .9); }
|
|
104
|
+
#timer [data-state="over"] { background: rgba(200, 40, 40, .92); }
|
|
105
|
+
/* ページ番号はタイマーの中に出す */
|
|
106
|
+
#stage.timed section.slide > .page-number { display: none; }
|
|
107
|
+
|
|
86
108
|
.embed {
|
|
87
109
|
position: relative;
|
|
88
110
|
flex: 1;
|