inkflow 0.1.0.dev0__tar.gz

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 (83) hide show
  1. inkflow-0.1.0.dev0/.claude/hooks/ruff-format.sh +8 -0
  2. inkflow-0.1.0.dev0/.claude/settings.json +15 -0
  3. inkflow-0.1.0.dev0/.gitattributes +1 -0
  4. inkflow-0.1.0.dev0/.githooks/pre-commit +13 -0
  5. inkflow-0.1.0.dev0/.github/workflows/ci.yml +22 -0
  6. inkflow-0.1.0.dev0/.github/workflows/release.yml +33 -0
  7. inkflow-0.1.0.dev0/.gitignore +7 -0
  8. inkflow-0.1.0.dev0/CLAUDE.md +82 -0
  9. inkflow-0.1.0.dev0/LICENSE +21 -0
  10. inkflow-0.1.0.dev0/PKG-INFO +12 -0
  11. inkflow-0.1.0.dev0/README.md +116 -0
  12. inkflow-0.1.0.dev0/ROADMAP.md +245 -0
  13. inkflow-0.1.0.dev0/docs/assets/logo-dark-icon.svg +17 -0
  14. inkflow-0.1.0.dev0/docs/assets/logo-dark-landscape.svg +18 -0
  15. inkflow-0.1.0.dev0/docs/assets/logo-dark-portrait.svg +17 -0
  16. inkflow-0.1.0.dev0/docs/assets/logo-light-icon.svg +17 -0
  17. inkflow-0.1.0.dev0/docs/assets/logo-light-landscape.svg +17 -0
  18. inkflow-0.1.0.dev0/docs/assets/logo-light-portrait.svg +17 -0
  19. inkflow-0.1.0.dev0/example/assets/demo.jpg +0 -0
  20. inkflow-0.1.0.dev0/example/assets/demo.mp4 +0 -0
  21. inkflow-0.1.0.dev0/example/deck.py +57 -0
  22. inkflow-0.1.0.dev0/example/layouts/content.svg +14 -0
  23. inkflow-0.1.0.dev0/example/layouts/main.svg +13 -0
  24. inkflow-0.1.0.dev0/example/layouts/media-right.svg +18 -0
  25. inkflow-0.1.0.dev0/example/slides/01-title.svg +15 -0
  26. inkflow-0.1.0.dev0/example/slides/02-diagram.svg +40 -0
  27. inkflow-0.1.0.dev0/example/slides/03-crossfade.svg +17 -0
  28. inkflow-0.1.0.dev0/example/slides/04-morph.svg +17 -0
  29. inkflow-0.1.0.dev0/example/slides/05-markdown.md +13 -0
  30. inkflow-0.1.0.dev0/example/slides/06-image.md +12 -0
  31. inkflow-0.1.0.dev0/example/slides/07-video.md +11 -0
  32. inkflow-0.1.0.dev0/pyproject.toml +49 -0
  33. inkflow-0.1.0.dev0/src/inkflow/__init__.py +31 -0
  34. inkflow-0.1.0.dev0/src/inkflow/cli.py +266 -0
  35. inkflow-0.1.0.dev0/src/inkflow/content.py +227 -0
  36. inkflow-0.1.0.dev0/src/inkflow/export.py +123 -0
  37. inkflow-0.1.0.dev0/src/inkflow/layout.py +279 -0
  38. inkflow-0.1.0.dev0/src/inkflow/manifest.py +145 -0
  39. inkflow-0.1.0.dev0/src/inkflow/markdown.py +158 -0
  40. inkflow-0.1.0.dev0/src/inkflow/ns.py +9 -0
  41. inkflow-0.1.0.dev0/src/inkflow/pdf.html +19 -0
  42. inkflow-0.1.0.dev0/src/inkflow/pipeline.py +244 -0
  43. inkflow-0.1.0.dev0/src/inkflow/presenter.css +196 -0
  44. inkflow-0.1.0.dev0/src/inkflow/presenter.html +50 -0
  45. inkflow-0.1.0.dev0/src/inkflow/presenter.js +400 -0
  46. inkflow-0.1.0.dev0/src/inkflow/server.py +380 -0
  47. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/center.svg +3 -0
  48. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/cover.svg +5 -0
  49. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/default.svg +4 -0
  50. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/end.svg +4 -0
  51. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/fact.svg +4 -0
  52. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/media-left.svg +5 -0
  53. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/media-right.svg +5 -0
  54. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/quote.svg +4 -0
  55. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/section.svg +4 -0
  56. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/statement.svg +3 -0
  57. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/two-cols-header.svg +5 -0
  58. inkflow-0.1.0.dev0/src/inkflow/theme/layouts/two-cols.svg +5 -0
  59. inkflow-0.1.0.dev0/src/inkflow/theme/main.svg +3 -0
  60. inkflow-0.1.0.dev0/src/inkflow/theme/numbered-main.svg +6 -0
  61. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/deck.py +18 -0
  62. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/01-cover.md +7 -0
  63. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/02-section.md +7 -0
  64. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/03-default.md +11 -0
  65. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/04-center.md +8 -0
  66. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/05-two-cols.md +17 -0
  67. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/06-two-cols-header.md +13 -0
  68. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/07-fact.md +7 -0
  69. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/08-quote.md +7 -0
  70. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/09-statement.md +3 -0
  71. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/10-media-left.md +15 -0
  72. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/11-media-right.md +16 -0
  73. inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/12-end.md +3 -0
  74. inkflow-0.1.0.dev0/src/inkflow/theme/styles.css +55 -0
  75. inkflow-0.1.0.dev0/src/inkflow/tui.py +128 -0
  76. inkflow-0.1.0.dev0/tests/__init__.py +0 -0
  77. inkflow-0.1.0.dev0/tests/test_content.py +211 -0
  78. inkflow-0.1.0.dev0/tests/test_layout.py +238 -0
  79. inkflow-0.1.0.dev0/tests/test_manifest.py +166 -0
  80. inkflow-0.1.0.dev0/tests/test_markdown.py +207 -0
  81. inkflow-0.1.0.dev0/tests/test_pipeline.py +300 -0
  82. inkflow-0.1.0.dev0/tests/test_server.py +147 -0
  83. inkflow-0.1.0.dev0/uv.lock +569 -0
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ FILE=$(jq -r '.tool_input.file_path' )
5
+
6
+ [[ "$FILE" == *.py ]] || exit 0
7
+
8
+ uv run ruff format "$FILE"
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/ruff-format.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ *.svg diff=inkscape-svg
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # Strip Inkscape editor metadata from staged SVG files before committing.
3
+ # Install: git config core.hooksPath .githooks
4
+
5
+ set -e
6
+
7
+ mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM | grep -E '\.svg$' || true)
8
+ [ ${#staged[@]} -eq 0 ] && exit 0
9
+
10
+ echo "[inkflow] cleaning staged SVGs..."
11
+ uv run inkflow clean "${staged[@]}"
12
+
13
+ git add "${staged[@]}"
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ check:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v6
11
+
12
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
13
+
14
+ - run: uv sync
15
+
16
+ - run: uv run ruff format --check src/ tests/
17
+
18
+ - run: uv run ruff check src/ tests/
19
+
20
+ - run: uv run basedpyright
21
+
22
+ - run: uv run pytest
@@ -0,0 +1,33 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ release:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+
17
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
18
+
19
+ - name: Check version matches tag
20
+ run: |
21
+ TAG="${GITHUB_REF_NAME#v}"
22
+ VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
23
+ if [ "$TAG" != "$VERSION" ]; then
24
+ echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $VERSION"
25
+ exit 1
26
+ fi
27
+
28
+ - run: uv build
29
+
30
+ - name: Smoke test
31
+ run: uv run --with dist/*.whl --no-project -- inkflow build example/deck.py -o /tmp/inkflow-smoke
32
+
33
+ - run: uv publish
@@ -0,0 +1,7 @@
1
+ *.egg-info/
2
+ *.pyc
3
+ .venv/
4
+ __pycache__/
5
+ build/
6
+ deck.pdf
7
+ dist/
@@ -0,0 +1,82 @@
1
+ # Inkflow — codebase guide
2
+
3
+ ## Running
4
+
5
+ ```bash
6
+ uv sync # install deps into .venv
7
+ uv run inkflow serve example/deck.py # start server at localhost:7777
8
+ ```
9
+
10
+ ## Git setup (one-time, per clone)
11
+
12
+ ```bash
13
+ uv run inkflow setup-git
14
+ ```
15
+
16
+ Configures two things:
17
+ - **Pre-commit hook** (`.githooks/pre-commit`) — strips Inkscape editor metadata from staged SVGs before every commit, so viewport pan/zoom/window state never lands in history
18
+ - **SVG diff driver** — `git diff`, `git log -p`, and GitHub's diff view show only visual changes even for SVGs that haven't been cleaned in-place
19
+
20
+ Git won't run this automatically on clone — that's an intentional git security boundary — so it needs to be run once. After that it's invisible.
21
+
22
+ SVG source files should be kept clean (no Inkscape metadata) in the repository. Run `uv run inkflow clean example/slides/*.svg` to clean any files committed before the hook was in place.
23
+
24
+ ## Project layout
25
+
26
+ ```
27
+ src/inkflow/
28
+ __init__.py exports: Deck, Slide, FadeIn, FadeOut, Bounce, Cut, Crossfade, Morph
29
+ manifest.py dataclasses for the deck DSL
30
+ pipeline.py SVG cleaning (lxml) + animation annotation
31
+ server.py HTTP server, WebSocket server, file watcher, build pipeline
32
+ cli.py CLI entry point
33
+ presenter.html shell template — inlined with CSS/JS at serve time
34
+ presenter.css presenter styles
35
+ presenter.js presenter logic (navigation, transitions, WS)
36
+ example/
37
+ deck.py 4-slide example deck
38
+ slides/ source SVGs (Inkscape-authored)
39
+ ```
40
+
41
+ ## Key architecture decisions
42
+
43
+ **No Inkscape subprocess at serve time.** Inkscape is only used by the human author in the GUI. The pipeline reads SVG files directly with lxml, strips Inkscape/Sodipodi editor namespaces, and annotates elements with animation classes. This avoids GUI window flashes and is instant.
44
+
45
+ **Live reload pushes slides over WebSocket, not `location.reload()`.** When files change the server sends `{"type":"update","slides":[...],"transitions":[...]}` and the presenter swaps content in place, preserving the current slide index. Errors are sent as `{"type":"error","message":"..."}` and displayed as an overlay. The HTTP response includes `Cache-Control: no-store` so hard refreshes always get fresh content.
46
+
47
+ **`loadSlide()` vs `applyStep()` in the presenter JS.** Step advances within a slide must NOT re-render `stage.innerHTML` — that would make CSS transitions invisible because the browser only paints once per JS task. `loadSlide()` sets innerHTML (elements start at opacity 0). Subsequent `applyStep()` calls only toggle `.active` on existing DOM elements, triggering CSS transitions.
48
+
49
+ **`deck.py` is a Python module, not YAML/TOML.** Loaded via `importlib.util.spec_from_file_location`. Must define a module-level `deck` variable of type `Deck`.
50
+
51
+ **Morph transition uses a rAF loop over SVG attributes, not CSS transforms.** CSS `transform: translate(Xpx)` on SVG elements is interpreted in SVG user units, not CSS viewport pixels — FLIP-based approaches produce a coordinate gap proportional to the viewBox scale. Instead, `morphSlide()` snapshots raw geometry attributes (`x`, `y`, `width`, `height`, `rx` for rects; `cx`, `cy`, `r` for circles) before the innerHTML swap, then drives a `requestAnimationFrame` loop that calls `setAttribute` each frame in SVG user units. Colors are lerped channel-by-channel. Exit-only elements are reconstructed as ghost nodes in the new SVG and faded out. Backward navigation passes the outgoing slide's transition to `loadSlide()` so the morph plays in reverse.
52
+
53
+ **`presenter.html`/`css`/`js` are inlined at serve time.** `_build_html()` in `server.py` reads all three files and substitutes `__CSS__`, `__JS__`, `__SLIDES_JSON__`, `__TRANSITIONS_JSON__`, `__WS_PORT__`, `__ERROR_JSON__` tokens. Edit the source files and reload the browser to see changes.
54
+
55
+ ## Server
56
+
57
+ - HTTP on port 7777 (asyncio streams, custom handler)
58
+ - WebSocket on port 7778 (websockets 16.0 — uses `websockets.asyncio.server.serve`, not the legacy `websockets.serve`)
59
+ - File watcher: `watchfiles.awatch` (async generator)
60
+ - Both run inside an `asyncio.TaskGroup`
61
+
62
+ ## Animation pipeline
63
+
64
+ `pipeline.py`:
65
+ 1. `clean_inkscape_svg(src)` — parse with lxml, remove elements/attrs in `http://www.inkscape.org/namespaces/inkscape` and `http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd`, call `etree.cleanup_namespaces()`, serialize to string
66
+ 2. `annotate_svg(svg_str, animations)` — find elements by id (stripping leading `#`), set `class` and `data-step` attributes
67
+
68
+ CSS class map: `Fade → anim-fade-in`, `FadeOut → anim-fade-out`, `Bounce → anim-bounce`
69
+
70
+ ## Dependencies
71
+
72
+ ```
73
+ lxml>=5.0 SVG processing
74
+ websockets>=12.0 WebSocket server (uses 16.x asyncio API)
75
+ watchfiles>=0.21 inotify-based file watcher
76
+ ```
77
+
78
+ ## Not yet implemented
79
+
80
+ - Layout chain inlining (`inkflow:parent` resolution, `inject-layout`, `inkflow new`) — see ROADMAP.md
81
+ - Font embedding (fonttools)
82
+ - Morph for non-primitive shapes: `<path>` and `<polygon>` fall back to instant cut; `<g>` groups are cloned and faded as a unit — put the ID on the `<g>` (not on individual children) when you want a shape+label pair to enter/exit together
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nick Le Large
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.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: inkflow
3
+ Version: 0.1.0.dev0
4
+ Summary: Beautiful slides from SVG. Your editor, your style.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: lxml>=5.0
9
+ Requires-Dist: markdown-it-py>=3.0
10
+ Requires-Dist: rich>=15.0.0
11
+ Requires-Dist: watchfiles>=0.21
12
+ Requires-Dist: websockets>=12.0
@@ -0,0 +1,116 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-light-landscape.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-dark-landscape.svg">
5
+ <img src="docs/assets/logo-dark-landscape.svg" width="80%">
6
+ </picture>
7
+ </p>
8
+
9
+ <p align="center"><strong>Beautiful slides from SVG. Your editor, your style.</strong></p>
10
+
11
+ > **Early-stage software.**
12
+ > The core pipeline works end-to-end —
13
+ > animations, transitions, live reload, markdown slides, static export.
14
+ > The API is not stable and key features are still missing.
15
+ > Use at your own risk.
16
+
17
+ ## The idea
18
+
19
+ Every presentation tool makes the same tradeoff:
20
+ either you get a nice visual authoring environment (PowerPoint, Keynote, Google Slides)
21
+ or you get something that plays well with version control and plaintext workflows (Beamer, Slidev, reveal.js).
22
+ You rarely get both,
23
+ and you almost never get freeform visual design with git-friendly source files and animated, sequenced output.
24
+ Worse, the tools with the best visual editors tend to lock your content into proprietary formats
25
+ that are tied to a specific platform or subscription.
26
+
27
+ Inkflow tries to bridge that gap.
28
+ The source files are SVGs, Markdown, and a Python config file.
29
+ Any SVG editor works.
30
+ Inkscape is the first-class supported environment,
31
+ but you can use Inkscape, Affinity Designer, a text editor, or anything else that produces valid SVG.
32
+ The presentation layer is a **Python pipeline** that turns those files into an animated, browser-based presenter.
33
+ Because everything is open formats, plaintext, and diffable,
34
+ you are not tied to any particular software or cloud service.
35
+ Switch editors, move repos, or swap tools at any time without losing your work.
36
+
37
+ ## How it works
38
+
39
+ A deck is a plain Python file:
40
+
41
+ ```python
42
+ from inkflow import Bounce, Crossfade, Cut, Deck, FadeIn, MarkdownSlide, Media, Morph, Slide
43
+
44
+ deck = Deck()
45
+
46
+ deck.slides = [
47
+ # SVG slide: draw freely in Inkscape, animate elements by id
48
+ Slide(
49
+ "slides/01-title.svg",
50
+ animations=[
51
+ FadeIn("#headline", step=1),
52
+ FadeIn("#subtitle", step=2),
53
+ ],
54
+ ),
55
+ Slide("slides/02-diagram.svg", transition=Crossfade(), animations=[
56
+ Bounce("#box-a", step=1),
57
+ Bounce("#box-b", step=2),
58
+ ]),
59
+ Slide("slides/03-chart.svg", transition=Morph(duration=0.7)),
60
+
61
+ # Markdown slide: write content in .md, render into a layout SVG
62
+ MarkdownSlide("layouts/content.svg", content="slides/04-notes.md"),
63
+ MarkdownSlide(
64
+ "layouts/media-right.svg",
65
+ content="slides/05-image.md",
66
+ media=Media("assets/photo.jpg", fit="cover"),
67
+ ),
68
+ ]
69
+ ```
70
+
71
+ Both slide types support injecting content into named SVG elements:
72
+ `TextBox` to fill a text placeholder, `Media` to embed an image or video.
73
+ `Slide` is SVG-first: the SVG carries the design, with optional content slots for dynamic parts.
74
+ `MarkdownSlide` is layout-first: a template SVG defines the structure,
75
+ a Markdown file provides the text, and named kwargs fill any additional slots.
76
+ It is shorthand for the common case, built on the same injection mechanism.
77
+
78
+ ## Quick start
79
+
80
+ ```bash
81
+ uv add inkflow # or: pip install inkflow
82
+ inkflow serve deck.py
83
+ # press "o" in the tui to open http://localhost:7777 in your browser,
84
+ # press ? in the presenter for keyboard shortcuts
85
+ ```
86
+
87
+ To try the bundled example:
88
+
89
+ ```bash
90
+ git clone https://github.com/ll-nick/inkflow
91
+ cd inkflow
92
+ uv run inkflow serve example/deck.py
93
+ ```
94
+
95
+ No SVG editor is invoked at serve time — Inkscape or any other tool writes the files, Inkflow reads them.
96
+ Saving a slide reloads the presenter automatically.
97
+
98
+ ## Commands
99
+
100
+ | Command | Description |
101
+ |---|---|
102
+ | `inkflow serve deck.py` | Start the live-reload presenter |
103
+ | `inkflow build deck.py` | Export a self-contained HTML directory for offline use |
104
+ | `inkflow export deck.py` | Export a PDF via headless Chromium |
105
+
106
+ ## Architecture
107
+
108
+ - **`deck.py`:** Python manifest; gives you autocomplete and programmatic slide generation for free
109
+ - **SVG pipeline:** lxml strips Inkscape editor metadata,
110
+ then annotates elements with CSS animation classes and `data-step` attributes based on the manifest
111
+ - **Layout system:** `MarkdownSlide` injects Markdown content into layout SVGs;
112
+ built-in theme layouts cover common slide types
113
+ - **Local server:** asyncio HTTP server serves the presenter HTML with slides embedded as JSON;
114
+ a WebSocket server pushes live-reload signals when files change
115
+ - **Browser presenter:** — vanilla HTML/JS/CSS, no framework
116
+
@@ -0,0 +1,245 @@
1
+ # Roadmap
2
+
3
+ Things that would need to exist before this is a tool someone would actually give a talk with.
4
+
5
+ Items are roughly ordered by how much they matter. The first two sections are load-bearing — without them the tool is a toy. Everything after is about closing the gap to a real presenter experience. The final section is honest about what is out of scope or architecturally not possible.
6
+
7
+ ---
8
+
9
+ ## Decided: design questions
10
+
11
+ **SVG editor agnosticism**
12
+ The tool has no hard dependency on Inkscape at runtime. The pipeline only reads and writes standard SVG; it never spawns Inkscape as a subprocess. Any vector editor that exports well-formed SVG — Inkscape, Figma, Affinity Designer, Sketch, or hand-coded files — works as an authoring tool. `clean_svg()` strips Inkscape/Sodipodi editor metadata if present, and is a no-op on files from other tools.
13
+
14
+ The one Inkscape-specific feature is `inject-layout`: it writes `inkscape:groupmode="layer"` and `sodipodi:insensitive` attributes so Inkscape's GUI renders the injected preview layers correctly. These attributes are harmless in any other tool. A future refinement is to make the Inkscape layer attributes opt-in and write only `data-inkflow-*` attributes by default.
15
+
16
+ **One file per slide vs multi-page SVG**
17
+ Staying with one file per slide. Inkscape's multi-page SVG format uses `inkscape:page` elements that are not standard SVG; parsing them would tightly couple the tool to Inkscape's internal implementation. One-file-per-slide produces cleaner git diffs and maps to the standard SVG model. Worth revisiting only if Inkscape formalises the format.
18
+
19
+ **Markdown slide types: foreignObject**
20
+ An existing Markdown library (`markdown-it-py`) converts content to HTML in one call — no custom Markdown parsing. The HTML is injected into a `<foreignObject>` at the placeholder zone's geometry. Typography and color come from the CSS cascade (theme `styles.css` + project `styles.css`) injected into the HTML `<head>`, which cascades into all `<foreignObject>` content. The SVG text generation approach is not pursued: it cannot support tables, images, or nested lists and hits a ceiling quickly.
21
+
22
+ **Zone naming convention**
23
+ Content placeholder zones in layout SVGs are `<rect>` elements with `id="zone-{name}"`. The `zone-` prefix is the single naming rule authors need to learn. Zone rects must carry explicit `x`, `y`, `width`, `height` attributes.
24
+
25
+ Standard zone names with reserved pipeline behaviour:
26
+ - `zone-title` — receives the leading `# H1` auto-extracted from a markdown file
27
+ - `zone-subtitle` — receives the `## H2` immediately following the title, before any body content
28
+ - `zone-content` — default content zone; receives everything not routed elsewhere
29
+ - `zone-slide-number` — a `<text>` element; text content replaced with the current slide number at pipeline time
30
+ - `zone-slide-total` — a `<text>` element; text content replaced with the total slide count
31
+
32
+ Unreferenced zone rects — zones present in a layout but not filled by the slide — are silently removed from the output.
33
+
34
+ **Markdown file format**
35
+ `MarkdownSlide` reads a single `.md` file per slide. Custom syntax is limited to two block markers using the same `::name::` form:
36
+
37
+ - `::zone-name::` — routes all content from this point to the named zone until the next marker
38
+ - `::step::` — inserts an animation step boundary within the current zone; the step counter continues from wherever the slide's SVG animations left off
39
+
40
+ Auto-extraction applies when the file contains no explicit `::` markers:
41
+ 1. A leading `# H1` is extracted into `zone-title`, if the layout has that zone
42
+ 2. A `## H2` immediately following (before any body content) is extracted into `zone-subtitle`
43
+ 3. Everything else goes to `zone-content`
44
+
45
+ Explicit markers always override auto-extraction. Single-zone layouts need no markers at all.
46
+
47
+ **Layout path resolution**
48
+ Prefix syntaxes bypass the search entirely:
49
+ - `local:foo` — `{project_root}/layouts/foo.svg`, error if not found
50
+ - `theme:foo` — `{theme_dir}/layouts/foo.svg`, error if not found or no theme set
51
+ - `builtin:foo` — inkflow built-in layouts
52
+ - `./foo`, `../foo` — relative to the current SVG file's location (used by theme-internal chains)
53
+ - `/absolute/path` — literal filesystem path
54
+
55
+ Bare single-part names (no prefix, no `/`) are resolved by a three-level search in order: project `layouts/` → active theme `layouts/` → inkflow built-in `layouts/`. First match wins.
56
+
57
+ This resolution applies uniformly wherever a layout name appears: the first argument of `MarkdownSlide`, the `inkflow:parent` attribute on any SVG, and `inkflow new`.
58
+
59
+ ---
60
+
61
+ ## Open design questions
62
+
63
+ **Layout overrides**
64
+ Some layouts visually override the base layout frame — a section divider or full-bleed title slide typically drops the footer and page number. The mechanism for opting out (per-layout or per-slide) is not yet decided. Candidates: a flag attribute on the layout SVG root (`inkflow:no-layout="true"`), or a `layout_zones` exclusion list in `deck.py`. This affects both inject-layout (which layers to inject) and the pipeline (which elements to include).
65
+
66
+ ---
67
+
68
+ ## Layout inheritance
69
+
70
+ The layout system uses a general parent/child model. Each SVG file can declare its parent via an `inkflow:parent` attribute on the root `<svg>` element. This creates an arbitrary-depth chain. A typical deck has three levels:
71
+
72
+ ```
73
+ slides/05-bullets.svg
74
+ ↑ inkflow:parent
75
+ layouts/bullets.svg ← layout: content zone positions
76
+ ↑ inkflow:parent
77
+ theme/numbered-main.svg ← adds zone-slide-number / zone-slide-total
78
+ ↑ inkflow:parent
79
+ theme/main.svg ← base: background, brand elements (no parent — chain terminates)
80
+ ```
81
+
82
+ The `inkflow:parent` attribute is set by tooling (`inkflow new`, `inject-layout`), not by hand.
83
+
84
+ **`inject-layout` command**
85
+ `inkflow inject-layout` resolves the parent chain for each file and injects each ancestor as a separate locked Inkscape layer below the slide's own content, providing a spatial reference during authoring. The command is idempotent: it compares `inkflow:layout-hash` on existing layers against current file content and only rewrites stale entries. The pipeline strips all layout layers before serving; they never appear in the browser.
86
+
87
+ `inkflow inject-layout --check` reports stale files without rewriting. `inkflow new <layout> <path>` calls `inject-layout` automatically after creating the file.
88
+
89
+ ---
90
+
91
+ ## Pipeline completeness
92
+
93
+ **Font embedding**
94
+ SVGs reference fonts by name. On the authoring machine this works; on any other machine it may not. Use fonttools to resolve fonts via fontconfig, base64-encode them, and inline `@font-face` declarations inside a `<defs>` block. Makes each output SVG self-contained without converting text to paths.
95
+
96
+ **PDF export**
97
+ Distinct from static HTML export. PDF is how you share slides with conference organizers, submit to proceedings, and post a permanent copy online. The cleanest path: build the static HTML, then print it to PDF via headless Chromium (`--print-to-pdf`). Each slide needs to map to exactly one PDF page, which requires a print stylesheet that shows one slide at a time.
98
+
99
+ **Static HTML export**
100
+ `inkflow build deck.py` produces a single self-contained HTML file — all slides inlined, fonts embedded, no server required. How you present from an unfamiliar machine and the intermediate step before PDF export.
101
+
102
+ ---
103
+
104
+ ## Presenter experience
105
+
106
+ **Slide picker**
107
+ `g` currently enters a numeric goto buffer (`g: 12_`, Enter jumps, Escape cancels). The next step is replacing this with a full picker overlay: a modal where typing digits narrows by slide number and Enter or click jumps. Requires an optional `title` field on `Slide` in `deck.py`; the manifest field is cleaner than auto-extracting from SVG.
108
+
109
+ **Hidden / draft slides**
110
+ `Slide("...", visible=False)` keeps a slide in `deck.py` and its SVG on disk but excludes it from the presentation. Essential for working decks where you trim slides depending on audience or time without deleting anything.
111
+
112
+ **Presenter view**
113
+ A second window (or `/presenter` URL) showing the current slide, the next slide preview, step counter, and a running clock. The two windows stay in sync via the same WebSocket connection. Essential for any talk longer than ten minutes.
114
+
115
+ **Remote control**
116
+ A `/remote` URL serving a minimal forward/back interface designed for a phone browser. Sends the same advance/retreat messages the keyboard sends. The WebSocket architecture makes this nearly free to implement.
117
+
118
+ **Speaker notes**
119
+ A `notes` field on `Slide` that appears in the presenter view but not the main display. Plain string in `deck.py`; rendered as markdown in the presenter window.
120
+
121
+ **Drawing and annotation mode**
122
+ During Q&A, freehand drawing on the current slide is more useful than a laser pointer. A toggle (e.g. D key) that overlays a canvas element and lets you draw with the mouse or stylus. Drawings are ephemeral — they don't persist between slides.
123
+
124
+ **Laser pointer**
125
+ A coloured dot that follows the mouse while a modifier key is held.
126
+
127
+ ---
128
+
129
+ ## Content features
130
+
131
+ **Per-step code line highlighting**
132
+ In technical presentations, stepping through a code block with specific lines highlighted per keypress is one of the most-used Slidev features. A `CodeSlide` template (or a `Highlight` animation that targets line ranges) that dims non-highlighted lines and brightens the relevant ones on each step.
133
+
134
+ **Math / LaTeX**
135
+ Enable the `dollarmath` plugin in `markdown-it-py` (emits `<span class="math-inline">` / `<div class="math-block">`), and load KaTeX in the presenter to render those spans client-side. Since `<foreignObject>` content is real HTML in the browser, KaTeX's auto-render works there without modification.
136
+
137
+ **Section dividers and table of contents**
138
+ A `SectionSlide("title")` type that marks a section boundary. The pipeline can auto-generate a TOC slide from all section boundaries, and section titles can appear in the presenter status bar.
139
+
140
+ ---
141
+
142
+ ## Authoring experience
143
+
144
+ **`inkflow set-parent <file> <parent>` command**
145
+ Updates `inkflow:parent` on an existing slide SVG and re-runs `inject-layout` on that file. Use when changing a slide's layout after initial creation.
146
+
147
+ **Element ID validation**
148
+ When `deck.py` references `#headline` but the SVG has no matching element, this should be a hard build error: name the slide file and the missing ID, stop the build. Currently a silent skip.
149
+
150
+ **Watch-only mode**
151
+ `inkflow watch deck.py` rebuilds on change but doesn't open a browser or run a server. Useful for catching `deck.py` errors immediately during authoring.
152
+
153
+ **Custom slide dimensions**
154
+ Currently hardcoded 1920×1080. Should be a per-deck setting on `Deck(width=..., height=...)`, used by the pipeline to set the SVG viewport and by the presenter to size the stage correctly.
155
+
156
+ **Inkscape layer conventions for Morph**
157
+ For within-slide morphing, the two element states need to live somewhere in the SVG. A layer naming convention needs to be documented with a worked example.
158
+
159
+ ---
160
+
161
+ ## Theme
162
+
163
+ **Implement the default theme**
164
+ The current built-in theme (`src/inkflow/theme/`) is a visual placeholder — bare zone rects on a flat background with no typography, brand, or decoration. It needs to become a real, polished default that demonstrates what inkflow can look like out of the box:
165
+ - A proper background with a coherent color palette using the `--inkflow-*` CSS variables
166
+ - Styled layout SVGs using the semantic CSS classes (`theme-accent`, `theme-surface`, etc.)
167
+ - Readable typography for headings, body text, and code blocks defined in `styles.css`
168
+ - Light and dark variants that both look intentional, not just inverted
169
+ - The showcase deck should serve as an example of a well-designed presentation, not just a test harness for every layout type
170
+
171
+ **Named theme support**
172
+ `_resolve_theme_dir` currently raises an error for bare theme names like `Deck(theme="catppuccin-mocha")`. Explicit path themes (`Deck(theme="./my-theme")`) work today. Named resolution requires a discovery mechanism — most naturally, a pip package naming convention (`inkflow-theme-{name}`).
173
+
174
+ **Pip-installable themes**
175
+ Auto-discovery via a naming convention would allow `Deck(theme="catppuccin-mocha")` to just work after `pip install inkflow-theme-catppuccin-mocha`, with no `deck.py` path configuration.
176
+
177
+ **Theme eject**
178
+ `inkflow eject-theme` copies a theme into the project directory and updates `deck.py` to point at the local copy. Useful for heavy customisation without forking the theme package.
179
+
180
+ ---
181
+
182
+ ## Documentation
183
+
184
+ The minimum bar is a small set of markdown files in the repository — a `README.md` that covers installation and a five-minute example, and a `docs/` folder with focused pages on the layout system, zone substitutions, `MarkdownSlide`, animations, and transitions.
185
+
186
+ The preferred target is a static site on GitHub Pages, auto-built on every push to `main`. The right tool is **MkDocs with the Material theme** (zero Node.js dependency, fits naturally in a Python project) or **VitePress** (what Slidev itself uses, requires Node.js but produces a better look with interactive demos).
187
+
188
+ Minimum page set:
189
+ - **Getting started** — installation, `inkflow serve example/deck.py`, first custom slide
190
+ - **Layout system** — `inkflow:parent`, zone rects, `inject-layout`, layout chains, path resolution
191
+ - **MarkdownSlide** — `::zone::` / `::step::` syntax, auto-extraction, `image=` / `video=` kwargs
192
+ - **Animations** — `FadeIn`, `FadeOut`, `Bounce`, step model, `steps=True`
193
+ - **Transitions** — Cut, Crossfade, Morph; per-slide and deck-level
194
+ - **`deck.py` reference** — all `Deck`, `Slide`, `MarkdownSlide` fields in one place
195
+ - **Themes** — directory structure, `styles.css` cascade, semantic CSS classes, `Deck(theme=...)`
196
+
197
+ ---
198
+
199
+ ## Reliability and packaging
200
+
201
+ **Port conflict handling**
202
+ If 7777 or 7778 are already in use the server crashes with an OS error. Should auto-detect a free port or give a useful message.
203
+
204
+ **CLI polish**
205
+ `--no-browser` flag to suppress auto-opening a tab. `--host` for presenting from a remote machine over SSH forwarding. `--version`. Better `--help` output.
206
+
207
+ **Packaging**
208
+ A PyPI release so `uvx inkflow serve deck.py` works without a checkout. Possibly a Nix flake. Right now you need to clone the repo and `uv sync`.
209
+
210
+ ---
211
+
212
+ ## Nice to have
213
+
214
+ - **More animation types** — `Scale`, `Rotate`, `Draw` (stroke-dashoffset path drawing), `Highlight` (brief colour pulse)
215
+ - **Morph for paths and groups** — the current morph interpolates geometry attributes for `<rect>`, `<circle>`, and `<ellipse>`; `<path>`, `<polygon>`, `<g>`, and `<text>` fall back to an instant cut
216
+ - **Auto-advance** — timed slides for kiosk or lightning-talk use
217
+ - **Slide overview** — press Escape for a thumbnail grid, click to jump
218
+ - **Hyperlinks** — SVG `<a>` elements open in a new tab during presentation
219
+ - **Within-slide Morph** — an element changes shape as part of a step sequence on a single slide, distinct from the cross-slide morph
220
+ - **Configurable keybindings** — a `keybindings` dict on `Deck` that overrides the defaults, injected into the presenter as JSON
221
+
222
+ ---
223
+
224
+ ## Out of scope and hard limits
225
+
226
+ **Auto-reflow and bullet lists for Inkscape-authored text**
227
+ Inkscape is a drawing tool that supports text, not a text layout tool. It has no native bullet list feature — you place bullet characters manually and indent by hand. Inkscape 1.2 added SVG 2.0 `shape-inside` text wrapping, but browser support is absent in Firefox and experimental in Chrome, so the pipeline cannot rely on it. In practice this is not a problem: if you need bullet lists or reflowing text, use a `TextBox` placeholder in Inkscape and write the content as Markdown in `deck.py`.
228
+
229
+ **PPTX export**
230
+ SVG is arbitrary vector geometry; PPTX has its own shape/text model. Conversion fidelity for anything non-trivial would be poor. If you need a PPTX, use PowerPoint.
231
+
232
+ **WYSIWYG layout preview in Inkscape**
233
+ A structural consequence of the pipeline-inlining approach: if the layout chain is resolved at build time, Inkscape cannot show it during authoring without `inject-layout`. The tool commits to this model — the injected layers are a spatial reference, not a live preview.
234
+
235
+ **Real-time collaboration**
236
+ SVG files on disk and a local Python server are the wrong substrate. Git handles version history already.
237
+
238
+ **Interaction-triggered animations**
239
+ Hover effects or click-a-specific-element-to-reveal. The step model advances globally on keypress; adding per-element interactivity would require a significant rethink of the animation model.
240
+
241
+ **Accessibility**
242
+ SVG element order does not correspond to visual reading order, there is no semantic structure, and screen reader support for inline SVG is inconsistent. Out of scope until the core tool is stable.
243
+
244
+ **Export to video / screen recording**
245
+ Automated recording as MP4 requires driving the presenter JS from outside the browser. The complexity is disproportionate; system screen recorders already exist.