scrolly 0.2.2__tar.gz → 0.2.3__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.
- {scrolly-0.2.2 → scrolly-0.2.3}/PKG-INFO +2 -2
- {scrolly-0.2.2 → scrolly-0.2.3}/README.md +1 -1
- {scrolly-0.2.2 → scrolly-0.2.3}/pyproject.toml +2 -2
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/introspect.py +2 -1
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/model.py +1 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/parser.py +34 -11
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/schema.py +11 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/assets/canvas.css +48 -4
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/assets/canvas.js +44 -7
- scrolly-0.2.3/scrolly/render/color.py +66 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/nav_data.py +15 -4
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/templates/index.html.j2 +12 -2
- {scrolly-0.2.2 → scrolly-0.2.3}/.gitignore +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/LICENSE +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_cli.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_errors.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_assets.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_common.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_dom.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_elements.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_slides.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_snaps.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_snapshot.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/_cli/_introspect/_timeline.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/inference.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/deck/validator.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/_catalog.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/_codes.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/_report.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/_validation_error.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E001.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E002.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E003.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E004.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E005.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E006.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E007.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E008.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E009.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E010.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E011.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E012.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E101.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E102.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E103.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E201.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E202.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E203.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E204.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E205.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E206.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E207.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E299.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E301.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E302.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E303.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E304.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E305.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E306.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E307.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E308.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E401.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E402.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E403.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E501.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E502.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E503.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E504.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E505.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E601.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E602.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E603.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E701.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/E702.md +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/errors/catalog/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/_bundler.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/assets.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/introspect.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/lint.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/loader.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/orchestrator.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/pipeline/writer.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/assembler.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/assets/mermaid-LICENSE +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/assets/mermaid.min.js +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/bundled_assets.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/fan.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/render/zoom_control.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/ir.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/processor.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/registry.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/rendered.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/_shared.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/html.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/iframe.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/image.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/image_sequence.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/markdown.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/element_ir/renderers/mermaid.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/html.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/introspect.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/_framework/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/_framework/animated_values.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/_framework/element.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/_framework/utils.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/ir/slide.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/processor.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/registry.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/renderers/__init__.py +0 -0
- {scrolly-0.2.2 → scrolly-0.2.3}/scrolly/slide/renderers/slide.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrolly
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation.
|
|
5
5
|
Project-URL: Source, https://github.com/bertpl/scrolly
|
|
6
6
|
Project-URL: Changelog, https://github.com/bertpl/scrolly/blob/main/CHANGELOG.md
|
|
@@ -35,7 +35,7 @@ Compile a JSON5 deck into a self-contained, scrollable 2D-canvas HTML presentati
|
|
|
35
35
|
|
|
36
36
|
*Liked by humans; understood by agents.*
|
|
37
37
|
|
|
38
|
-
[](https://github.com/bertpl/scrolly)
|
|
39
39
|
|
|
40
40
|
[](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
|
|
41
41
|
[](https://pypi.org/project/scrolly/)
|
|
@@ -4,7 +4,7 @@ Compile a JSON5 deck into a self-contained, scrollable 2D-canvas HTML presentati
|
|
|
4
4
|
|
|
5
5
|
*Liked by humans; understood by agents.*
|
|
6
6
|
|
|
7
|
-
[](https://github.com/bertpl/scrolly)
|
|
8
8
|
|
|
9
9
|
[](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
|
|
10
10
|
[](https://pypi.org/project/scrolly/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scrolly"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
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.
|
|
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`),
|
|
@@ -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
|
|
@@ -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 =
|
|
83
|
-
|
|
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
|
|
96
|
-
"""Parse and validate an optional hex color from a group object.
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
if not isinstance(
|
|
101
|
-
raise DeckParseError(code="E004", message=f"{ctx}: '
|
|
102
|
-
if not _HEX_COLOR_RE.match(
|
|
103
|
-
raise DeckParseError(code="E009", message=f"{ctx}: '
|
|
104
|
-
return
|
|
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.",
|
|
@@ -93,9 +93,13 @@ body {
|
|
|
93
93
|
initial-value: 0.5;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/* `--view-scale` is the deck→slide interpolated scale applied to `.canvas`.
|
|
97
|
+
* `inherits: true` so the connector overlay (`.canvas-edge` / `#edge-dot`)
|
|
98
|
+
* can divide its stroke widths by it and stay a constant on-screen size
|
|
99
|
+
* regardless of how far deck view zooms out. */
|
|
96
100
|
@property --view-scale {
|
|
97
101
|
syntax: "<number>";
|
|
98
|
-
inherits:
|
|
102
|
+
inherits: true;
|
|
99
103
|
initial-value: 1;
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -859,7 +863,7 @@ body.view-deck .slide-container.selected:hover .subcanvas {
|
|
|
859
863
|
.slide-group-label {
|
|
860
864
|
position: absolute;
|
|
861
865
|
transform: translate(-50%, -50%);
|
|
862
|
-
color: #000000;
|
|
866
|
+
color: var(--slide-group-label-color, #000000);
|
|
863
867
|
font-family: system-ui, -apple-system, sans-serif;
|
|
864
868
|
font-size: 5dvmax;
|
|
865
869
|
font-weight: 600;
|
|
@@ -883,7 +887,21 @@ body.view-deck .slide-container.selected:hover .subcanvas {
|
|
|
883
887
|
display: block;
|
|
884
888
|
z-index: 5;
|
|
885
889
|
opacity: calc(1 - var(--view-zoom, 1));
|
|
886
|
-
--connector-color: #
|
|
890
|
+
--connector-color: #333333;
|
|
891
|
+
/* Connector line + end-dot sizes, in on-screen px. The rules below divide
|
|
892
|
+
* these by --view-scale so they stay visually constant regardless of how far
|
|
893
|
+
* deck view zooms out (which grows with the slide grid). Tune for thickness. */
|
|
894
|
+
--edge-width: 1.25;
|
|
895
|
+
--edge-dot: 4;
|
|
896
|
+
/* Sharp casing: a second, slightly wider set of connectors + dots is drawn
|
|
897
|
+
* in an opaque light tint (--edge-halo-color) directly behind the dark ones
|
|
898
|
+
* (.canvas-edge-halo / .edge-dot-cap-halo, emitted by BezierOverlay), giving
|
|
899
|
+
* a crisp light edge that separates them from dark backdrops without the
|
|
900
|
+
* softness of a blur. --edge-halo-ring is the extra on-screen px per side,
|
|
901
|
+
* divided by --view-scale to stay constant on-screen like the stroke widths
|
|
902
|
+
* above. */
|
|
903
|
+
--edge-halo-ring: 0.75;
|
|
904
|
+
--edge-halo-color: #cccccc;
|
|
887
905
|
}
|
|
888
906
|
|
|
889
907
|
/* ---- Slide-to-slide pan scale dip ------------------------------------- */
|
|
@@ -909,11 +927,37 @@ body.pan-transitioning .canvas {
|
|
|
909
927
|
.canvas-edge {
|
|
910
928
|
fill: none;
|
|
911
929
|
stroke: var(--connector-color);
|
|
912
|
-
|
|
930
|
+
/* Divide by --view-scale so the line keeps a constant on-screen width
|
|
931
|
+
* regardless of grid size: non-scaling-stroke cancels the SVG-internal
|
|
932
|
+
* viewBox scale, the division cancels the .canvas deck-view transform. */
|
|
933
|
+
stroke-width: calc(var(--edge-width) / var(--view-scale, 1));
|
|
913
934
|
vector-effect: non-scaling-stroke;
|
|
914
935
|
stroke-linecap: round;
|
|
915
936
|
}
|
|
916
937
|
|
|
938
|
+
/* White casing drawn behind every connector (--edge-halo-ring px wider per
|
|
939
|
+
* side). BezierOverlay emits all casing paths before any dark path, so the
|
|
940
|
+
* casing never paints over a dark line where two connectors cross. */
|
|
941
|
+
.canvas-edge-halo {
|
|
942
|
+
fill: none;
|
|
943
|
+
stroke: var(--edge-halo-color);
|
|
944
|
+
stroke-width: calc((var(--edge-width) + 2 * var(--edge-halo-ring)) / var(--view-scale, 1));
|
|
945
|
+
vector-effect: non-scaling-stroke;
|
|
946
|
+
stroke-linecap: round;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/* End-of-connector dots: round caps on a zero-length line in the #edge-dot
|
|
950
|
+
* marker. Same constant-on-screen-size treatment as the connector line. */
|
|
951
|
+
.edge-dot-cap {
|
|
952
|
+
stroke-width: calc(var(--edge-dot) / var(--view-scale, 1));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* Matching wider white casing for the dots, via the #edge-dot-halo marker. */
|
|
956
|
+
.edge-dot-cap-halo {
|
|
957
|
+
stroke: var(--edge-halo-color);
|
|
958
|
+
stroke-width: calc((var(--edge-dot) + 2 * var(--edge-halo-ring)) / var(--view-scale, 1));
|
|
959
|
+
}
|
|
960
|
+
|
|
917
961
|
/* ---- Help button -------------------------------------------------------- */
|
|
918
962
|
|
|
919
963
|
/* Styled like the simplified zoom control: same size, background, colour,
|
|
@@ -601,6 +601,10 @@
|
|
|
601
601
|
this._config = snapConfig;
|
|
602
602
|
this._containerFn = containerFn;
|
|
603
603
|
this._enabled = true;
|
|
604
|
+
// The idle auto-snap (settle to nearest snap after IDLE_MS of scroll
|
|
605
|
+
// inactivity) can be turned off independently of `_enabled`, leaving the
|
|
606
|
+
// snap control, dots, and manual up()/down() intact. See setIdleSnap().
|
|
607
|
+
this._idleSnap = true;
|
|
604
608
|
this._timer = null;
|
|
605
609
|
this._anim = null;
|
|
606
610
|
this._animTarget = null;
|
|
@@ -658,11 +662,21 @@
|
|
|
658
662
|
|
|
659
663
|
schedule(slideId) {
|
|
660
664
|
this.cancel();
|
|
661
|
-
if (!this._enabled) return;
|
|
665
|
+
if (!this._enabled || !this._idleSnap) return;
|
|
662
666
|
if (!this._snapsFor(slideId)) return;
|
|
663
667
|
this._timer = setTimeout(() => { this._timer = null; this._animateToNearest(slideId); }, SnapManager.IDLE_MS);
|
|
664
668
|
}
|
|
665
669
|
|
|
670
|
+
// Toggle the idle auto-snap only. Unlike `_setEnabled`, this leaves the
|
|
671
|
+
// snap feature on (control, scrollbar snap dots, and manual up()/down()
|
|
672
|
+
// nav all keep working) — it just stops the post-idle settle from firing.
|
|
673
|
+
// The capture harness sets this off so scripted setScroll positions render
|
|
674
|
+
// exactly instead of being pulled toward a snap mid-capture.
|
|
675
|
+
setIdleSnap(enabled) {
|
|
676
|
+
this._idleSnap = enabled;
|
|
677
|
+
if (!enabled) this.cancel();
|
|
678
|
+
}
|
|
679
|
+
|
|
666
680
|
cancel() {
|
|
667
681
|
if (this._timer !== null) { clearTimeout(this._timer); this._timer = null; }
|
|
668
682
|
if (this._anim !== null) { cancelAnimationFrame(this._anim); this._anim = null; }
|
|
@@ -689,8 +703,13 @@
|
|
|
689
703
|
|
|
690
704
|
this._animTarget = target;
|
|
691
705
|
const start = current;
|
|
692
|
-
|
|
706
|
+
// Anchor t0 to the first frame's timestamp, not performance.now():
|
|
707
|
+
// a rAF timestamp can predate a now() taken just before scheduling,
|
|
708
|
+
// making elapsed (and thus t) negative on frame 1, which drives
|
|
709
|
+
// easeOutQuad below 0 and kicks the position backward for one frame.
|
|
710
|
+
let t0 = null;
|
|
693
711
|
const step = (now) => {
|
|
712
|
+
if (t0 === null) t0 = now;
|
|
694
713
|
const elapsed = now - t0;
|
|
695
714
|
const t = Math.min(1, elapsed / SnapManager.DURATION_MS);
|
|
696
715
|
const ease = SnapManager.easeOutQuad(t);
|
|
@@ -1014,6 +1033,11 @@
|
|
|
1014
1033
|
const label = document.createElement("span");
|
|
1015
1034
|
label.className = "slide-group-label";
|
|
1016
1035
|
label.textContent = group.label;
|
|
1036
|
+
// `label_color` is resolved server-side (override or auto-contrast
|
|
1037
|
+
// pick); fall back to the CSS default if absent.
|
|
1038
|
+
if (group.label_color) {
|
|
1039
|
+
label.style.setProperty("--slide-group-label-color", group.label_color);
|
|
1040
|
+
}
|
|
1017
1041
|
this._elements.push({ group, svg, path, label });
|
|
1018
1042
|
}
|
|
1019
1043
|
const firstSlideContainer = this._canvas.querySelector(".slide-container");
|
|
@@ -1293,15 +1317,21 @@
|
|
|
1293
1317
|
this._svg.style.height = data.height;
|
|
1294
1318
|
|
|
1295
1319
|
const ns = "http://www.w3.org/2000/svg";
|
|
1296
|
-
|
|
1320
|
+
// Two passes so every white casing sits behind every dark connector:
|
|
1321
|
+
// all halos first (wider, opaque white), then all dark lines on top.
|
|
1322
|
+
// Interleaving would let a later edge's casing paint over an earlier
|
|
1323
|
+
// edge's dark line at a crossing.
|
|
1324
|
+
const addPath = (d, cls, marker) => {
|
|
1297
1325
|
const path = document.createElementNS(ns, "path");
|
|
1298
|
-
path.setAttribute("class",
|
|
1299
|
-
path.setAttribute("marker-start",
|
|
1300
|
-
path.setAttribute("marker-end",
|
|
1326
|
+
path.setAttribute("class", cls);
|
|
1327
|
+
path.setAttribute("marker-start", `url(#${marker})`);
|
|
1328
|
+
path.setAttribute("marker-end", `url(#${marker})`);
|
|
1301
1329
|
path.setAttribute("d", d);
|
|
1302
1330
|
this._svg.appendChild(path);
|
|
1303
1331
|
this._paths.push(path);
|
|
1304
|
-
}
|
|
1332
|
+
};
|
|
1333
|
+
for (const d of data.paths) addPath(d, "canvas-edge-halo", "edge-dot-halo");
|
|
1334
|
+
for (const d of data.paths) addPath(d, "canvas-edge", "edge-dot");
|
|
1305
1335
|
}
|
|
1306
1336
|
}
|
|
1307
1337
|
|
|
@@ -2174,6 +2204,13 @@
|
|
|
2174
2204
|
scrollManager,
|
|
2175
2205
|
isAnimating: () => document.body.classList.contains("view-transitioning"),
|
|
2176
2206
|
});
|
|
2207
|
+
// Turn off the idle auto-snap under automation: scripted setScroll
|
|
2208
|
+
// positions are exact, but the idle-snap timer would otherwise fire between
|
|
2209
|
+
// the capture's (slow) screenshots and animate the scroll toward the
|
|
2210
|
+
// nearest snap — a visible wobble + catch-up jump. The snap feature stays
|
|
2211
|
+
// enabled, so the scrollbar's snap dots (a key debug-mode cue) and the
|
|
2212
|
+
// scripted Shift+Arrow nav both keep working.
|
|
2213
|
+
snapManager.setIdleSnap(false);
|
|
2177
2214
|
console.log("scrolly automation hook active");
|
|
2178
2215
|
}
|
|
2179
2216
|
})(typeof module !== "undefined" ? module.exports : {});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Color utilities for rendering: luminance and legible-text-color picks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
|
|
11
|
+
"""Parse a ``#RGB`` or ``#RRGGBB`` string into 0-255 ``(r, g, b)`` bytes.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
hex_color: A ``#RGB`` or ``#RRGGBB`` hex color string.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The ``(r, g, b)`` byte triple.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If ``hex_color`` is not a ``#RGB`` or ``#RRGGBB`` color.
|
|
21
|
+
"""
|
|
22
|
+
if not _HEX_COLOR_RE.match(hex_color):
|
|
23
|
+
raise ValueError(f"not a #RGB or #RRGGBB color: {hex_color!r}")
|
|
24
|
+
digits = hex_color[1:]
|
|
25
|
+
if len(digits) == 3:
|
|
26
|
+
digits = "".join(c * 2 for c in digits)
|
|
27
|
+
return int(digits[0:2], 16), int(digits[2:4], 16), int(digits[4:6], 16)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _srgb_to_linear(channel: int) -> float:
|
|
31
|
+
"""Linearize one sRGB 0-255 channel to its 0-1 linear-light value."""
|
|
32
|
+
c = channel / 255
|
|
33
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def relative_luminance(hex_color: str) -> float:
|
|
37
|
+
"""Compute the WCAG relative luminance of a hex color.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
hex_color: A ``#RGB`` or ``#RRGGBB`` hex color string.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Relative luminance in ``[0, 1]`` (sRGB channels linearized and weighted
|
|
44
|
+
per WCAG 2.x).
|
|
45
|
+
"""
|
|
46
|
+
r, g, b = _hex_to_rgb(hex_color)
|
|
47
|
+
return 0.2126 * _srgb_to_linear(r) + 0.7152 * _srgb_to_linear(g) + 0.0722 * _srgb_to_linear(b)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def legible_text_color(background: str) -> str:
|
|
51
|
+
"""Pick black or white for the most legible text on a background color.
|
|
52
|
+
|
|
53
|
+
Chooses whichever of black or white yields the higher WCAG contrast ratio
|
|
54
|
+
against ``background``. The crossover sits at luminance ≈ 0.179, so light
|
|
55
|
+
and mid-tone backgrounds get black and only genuinely dark ones get white.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
background: A ``#RGB`` or ``#RRGGBB`` background color string.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
``"#000000"`` or ``"#ffffff"``.
|
|
62
|
+
"""
|
|
63
|
+
luminance = relative_luminance(background)
|
|
64
|
+
contrast_black = (luminance + 0.05) / 0.05
|
|
65
|
+
contrast_white = 1.05 / (luminance + 0.05)
|
|
66
|
+
return "#000000" if contrast_black >= contrast_white else "#ffffff"
|
|
@@ -23,9 +23,15 @@ from __future__ import annotations
|
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
from scrolly.deck import Deck
|
|
26
|
+
from scrolly.render.color import legible_text_color
|
|
26
27
|
from scrolly.render.fan import FAN_SPACING_FACTOR, compute_fan_offsets
|
|
27
28
|
from scrolly.slide import SlideHTML
|
|
28
29
|
|
|
30
|
+
# Default group-background fill, used when a group sets no explicit `color`.
|
|
31
|
+
# Mirrors `.slide-group-bg { fill }` in canvas.css so the label auto-contrast
|
|
32
|
+
# pick matches the rendered background.
|
|
33
|
+
DEFAULT_GROUP_BACKGROUND = "#dcdcdc"
|
|
34
|
+
|
|
29
35
|
|
|
30
36
|
def build_nav_data(deck: Deck, chunks: dict[str, SlideHTML]) -> dict[str, Any]:
|
|
31
37
|
"""Return a JSON-serialisable representation of the deck for the client.
|
|
@@ -93,10 +99,15 @@ def build_nav_data(deck: Deck, chunks: dict[str, SlideHTML]) -> dict[str, Any]:
|
|
|
93
99
|
|
|
94
100
|
edges_data = _build_edges(deck, fan)
|
|
95
101
|
|
|
96
|
-
groups_data = [
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
groups_data = []
|
|
103
|
+
for g in deck.groups:
|
|
104
|
+
entry: dict[str, Any] = {"label": g.label, "slide_ids": list(g.slide_ids)}
|
|
105
|
+
if g.color:
|
|
106
|
+
entry["color"] = g.color
|
|
107
|
+
# Resolve the label color here so the client just applies it: an explicit
|
|
108
|
+
# `label_color` override wins, else auto-contrast against the background.
|
|
109
|
+
entry["label_color"] = g.label_color or legible_text_color(g.color or DEFAULT_GROUP_BACKGROUND)
|
|
110
|
+
groups_data.append(entry)
|
|
100
111
|
|
|
101
112
|
return {
|
|
102
113
|
"initial_slide": deck.slides[0].id if deck.slides else None,
|
|
@@ -23,9 +23,19 @@
|
|
|
23
23
|
markerWidth="0.001" markerHeight="0.001"
|
|
24
24
|
refX="0" refY="0"
|
|
25
25
|
overflow="visible">
|
|
26
|
-
<line
|
|
26
|
+
<line class="edge-dot-cap"
|
|
27
|
+
x1="0" y1="0" x2="0" y2="0"
|
|
27
28
|
stroke="var(--connector-color)"
|
|
28
|
-
stroke-
|
|
29
|
+
stroke-linecap="round"
|
|
30
|
+
vector-effect="non-scaling-stroke"/>
|
|
31
|
+
</marker>
|
|
32
|
+
<marker id="edge-dot-halo"
|
|
33
|
+
markerUnits="userSpaceOnUse"
|
|
34
|
+
markerWidth="0.001" markerHeight="0.001"
|
|
35
|
+
refX="0" refY="0"
|
|
36
|
+
overflow="visible">
|
|
37
|
+
<line class="edge-dot-cap-halo"
|
|
38
|
+
x1="0" y1="0" x2="0" y2="0"
|
|
29
39
|
stroke-linecap="round"
|
|
30
40
|
vector-effect="non-scaling-stroke"/>
|
|
31
41
|
</marker>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|