scrolly 0.2.2__tar.gz → 0.2.4__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 (124) hide show
  1. {scrolly-0.2.2 → scrolly-0.2.4}/.gitignore +3 -0
  2. scrolly-0.2.4/PKG-INFO +118 -0
  3. scrolly-0.2.4/README.md +85 -0
  4. {scrolly-0.2.2 → scrolly-0.2.4}/pyproject.toml +4 -2
  5. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_cli.py +2 -46
  6. scrolly-0.2.4/scrolly/_cli/_schema.py +167 -0
  7. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/introspect.py +2 -1
  8. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/model.py +1 -0
  9. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/parser.py +34 -11
  10. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/schema.py +11 -0
  11. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E003.md +2 -2
  12. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E299.md +2 -2
  13. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E601.md +3 -2
  14. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/assets/canvas.css +49 -5
  15. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/assets/canvas.js +59 -8
  16. scrolly-0.2.4/scrolly/render/color.py +66 -0
  17. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/nav_data.py +15 -4
  18. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/templates/index.html.j2 +12 -2
  19. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/__init__.py +2 -1
  20. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/__init__.py +2 -0
  21. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/_framework/element.py +49 -1
  22. scrolly-0.2.2/PKG-INFO +0 -81
  23. scrolly-0.2.2/README.md +0 -50
  24. {scrolly-0.2.2 → scrolly-0.2.4}/LICENSE +0 -0
  25. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/__init__.py +0 -0
  26. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/__init__.py +0 -0
  27. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_errors.py +0 -0
  28. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/__init__.py +0 -0
  29. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_assets.py +0 -0
  30. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_common.py +0 -0
  31. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_dom.py +0 -0
  32. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_elements.py +0 -0
  33. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_slides.py +0 -0
  34. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_snaps.py +0 -0
  35. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_snapshot.py +0 -0
  36. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/_cli/_introspect/_timeline.py +0 -0
  37. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/__init__.py +0 -0
  38. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/inference.py +0 -0
  39. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/deck/validator.py +0 -0
  40. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/__init__.py +0 -0
  41. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/_catalog.py +0 -0
  42. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/_codes.py +0 -0
  43. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/_report.py +0 -0
  44. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/_validation_error.py +0 -0
  45. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E001.md +0 -0
  46. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E002.md +0 -0
  47. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E004.md +0 -0
  48. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E005.md +0 -0
  49. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E006.md +0 -0
  50. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E007.md +0 -0
  51. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E008.md +0 -0
  52. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E009.md +0 -0
  53. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E010.md +0 -0
  54. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E011.md +0 -0
  55. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E012.md +0 -0
  56. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E101.md +0 -0
  57. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E102.md +0 -0
  58. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E103.md +0 -0
  59. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E201.md +0 -0
  60. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E202.md +0 -0
  61. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E203.md +0 -0
  62. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E204.md +0 -0
  63. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E205.md +0 -0
  64. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E206.md +0 -0
  65. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E207.md +0 -0
  66. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E301.md +0 -0
  67. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E302.md +0 -0
  68. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E303.md +0 -0
  69. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E304.md +0 -0
  70. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E305.md +0 -0
  71. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E306.md +0 -0
  72. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E307.md +0 -0
  73. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E308.md +0 -0
  74. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E401.md +0 -0
  75. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E402.md +0 -0
  76. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E403.md +0 -0
  77. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E501.md +0 -0
  78. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E502.md +0 -0
  79. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E503.md +0 -0
  80. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E504.md +0 -0
  81. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E505.md +0 -0
  82. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E602.md +0 -0
  83. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E603.md +0 -0
  84. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E701.md +0 -0
  85. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/E702.md +0 -0
  86. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/errors/catalog/__init__.py +0 -0
  87. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/__init__.py +0 -0
  88. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/_bundler.py +0 -0
  89. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/assets.py +0 -0
  90. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/introspect.py +0 -0
  91. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/lint.py +0 -0
  92. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/loader.py +0 -0
  93. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/orchestrator.py +0 -0
  94. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/pipeline/writer.py +0 -0
  95. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/__init__.py +0 -0
  96. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/assembler.py +0 -0
  97. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/assets/mermaid-LICENSE +0 -0
  98. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/assets/mermaid.min.js +0 -0
  99. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/bundled_assets.py +0 -0
  100. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/fan.py +0 -0
  101. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/render/zoom_control.py +0 -0
  102. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/__init__.py +0 -0
  103. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/ir.py +0 -0
  104. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/processor.py +0 -0
  105. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/registry.py +0 -0
  106. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/rendered.py +0 -0
  107. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/__init__.py +0 -0
  108. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/_shared.py +0 -0
  109. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/html.py +0 -0
  110. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/iframe.py +0 -0
  111. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/image.py +0 -0
  112. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/image_sequence.py +0 -0
  113. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/markdown.py +0 -0
  114. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/element_ir/renderers/mermaid.py +0 -0
  115. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/html.py +0 -0
  116. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/introspect.py +0 -0
  117. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/_framework/__init__.py +0 -0
  118. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/_framework/animated_values.py +0 -0
  119. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/_framework/utils.py +0 -0
  120. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/ir/slide.py +0 -0
  121. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/processor.py +0 -0
  122. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/registry.py +0 -0
  123. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/renderers/__init__.py +0 -0
  124. {scrolly-0.2.2 → scrolly-0.2.4}/scrolly/slide/renderers/slide.py +0 -0
