pele-animate 0.11.0 → 0.12.0

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.
package/README.md CHANGED
@@ -82,7 +82,9 @@ flowchart LR
82
82
  ```
83
83
 
84
84
  `form: "square", h: 88` makes a large frameless glyph; leave both out for a small icon in a frame.
85
- `%% pa: 1=P 2=O 3=D` pins nodes to steps (optional). Sequence diagrams animate actors, then messages in order.
85
+ `%% pa: 1=P 2=O 3=D` pins nodes to steps (optional). `X>Y` pins the edge from X to Y, so an edge between nodes that are
86
+ already visible can have its own step: `%% pa: 1=A,B 2=C,B>C 3=C>B`. Pin the nodes first; an edge pinned before its target appears lands early.
87
+ Sequence diagrams animate actors, then messages in order.
86
88
 
87
89
  ## API
88
90
 
@@ -91,7 +93,8 @@ flowchart LR
91
93
  | `watch(container, source?)` | Animate the SVG inside `container` (and every re-render of it); adds the scene panel and step mode (click / Enter); pass `{ controls: false }` as a third argument for click-to-replay instead |
92
94
  | `animate(svg, { hover, source })` | Animate one SVG; you handle re-renders |
93
95
  | `replay(svg)` | Play the intro again |
94
- | `parseOrder(source)` | Read the `%% pa:` pins |
96
+ | `parseOrder(source)` | Read the `%% pa:` pins: `Map(nodeId or 'X>Y' -> level)` |
97
+ | `svg.paSteps` | Set by `animate()`: one array per step, each item `[element, parts that animate in]`. Step mode uses it; read it to check the order |
95
98
  | `registerGlyphs(pack, strokeWidth = 1.1)` (`/glyphs`) | Add glyphs: `{ name: '<inner markup of a 24x24 svg>' }`; colors via `--pa-solid` / `--pa-light` |
96
99
  | `icons` (`/glyphs`) | Pele `icons` resolver |
97
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pele-animate",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Animated, illustrated SVG scenes from plain Mermaid code, as a post-processor for Pele.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/pele-animate.css CHANGED
@@ -58,8 +58,9 @@
58
58
  .pa-scene.pa-stepping { flex-direction: column; align-items: center; gap: .75rem; }
59
59
  .pa-controls { display: flex; align-items: center; gap: .125rem; padding: .25rem; border-radius: 999px; cursor: default;
60
60
  background: var(--pele-surface); color: var(--pele-fg); border: 1px solid var(--pele-border); font: 500 .8rem/1 system-ui, sans-serif; }
61
- .pa-controls button { width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 50%; background: none; color: inherit; font: inherit; font-size: 1rem; cursor: pointer; }
62
- .pa-controls button:hover:not(:disabled) { background: var(--pele-surface-alt); }
63
- .pa-controls button:disabled { opacity: .35; cursor: default; }
61
+ .pa-scene .pa-controls > * { margin: 0; } /* host sites often add sibling margins (e.g. prose `* + *` rules) */
62
+ .pa-controls button { display: flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 50%; background: none; color: inherit; font: inherit; font-size: 1rem; cursor: pointer; }
63
+ .pa-controls button:hover:not([aria-disabled=true]) { background: var(--pele-surface-alt); }
64
+ .pa-controls button[aria-disabled=true] { opacity: .35; cursor: default; }
64
65
  .pa-controls button:focus-visible { outline: 2px solid var(--pa-solid); }
65
- .pa-count { min-width: 4ch; padding: 0 .5rem; text-align: center; font-variant-numeric: tabular-nums; }
66
+ .pa-count { min-width: 4ch; line-height: 2rem; padding: 0 .5rem; text-align: center; font-variant-numeric: tabular-nums; }
package/pele-animate.js CHANGED
@@ -6,7 +6,8 @@ const MAX_INTRO = 3; // a long diagram squeezes its steps so the intro never run
6
6
  const TOKEN = 0.5; // seconds a token takes to cross an edge
7
7
  const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
8
8
 
9
- // `%% pa: 1=A,B 2=C` pins nodes to steps (1-based); returns Map(nodeId -> level). Everything else keeps its BFS level.
9
+ // `%% pa: 1=A,B 2=C 3=A>B` pins nodes to steps (1-based); `X>Y` pins the edge from X to Y. Returns Map(nodeId or 'X>Y' -> level).
10
+ // Everything else keeps its BFS level.
10
11
  export function parseOrder(source = '') {
11
12
  const m = source.match(/^\s*%%\s*pa:\s*(.+)$/m);
12
13
  const levels = new Map();
@@ -103,8 +104,11 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
103
104
  // Start after the source is up, but never so early that the arrow lands before its target appears (pinned steps).
104
105
  edges.forEach((e, i) => {
105
106
  const [s, d] = pairs[i] ?? [];
106
- step.set(e, Math.max(level.get(s) ?? 0, level.get(d) ?? 0));
107
- times.set(e, Math.max((level.get(s) ?? 0) * u + 0.3 * k, (level.get(d) ?? 0) * u - 0.5 * k));
107
+ const pin = level.get(`${s}>${d}`); // edge pinned to a step: starts with that step, once its source is up
108
+ step.set(e, pin ?? Math.max(level.get(s) ?? 0, level.get(d) ?? 0));
109
+ times.set(e, pin !== undefined
110
+ ? Math.max(pin * u, (level.get(s) ?? 0) * u + 0.3 * k)
111
+ : Math.max((level.get(s) ?? 0) * u + 0.3 * k, (level.get(d) ?? 0) * u - 0.5 * k));
108
112
  });
109
113
  }
110
114
 
@@ -169,6 +173,15 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
169
173
  }
170
174
 
171
175
  const STEP_MS = 1200; // autoplay pace in step mode
176
+ // Control icons are SVG, not text glyphs: glyphs like ‹ and ▶ sit at different heights in every font.
177
+ const icon = (d, fill) => `<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" ${fill ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"'}>${d}</svg>`;
178
+ const ICON = {
179
+ prev: icon('<path d="M15 5l-7 7 7 7"/>'),
180
+ next: icon('<path d="M9 5l7 7-7 7"/>'),
181
+ play: icon('<path d="M8 5v14l11-7z"/>', 1),
182
+ pause: icon('<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/>', 1),
183
+ exit: icon('<path d="M6 6l12 12M18 6L6 18"/>'),
184
+ };
172
185
 
