inkflow 0.1.0.dev0__py3-none-any.whl
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.
- inkflow/__init__.py +31 -0
- inkflow/cli.py +266 -0
- inkflow/content.py +227 -0
- inkflow/export.py +123 -0
- inkflow/layout.py +279 -0
- inkflow/manifest.py +145 -0
- inkflow/markdown.py +158 -0
- inkflow/ns.py +9 -0
- inkflow/pdf.html +19 -0
- inkflow/pipeline.py +244 -0
- inkflow/presenter.css +196 -0
- inkflow/presenter.html +50 -0
- inkflow/presenter.js +400 -0
- inkflow/server.py +380 -0
- inkflow/theme/layouts/center.svg +3 -0
- inkflow/theme/layouts/cover.svg +5 -0
- inkflow/theme/layouts/default.svg +4 -0
- inkflow/theme/layouts/end.svg +4 -0
- inkflow/theme/layouts/fact.svg +4 -0
- inkflow/theme/layouts/media-left.svg +5 -0
- inkflow/theme/layouts/media-right.svg +5 -0
- inkflow/theme/layouts/quote.svg +4 -0
- inkflow/theme/layouts/section.svg +4 -0
- inkflow/theme/layouts/statement.svg +3 -0
- inkflow/theme/layouts/two-cols-header.svg +5 -0
- inkflow/theme/layouts/two-cols.svg +5 -0
- inkflow/theme/main.svg +3 -0
- inkflow/theme/numbered-main.svg +6 -0
- inkflow/theme/showcase/deck.py +18 -0
- inkflow/theme/showcase/slides/01-cover.md +7 -0
- inkflow/theme/showcase/slides/02-section.md +7 -0
- inkflow/theme/showcase/slides/03-default.md +11 -0
- inkflow/theme/showcase/slides/04-center.md +8 -0
- inkflow/theme/showcase/slides/05-two-cols.md +17 -0
- inkflow/theme/showcase/slides/06-two-cols-header.md +13 -0
- inkflow/theme/showcase/slides/07-fact.md +7 -0
- inkflow/theme/showcase/slides/08-quote.md +7 -0
- inkflow/theme/showcase/slides/09-statement.md +3 -0
- inkflow/theme/showcase/slides/10-media-left.md +15 -0
- inkflow/theme/showcase/slides/11-media-right.md +16 -0
- inkflow/theme/showcase/slides/12-end.md +3 -0
- inkflow/theme/styles.css +55 -0
- inkflow/tui.py +128 -0
- inkflow-0.1.0.dev0.dist-info/METADATA +12 -0
- inkflow-0.1.0.dev0.dist-info/RECORD +48 -0
- inkflow-0.1.0.dev0.dist-info/WHEEL +4 -0
- inkflow-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- inkflow-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
inkflow/pipeline.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lxml import etree
|
|
6
|
+
|
|
7
|
+
from inkflow import ns
|
|
8
|
+
from inkflow.content import (
|
|
9
|
+
inject_style,
|
|
10
|
+
remove_unreferenced_zones,
|
|
11
|
+
substitute_content,
|
|
12
|
+
substitute_zone_numbers,
|
|
13
|
+
)
|
|
14
|
+
from inkflow.layout import resolve_chain, resolve_parent_path, strip_layout_layers
|
|
15
|
+
from inkflow.manifest import (
|
|
16
|
+
Animation,
|
|
17
|
+
Bounce,
|
|
18
|
+
Deck,
|
|
19
|
+
FadeIn,
|
|
20
|
+
FadeOut,
|
|
21
|
+
MarkdownSlide,
|
|
22
|
+
Slide,
|
|
23
|
+
Transition,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ── Path conventions ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_slide_src(src: str, project_dir: Path) -> Path:
|
|
30
|
+
"""Resolve a Slide.src string to an absolute Path.
|
|
31
|
+
|
|
32
|
+
Bare single-part names (no separator) are looked up in slides/.
|
|
33
|
+
Explicit paths (with / or absolute) are resolved relative to project_dir.
|
|
34
|
+
"""
|
|
35
|
+
p = Path(src)
|
|
36
|
+
if p.is_absolute():
|
|
37
|
+
return p if p.suffix else p.with_suffix(".svg")
|
|
38
|
+
if len(p.parts) == 1:
|
|
39
|
+
name = p.stem if p.suffix else src
|
|
40
|
+
return project_dir / "slides" / (name + ".svg")
|
|
41
|
+
if not p.suffix:
|
|
42
|
+
p = p.with_suffix(".svg")
|
|
43
|
+
return (project_dir / p).resolve()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_content_src(src: str, project_dir: Path) -> Path:
|
|
47
|
+
"""Resolve a MarkdownSlide content path to an absolute Path.
|
|
48
|
+
|
|
49
|
+
Bare single-part names are looked up in slides/ with a .md suffix.
|
|
50
|
+
"""
|
|
51
|
+
p = Path(src)
|
|
52
|
+
if p.is_absolute():
|
|
53
|
+
return p if p.suffix else p.with_suffix(".md")
|
|
54
|
+
if len(p.parts) == 1:
|
|
55
|
+
name = p.stem if p.suffix else src
|
|
56
|
+
return project_dir / "slides" / (name + ".md")
|
|
57
|
+
if not p.suffix:
|
|
58
|
+
p = p.with_suffix(".md")
|
|
59
|
+
return (project_dir / p).resolve()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Animation classes ─────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
_ANIM_CLASS: dict[type, str] = {
|
|
65
|
+
FadeIn: "anim-fade-in",
|
|
66
|
+
FadeOut: "anim-fade-out",
|
|
67
|
+
Bounce: "anim-bounce",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_INKSCAPE_NAMESPACES: frozenset[str] = frozenset({ns.INKSCAPE, ns.SODIPODI})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clean_inkscape_svg(src: Path) -> str:
|
|
74
|
+
tree = etree.parse(src)
|
|
75
|
+
root = tree.getroot()
|
|
76
|
+
|
|
77
|
+
for ns_uri in _INKSCAPE_NAMESPACES:
|
|
78
|
+
for el in root.findall(f".//{{{ns_uri}}}*"):
|
|
79
|
+
parent = el.getparent()
|
|
80
|
+
if parent is not None:
|
|
81
|
+
parent.remove(el)
|
|
82
|
+
|
|
83
|
+
for el in root.iter():
|
|
84
|
+
to_del = [
|
|
85
|
+
k
|
|
86
|
+
for k in el.attrib
|
|
87
|
+
if isinstance(k, str)
|
|
88
|
+
and any(k.startswith(f"{{{ns_uri}}}") for ns_uri in _INKSCAPE_NAMESPACES)
|
|
89
|
+
]
|
|
90
|
+
for k in to_del:
|
|
91
|
+
del el.attrib[k]
|
|
92
|
+
|
|
93
|
+
etree.cleanup_namespaces(root)
|
|
94
|
+
return etree.tostring(root, encoding="unicode", pretty_print=True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def annotate_svg(svg_str: str, animations: list[Animation]) -> str:
|
|
98
|
+
root = etree.fromstring(svg_str.encode())
|
|
99
|
+
|
|
100
|
+
for anim in animations:
|
|
101
|
+
css_class = _ANIM_CLASS.get(type(anim))
|
|
102
|
+
if css_class is None:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
eid = anim.element.lstrip("#")
|
|
106
|
+
el = root.find(f'.//*[@id="{eid}"]')
|
|
107
|
+
if el is None:
|
|
108
|
+
print(f"[inkflow] warning: element #{eid} not found in SVG")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
existing = el.get("class", "")
|
|
112
|
+
el.set("class", f"{existing} {css_class}".strip())
|
|
113
|
+
el.set("data-step", str(anim.step))
|
|
114
|
+
|
|
115
|
+
return etree.tostring(root, encoding="unicode")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _serialize_transition(t: Transition | None) -> dict[str, object]:
|
|
119
|
+
if t is None:
|
|
120
|
+
return {"type": "cut", "duration": 0.0}
|
|
121
|
+
return {"type": type(t).__name__.lower(), **vars(t)}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_transitions(deck: Deck) -> list[dict[str, object]]:
|
|
125
|
+
return [
|
|
126
|
+
_serialize_transition(
|
|
127
|
+
slide.transition if slide.transition is not None else deck.transition
|
|
128
|
+
)
|
|
129
|
+
for slide in deck.slides
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def compose_with_ancestors(svg_str: str, chain: list[Path]) -> str:
|
|
134
|
+
"""Prepend ancestor SVG content below the slide's own content."""
|
|
135
|
+
slide_root = etree.fromstring(svg_str.encode())
|
|
136
|
+
strip_layout_layers(slide_root)
|
|
137
|
+
|
|
138
|
+
ancestor_groups: list[etree._Element] = [] # pyright: ignore[reportPrivateUsage]
|
|
139
|
+
merged_defs: list[etree._Element] = [] # pyright: ignore[reportPrivateUsage]
|
|
140
|
+
|
|
141
|
+
for ancestor_path in chain:
|
|
142
|
+
anc_str = clean_inkscape_svg(ancestor_path)
|
|
143
|
+
anc_root = etree.fromstring(anc_str.encode())
|
|
144
|
+
strip_layout_layers(anc_root)
|
|
145
|
+
|
|
146
|
+
for defs_el in anc_root.findall(f"{{{ns.SVG}}}defs"):
|
|
147
|
+
merged_defs.extend(list(defs_el))
|
|
148
|
+
|
|
149
|
+
children = [el for el in anc_root if el.tag != f"{{{ns.SVG}}}defs"]
|
|
150
|
+
if children:
|
|
151
|
+
g = etree.Element(f"{{{ns.SVG}}}g")
|
|
152
|
+
for child in children:
|
|
153
|
+
g.append(child)
|
|
154
|
+
ancestor_groups.append(g)
|
|
155
|
+
|
|
156
|
+
if merged_defs:
|
|
157
|
+
slide_defs = slide_root.find(f"{{{ns.SVG}}}defs")
|
|
158
|
+
if slide_defs is None:
|
|
159
|
+
slide_defs = etree.Element(f"{{{ns.SVG}}}defs")
|
|
160
|
+
slide_root.insert(0, slide_defs)
|
|
161
|
+
for i, def_el in enumerate(merged_defs):
|
|
162
|
+
slide_defs.insert(i, def_el)
|
|
163
|
+
|
|
164
|
+
insert_pos = next(
|
|
165
|
+
(i + 1 for i, el in enumerate(slide_root) if el.tag == f"{{{ns.SVG}}}defs"),
|
|
166
|
+
0,
|
|
167
|
+
)
|
|
168
|
+
for i, group in enumerate(ancestor_groups):
|
|
169
|
+
slide_root.insert(insert_pos + i, group)
|
|
170
|
+
|
|
171
|
+
return etree.tostring(slide_root, encoding="unicode")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _resolve_markdown_slide(
|
|
175
|
+
ms: MarkdownSlide, project_dir: Path, theme: str | None
|
|
176
|
+
) -> Slide:
|
|
177
|
+
from inkflow.markdown import build_slide_content
|
|
178
|
+
|
|
179
|
+
# Use project_dir as the synthetic svg_path parent so multi-part relative
|
|
180
|
+
# paths in the template string resolve relative to the project root.
|
|
181
|
+
template_path = resolve_parent_path(
|
|
182
|
+
ms.template, project_dir / "_", project_dir, theme
|
|
183
|
+
)
|
|
184
|
+
content_path = (
|
|
185
|
+
_resolve_content_src(ms.content, project_dir)
|
|
186
|
+
if ms.content is not None
|
|
187
|
+
else None
|
|
188
|
+
)
|
|
189
|
+
content = build_slide_content(content_path, ms.steps, ms._extra) # pyright: ignore[reportPrivateUsage]
|
|
190
|
+
return Slide(
|
|
191
|
+
src=str(template_path),
|
|
192
|
+
animations=ms.animations,
|
|
193
|
+
content=content,
|
|
194
|
+
transition=ms.transition,
|
|
195
|
+
style=ms.style,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def process_slide(
|
|
200
|
+
slide: Slide,
|
|
201
|
+
project_dir: Path,
|
|
202
|
+
theme: str | None,
|
|
203
|
+
slide_number: int,
|
|
204
|
+
total_slides: int,
|
|
205
|
+
deck_style: str = "",
|
|
206
|
+
font_size: int = 36,
|
|
207
|
+
) -> str:
|
|
208
|
+
src = resolve_slide_src(slide.src, project_dir)
|
|
209
|
+
svg_str = clean_inkscape_svg(src)
|
|
210
|
+
chain = resolve_chain(src, project_dir, theme)
|
|
211
|
+
if chain:
|
|
212
|
+
svg_str = compose_with_ancestors(svg_str, chain)
|
|
213
|
+
svg_str = substitute_zone_numbers(svg_str, slide_number, total_slides)
|
|
214
|
+
if slide.content:
|
|
215
|
+
svg_str = substitute_content(svg_str, slide.content, project_dir, font_size)
|
|
216
|
+
if slide.animations:
|
|
217
|
+
svg_str = annotate_svg(svg_str, slide.animations)
|
|
218
|
+
combined_css = "\n".join(filter(None, [deck_style, slide.style]))
|
|
219
|
+
if combined_css:
|
|
220
|
+
svg_str = inject_style(svg_str, combined_css)
|
|
221
|
+
svg_str = remove_unreferenced_zones(svg_str)
|
|
222
|
+
return svg_str
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def process_deck(deck: Deck, project_dir: Path) -> list[str]:
|
|
226
|
+
total = len(deck.slides)
|
|
227
|
+
results: list[str] = []
|
|
228
|
+
for i, entry in enumerate(deck.slides):
|
|
229
|
+
if isinstance(entry, MarkdownSlide):
|
|
230
|
+
slide = _resolve_markdown_slide(entry, project_dir, deck.theme)
|
|
231
|
+
else:
|
|
232
|
+
slide = entry
|
|
233
|
+
results.append(
|
|
234
|
+
process_slide(
|
|
235
|
+
slide,
|
|
236
|
+
project_dir,
|
|
237
|
+
deck.theme,
|
|
238
|
+
i + 1,
|
|
239
|
+
total,
|
|
240
|
+
deck.style,
|
|
241
|
+
deck.font_size,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
return results
|
inkflow/presenter.css
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* Catppuccin Mocha */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #1e1e2e;
|
|
4
|
+
--surface: #313244;
|
|
5
|
+
--overlay: #45475a;
|
|
6
|
+
--text: #cdd6f4;
|
|
7
|
+
--subtext: #a6adc8;
|
|
8
|
+
--accent: #cba6f7;
|
|
9
|
+
--green: #a6e3a1;
|
|
10
|
+
--red: #f38ba8;
|
|
11
|
+
--blue: #89b4fa;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background: var(--bg);
|
|
18
|
+
color: var(--text);
|
|
19
|
+
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
|
20
|
+
height: 100vh;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
user-select: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#stage {
|
|
28
|
+
flex: 1;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
padding: 0.5rem;
|
|
33
|
+
min-height: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#stage > svg,
|
|
37
|
+
#stage > :first-child {
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
max-height: 100%;
|
|
40
|
+
width: auto;
|
|
41
|
+
height: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#statusbar {
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
background: var(--surface);
|
|
47
|
+
color: var(--subtext);
|
|
48
|
+
font-size: 0.7rem;
|
|
49
|
+
padding: 0.25rem 0.75rem;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 1rem;
|
|
53
|
+
border-top: 1px solid var(--accent);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#statusbar .sep { color: var(--overlay); }
|
|
57
|
+
|
|
58
|
+
#ws-dot {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
width: 6px;
|
|
61
|
+
height: 6px;
|
|
62
|
+
border-radius: 50%;
|
|
63
|
+
background: var(--red);
|
|
64
|
+
margin-right: 4px;
|
|
65
|
+
transition: background 0.3s;
|
|
66
|
+
}
|
|
67
|
+
#ws-dot.connected { background: var(--green); }
|
|
68
|
+
|
|
69
|
+
#hint {
|
|
70
|
+
margin-left: auto;
|
|
71
|
+
color: var(--overlay);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ── Curtain (blackout / whiteout) ── */
|
|
75
|
+
#curtain {
|
|
76
|
+
display: none;
|
|
77
|
+
position: fixed;
|
|
78
|
+
inset: 0;
|
|
79
|
+
z-index: 20;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
#curtain.visible { display: block; }
|
|
83
|
+
|
|
84
|
+
/* ── Help overlay ── */
|
|
85
|
+
#help {
|
|
86
|
+
display: none;
|
|
87
|
+
position: fixed;
|
|
88
|
+
inset: 0;
|
|
89
|
+
z-index: 30;
|
|
90
|
+
background: rgba(0, 0, 0, 0.6);
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
}
|
|
94
|
+
#help.visible { display: flex; }
|
|
95
|
+
|
|
96
|
+
#help-box {
|
|
97
|
+
background: var(--surface);
|
|
98
|
+
border: 1px solid var(--accent);
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
padding: 1.5rem 2rem;
|
|
101
|
+
min-width: 26rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#help-box h2 {
|
|
105
|
+
color: var(--accent);
|
|
106
|
+
font-size: 0.85rem;
|
|
107
|
+
margin-bottom: 1rem;
|
|
108
|
+
letter-spacing: 0.05em;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#help-box table { border-collapse: collapse; width: 100%; }
|
|
112
|
+
#help-box td { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
|
|
113
|
+
#help-box td:first-child { color: var(--accent); white-space: nowrap; text-align: right; }
|
|
114
|
+
#help-box td:last-child { color: var(--subtext); padding-left: 1rem; }
|
|
115
|
+
|
|
116
|
+
#help-box p {
|
|
117
|
+
margin-top: 1rem;
|
|
118
|
+
font-size: 0.7rem;
|
|
119
|
+
color: var(--overlay);
|
|
120
|
+
text-align: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
kbd {
|
|
124
|
+
background: var(--overlay);
|
|
125
|
+
color: var(--text);
|
|
126
|
+
border-radius: 3px;
|
|
127
|
+
padding: 0.1em 0.35em;
|
|
128
|
+
font-size: 0.85em;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Error overlay ── */
|
|
132
|
+
#error-overlay {
|
|
133
|
+
display: none;
|
|
134
|
+
position: fixed;
|
|
135
|
+
inset: 0;
|
|
136
|
+
z-index: 50;
|
|
137
|
+
background: rgba(30, 30, 46, 0.95);
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
padding: 3rem 2rem;
|
|
141
|
+
overflow-y: auto;
|
|
142
|
+
}
|
|
143
|
+
#error-overlay.visible { display: flex; }
|
|
144
|
+
|
|
145
|
+
#error-box {
|
|
146
|
+
background: var(--surface);
|
|
147
|
+
border: 1px solid var(--red);
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
padding: 1.5rem 2rem;
|
|
150
|
+
max-width: 72rem;
|
|
151
|
+
width: 100%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#error-box h2 {
|
|
155
|
+
color: var(--red);
|
|
156
|
+
font-size: 0.85rem;
|
|
157
|
+
margin-bottom: 1rem;
|
|
158
|
+
letter-spacing: 0.05em;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#error-box pre {
|
|
162
|
+
color: var(--text);
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
white-space: pre-wrap;
|
|
165
|
+
word-break: break-all;
|
|
166
|
+
line-height: 1.6;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── Animation classes ── */
|
|
170
|
+
|
|
171
|
+
.anim-fade-in {
|
|
172
|
+
opacity: 0;
|
|
173
|
+
transition: opacity 0.4s ease;
|
|
174
|
+
}
|
|
175
|
+
.anim-fade-in.active {
|
|
176
|
+
opacity: 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.anim-fade-out {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
transition: opacity 0.4s ease;
|
|
182
|
+
}
|
|
183
|
+
.anim-fade-out.active {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Bounce-in from below */
|
|
188
|
+
.anim-bounce {
|
|
189
|
+
opacity: 0;
|
|
190
|
+
transform: translateY(14px);
|
|
191
|
+
transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
192
|
+
}
|
|
193
|
+
.anim-bounce.active {
|
|
194
|
+
opacity: 1;
|
|
195
|
+
transform: translateY(0);
|
|
196
|
+
}
|
inkflow/presenter.html
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="__DATA_THEME__">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Inkflow</title>
|
|
7
|
+
<style>__STYLES__</style>
|
|
8
|
+
<style>__CSS__</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="error-overlay">
|
|
12
|
+
<div id="error-box">
|
|
13
|
+
<h2>BUILD ERROR</h2>
|
|
14
|
+
<pre id="error-msg"></pre>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="curtain"></div>
|
|
18
|
+
<div id="help" role="dialog" aria-modal="true">
|
|
19
|
+
<div id="help-box">
|
|
20
|
+
<h2>KEYBINDINGS</h2>
|
|
21
|
+
<table>
|
|
22
|
+
<tr><td>→ Space l</td><td>Advance step / next slide</td></tr>
|
|
23
|
+
<tr><td>← Backspace h</td><td>Back one step / previous slide</td></tr>
|
|
24
|
+
<tr><td>↓ j</td><td>Next slide (skip steps)</td></tr>
|
|
25
|
+
<tr><td>↑ k</td><td>Previous slide (skip steps)</td></tr>
|
|
26
|
+
<tr><td>^ Home</td><td>First slide</td></tr>
|
|
27
|
+
<tr><td>$ End</td><td>Last slide</td></tr>
|
|
28
|
+
<tr><td>g 12 ↵</td><td>Go to slide 12</td></tr>
|
|
29
|
+
<tr><td>b .</td><td>Blackout</td></tr>
|
|
30
|
+
<tr><td>w</td><td>Whiteout</td></tr>
|
|
31
|
+
<tr><td>f</td><td>Toggle fullscreen</td></tr>
|
|
32
|
+
<tr><td>t</td><td>Toggle dark / light mode</td></tr>
|
|
33
|
+
<tr><td>?</td><td>This help</td></tr>
|
|
34
|
+
</table>
|
|
35
|
+
<p>Press <kbd>?</kbd> or <kbd>Esc</kbd> to close</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="stage"></div>
|
|
39
|
+
<div id="statusbar">
|
|
40
|
+
<span id="slide-info">– / –</span>
|
|
41
|
+
<span class="sep">|</span>
|
|
42
|
+
<span id="step-info">step –</span>
|
|
43
|
+
<span class="sep">|</span>
|
|
44
|
+
<span><span id="ws-dot"></span><span id="ws-label">disconnected</span></span>
|
|
45
|
+
<span id="hint">← → Space · navigate · g goto · ? help</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<script>__JS__</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|