inkflow 0.1.0__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 (223) hide show
  1. inkflow-0.1.0/.claude/hooks/autofix.sh +19 -0
  2. inkflow-0.1.0/.claude/settings.json +19 -0
  3. inkflow-0.1.0/.gitattributes +1 -0
  4. inkflow-0.1.0/.githooks/pre-commit +23 -0
  5. inkflow-0.1.0/.github/workflows/ci.yml +66 -0
  6. inkflow-0.1.0/.github/workflows/docs.yml +45 -0
  7. inkflow-0.1.0/.github/workflows/release.yml +39 -0
  8. inkflow-0.1.0/.gitignore +12 -0
  9. inkflow-0.1.0/.python-version +1 -0
  10. inkflow-0.1.0/CLAUDE.md +252 -0
  11. inkflow-0.1.0/LICENSE +21 -0
  12. inkflow-0.1.0/PKG-INFO +155 -0
  13. inkflow-0.1.0/README.md +122 -0
  14. inkflow-0.1.0/ROADMAP.md +12 -0
  15. inkflow-0.1.0/biome.json +42 -0
  16. inkflow-0.1.0/demo/assets/cover-dark.webp +0 -0
  17. inkflow-0.1.0/demo/assets/cover-light.webp +0 -0
  18. inkflow-0.1.0/demo/assets/logo.mp4 +0 -0
  19. inkflow-0.1.0/demo/deck.py +165 -0
  20. inkflow-0.1.0/demo/layouts/center.svg +63 -0
  21. inkflow-0.1.0/demo/layouts/content.svg +51 -0
  22. inkflow-0.1.0/demo/layouts/footer.svg +51 -0
  23. inkflow-0.1.0/demo/notes/animations.md +7 -0
  24. inkflow-0.1.0/demo/notes/deckpy.md +11 -0
  25. inkflow-0.1.0/demo/notes/end.md +5 -0
  26. inkflow-0.1.0/demo/notes/features.md +6 -0
  27. inkflow-0.1.0/demo/notes/hackable.md +7 -0
  28. inkflow-0.1.0/demo/notes/how-it-works.md +10 -0
  29. inkflow-0.1.0/demo/notes/interface.md +5 -0
  30. inkflow-0.1.0/demo/notes/markdown.md +13 -0
  31. inkflow-0.1.0/demo/notes/media.md +7 -0
  32. inkflow-0.1.0/demo/notes/morph.md +5 -0
  33. inkflow-0.1.0/demo/notes/title.md +5 -0
  34. inkflow-0.1.0/demo/scripts.js +36 -0
  35. inkflow-0.1.0/demo/slides/animations.svg +101 -0
  36. inkflow-0.1.0/demo/slides/deckpy.md +18 -0
  37. inkflow-0.1.0/demo/slides/end.md +7 -0
  38. inkflow-0.1.0/demo/slides/features.md +16 -0
  39. inkflow-0.1.0/demo/slides/hackable.md +17 -0
  40. inkflow-0.1.0/demo/slides/how-it-works.svg +111 -0
  41. inkflow-0.1.0/demo/slides/interface.md +19 -0
  42. inkflow-0.1.0/demo/slides/markdown.md +24 -0
  43. inkflow-0.1.0/demo/slides/media.svg +87 -0
  44. inkflow-0.1.0/demo/slides/morph.svg +95 -0
  45. inkflow-0.1.0/demo/slides/title.svg +67 -0
  46. inkflow-0.1.0/demo/styles.css +47 -0
  47. inkflow-0.1.0/docs/assets/demo-button.svg +5 -0
  48. inkflow-0.1.0/docs/assets/logo-dark-icon.svg +17 -0
  49. inkflow-0.1.0/docs/assets/logo-dark-landscape.svg +18 -0
  50. inkflow-0.1.0/docs/assets/logo-dark-portrait.svg +17 -0
  51. inkflow-0.1.0/docs/assets/logo-light-icon.svg +17 -0
  52. inkflow-0.1.0/docs/assets/logo-light-landscape.svg +17 -0
  53. inkflow-0.1.0/docs/assets/logo-light-portrait.svg +17 -0
  54. inkflow-0.1.0/docs/built-in-theme/index.md +55 -0
  55. inkflow-0.1.0/docs/concepts.md +138 -0
  56. inkflow-0.1.0/docs/getting-started.md +123 -0
  57. inkflow-0.1.0/docs/guides/export.md +104 -0
  58. inkflow-0.1.0/docs/guides/fonts.md +77 -0
  59. inkflow-0.1.0/docs/guides/layout-system.md +272 -0
  60. inkflow-0.1.0/docs/guides/presenter-view.md +108 -0
  61. inkflow-0.1.0/docs/guides/slides.md +593 -0
  62. inkflow-0.1.0/docs/guides/themes.md +197 -0
  63. inkflow-0.1.0/docs/guides/transitions.md +180 -0
  64. inkflow-0.1.0/docs/index.md +79 -0
  65. inkflow-0.1.0/docs/reference/animations.md +28 -0
  66. inkflow-0.1.0/docs/reference/cli.md +53 -0
  67. inkflow-0.1.0/docs/reference/enums.md +62 -0
  68. inkflow-0.1.0/docs/reference/manifest.md +28 -0
  69. inkflow-0.1.0/docs/reference/transitions.md +24 -0
  70. inkflow-0.1.0/docs/stylesheets/extra.css +98 -0
  71. inkflow-0.1.0/docs/theme_overrides/python/material/attribute.html.jinja +8 -0
  72. inkflow-0.1.0/mise.toml +53 -0
  73. inkflow-0.1.0/mkdocs.yml +88 -0
  74. inkflow-0.1.0/package-lock.json +2409 -0
  75. inkflow-0.1.0/package.json +10 -0
  76. inkflow-0.1.0/pyproject.toml +79 -0
  77. inkflow-0.1.0/src/css/presenter/base.css +174 -0
  78. inkflow-0.1.0/src/css/presenter/fullscreen.css +25 -0
  79. inkflow-0.1.0/src/css/presenter/index.css +11 -0
  80. inkflow-0.1.0/src/css/presenter/laser.css +56 -0
  81. inkflow-0.1.0/src/css/presenter/mobile.css +89 -0
  82. inkflow-0.1.0/src/css/presenter/overlays.css +207 -0
  83. inkflow-0.1.0/src/css/presenter/overview.css +68 -0
  84. inkflow-0.1.0/src/css/presenter/picker.css +80 -0
  85. inkflow-0.1.0/src/css/presenter/pv.css +195 -0
  86. inkflow-0.1.0/src/css/presenter/sync.css +77 -0
  87. inkflow-0.1.0/src/css/shared/animations.css +141 -0
  88. inkflow-0.1.0/src/css/shared/theme-vars.css +27 -0
  89. inkflow-0.1.0/src/inkflow/__init__.py +48 -0
  90. inkflow-0.1.0/src/inkflow/animations.py +105 -0
  91. inkflow-0.1.0/src/inkflow/bundles/presenter.css +991 -0
  92. inkflow-0.1.0/src/inkflow/bundles/presenter.js +2487 -0
  93. inkflow-0.1.0/src/inkflow/clean.py +72 -0
  94. inkflow-0.1.0/src/inkflow/cli/__init__.py +19 -0
  95. inkflow-0.1.0/src/inkflow/cli/_common.py +199 -0
  96. inkflow-0.1.0/src/inkflow/cli/authoring.py +407 -0
  97. inkflow-0.1.0/src/inkflow/cli/color.py +102 -0
  98. inkflow-0.1.0/src/inkflow/cli/present.py +122 -0
  99. inkflow-0.1.0/src/inkflow/cli/project.py +88 -0
  100. inkflow-0.1.0/src/inkflow/cli/verify.py +89 -0
  101. inkflow-0.1.0/src/inkflow/colors.py +335 -0
  102. inkflow-0.1.0/src/inkflow/completions/inkflow.yaml +164 -0
  103. inkflow-0.1.0/src/inkflow/content.py +474 -0
  104. inkflow-0.1.0/src/inkflow/enums.py +212 -0
  105. inkflow-0.1.0/src/inkflow/export.py +183 -0
  106. inkflow-0.1.0/src/inkflow/fonts.py +438 -0
  107. inkflow-0.1.0/src/inkflow/git_setup.py +190 -0
  108. inkflow-0.1.0/src/inkflow/init.py +52 -0
  109. inkflow-0.1.0/src/inkflow/layout.py +464 -0
  110. inkflow-0.1.0/src/inkflow/loaders.py +95 -0
  111. inkflow-0.1.0/src/inkflow/logging.py +238 -0
  112. inkflow-0.1.0/src/inkflow/manifest.py +347 -0
  113. inkflow-0.1.0/src/inkflow/markdown.py +256 -0
  114. inkflow-0.1.0/src/inkflow/ns.py +10 -0
  115. inkflow-0.1.0/src/inkflow/pdf.html +21 -0
  116. inkflow-0.1.0/src/inkflow/pipeline.py +350 -0
  117. inkflow-0.1.0/src/inkflow/presenter.html +243 -0
  118. inkflow-0.1.0/src/inkflow/server.py +552 -0
  119. inkflow-0.1.0/src/inkflow/svg.py +81 -0
  120. inkflow-0.1.0/src/inkflow/svgio.py +62 -0
  121. inkflow-0.1.0/src/inkflow/theme/icon.svg +14 -0
  122. inkflow-0.1.0/src/inkflow/theme/layouts/base.svg +37 -0
  123. inkflow-0.1.0/src/inkflow/theme/layouts/center.svg +42 -0
  124. inkflow-0.1.0/src/inkflow/theme/layouts/cover.svg +43 -0
  125. inkflow-0.1.0/src/inkflow/theme/layouts/default.svg +46 -0
  126. inkflow-0.1.0/src/inkflow/theme/layouts/end.svg +41 -0
  127. inkflow-0.1.0/src/inkflow/theme/layouts/fact.svg +43 -0
  128. inkflow-0.1.0/src/inkflow/theme/layouts/media-left.svg +46 -0
  129. inkflow-0.1.0/src/inkflow/theme/layouts/media-right.svg +46 -0
  130. inkflow-0.1.0/src/inkflow/theme/layouts/numbered.svg +47 -0
  131. inkflow-0.1.0/src/inkflow/theme/layouts/quote.svg +43 -0
  132. inkflow-0.1.0/src/inkflow/theme/layouts/section.svg +42 -0
  133. inkflow-0.1.0/src/inkflow/theme/layouts/two-cols.svg +46 -0
  134. inkflow-0.1.0/src/inkflow/theme/showcase/deck.py +47 -0
  135. inkflow-0.1.0/src/inkflow/theme/showcase/slides/center.md +1 -0
  136. inkflow-0.1.0/src/inkflow/theme/showcase/slides/cover.md +4 -0
  137. inkflow-0.1.0/src/inkflow/theme/showcase/slides/default.md +10 -0
  138. inkflow-0.1.0/src/inkflow/theme/showcase/slides/end.md +3 -0
  139. inkflow-0.1.0/src/inkflow/theme/showcase/slides/fact.md +5 -0
  140. inkflow-0.1.0/src/inkflow/theme/showcase/slides/media-left.md +4 -0
  141. inkflow-0.1.0/src/inkflow/theme/showcase/slides/media-right.md +3 -0
  142. inkflow-0.1.0/src/inkflow/theme/showcase/slides/quote.md +5 -0
  143. inkflow-0.1.0/src/inkflow/theme/showcase/slides/section.md +3 -0
  144. inkflow-0.1.0/src/inkflow/theme/showcase/slides/two-cols.md +15 -0
  145. inkflow-0.1.0/src/inkflow/theme/styles.css +623 -0
  146. inkflow-0.1.0/src/inkflow/titles.py +17 -0
  147. inkflow-0.1.0/src/inkflow/transitions.py +115 -0
  148. inkflow-0.1.0/src/inkflow/tui.py +152 -0
  149. inkflow-0.1.0/src/inkflow/verify.py +177 -0
  150. inkflow-0.1.0/src/inkflow/zones.py +514 -0
  151. inkflow-0.1.0/src/ts/globals.d.ts +27 -0
  152. inkflow-0.1.0/src/ts/presenter/keyboard.ts +210 -0
  153. inkflow-0.1.0/src/ts/presenter/laser.ts +84 -0
  154. inkflow-0.1.0/src/ts/presenter/logs.test.ts +97 -0
  155. inkflow-0.1.0/src/ts/presenter/main.ts +49 -0
  156. inkflow-0.1.0/src/ts/presenter/morph.ts +974 -0
  157. inkflow-0.1.0/src/ts/presenter/navigation.ts +113 -0
  158. inkflow-0.1.0/src/ts/presenter/overview.test.ts +167 -0
  159. inkflow-0.1.0/src/ts/presenter/overview.ts +272 -0
  160. inkflow-0.1.0/src/ts/presenter/picker.test.ts +63 -0
  161. inkflow-0.1.0/src/ts/presenter/picker.ts +120 -0
  162. inkflow-0.1.0/src/ts/presenter/progress-driver.test.ts +79 -0
  163. inkflow-0.1.0/src/ts/presenter/progress-driver.ts +53 -0
  164. inkflow-0.1.0/src/ts/presenter/pv.ts +136 -0
  165. inkflow-0.1.0/src/ts/presenter/state.ts +16 -0
  166. inkflow-0.1.0/src/ts/presenter/status.ts +94 -0
  167. inkflow-0.1.0/src/ts/presenter/syncmenu.ts +98 -0
  168. inkflow-0.1.0/src/ts/presenter/transitions.test.ts +336 -0
  169. inkflow-0.1.0/src/ts/presenter/transitions.ts +558 -0
  170. inkflow-0.1.0/src/ts/presenter/ui.ts +213 -0
  171. inkflow-0.1.0/src/ts/presenter/video.ts +98 -0
  172. inkflow-0.1.0/src/ts/presenter/websocket.ts +204 -0
  173. inkflow-0.1.0/src/ts/shared/easing.test.ts +46 -0
  174. inkflow-0.1.0/src/ts/shared/easing.ts +89 -0
  175. inkflow-0.1.0/src/ts/shared/escape.ts +12 -0
  176. inkflow-0.1.0/src/ts/shared/morph-math.test.ts +272 -0
  177. inkflow-0.1.0/src/ts/shared/morph-math.ts +222 -0
  178. inkflow-0.1.0/src/ts/shared/ring.ts +30 -0
  179. inkflow-0.1.0/src/ts/shared/step.test.ts +87 -0
  180. inkflow-0.1.0/src/ts/shared/step.ts +73 -0
  181. inkflow-0.1.0/src/ts/shared/types.ts +58 -0
  182. inkflow-0.1.0/tests/__init__.py +0 -0
  183. inkflow-0.1.0/tests/decks/assets/logo.mp4 +1 -0
  184. inkflow-0.1.0/tests/decks/assets/poster.webp +1 -0
  185. inkflow-0.1.0/tests/decks/scripts.js +26 -0
  186. inkflow-0.1.0/tests/decks/slides/anim-targets.svg +40 -0
  187. inkflow-0.1.0/tests/decks/slides/cover.svg +4 -0
  188. inkflow-0.1.0/tests/decks/slides/crossfade.svg +4 -0
  189. inkflow-0.1.0/tests/decks/slides/cut.svg +4 -0
  190. inkflow-0.1.0/tests/decks/slides/fade.svg +4 -0
  191. inkflow-0.1.0/tests/decks/slides/flip.svg +4 -0
  192. inkflow-0.1.0/tests/decks/slides/media-steps.svg +7 -0
  193. inkflow-0.1.0/tests/decks/slides/media.svg +5 -0
  194. inkflow-0.1.0/tests/decks/slides/morph-a.svg +5 -0
  195. inkflow-0.1.0/tests/decks/slides/morph-b.svg +5 -0
  196. inkflow-0.1.0/tests/decks/slides/push.svg +4 -0
  197. inkflow-0.1.0/tests/decks/slides/title.svg +5 -0
  198. inkflow-0.1.0/tests/decks/slides/wipe.svg +4 -0
  199. inkflow-0.1.0/tests/decks/slides/zoom.svg +4 -0
  200. inkflow-0.1.0/tests/decks/step_animations.py +92 -0
  201. inkflow-0.1.0/tests/decks/transitions.py +69 -0
  202. inkflow-0.1.0/tests/decks/video.py +120 -0
  203. inkflow-0.1.0/tests/test_animations.py +73 -0
  204. inkflow-0.1.0/tests/test_clean.py +241 -0
  205. inkflow-0.1.0/tests/test_cli.py +356 -0
  206. inkflow-0.1.0/tests/test_colors.py +195 -0
  207. inkflow-0.1.0/tests/test_content.py +492 -0
  208. inkflow-0.1.0/tests/test_decks.py +24 -0
  209. inkflow-0.1.0/tests/test_export.py +240 -0
  210. inkflow-0.1.0/tests/test_fonts.py +404 -0
  211. inkflow-0.1.0/tests/test_layout.py +352 -0
  212. inkflow-0.1.0/tests/test_loaders.py +85 -0
  213. inkflow-0.1.0/tests/test_logging.py +229 -0
  214. inkflow-0.1.0/tests/test_manifest.py +309 -0
  215. inkflow-0.1.0/tests/test_markdown.py +176 -0
  216. inkflow-0.1.0/tests/test_pipeline.py +466 -0
  217. inkflow-0.1.0/tests/test_server.py +195 -0
  218. inkflow-0.1.0/tests/test_svg.py +56 -0
  219. inkflow-0.1.0/tests/test_svgio.py +57 -0
  220. inkflow-0.1.0/tests/test_verify.py +224 -0
  221. inkflow-0.1.0/tests/test_zones.py +664 -0
  222. inkflow-0.1.0/tsconfig.json +12 -0
  223. inkflow-0.1.0/uv.lock +1286 -0
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ FILE=$(jq -r '.tool_input.file_path')
5
+
6
+ case "$FILE" in
7
+ *.py)
8
+ # --unfixable F401: keep import sorting and every other autofix, but do NOT
9
+ # strip "unused" imports — mid-edit an import is often added before the line
10
+ # that uses it, and stripping it here breaks the file. --exit-zero so a
11
+ # transient unused import doesn't abort the hook before `ruff format` runs.
12
+ # `mise run check` remains the real gate for genuinely unused imports.
13
+ uv run ruff check --fix --unfixable F401 --exit-zero "$FILE"
14
+ uv run ruff format "$FILE"
15
+ ;;
16
+ *.js|*.mjs|*.cjs|*.jsx|*.ts|*.mts|*.cts|*.tsx|*.css|*.json|*.jsonc)
17
+ npx biome check --write "$FILE"
18
+ ;;
19
+ esac
@@ -0,0 +1,19 @@
1
+ {
2
+ "permissions": {
3
+ "allow": ["Bash(mise run *)"],
4
+ "ask": ["Edit(mise.toml)", "Write(mise.toml)"]
5
+ },
6
+ "hooks": {
7
+ "PostToolUse": [
8
+ {
9
+ "matcher": "Edit|Write",
10
+ "hooks": [
11
+ {
12
+ "type": "command",
13
+ "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/autofix.sh"
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ *.svg diff=inkscape-svg
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Strip Inkscape editor metadata from staged SVG files before committing.
3
+ # Installed by: inkflow setup-git
4
+
5
+ set -e
6
+
7
+ mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM \
8
+ | grep -E '\.svg$' || true)
9
+ [ ${#staged[@]} -eq 0 ] && exit 0
10
+
11
+ if [ -x ".venv/bin/inkflow" ]; then
12
+ INKFLOW=".venv/bin/inkflow"
13
+ elif command -v inkflow &>/dev/null; then
14
+ INKFLOW="inkflow"
15
+ else
16
+ echo "[inkflow] inkflow not found, skipping SVG clean" >&2
17
+ exit 0
18
+ fi
19
+
20
+ echo "[inkflow] cleaning staged SVGs..."
21
+ "$INKFLOW" clean "${staged[@]}"
22
+
23
+ git add "${staged[@]}"
@@ -0,0 +1,66 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ check:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+
15
+ - uses: jdx/mise-action@v4
16
+
17
+ - run: npm ci
18
+
19
+ - name: JS checks
20
+ run: |
21
+ npx biome check src/ts/ src/css/
22
+ npx tsc --noEmit
23
+
24
+ - name: Check bundles are up to date
25
+ run: |
26
+ mise run bundle
27
+ git diff --exit-code src/inkflow/bundles/
28
+
29
+ - run: uv run ruff format --check src/ tests/
30
+
31
+ - run: uv run ruff check src/ tests/
32
+
33
+ - run: uv run basedpyright
34
+
35
+ - run: uv run deptry src
36
+
37
+ - run: uv run pytest
38
+
39
+ - run: git ls-files '*.svg' | xargs --no-run-if-empty uv run inkflow clean --check
40
+
41
+ - name: Build demo deck
42
+ run: uv run inkflow build --deck demo/deck.py
43
+
44
+ - name: Build theme showcase deck
45
+ run: uv run inkflow build --deck src/inkflow/theme/showcase/deck.py
46
+
47
+ - name: Build init deck
48
+ run: |
49
+ uv run inkflow init /tmp/test-init
50
+ uv run inkflow build --deck /tmp/test-init/deck.py
51
+
52
+ export:
53
+ runs-on: ubuntu-latest
54
+ if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request'
55
+ steps:
56
+ - uses: actions/checkout@v6
57
+
58
+ - uses: jdx/mise-action@v4
59
+
60
+ - name: Install Google Chrome
61
+ run: |
62
+ wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
63
+ sudo apt-get install -y ./google-chrome-stable_current_amd64.deb
64
+
65
+ - name: Export demo deck
66
+ run: uv run inkflow export --deck demo/deck.py --no-sandbox
@@ -0,0 +1,45 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ permissions:
12
+ contents: write
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+
20
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
21
+
22
+ - run: uv sync --group docs
23
+
24
+ - run: uv run inkflow build --deck demo/deck.py --output docs/demo
25
+
26
+ - run: uv run inkflow build --deck src/inkflow/theme/showcase/deck.py --output docs/built-in-theme/presentation
27
+
28
+ - run: uv run mkdocs build --strict
29
+
30
+ deploy:
31
+ if: github.event_name == 'push'
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v6
36
+
37
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
38
+
39
+ - run: uv sync --group docs
40
+
41
+ - run: uv run inkflow build --deck demo/deck.py --output docs/demo
42
+
43
+ - run: uv run inkflow build --deck src/inkflow/theme/showcase/deck.py --output docs/built-in-theme/presentation
44
+
45
+ - run: uv run mkdocs gh-deploy --force
@@ -0,0 +1,39 @@
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
+ contents: write
14
+
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+
18
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
19
+
20
+ - name: Check version matches tag
21
+ run: |
22
+ TAG="${GITHUB_REF_NAME#v}"
23
+ VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
24
+ if [ "$TAG" != "$VERSION" ]; then
25
+ echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $VERSION"
26
+ exit 1
27
+ fi
28
+
29
+ - run: uv build
30
+
31
+ - name: Smoke test
32
+ run: uv run --with dist/*.whl --no-project -- inkflow build --deck demo/deck.py --output /tmp/inkflow-smoke
33
+
34
+ - run: uv publish
35
+
36
+ - name: Create GitHub release
37
+ env:
38
+ GITHUB_TOKEN: ${{ github.token }}
39
+ run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ *.egg-info/
3
+ *.pyc
4
+ .venv/
5
+ __pycache__/
6
+ build/
7
+ deck.pdf
8
+ dist/
9
+ docs/demo/index.html
10
+ docs/demo/assets/
11
+ docs/built-in-theme/presentation/
12
+ site/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,252 @@
1
+ # Inkflow — codebase guide
2
+
3
+ ## Running
4
+
5
+ ```bash
6
+ uv run inkflow serve --deck demo/deck.py # start server at localhost:7777
7
+ mise run check # lint + format + typecheck + test (Python and JS)
8
+ mise run bundle # rebuild JS/CSS bundles from src/ts/ and src/css/
9
+ ```
10
+
11
+ ## Git setup (one-time, per clone)
12
+
13
+ ```bash
14
+ uv run inkflow setup-git
15
+ ```
16
+
17
+ Configures two things:
18
+ - **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
19
+ - **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
20
+
21
+ 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.
22
+
23
+ SVG source files should be kept clean (no Inkscape metadata) in the repository. Run `uv run inkflow clean demo/slides/*.svg` to clean any files committed before the hook was in place.
24
+
25
+ ## Project layout
26
+
27
+ ```
28
+ src/
29
+ inkflow/
30
+ __init__.py exports: Deck, Slide, Image, Video, Media, TextBox, Animation,
31
+ Transition, Align, VAlign, Direction, Inline, Content,
32
+ ZoneContent, ColorMode, MediaFit, MediaAlign, Muted
33
+ and the `animations` and `transitions` namespaces
34
+ manifest.py dataclasses for the deck DSL; Animation/Transition base.
35
+ Media is a `_MediaBase` shared by Image + Video; `Media`
36
+ is the `Image | Video` union alias (not callable).
37
+ Video adds playback fields (controls, autoplay, muted,
38
+ loop, poster, start, end, play_on_step)
39
+ Deck params: slides, transition, theme, mode: ColorMode,
40
+ style, font_size, embed_fonts
41
+ Slide params: src, id, md, zones, animations, transition,
42
+ extra_style, title, notes, visible, font_size
43
+ enums.py shared enums (Direction, Align, VAlign, MediaFit, MediaAlign,
44
+ ColorMode, Muted); `_KebabStrEnum` base emits CSS token
45
+ values (Muted is a plain Enum, resolved in Python)
46
+ animations.py concrete animation types (FadeIn, FadeOut, Bounce, SlideIn/Out,
47
+ ZoomIn/Out, Highlight) subclassing manifest.Animation
48
+ transitions.py concrete transition types (Cut, Crossfade, Morph, Push, Cover,
49
+ Zoom, Fade, Wipe) subclassing manifest.Transition
50
+ pipeline.py animation annotation + layout inlining
51
+ content.py TextBox / Image / Video injection into zone rects, with alignment
52
+ support; Video emits data-* playback attrs (driven by video.ts)
53
+ layout.py parent inject/set/strip: layout chain resolution and Inkscape layer writing
54
+ markdown.py markdown-it-py rendering only: code-fence highlighting, LaTeX math,
55
+ HTML->well-formed-XML normalization (no inkflow-specific grammar)
56
+ zones.py ::zone:: / ::step:: marker grammar, zone param extraction, and slide
57
+ assembly (parsed markdown -> per-zone TextBox/Media)
58
+ server.py HTTP server, WebSocket server, file watcher, build pipeline
59
+ export.py static HTML export (inkflow build) and PDF export (inkflow export)
60
+ cli/ CLI package (entry point inkflow.cli:main). _common.py holds the
61
+ `main` group, shared options, and the Project/Target helpers;
62
+ commands are grouped by area: project.py (init, setup-git,
63
+ completion), present.py (serve, build, export), authoring.py
64
+ (clean, add, parent group, sync, layouts), color.py (colorize,
65
+ palette), verify.py. Submodules register on `main` by import.
66
+ clean.py SVG Inkscape metadata stripping (used by cli and pre-commit hook)
67
+ colors.py CSS color token extraction, hex→class mapping, SVG colorization, GPL palette
68
+ git_setup.py git hook + SVG diff driver setup
69
+ init.py project scaffolding (inkflow init)
70
+ loaders.py deck style / script loading helpers
71
+ logging.py unified log sink over stdlib logging: `logger`, shared Rich
72
+ `console`, `report` (cargo-style status), `collect_logs`
73
+ (per-rebuild capture), and three independent sinks
74
+ (console/file/browser), each a level, resolved by
75
+ `resolve_levels` (`--log-level*` flags + INKFLOW_LOG_LEVEL*
76
+ env, `off` disables) and installed by `configure`
77
+ svg.py SVG tree utilities (ensure_defs, with_namespaces, compose_with_ancestors)
78
+ svgio.py SVG parse/serialize primitives: one hardened parser, SvgElement alias
79
+ verify.py slide authoring checks (inkflow verify)
80
+ ns.py XML namespace constants
81
+ tui.py terminal UI (Rich)
82
+ presenter.html shell template — inlined with CSS/JS at serve time
83
+ pdf.html PDF export template
84
+ bundles/ pre-built JS/CSS output (committed; no Node needed at install time)
85
+ presenter.js navigation, transitions, WebSocket, presenter panel
86
+ presenter.css all presenter styles including the sidebar panel
87
+ theme/ built-in theme: main.svg, layouts/*.svg, styles.css
88
+ ts/ TypeScript source
89
+ globals.d.ts ambient declarations for Python-injected globals (__SLIDES_JSON__ etc.)
90
+ shared/ types, step logic, step-ring SVG builder, cubic-bezier easing
91
+ presenter/ main presenter modules — navigation, transitions (progress-driven
92
+ via progress-driver.ts), overview, picker, websocket, status bar,
93
+ keyboard, syncmenu.ts (sync-mode status-bar control),
94
+ pv.ts (presenter panel sidebar), and video.ts (step-driven
95
+ <video> playback, wired in via status.ts)
96
+ css/ CSS source
97
+ shared/ theme variables, animation keyframes
98
+ presenter/ presenter partials including pv.css (sidebar panel)
99
+ demo/
100
+ deck.py 11-slide demo deck (SVG slides, some filling zones with Markdown via md=)
101
+ slides/ source SVGs and Markdown content files
102
+ mise.toml task runner + tool versions (replaces poethepoet)
103
+ package.json JS devDependencies: biome, esbuild, typescript
104
+ biome.json Biome lint + format config (4-space indent, noUnusedVariables=error)
105
+ tsconfig.json TypeScript config (noEmit, verbatimModuleSyntax — tsc as type-checker only)
106
+ ```
107
+
108
+ ## Key architecture decisions
109
+
110
+ **No SVG editor subprocess at serve time.**
111
+ Any SVG editor writes the files; the pipeline reads them directly with lxml,
112
+ strips Inkscape/Sodipodi editor namespaces, and annotates elements with animation classes.
113
+ No GUI window flashes, instant processing.
114
+
115
+ **Live reload pushes slides over WebSocket, not `location.reload()`.**
116
+ When files change the server sends `{"type":"update","slides":[...],"transitions":[...],"logs":[{"level","message"},...]}` and the presenter swaps content in place, preserving the current slide index.
117
+ Non-fatal records collected during the rebuild (`inkflow.logging.collect_logs`, filtered to the browser sink's level) ride along on the `update` message and show as a dismissible `#warning-banner`, each entry styled by level via a `log-<level>` class (dismissal sticks across rebuilds until the log set changes, keyed on a signature in `ui.ts`); a fatal build error is sent separately as `{"type":"error","message":"..."}` and displayed as the full-screen overlay (also logged to the file sink). Static `build` sends no logs to the page (they go to the CLI instead).
118
+ The HTTP response includes `Cache-Control: no-store` so hard refreshes always get fresh content.
119
+
120
+ **Position sync is a dumb relay with client-side authority + modes.**
121
+ Clients send `{"type":"nav","slideIndex","step"}` (validated + clamped server-side by `_coerce_nav_position`); the server stores the last position and rebroadcasts it as `{"type":"position",...}` to the *other* clients, and pushes it once to each newly connected client. A window that booted from a deep link (URL slide segment, captured by `readURL()` before `syncURL()` rewrites the bar) or reconnected asserts its own position and ignores that first push; a bare window adopts it. Each client also has a per-tab **sync mode** (`two-way`/`present`/`follow`/`solo`, `shared/types.ts`) deciding locally whether it broadcasts nav (`sends()`) and applies incoming positions (`receives()`) — the server knows nothing about modes. `s` cycles the mode; `syncmenu.ts` owns the status-bar widget, `websocket.ts` the network/state. Switching into a receiving mode sends `{"type":"sync-request"}` to catch up. Persisted in `sessionStorage`.
122
+
123
+ **`loadSlide()` vs `applyStep()` in the presenter JS.**
124
+ 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.
125
+ `loadSlide()` sets innerHTML (elements start at opacity 0).
126
+ Subsequent `applyStep()` calls only toggle `.active` on existing DOM elements, triggering CSS transitions.
127
+
128
+ **`deck.py` is a Python module, not YAML/TOML.**
129
+ Loaded via `importlib.util.spec_from_file_location`. Must define a `main() -> Deck` function.
130
+
131
+ **Morph transition uses a rAF loop over SVG attributes, not CSS transforms.**
132
+ 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.
133
+ 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.
134
+ Colors are lerped channel-by-channel.
135
+ Exit-only elements are reconstructed as ghost nodes in the new SVG and faded out.
136
+ Backward navigation passes the outgoing slide's transition to `loadSlide()` so the morph plays in reverse.
137
+
138
+ **`presenter.html`/`css`/`js` are inlined at serve time.**
139
+ `build_html()` in `server.py` reads the template and the two bundles, substituting `__CSS__`, `__JS__`, `__STYLES__`, `__DATA_THEME__`, `__SLIDES_JSON__`, `__TRANSITIONS_JSON__`, `__WS_PORT__`, `__ERROR_JSON__` tokens.
140
+ Edit the source files, run `mise run bundle`, and reload the browser to see changes.
141
+
142
+ **The presenter panel is a sidebar inside the single presenter page, not a separate route.**
143
+ `<aside id="pv">` lives in `presenter.html` and is hidden (`width: 0`) by default.
144
+ Pressing `p` toggles `body.pv-open`, which CSS-transitions the sidebar to 30% width while the stage flexes back.
145
+ `pv.ts` owns all panel logic (clock, next-preview, notes); it reads directly from `state.slides` so no second WS connection or position sync is needed.
146
+ For second-screen use, open the same URL in two windows and toggle the panel in one.
147
+
148
+ **Font embedding is automatic and zero-config.**
149
+ After `process_deck()`, `fonts.embed_fonts_css()` (serve) or `fonts.embed_fonts_css_subsetted()`
150
+ (build/export) scans every slide SVG for named `font-family` values, discovers matching font
151
+ files, and injects `@font-face` blocks (base64 data-URI) into the global CSS via `__STYLES__`.
152
+ Generic families (`sans-serif`, `serif`, `monospace`, etc.) are always skipped.
153
+
154
+ Font search order: `<project_dir>/fonts/` → user font dirs → system font dirs (all OS-specific).
155
+ Committing fonts in `fonts/` gives fully reproducible output independent of the host system.
156
+
157
+ For serve: full font files are embedded; the font index is cached at module level so only the
158
+ first rebuild in a session pays the directory-scan cost. For build/export: fonts are subsetted
159
+ to only the codepoints present in the slides (via `fonttools`), typically 10–30 KB per variant.
160
+ `brotli` is a required dependency, so subsetted fonts are always emitted as WOFF2. If subsetting fails for a given font (for example a corrupt or unreadable file), the full font file is embedded instead.
161
+
162
+ Unresolvable fonts produce a yellow TUI warning and fall back to system rendering.
163
+ Opt out per-deck: `Deck(embed_fonts=False)`.
164
+
165
+ **Markdown content injection (`md=`) uses `<foreignObject>`.**
166
+ Markdown is rendered to HTML via `markdown-it-py`.
167
+ Zone `<rect>` elements in the layout SVG are replaced with `<foreignObject>` of the same geometry containing the rendered HTML.
168
+ Typography and color come from the CSS cascade (`theme/styles.css` + per-deck/per-slide `style=`) injected into the `<foreignObject>` HTML head.
169
+
170
+ **Text zone alignment — three layers, increasing specificity.**
171
+
172
+ *1. Layout SVG CSS variables* — set once, applies to every slide that uses the layout:
173
+ ```css
174
+ /* inside the layout SVG's <defs><style> */
175
+ #zone-title { --inkflow-valign: center; }
176
+ #zone-content { --inkflow-padding: 40px; }
177
+ ```
178
+ `--inkflow-align` (`left`/`center`/`right`/`justify`), `--inkflow-valign` (`start`/`center`/`end`), and `--inkflow-padding` (any CSS length) are consumed by `.inkflow-wrapper` and `.inkflow-content` in `theme/styles.css` via `var()`. No pipeline extraction; pure browser cascade.
179
+
180
+ *2. Markdown zone marker parameters* — per-zone, per-slide, directly in the `.md` file:
181
+ ```
182
+ ::content align=center valign=center padding=60::
183
+ ```
184
+ `align`, `valign`, and `padding` are the supported keys. `valign` accepts `top`/`center`/`bottom` (mapped to flexbox `start`/`center`/`end`). `padding` is in SVG user units. These translate to inline `style` attributes on the generated `<foreignObject>` wrapper and content divs, overriding CSS variables.
185
+
186
+ *3. Python `TextBox` explicit params* — in `deck.py`:
187
+ ```python
188
+ from inkflow import Align, Slide, TextBox, VAlign
189
+ Slide(
190
+ "layout.svg",
191
+ zones={"content": TextBox(text="...", align=Align.CENTER, valign=VAlign.TOP, padding=40)},
192
+ )
193
+ ```
194
+ The target zone is the `zones` dict key (`"content"` → `zone-content`); `TextBox` has no selector argument, its first positional is `text`. `Align` and `VAlign` are `StrEnum`s exported from the top-level package. `None` (the default) means "defer to CSS variable".
195
+
196
+ **foreignObject DOM structure after injection:**
197
+ ```
198
+ <foreignObject>
199
+ <div class="inkflow-wrapper" [style="justify-content:…;padding:…;"]>
200
+ <div class="inkflow-content" [style="text-align:…;"]>
201
+ {rendered HTML}
202
+ </div>
203
+ </div>
204
+ </foreignObject>
205
+ ```
206
+ Inline styles are only emitted when the corresponding param is non-`None`; CSS variables handle layout-level defaults without touching the element's `style`.
207
+
208
+ **Layout chain resolution at build time, not on disk.**
209
+ `inject-layout` writes locked Inkscape preview layers into SVGs for authoring reference,
210
+ but the pipeline resolves `inkflow:parent` chains in memory and composites layers on the fly.
211
+ SVG files on disk are never modified by the serve/build pipeline.
212
+
213
+ ## Server
214
+
215
+ - HTTP on port 7777 (asyncio streams, custom handler)
216
+ - WebSocket on port 7778 (websockets 16.0 — uses `websockets.asyncio.server.serve`, not the legacy `websockets.serve`)
217
+ - File watcher: `watchfiles.awatch` (async generator)
218
+ - Both run inside an `asyncio.TaskGroup`
219
+
220
+ ## Animation pipeline
221
+
222
+ `pipeline.py` processes each slide as a single lxml tree: parsed once via the hardened parser in `svgio.py`, threaded through the pipeline, serialized once at the end. `SlideSvg` wraps the tree and each pipeline step is a method that mutates it in place (like `list.sort()`), delegating the DOM work to `content.py`/`svg.py` functions that take and return the root element. Key steps:
223
+ 1. `clean_inkscape_tree(src)` — parse with the hardened lxml parser, remove elements/attrs in `http://www.inkscape.org/namespaces/inkscape` and `http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd`, call `etree.cleanup_namespaces()`. (`clean_inkscape_svg` wraps this and serializes to a pretty-printed string for the CLI/pre-commit hook.)
224
+ 2. `annotate_svg(root, animations)` — find elements by id (stripping leading `#`), set `class`, `data-step`, and merge `--anim-*` custom properties into `style`
225
+
226
+ The CSS class is `anim-<slug>`, where the slug is the kebab-cased type name (`Animation.slug()` / `Transition.slug()`, a `_Slugged` mixin in `manifest.py`): `FadeIn → anim-fade-in`, `SlideIn → anim-slide-in`, `Highlight → anim-highlight`. There is no per-type registry. `_anim_style` emits one `--anim-<field>` custom property per non-`None` parameter (via the shared `_set_fields` field-walk, with a unit table for `duration`/`delay`/`distance`); the `direction` field instead becomes an `anim-from-<value>` modifier class (`_anim_classes`). CSS in `src/css/shared/animations.css` consumes the custom props via `var(--anim-…, default)`.
227
+
228
+ All SVG parsing routes through `svgio.py` (`parse_svg`, `parse_svg_file`, `serialize_svg`), which uses one hardened parser config (`resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False`, constructed per call since lxml parsers are not thread-safe). This is defense-in-depth plus crash-robustness: an SVG referencing an external/DTD entity degrades to an inert node instead of crashing the rebuild. `svgio.py` also exports the `SvgElement` type alias used across the backend.
229
+
230
+ ## JS toolchain
231
+
232
+ Three tools, each with a distinct role:
233
+
234
+ - **Biome** — linter and formatter for TypeScript and CSS. Run via `mise run lint-js`. Config in `biome.json`.
235
+ - **tsc** — type-checker only (`noEmit: true`). Never emits files; esbuild does that. Run via `mise run typecheck-js`.
236
+ - **esbuild** — bundler. Produces committed bundles in `src/inkflow/bundles/` that the Python inlining pipeline reads at serve/build time. Run via `mise run bundle`.
237
+
238
+ `pip install inkflow` ships the pre-built bundles — no Node at install time.
239
+
240
+ `verbatimModuleSyntax: true` in tsconfig enforces `import type` for type-only imports, which esbuild requires since it transpiles files individually without type information.
241
+
242
+ ## Dependencies
243
+
244
+ ```
245
+ click>=8.0 CLI
246
+ lxml>=5.0 SVG processing
247
+ markdown-it-py>=3.0 Markdown rendering
248
+ platformdirs>=4.0 per-user log + font directories (inkflow.logging, fonts.py)
249
+ rich>=15.0 terminal UI
250
+ watchfiles>=0.21 inotify-based file watcher
251
+ websockets>=12.0 WebSocket server (uses 16.x asyncio API)
252
+ ```
inkflow-0.1.0/LICENSE ADDED
@@ -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.
inkflow-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: inkflow
3
+ Version: 0.1.0
4
+ Summary: Beautiful slides from SVG. Your editor, your style.
5
+ Project-URL: Homepage, https://ll-nick.github.io/inkflow
6
+ Project-URL: Repository, https://github.com/ll-nick/inkflow
7
+ Author: Nick Le Large
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: animation,inkscape,presentation,slides,svg
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Multimedia :: Graphics :: Presentation
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: brotli>=1.2.0
20
+ Requires-Dist: click>=8.0
21
+ Requires-Dist: fonttools>=4.63.0
22
+ Requires-Dist: latex2mathml>=3.81.0
23
+ Requires-Dist: lxml>=5.0
24
+ Requires-Dist: markdown-it-py>=3.0
25
+ Requires-Dist: mdit-py-plugins>=0.6.1
26
+ Requires-Dist: platformdirs>=4.0
27
+ Requires-Dist: pygments>=2.20.0
28
+ Requires-Dist: rich>=15.0.0
29
+ Requires-Dist: typing-extensions>=4.15.0
30
+ Requires-Dist: watchfiles>=0.21
31
+ Requires-Dist: websockets>=12.0
32
+ Description-Content-Type: text/markdown
33
+
34
+ <p align="center">
35
+ <picture>
36
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-light-landscape.svg">
37
+ <source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-dark-landscape.svg">
38
+ <img src="docs/assets/logo-dark-landscape.svg" width="80%">
39
+ </picture>
40
+ </p>
41
+
42
+ <p align="center"><strong>Beautiful slides from SVG. Your editor, your style.</strong></p>
43
+
44
+ <p align="center">
45
+ <a href="https://ll-nick.github.io/inkflow/demo/">
46
+ <img src="docs/assets/demo-button.svg" alt="Try the live demo">
47
+ </a>
48
+ </p>
49
+
50
+ > **Early-stage software.**
51
+ > Expect bugs, missing features, and breaking changes.
52
+
53
+ ## The idea
54
+
55
+ Every presentation tool makes you choose.
56
+
57
+ **Visual editors** (PowerPoint, Keynote, Google Slides) give you a canvas.
58
+ Drag shapes, resize freely, iterate until it looks right.
59
+ But your work lives in proprietary formats tied to a platform or subscription,
60
+ and exporting to anything else means fighting a lossy conversion.
61
+
62
+ **Code-based tools** (Beamer, Slidev, reveal.js) keep everything as plain text.
63
+ Files are diffable, version-controlled, reproducible.
64
+ But you describe layout in markup instead of drawing it.
65
+ Creativity suffers when moving a box means editing a coordinate.
66
+ The blank page is a text cursor, not a canvas.
67
+
68
+ **Inkflow gives you both.**
69
+ Your authoring environment is a proper vector editor.
70
+ Draw freely, iterate visually.
71
+ Your source files are SVG, Markdown, and Python: open formats, plain text, not tied to any software or service.
72
+
73
+ ## How it works
74
+
75
+ A deck is a plain Python file:
76
+
77
+ ```python
78
+ from inkflow import Deck, Image, MediaFit, Slide, Video, animations, transitions
79
+
80
+ def main() -> Deck:
81
+ return Deck(
82
+ slides=[
83
+ # SVG slide: draw freely in Inkscape, animate elements by id
84
+ Slide(
85
+ "title.svg",
86
+ animations=[
87
+ animations.FadeIn("#headline", step=1),
88
+ animations.FadeIn("#subtitle", step=2),
89
+ ],
90
+ ),
91
+ Slide(
92
+ "diagram.svg",
93
+ # Fill predefined content zones using Markdown
94
+ md="diagram.md",
95
+ # Set a transition for the whole slide, and multiple animations for individual elements
96
+ transition=transitions.Crossfade(),
97
+ animations=[
98
+ animations.Bounce("#box-a", step=1),
99
+ animations.Bounce("#box-b", step=2),
100
+ ],
101
+ ),
102
+ Slide(
103
+ # Reuse a built-in, theme or project-local layout
104
+ "media-right",
105
+ md="image.md",
106
+ # Fill a named zone with an image, or a video with playback control
107
+ # (autoplay, loop, mute, trim, play-on-step)
108
+ zones={"media": Image("assets/photo.jpg", fit=MediaFit.COVER)},
109
+ ),
110
+ Slide(
111
+ "media-left",
112
+ md="clip.md",
113
+ zones={"media": Video("assets/demo.mp4", autoplay=True, loop=True)},
114
+ ),
115
+ ]
116
+ )
117
+ ```
118
+
119
+ When you run `inkflow serve`, Inkflow reads the slides as defined in the Python file
120
+ and processes them into a web-based presentation.
121
+ It will inject the Markdown and media files into the SVGs, apply the transitions and animations, and serve the result to your browser.
122
+
123
+ ## Quick start
124
+
125
+ ```bash
126
+ uv add inkflow # or: pip install inkflow
127
+ inkflow init my-deck
128
+ cd my-deck
129
+ inkflow serve
130
+ # press "o" in the tui to open http://localhost:7777 in your browser,
131
+ # press ? in the presenter for keyboard shortcuts
132
+ ```
133
+
134
+ To try the bundled demo:
135
+
136
+ ```bash
137
+ git clone https://github.com/ll-nick/inkflow
138
+ cd inkflow
139
+ uv run inkflow serve --deck demo/deck.py
140
+ ```
141
+
142
+ No SVG editor is invoked at serve time. Inkscape or any other tool writes the files, Inkflow reads them.
143
+ Saving a slide reloads the presenter automatically.
144
+
145
+ ## Acknowledgements
146
+
147
+ [Slidev](https://sli.dev) is an excellent presentation tool and a direct inspiration for this project.
148
+ It's a Node.js project with a very different architecture and feature set,
149
+ including many things Inkflow will never do.
150
+
151
+ This project was built making heavy use of coding agents and would not have been possible without them.
152
+ Does that make it "slopware"?
153
+ I'll let you be the judge of that but I can say that I try my best to keep the agents in check.
154
+ Every architectural decision is mine, there's a CI to ensure a certain level of code quality,
155
+ and before merging, I spend a good amount of time reviewing all changes in good old-fashioned manual labor.