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.
- inkflow-0.1.0/.claude/hooks/autofix.sh +19 -0
- inkflow-0.1.0/.claude/settings.json +19 -0
- inkflow-0.1.0/.gitattributes +1 -0
- inkflow-0.1.0/.githooks/pre-commit +23 -0
- inkflow-0.1.0/.github/workflows/ci.yml +66 -0
- inkflow-0.1.0/.github/workflows/docs.yml +45 -0
- inkflow-0.1.0/.github/workflows/release.yml +39 -0
- inkflow-0.1.0/.gitignore +12 -0
- inkflow-0.1.0/.python-version +1 -0
- inkflow-0.1.0/CLAUDE.md +252 -0
- inkflow-0.1.0/LICENSE +21 -0
- inkflow-0.1.0/PKG-INFO +155 -0
- inkflow-0.1.0/README.md +122 -0
- inkflow-0.1.0/ROADMAP.md +12 -0
- inkflow-0.1.0/biome.json +42 -0
- inkflow-0.1.0/demo/assets/cover-dark.webp +0 -0
- inkflow-0.1.0/demo/assets/cover-light.webp +0 -0
- inkflow-0.1.0/demo/assets/logo.mp4 +0 -0
- inkflow-0.1.0/demo/deck.py +165 -0
- inkflow-0.1.0/demo/layouts/center.svg +63 -0
- inkflow-0.1.0/demo/layouts/content.svg +51 -0
- inkflow-0.1.0/demo/layouts/footer.svg +51 -0
- inkflow-0.1.0/demo/notes/animations.md +7 -0
- inkflow-0.1.0/demo/notes/deckpy.md +11 -0
- inkflow-0.1.0/demo/notes/end.md +5 -0
- inkflow-0.1.0/demo/notes/features.md +6 -0
- inkflow-0.1.0/demo/notes/hackable.md +7 -0
- inkflow-0.1.0/demo/notes/how-it-works.md +10 -0
- inkflow-0.1.0/demo/notes/interface.md +5 -0
- inkflow-0.1.0/demo/notes/markdown.md +13 -0
- inkflow-0.1.0/demo/notes/media.md +7 -0
- inkflow-0.1.0/demo/notes/morph.md +5 -0
- inkflow-0.1.0/demo/notes/title.md +5 -0
- inkflow-0.1.0/demo/scripts.js +36 -0
- inkflow-0.1.0/demo/slides/animations.svg +101 -0
- inkflow-0.1.0/demo/slides/deckpy.md +18 -0
- inkflow-0.1.0/demo/slides/end.md +7 -0
- inkflow-0.1.0/demo/slides/features.md +16 -0
- inkflow-0.1.0/demo/slides/hackable.md +17 -0
- inkflow-0.1.0/demo/slides/how-it-works.svg +111 -0
- inkflow-0.1.0/demo/slides/interface.md +19 -0
- inkflow-0.1.0/demo/slides/markdown.md +24 -0
- inkflow-0.1.0/demo/slides/media.svg +87 -0
- inkflow-0.1.0/demo/slides/morph.svg +95 -0
- inkflow-0.1.0/demo/slides/title.svg +67 -0
- inkflow-0.1.0/demo/styles.css +47 -0
- inkflow-0.1.0/docs/assets/demo-button.svg +5 -0
- inkflow-0.1.0/docs/assets/logo-dark-icon.svg +17 -0
- inkflow-0.1.0/docs/assets/logo-dark-landscape.svg +18 -0
- inkflow-0.1.0/docs/assets/logo-dark-portrait.svg +17 -0
- inkflow-0.1.0/docs/assets/logo-light-icon.svg +17 -0
- inkflow-0.1.0/docs/assets/logo-light-landscape.svg +17 -0
- inkflow-0.1.0/docs/assets/logo-light-portrait.svg +17 -0
- inkflow-0.1.0/docs/built-in-theme/index.md +55 -0
- inkflow-0.1.0/docs/concepts.md +138 -0
- inkflow-0.1.0/docs/getting-started.md +123 -0
- inkflow-0.1.0/docs/guides/export.md +104 -0
- inkflow-0.1.0/docs/guides/fonts.md +77 -0
- inkflow-0.1.0/docs/guides/layout-system.md +272 -0
- inkflow-0.1.0/docs/guides/presenter-view.md +108 -0
- inkflow-0.1.0/docs/guides/slides.md +593 -0
- inkflow-0.1.0/docs/guides/themes.md +197 -0
- inkflow-0.1.0/docs/guides/transitions.md +180 -0
- inkflow-0.1.0/docs/index.md +79 -0
- inkflow-0.1.0/docs/reference/animations.md +28 -0
- inkflow-0.1.0/docs/reference/cli.md +53 -0
- inkflow-0.1.0/docs/reference/enums.md +62 -0
- inkflow-0.1.0/docs/reference/manifest.md +28 -0
- inkflow-0.1.0/docs/reference/transitions.md +24 -0
- inkflow-0.1.0/docs/stylesheets/extra.css +98 -0
- inkflow-0.1.0/docs/theme_overrides/python/material/attribute.html.jinja +8 -0
- inkflow-0.1.0/mise.toml +53 -0
- inkflow-0.1.0/mkdocs.yml +88 -0
- inkflow-0.1.0/package-lock.json +2409 -0
- inkflow-0.1.0/package.json +10 -0
- inkflow-0.1.0/pyproject.toml +79 -0
- inkflow-0.1.0/src/css/presenter/base.css +174 -0
- inkflow-0.1.0/src/css/presenter/fullscreen.css +25 -0
- inkflow-0.1.0/src/css/presenter/index.css +11 -0
- inkflow-0.1.0/src/css/presenter/laser.css +56 -0
- inkflow-0.1.0/src/css/presenter/mobile.css +89 -0
- inkflow-0.1.0/src/css/presenter/overlays.css +207 -0
- inkflow-0.1.0/src/css/presenter/overview.css +68 -0
- inkflow-0.1.0/src/css/presenter/picker.css +80 -0
- inkflow-0.1.0/src/css/presenter/pv.css +195 -0
- inkflow-0.1.0/src/css/presenter/sync.css +77 -0
- inkflow-0.1.0/src/css/shared/animations.css +141 -0
- inkflow-0.1.0/src/css/shared/theme-vars.css +27 -0
- inkflow-0.1.0/src/inkflow/__init__.py +48 -0
- inkflow-0.1.0/src/inkflow/animations.py +105 -0
- inkflow-0.1.0/src/inkflow/bundles/presenter.css +991 -0
- inkflow-0.1.0/src/inkflow/bundles/presenter.js +2487 -0
- inkflow-0.1.0/src/inkflow/clean.py +72 -0
- inkflow-0.1.0/src/inkflow/cli/__init__.py +19 -0
- inkflow-0.1.0/src/inkflow/cli/_common.py +199 -0
- inkflow-0.1.0/src/inkflow/cli/authoring.py +407 -0
- inkflow-0.1.0/src/inkflow/cli/color.py +102 -0
- inkflow-0.1.0/src/inkflow/cli/present.py +122 -0
- inkflow-0.1.0/src/inkflow/cli/project.py +88 -0
- inkflow-0.1.0/src/inkflow/cli/verify.py +89 -0
- inkflow-0.1.0/src/inkflow/colors.py +335 -0
- inkflow-0.1.0/src/inkflow/completions/inkflow.yaml +164 -0
- inkflow-0.1.0/src/inkflow/content.py +474 -0
- inkflow-0.1.0/src/inkflow/enums.py +212 -0
- inkflow-0.1.0/src/inkflow/export.py +183 -0
- inkflow-0.1.0/src/inkflow/fonts.py +438 -0
- inkflow-0.1.0/src/inkflow/git_setup.py +190 -0
- inkflow-0.1.0/src/inkflow/init.py +52 -0
- inkflow-0.1.0/src/inkflow/layout.py +464 -0
- inkflow-0.1.0/src/inkflow/loaders.py +95 -0
- inkflow-0.1.0/src/inkflow/logging.py +238 -0
- inkflow-0.1.0/src/inkflow/manifest.py +347 -0
- inkflow-0.1.0/src/inkflow/markdown.py +256 -0
- inkflow-0.1.0/src/inkflow/ns.py +10 -0
- inkflow-0.1.0/src/inkflow/pdf.html +21 -0
- inkflow-0.1.0/src/inkflow/pipeline.py +350 -0
- inkflow-0.1.0/src/inkflow/presenter.html +243 -0
- inkflow-0.1.0/src/inkflow/server.py +552 -0
- inkflow-0.1.0/src/inkflow/svg.py +81 -0
- inkflow-0.1.0/src/inkflow/svgio.py +62 -0
- inkflow-0.1.0/src/inkflow/theme/icon.svg +14 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/base.svg +37 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/center.svg +42 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/cover.svg +43 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/default.svg +46 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/end.svg +41 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/fact.svg +43 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/media-left.svg +46 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/media-right.svg +46 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/numbered.svg +47 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/quote.svg +43 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/section.svg +42 -0
- inkflow-0.1.0/src/inkflow/theme/layouts/two-cols.svg +46 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/deck.py +47 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/center.md +1 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/cover.md +4 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/default.md +10 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/end.md +3 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/fact.md +5 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/media-left.md +4 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/media-right.md +3 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/quote.md +5 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/section.md +3 -0
- inkflow-0.1.0/src/inkflow/theme/showcase/slides/two-cols.md +15 -0
- inkflow-0.1.0/src/inkflow/theme/styles.css +623 -0
- inkflow-0.1.0/src/inkflow/titles.py +17 -0
- inkflow-0.1.0/src/inkflow/transitions.py +115 -0
- inkflow-0.1.0/src/inkflow/tui.py +152 -0
- inkflow-0.1.0/src/inkflow/verify.py +177 -0
- inkflow-0.1.0/src/inkflow/zones.py +514 -0
- inkflow-0.1.0/src/ts/globals.d.ts +27 -0
- inkflow-0.1.0/src/ts/presenter/keyboard.ts +210 -0
- inkflow-0.1.0/src/ts/presenter/laser.ts +84 -0
- inkflow-0.1.0/src/ts/presenter/logs.test.ts +97 -0
- inkflow-0.1.0/src/ts/presenter/main.ts +49 -0
- inkflow-0.1.0/src/ts/presenter/morph.ts +974 -0
- inkflow-0.1.0/src/ts/presenter/navigation.ts +113 -0
- inkflow-0.1.0/src/ts/presenter/overview.test.ts +167 -0
- inkflow-0.1.0/src/ts/presenter/overview.ts +272 -0
- inkflow-0.1.0/src/ts/presenter/picker.test.ts +63 -0
- inkflow-0.1.0/src/ts/presenter/picker.ts +120 -0
- inkflow-0.1.0/src/ts/presenter/progress-driver.test.ts +79 -0
- inkflow-0.1.0/src/ts/presenter/progress-driver.ts +53 -0
- inkflow-0.1.0/src/ts/presenter/pv.ts +136 -0
- inkflow-0.1.0/src/ts/presenter/state.ts +16 -0
- inkflow-0.1.0/src/ts/presenter/status.ts +94 -0
- inkflow-0.1.0/src/ts/presenter/syncmenu.ts +98 -0
- inkflow-0.1.0/src/ts/presenter/transitions.test.ts +336 -0
- inkflow-0.1.0/src/ts/presenter/transitions.ts +558 -0
- inkflow-0.1.0/src/ts/presenter/ui.ts +213 -0
- inkflow-0.1.0/src/ts/presenter/video.ts +98 -0
- inkflow-0.1.0/src/ts/presenter/websocket.ts +204 -0
- inkflow-0.1.0/src/ts/shared/easing.test.ts +46 -0
- inkflow-0.1.0/src/ts/shared/easing.ts +89 -0
- inkflow-0.1.0/src/ts/shared/escape.ts +12 -0
- inkflow-0.1.0/src/ts/shared/morph-math.test.ts +272 -0
- inkflow-0.1.0/src/ts/shared/morph-math.ts +222 -0
- inkflow-0.1.0/src/ts/shared/ring.ts +30 -0
- inkflow-0.1.0/src/ts/shared/step.test.ts +87 -0
- inkflow-0.1.0/src/ts/shared/step.ts +73 -0
- inkflow-0.1.0/src/ts/shared/types.ts +58 -0
- inkflow-0.1.0/tests/__init__.py +0 -0
- inkflow-0.1.0/tests/decks/assets/logo.mp4 +1 -0
- inkflow-0.1.0/tests/decks/assets/poster.webp +1 -0
- inkflow-0.1.0/tests/decks/scripts.js +26 -0
- inkflow-0.1.0/tests/decks/slides/anim-targets.svg +40 -0
- inkflow-0.1.0/tests/decks/slides/cover.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/crossfade.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/cut.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/fade.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/flip.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/media-steps.svg +7 -0
- inkflow-0.1.0/tests/decks/slides/media.svg +5 -0
- inkflow-0.1.0/tests/decks/slides/morph-a.svg +5 -0
- inkflow-0.1.0/tests/decks/slides/morph-b.svg +5 -0
- inkflow-0.1.0/tests/decks/slides/push.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/title.svg +5 -0
- inkflow-0.1.0/tests/decks/slides/wipe.svg +4 -0
- inkflow-0.1.0/tests/decks/slides/zoom.svg +4 -0
- inkflow-0.1.0/tests/decks/step_animations.py +92 -0
- inkflow-0.1.0/tests/decks/transitions.py +69 -0
- inkflow-0.1.0/tests/decks/video.py +120 -0
- inkflow-0.1.0/tests/test_animations.py +73 -0
- inkflow-0.1.0/tests/test_clean.py +241 -0
- inkflow-0.1.0/tests/test_cli.py +356 -0
- inkflow-0.1.0/tests/test_colors.py +195 -0
- inkflow-0.1.0/tests/test_content.py +492 -0
- inkflow-0.1.0/tests/test_decks.py +24 -0
- inkflow-0.1.0/tests/test_export.py +240 -0
- inkflow-0.1.0/tests/test_fonts.py +404 -0
- inkflow-0.1.0/tests/test_layout.py +352 -0
- inkflow-0.1.0/tests/test_loaders.py +85 -0
- inkflow-0.1.0/tests/test_logging.py +229 -0
- inkflow-0.1.0/tests/test_manifest.py +309 -0
- inkflow-0.1.0/tests/test_markdown.py +176 -0
- inkflow-0.1.0/tests/test_pipeline.py +466 -0
- inkflow-0.1.0/tests/test_server.py +195 -0
- inkflow-0.1.0/tests/test_svg.py +56 -0
- inkflow-0.1.0/tests/test_svgio.py +57 -0
- inkflow-0.1.0/tests/test_verify.py +224 -0
- inkflow-0.1.0/tests/test_zones.py +664 -0
- inkflow-0.1.0/tsconfig.json +12 -0
- 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
|
inkflow-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
inkflow-0.1.0/CLAUDE.md
ADDED
|
@@ -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.
|