173
186
  // Show steps 0..k and hide the rest; `pop` animates the elements of step k.
174
187
  function showStep(svg, k, pop) {
@@ -197,17 +210,18 @@ export function watch(container, source = '', { controls = true } = {}) {
197
210
  bar.setAttribute('role', 'toolbar');
198
211
  bar.setAttribute('aria-label', 'Step through the diagram');
199
212
  bar.title = ''; // an empty title stops the container's tooltip showing through the gaps
200
- bar.innerHTML = '<button type="button" data-a="prev" aria-label="Previous step" title="Previous step (\u2190)">\u2039</button>'
213
+ bar.innerHTML = '<button type="button" data-a="prev" aria-label="Previous step" title="Previous step (\u2190)">' + ICON.prev + '</button>'
201
214
  + '<button type="button" data-a="play"></button>'
202
- + '<button type="button" data-a="next" aria-label="Next step" title="Next step (\u2192)">\u203A</button>'
215
+ + '<button type="button" data-a="next" aria-label="Next step" title="Next step (\u2192)">' + ICON.next + '</button>'
203
216
  + '<span class="pa-count" aria-live="polite" title="Current step"></span>'
204
- + '<button type="button" data-a="exit" aria-label="Exit step mode" title="Exit step mode (Esc)">\u2715\uFE0E</button>';
217
+ + '<button type="button" data-a="exit" aria-label="Exit step mode" title="Exit step mode (Esc)">' + ICON.exit + '</button>';
205
218
  const btn = (a) => bar.querySelector(`[data-a=${a}]`);
206
219
  const last = () => (cur()?.paSteps?.length ?? 1) - 1;
207
220
  const ui = () => {
208
- btn('prev').disabled = k <= 0;
209
- btn('next').disabled = k >= last();
210
- btn('play').textContent = timer ? '\u275A\u275A' : '\u25B6\uFE0E';
221
+ // aria-disabled, not `disabled`: a disabled button passes hover to the container, whose tooltip then shows over it
222
+ btn('prev').setAttribute('aria-disabled', String(k <= 0));
223
+ btn('next').setAttribute('aria-disabled', String(k >= last()));
224
+ btn('play').innerHTML = timer ? ICON.pause : ICON.play;
211
225
  const label = timer ? 'Pause (Space)' : 'Play (Space)';
212
226
  btn('play').setAttribute('aria-label', label);
213
227
  btn('play').title = label;
@@ -251,7 +265,8 @@ export function watch(container, source = '', { controls = true } = {}) {
251
265
  const actions = { prev: () => manual(k - 1), next: () => manual(k + 1), play: toggle, exit };
252
266
  bar.addEventListener('click', (e) => {
253
267
  e.stopPropagation(); // else the container's click would re-enter step mode right after Exit
254
- actions[e.target.closest('button')?.dataset.a]?.();
268
+ const b = e.target.closest('button');
269
+ if (b?.getAttribute('aria-disabled') !== 'true') actions[b?.dataset.a]?.();
255
270
  });
256
271
 
257
272
  const run = () => {