@@ -5,6 +5,9 @@ dist/
5
5
  .pytest_cache/
6
6
  .ruff_cache/
7
7
 
8
+ # Docs site build output (mkdocs build)
9
+ /site/
10
+
8
11
  # Golden-test debug output (written next to the fixture on mismatch)
9
12
  *.actual.html
10
13
 
scrolly-0.2.4/PKG-INFO ADDED
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: scrolly
3
+ Version: 0.2.4
4
+ Summary: CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation.
5
+ Project-URL: Homepage, https://scrolly.readthedocs.io/en/stable/
6
+ Project-URL: Documentation, https://scrolly.readthedocs.io/en/stable/
7
+ Project-URL: Source, https://github.com/bertpl/scrolly
8
+ Project-URL: Changelog, https://github.com/bertpl/scrolly/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/bertpl/scrolly/issues
10
+ Author-email: Bert Pluymers <bert.pluymers@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: cli,html,presentation,scrollytelling,slides
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Multimedia :: Graphics :: Presentation
23
+ Classifier: Topic :: Text Processing :: Markup :: HTML
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: click>=8.2.0
26
+ Requires-Dist: jinja2>=3.1
27
+ Requires-Dist: json5>=0.13.0
28
+ Requires-Dist: markdown>=3.4
29
+ Requires-Dist: pydantic>=2.0
30
+ Requires-Dist: pyyaml>=6.0
31
+ Requires-Dist: rich>=13.0.0
32
+ Description-Content-Type: text/markdown
33
+
34
+ # scrolly
35
+
36
+ Compile a JSON5 deck into a single, self-contained, scrollable 2D-canvas HTML presentation.
37
+
38
+ *Liked by humans; understood by agents.*
39
+
40
+ [![scrolly — scroll-driven 2D-canvas presentations](https://raw.githubusercontent.com/bertpl/scrolly/main/docs/assets/hero-v4.webp)](https://scrolly.readthedocs.io/en/stable/)
41
+
42
+ [![CI](https://img.shields.io/github/actions/workflow/status/bertpl/scrolly/push_to_main.yml?branch=main&label=CI)](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/scrolly.svg)](https://pypi.org/project/scrolly/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/scrolly.svg)](https://pypi.org/project/scrolly/)
45
+ [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/bertpl/scrolly/blob/main/LICENSE)
46
+
47
+ ## Why it's different
48
+
49
+ - **A 2D canvas you fly around**, not a linear reel. Slides sit on an
50
+ integer grid connected by edges; readers zoom out to a deck map and
51
+ zoom into any slide to scroll through it.
52
+ - **Scroll-driven keyframe animation.** Element properties (position,
53
+ size, opacity, scale, angle) interpolate as the reader scrolls — no
54
+ timeline, no autoplay.
55
+ - **One self-contained HTML file.** A default build inlines every
56
+ asset into a single `index.html` with zero external loads — open it
57
+ by double-clicking, host it anywhere, email it.
58
+ - **Agent-friendly.** The CLI exposes full help, input schemas, deck
59
+ introspection, and numbered error codes, so agentic coding tools
60
+ immediately understand how to author and debug scrolly decks.
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install scrolly # or: uv tool install scrolly
66
+ ```
67
+
68
+ ## Quickstart
69
+
70
+ ```bash
71
+ scrolly init my-deck # scaffold a starter deck
72
+ scrolly build my-deck/deck.deck.json --out out --force # compile to one HTML file
73
+ open out/index.html # double-click anywhere
74
+ ```
75
+
76
+ A deck is a `.deck.json` manifest plus one `.slide.json` per slide (both
77
+ parsed as JSON5):
78
+
79
+ ```json5
80
+ // deck.deck.json
81
+ {
82
+ title: "My Deck",
83
+ slides: [
84
+ { id: "intro", position: [0, 0], source: "slides/intro.slide.json" },
85
+ ],
86
+ edges: [],
87
+ }
88
+ ```
89
+
90
+ ```json5
91
+ // slides/intro.slide.json
92
+ {
93
+ title: "My Deck",
94
+ elements: [
95
+ { markdown: "# My Deck\n\nWelcome to your new presentation." },
96
+ ],
97
+ }
98
+ ```
99
+
100
+ ## Elements
101
+
102
+ Slides hold elements of six types — `markdown`, `image`, `image_sequence`,
103
+ `mermaid`, `html`, and `iframe` — each animatable via scroll-driven
104
+ keyframes. See the [element reference](https://scrolly.readthedocs.io/en/stable/reference/elements/)
105
+ for fields and examples.
106
+
107
+ ## Learn more
108
+
109
+ - **[Documentation](https://scrolly.readthedocs.io/en/stable/)** — guided
110
+ walkthrough, concepts, authoring, and the full reference, with a live
111
+ interactive deck right on the homepage.
112
+ - **[Examples](examples/)** — including the
113
+ [stacked-diffs](examples/stacked-diffs/) hero deck.
114
+ - **[Changelog](CHANGELOG.md)**.
115
+
116
+ ## License
117
+
118
+ [MIT](https://github.com/bertpl/scrolly/blob/main/LICENSE).
@@ -0,0 +1,85 @@
1
+ # scrolly
2
+
3
+ Compile a JSON5 deck into a single, self-contained, scrollable 2D-canvas HTML presentation.
4
+
5
+ *Liked by humans; understood by agents.*
6
+
7
+ [![scrolly — scroll-driven 2D-canvas presentations](https://raw.githubusercontent.com/bertpl/scrolly/main/docs/assets/hero-v4.webp)](https://scrolly.readthedocs.io/en/stable/)
8
+
9
+ [![CI](https://img.shields.io/github/actions/workflow/status/bertpl/scrolly/push_to_main.yml?branch=main&label=CI)](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/scrolly.svg)](https://pypi.org/project/scrolly/)
11
+ [![Python](https://img.shields.io/pypi/pyversions/scrolly.svg)](https://pypi.org/project/scrolly/)
12
+ [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/bertpl/scrolly/blob/main/LICENSE)
13
+
14
+ ## Why it's different
15
+
16
+ - **A 2D canvas you fly around**, not a linear reel. Slides sit on an
17
+ integer grid connected by edges; readers zoom out to a deck map and
18
+ zoom into any slide to scroll through it.
19
+ - **Scroll-driven keyframe animation.** Element properties (position,
20
+ size, opacity, scale, angle) interpolate as the reader scrolls — no
21
+ timeline, no autoplay.
22
+ - **One self-contained HTML file.** A default build inlines every
23
+ asset into a single `index.html` with zero external loads — open it
24
+ by double-clicking, host it anywhere, email it.
25
+ - **Agent-friendly.** The CLI exposes full help, input schemas, deck
26
+ introspection, and numbered error codes, so agentic coding tools
27
+ immediately understand how to author and debug scrolly decks.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install scrolly # or: uv tool install scrolly
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```bash
38
+ scrolly init my-deck # scaffold a starter deck
39
+ scrolly build my-deck/deck.deck.json --out out --force # compile to one HTML file
40
+ open out/index.html # double-click anywhere
41
+ ```
42
+
43
+ A deck is a `.deck.json` manifest plus one `.slide.json` per slide (both
44
+ parsed as JSON5):
45
+
46
+ ```json5
47
+ // deck.deck.json
48
+ {
49
+ title: "My Deck",
50
+ slides: [
51
+ { id: "intro", position: [0, 0], source: "slides/intro.slide.json" },
52
+ ],
53
+ edges: [],
54
+ }
55
+ ```
56
+
57
+ ```json5
58
+ // slides/intro.slide.json
59
+ {
60
+ title: "My Deck",
61
+ elements: [
62
+ { markdown: "# My Deck\n\nWelcome to your new presentation." },
63
+ ],
64
+ }
65
+ ```
66
+
67
+ ## Elements
68
+
69
+ Slides hold elements of six types — `markdown`, `image`, `image_sequence`,
70
+ `mermaid`, `html`, and `iframe` — each animatable via scroll-driven
71
+ keyframes. See the [element reference](https://scrolly.readthedocs.io/en/stable/reference/elements/)
72
+ for fields and examples.
73
+
74
+ ## Learn more
75
+
76
+ - **[Documentation](https://scrolly.readthedocs.io/en/stable/)** — guided
77
+ walkthrough, concepts, authoring, and the full reference, with a live
78
+ interactive deck right on the homepage.
79
+ - **[Examples](examples/)** — including the
80
+ [stacked-diffs](examples/stacked-diffs/) hero deck.
81
+ - **[Changelog](CHANGELOG.md)**.
82
+
83
+ ## License
84
+
85
+ [MIT](https://github.com/bertpl/scrolly/blob/main/LICENSE).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrolly"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -40,7 +40,7 @@ dev = [
40
40
  # Exact pin: gitsvg is pre-1.0 and its output schema can shift
41
41
  # between releases. Used to regenerate the stacked-diffs example's
42
42
  # gitsvg frames.
43
- "gitsvg==0.2.2",
43
+ "gitsvg==0.2.3",
44
44
  ]
45
45
  # Optional group for the hero-animation capture pipeline (docs/_gen).
46
46
  # Heavy (Playwright ships browser binaries via `make capture-setup`),
@@ -54,6 +54,8 @@ capture = [
54
54
  scrolly = "scrolly._cli._cli:cli"
55
55
 
56
56
  [project.urls]
57
+ Homepage = "https://scrolly.readthedocs.io/en/stable/"
58
+ Documentation = "https://scrolly.readthedocs.io/en/stable/"
57
59
  Source = "https://github.com/bertpl/scrolly"
58
60
  Changelog = "https://github.com/bertpl/scrolly/blob/main/CHANGELOG.md"
59
61
  Issues = "https://github.com/bertpl/scrolly/issues"
@@ -8,6 +8,7 @@ from rich.console import Console
8
8
  from scrolly import __version__
9
9
  from scrolly._cli._errors import errors_command
10
10
  from scrolly._cli._introspect import introspect
11
+ from scrolly._cli._schema import schema
11
12
  from scrolly.errors import ScrollyError, ValidationError
12
13
  from scrolly.pipeline import build_deck, load_deck
13
14
  from scrolly.pipeline.lint import lint_deck
@@ -82,52 +83,6 @@ def build(
82
83
  click.echo(f"Built '{deck.title or '(untitled)'}': {len(deck.slides)} slides, {len(deck.edges)} edges → {out_dir}")
83
84
 
84
85
 
85
- @cli.command()
86
- @click.argument("type_name", required=False)
87
- @click.option(
88
- "--list-types",
89
- "list_types",
90
- is_flag=True,
91
- help="Print bare type names one per line (no descriptions) for scripting use.",
92
- )
93
- def schema(type_name: str | None, list_types: bool) -> None:
94
- """Show source file schemas.
95
-
96
- \b
97
- scrolly schema → formatted index of available types
98
- scrolly schema <type> → JSON Schema for <type>
99
- scrolly schema --list-types → bare type names, one per line (agent / scripting)
100
- """
101
- from scrolly.deck import deck_source_schema
102
- from scrolly.slide import registered_ir_types
103
-
104
- ir_types = registered_ir_types()
105
- all_type_names = sorted(["deck", *ir_types])
106
-
107
- if list_types:
108
- for name in all_type_names:
109
- click.echo(name)
110
- return
111
-
112
- if type_name is None:
113
- click.echo("Available schemas:\n")
114
- click.echo(f" {'deck':<17}{'.deck.json':<24}Deck structure (slides + edges)")
115
- for name in sorted(ir_types):
116
- cls = ir_types[name]
117
- click.echo(f" {name:<17}{cls.SUFFIX:<24}{cls.DESCRIPTION}")
118
- return
119
-
120
- if type_name == "deck":
121
- click.echo(json.dumps(deck_source_schema(), indent=2))
122
- return
123
-
124
- if type_name not in ir_types:
125
- _err_console.print(f"[red]error:[/red] unknown type '{type_name}' (known: {', '.join(all_type_names)})")
126
- sys.exit(1)
127
-
128
- click.echo(json.dumps(ir_types[type_name].source_schema(), indent=2))
129
-
130
-
131
86
  @cli.command()
132
87
  @click.argument("deck_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
133
88
  @click.option("--strict", is_flag=True, help="Enable additional lint checks (e.g. out-of-range keyframes).")
@@ -217,3 +172,4 @@ def init(dir_path: Path) -> None:
217
172
 
218
173
  cli.add_command(errors_command)
219
174
  cli.add_command(introspect)
175
+ cli.add_command(schema)
@@ -0,0 +1,167 @@
1
+ """``scrolly schema`` — show source-file and slide-element schemas from the CLI.
2
+
3
+ A Click group with two parallel subcommands, mirroring the
4
+ subcommand-group structure of ``scrolly introspect``:
5
+
6
+ * ``scrolly schema`` — combined index of file and element schemas.
7
+ * ``scrolly schema file`` — index of source-file schemas (deck, slide).
8
+ * ``scrolly schema file <type>`` — JSON Schema for a source-file type.
9
+ * ``scrolly schema file --list-types`` — bare file-type names, one per line.
10
+ * ``scrolly schema element`` — index of element schemas.
11
+ * ``scrolly schema element <type>`` — JSON Schema for an element type.
12
+ * ``scrolly schema element --list-types`` — bare element keys, one per line.
13
+
14
+ ``schema`` shows static *type* definitions; ``introspect`` inspects a
15
+ specific *resolved deck instance* — different surfaces, kept distinct.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import sys
22
+
23
+ import click
24
+ from rich.console import Console
25
+
26
+ _err_console = Console(stderr=True, highlight=False)
27
+
28
+ # Width of the name / suffix columns in the human-readable index, matching
29
+ # the alignment used by ``scrolly errors``.
30
+ _NAME_COL = 17
31
+ _SUFFIX_COL = 24
32
+
33
+
34
+ # ==================================================================================================
35
+ # schema group
36
+ # ==================================================================================================
37
+ @click.group(name="schema", invoke_without_command=True)
38
+ @click.pass_context
39
+ def schema(ctx: click.Context) -> None:
40
+ """Show source-file and slide-element schemas.
41
+
42
+ \b
43
+ scrolly schema → combined index (file + element schemas)
44
+ scrolly schema file [<type>] → source-file schemas (deck, slide)
45
+ scrolly schema element [<type>] → slide-element schemas (markdown, image, …)
46
+
47
+ Append --list-types to either subcommand for bare names (agent / scripting).
48
+ Shows static type definitions; use `scrolly introspect` for a resolved deck.
49
+ """
50
+ if ctx.invoked_subcommand is None:
51
+ _print_file_index()
52
+ click.echo()
53
+ _print_element_index()
54
+
55
+
56
+ # --------------------------------------------------------------------------
57
+ # Subcommands
58
+ # --------------------------------------------------------------------------
59
+ @schema.command(name="file")
60
+ @click.argument("type_name", required=False)
61
+ @click.option(
62
+ "--list-types",
63
+ "list_types",
64
+ is_flag=True,
65
+ help="Print bare file-type names one per line (no descriptions) for scripting use.",
66
+ )
67
+ def schema_file(type_name: str | None, list_types: bool) -> None:
68
+ """Source-file schemas (deck, slide).
69
+
70
+ \b
71
+ scrolly schema file → formatted index of file types
72
+ scrolly schema file <type> → JSON Schema for <type>
73
+ scrolly schema file --list-types → bare type names, one per line (agent / scripting)
74
+ """
75
+ names = _file_type_names()
76
+
77
+ if list_types:
78
+ for name in names:
79
+ click.echo(name)
80
+ return
81
+
82
+ if type_name is None:
83
+ _print_file_index()
84
+ return
85
+
86
+ schema_dict = _file_schema(type_name)
87
+ if schema_dict is None:
88
+ _err_console.print(f"[red]error:[/red] unknown file type '{type_name}' (known: {', '.join(names)})")
89
+ sys.exit(1)
90
+ click.echo(json.dumps(schema_dict, indent=2))
91
+
92
+
93
+ @schema.command(name="element")
94
+ @click.argument("type_name", required=False)
95
+ @click.option(
96
+ "--list-types",
97
+ "list_types",
98
+ is_flag=True,
99
+ help="Print bare element keys one per line (no descriptions) for scripting use.",
100
+ )
101
+ def schema_element(type_name: str | None, list_types: bool) -> None:
102
+ """Slide-element schemas (markdown, image, …).
103
+
104
+ \b
105
+ scrolly schema element → formatted index of element types
106
+ scrolly schema element <type> → JSON Schema for <type>
107
+ scrolly schema element --list-types → bare element keys, one per line (agent / scripting)
108
+ """
109
+ from scrolly.slide import element_source_types
110
+
111
+ elements = element_source_types()
112
+
113
+ if list_types:
114
+ for key in elements:
115
+ click.echo(key)
116
+ return
117
+
118
+ if type_name is None:
119
+ _print_element_index()
120
+ return
121
+
122
+ if type_name not in elements:
123
+ _err_console.print(f"[red]error:[/red] unknown element type '{type_name}' (known: {', '.join(elements)})")
124
+ sys.exit(1)
125
+ click.echo(json.dumps(elements[type_name].source_schema(), indent=2))
126
+
127
+
128
+ # --------------------------------------------------------------------------
129
+ # Schema lookup + index rendering
130
+ # --------------------------------------------------------------------------
131
+ def _file_type_names() -> list[str]:
132
+ """Return the sorted source-file type names (deck + registered slide types)."""
133
+ from scrolly.slide import registered_ir_types
134
+
135
+ return sorted(["deck", *registered_ir_types()])
136
+
137
+
138
+ def _file_schema(type_name: str) -> dict | None:
139
+ """Return the JSON Schema for a source-file type, or ``None`` if unknown."""
140
+ from scrolly.deck import deck_source_schema
141
+ from scrolly.slide import registered_ir_types
142
+
143
+ if type_name == "deck":
144
+ return deck_source_schema()
145
+ ir_types = registered_ir_types()
146
+ if type_name in ir_types:
147
+ return ir_types[type_name].source_schema()
148
+ return None
149
+
150
+
151
+ def _print_file_index() -> None:
152
+ """Print the human-readable index of source-file schemas."""
153
+ from scrolly.slide import registered_ir_types
154
+
155
+ click.echo("File schemas (source files):\n")
156
+ click.echo(f" {'deck':<{_NAME_COL}}{'.deck.json':<{_SUFFIX_COL}}Deck structure (slides + edges)")
157
+ for name, cls in sorted(registered_ir_types().items()):
158
+ click.echo(f" {name:<{_NAME_COL}}{cls.SUFFIX:<{_SUFFIX_COL}}{cls.DESCRIPTION}")
159
+
160
+
161
+ def _print_element_index() -> None:
162
+ """Print the human-readable index of slide-element schemas."""
163
+ from scrolly.slide import element_source_types
164
+
165
+ click.echo("Element schemas (slide elements):\n")
166
+ for key, cls in element_source_types().items():
167
+ click.echo(f" {key:<{_NAME_COL}}{cls.DESCRIPTION}")
@@ -35,7 +35,7 @@ def slides_to_json(
35
35
  ``slides``: map of slide id → {position, title, scroll_range,
36
36
  element_count, snap_position_count}.
37
37
  ``edges``: list of {a: {slide, side}, b: {slide, side}}.
38
- ``groups``: list of {label, color, slide_ids}.
38
+ ``groups``: list of {label, color, label_color, slide_ids}.
39
39
  """
40
40
  del slide_ids # always deck-wide; param exists for signature uniformity
41
41
 
@@ -63,6 +63,7 @@ def slides_to_json(
63
63
  {
64
64
  "label": group.label,
65
65
  "color": group.color,
66
+ "label_color": group.label_color,
66
67
  "slide_ids": list(group.slide_ids),
67
68
  }
68
69
  for group in deck.groups
@@ -82,6 +82,7 @@ class SlideGroup:
82
82
  label: str
83
83
  slide_ids: tuple[str, ...]
84
84
  color: str | None = None
85
+ label_color: str | None = None
85
86
 
86
87
 
87
88
  @dataclass(frozen=True)
@@ -79,8 +79,16 @@ def _parse_slides_and_groups(slides_raw: list, deck_path: Path) -> tuple[tuple[S
79
79
  group_slide_ids.append(slide.id)
80
80
  flat_idx += 1
81
81
 
82
- color = _parse_group_color(item, ctx)
83
- groups.append(SlideGroup(label=label, slide_ids=tuple(group_slide_ids), color=color))
82
+ color = _parse_group_hex_color(item, ctx, "color")
83
+ label_color = _parse_group_hex_color(item, ctx, "label_color")
84
+ groups.append(
85
+ SlideGroup(
86
+ label=label,
87
+ slide_ids=tuple(group_slide_ids),
88
+ color=color,
89
+ label_color=label_color,
90
+ )
91
+ )
84
92
  else:
85
93
  slide = _parse_slide(item, deck_dir, flat_idx, ctx)
86
94
  slides.append(slide)
@@ -92,16 +100,31 @@ def _parse_slides_and_groups(slides_raw: list, deck_path: Path) -> tuple[tuple[S
92
100
  _HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
93
101
 
94
102
 
95
- def _parse_group_color(raw: dict, ctx: str) -> str | None:
96
- """Parse and validate an optional hex color from a group object."""
97
- if "color" not in raw:
103
+ def _parse_group_hex_color(raw: dict, ctx: str, key: str) -> str | None:
104
+ """Parse and validate an optional hex color field from a group object.
105
+
106
+ Args:
107
+ raw: The raw group object.
108
+ ctx: Error-context prefix (e.g. ``slides[2]``).
109
+ key: Which field to read — ``color`` (background) or ``label_color``
110
+ (label override).
111
+
112
+ Returns:
113
+ The validated ``#RGB`` / ``#RRGGBB`` string, or ``None`` if the field
114
+ is absent.
115
+
116
+ Raises:
117
+ DeckParseError: If present but not a string (E004) or not a valid hex
118
+ color (E009).
119
+ """
120
+ if key not in raw:
98
121
  return None
99
- color = raw["color"]
100
- if not isinstance(color, str):
101
- raise DeckParseError(code="E004", message=f"{ctx}: 'color' must be a string, got {type(color).__name__}")
102
- if not _HEX_COLOR_RE.match(color):
103
- raise DeckParseError(code="E009", message=f"{ctx}: 'color' must be #RGB or #RRGGBB, got '{color}'")
104
- return color
122
+ value = raw[key]
123
+ if not isinstance(value, str):
124
+ raise DeckParseError(code="E004", message=f"{ctx}: '{key}' must be a string, got {type(value).__name__}")
125
+ if not _HEX_COLOR_RE.match(value):
126
+ raise DeckParseError(code="E009", message=f"{ctx}: '{key}' must be #RGB or #RRGGBB, got '{value}'")
127
+ return value
105
128
 
106
129
 
107
130
  def _require_list(d: dict, key: str, deck_path: Path) -> list:
@@ -55,6 +55,17 @@ def deck_source_schema() -> dict:
55
55
  "type": "string",
56
56
  "description": "Group label, displayed above the group rectangle in deck view.",
57
57
  },
58
+ "color": {
59
+ "type": "string",
60
+ "description": "Group background fill as #RGB or #RRGGBB. Defaults to a neutral gray.",
61
+ },
62
+ "label_color": {
63
+ "type": "string",
64
+ "description": (
65
+ "Group label color as #RGB or #RRGGBB. Defaults to a "
66
+ "black-or-white auto-contrast pick against the background."
67
+ ),
68
+ },
58
69
  "slides": {
59
70
  "type": "array",
60
71
  "description": "Slides in this group.",
@@ -14,5 +14,5 @@ require `title` and `elements`; per-slide entries in a deck require
14
14
 
15
15
  ## How to fix
16
16
 
17
- Add the missing field. Run `scrolly schema deck` or `scrolly schema
18
- slide` to see the full required shape for each file type.
17
+ Add the missing field. Run `scrolly schema file deck` or `scrolly schema
18
+ file slide` to see the full required schema for each file type.
@@ -17,5 +17,5 @@ path embedded in the error message.
17
17
  ## How to fix
18
18
 
19
19
  Read the embedded Pydantic message — it names the field path and the
20
- constraint that failed. Cross-reference with `scrolly schema slide`
21
- for the expected shape of the slide source.
20
+ constraint that failed. Cross-reference with `scrolly schema file slide`
21
+ for the expected schema of the slide source.
@@ -13,5 +13,6 @@ An element object with no recognised type field — no `markdown:`,
13
13
 
14
14
  ## How to fix
15
15
 
16
- Check the element JSON against `scrolly schema slide`. Each element
17
- must include the field that names its type.
16
+ Run `scrolly schema element --list-types` to see the valid element
17
+ types, then `scrolly schema element <type>` for one's fields. Each
18
+ element must include the field that names its type.