footprint-storydeck 0.3.0 → 0.5.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/CHANGELOG.md +56 -0
- package/PostView.jsx +1 -1
- package/README.md +160 -3
- package/SlideDeck.jsx +20 -1
- package/focus.css +30 -4
- package/focus.d.ts +47 -5
- package/focus.js +155 -23
- package/focusHtml.d.ts +14 -3
- package/focusHtml.js +138 -20
- package/index.d.ts +13 -3
- package/index.js +2 -1
- package/narrationHtml.d.ts +4 -1
- package/narrationHtml.js +6 -3
- package/package.json +88 -13
- package/plugin/skills/storydeck/SKILL.md +35 -6
- package/pptx.d.ts +75 -0
- package/pptx.js +313 -0
- package/pptxCheck.js +167 -0
- package/pptxParts.js +262 -0
- package/tour.d.ts +71 -0
- package/tour.js +206 -0
- package/video.js +8 -0
- package/xml.js +123 -0
- package/zip.js +155 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — PowerPoint, the exploded view, and a tour for decks nobody presents
|
|
4
|
+
|
|
5
|
+
- **`footprint-storydeck/pptx`: the deck as a PowerPoint file.** `exportPptx` makes a slide per click: its settled
|
|
6
|
+
picture full-bleed, its notes as speaker notes (a paragraph per line), its headline or label as an off-page title
|
|
7
|
+
(the outline and screen readers read it, the audience does not see it), and a fade. With clips, each slide plays
|
|
8
|
+
its clip and moves on after it (plus `gap`): a narrated deck that runs by itself. It drives the browser through
|
|
9
|
+
the video's port (`deckStageDriver`). The export is a footprintjs flowchart, so every run leaves a record
|
|
10
|
+
(`narrative`), and each check is a `decide()` with its evidence. A clip that is missing, empty, not the sound its
|
|
11
|
+
name says, over `maxClip` or never voiced, a click without a 16:9 PNG, or a package that does not read back is
|
|
12
|
+
refused before a browser opens or a file is written, naming the fix. Media are shared by their bytes, never by
|
|
13
|
+
name. The ZIP, the PresentationML parts and the read-back are written by hand and exported (`zip`, `unzip`,
|
|
14
|
+
`pptxParts`, `checkPptx`). Opened in PowerPoint 16 with no repair prompt; every part validates against the
|
|
15
|
+
ISO/IEC 29500 schemas. It is pictures, notes and sound, not editable native shapes. footprintjs is an optional
|
|
16
|
+
peer dependency: `npm i footprintjs` beside StoryDeck to export; nothing else loads it.
|
|
17
|
+
- **`apart`, the exploded view.** A focus mover: named groups fly apart along the vectors you give
|
|
18
|
+
(`data-focus-apart="lid: 0 -120 | core: 160 0 | base: 0 120 1.1"`), one spring each, `data-focus-stagger` ms apart
|
|
19
|
+
(35), out in the order written and back in reverse on a later click without it; closing groups settle for one
|
|
20
|
+
more click. A click reschedules only the groups not yet at its target, in order, never sooner than a start they
|
|
21
|
+
were already waiting for, so the earlier groups lead. `focusRuntime` plays the springs as Web Animations, each deck
|
|
22
|
+
its own, so at rest a piece is exactly as drawn and the video renderer steps them; `SlideDeck` now installs it.
|
|
23
|
+
Two movers on one click are refused, naming the fix; a group's numbers are strict and its scale must be above 0
|
|
24
|
+
(for `place` too). Every `focusStep` look now carries `apart` (`null` without it), and every piece `apart`.
|
|
25
|
+
- **`footprint-storydeck/tour`: a tour for decks nobody presents.** `idleTour` plays a deck's stops through its own
|
|
26
|
+
`goTo` after 1.4 s with nobody touching it. A key, tap, click or real move hands the deck over until 45 s of quiet;
|
|
27
|
+
Escape or the "Stop tour" control (its name is its visible words; hidden while no tour can move in view) stops
|
|
28
|
+
every tour for the page view (WCAG 2.2.2). It waits while out of sight or while listen mode plays the deck
|
|
29
|
+
(`data-listening`, now set by `listenRuntime`), and never runs under reduced motion. Declared as
|
|
30
|
+
`<deck-stage data-tour>`, `SlideDeck`'s `tour`, or post.json's `tour`; `tourPlan` and `tourText` give the plan as
|
|
31
|
+
data and in one line.
|
|
32
|
+
- **Video:** the driver finishes click n−1's motion before stepping on, so a strip renders the same frames as the
|
|
33
|
+
full video. `focusRuntime`'s install flag is its version, so an older inlined runtime no longer blocks it.
|
|
34
|
+
|
|
35
|
+
## 0.4.0 — a path: the subject piece after piece along a route
|
|
36
|
+
|
|
37
|
+
- **A fourth kind of focus strategy, the order — how the subject comes on — and its first strategy, `path`.**
|
|
38
|
+
A click lights its subject piece after piece along a route, as a request travels it (node, wire, node): each lit
|
|
39
|
+
piece on it lights at its turn, `step` seconds apart (0.35), and glows as it is reached — on every forward click,
|
|
40
|
+
so the request travels again; `route` names the order (default, and for an empty `route`: the click's `on`, or its
|
|
41
|
+
`in`), a name shared by several pieces is one turn, a name with nothing lit takes none (a greyed piece never waits
|
|
42
|
+
or glows grey), and a lit piece off the route lights at once. A click that asks for a path with no order at all
|
|
43
|
+
(`on: ['*']`) is refused, naming the fix: write the pieces in the order they light. A `line` names the route, shown
|
|
44
|
+
as the last turn starts (`lineAt` places it; default the area's bottom left; a blank one is none). Borrowed from the isometric figures where a
|
|
45
|
+
request lights its path.
|
|
46
|
+
- Everything on a path keeps its turn. A piece lit from grey plays one animation, `sdLightPulse`, whose filter lists
|
|
47
|
+
the same functions at every keyframe, so it glides into colour and the glow (a light and a pulse on one filter
|
|
48
|
+
would flash); a ring (`hot`) comes .3 s after its piece's turn; a dashed twin stays until its solid twin's turn,
|
|
49
|
+
then steps aside, so the wire is never missing; and under a new blur the whole path, line included, waits .4 s for
|
|
50
|
+
it, as what a click adds does. One variable carries a piece's moment, `--sd-turn`.
|
|
51
|
+
- `focusStep` gives each piece its `path` (its turn's delay, or null) and `twinTurn` (stepping aside for a lit twin
|
|
52
|
+
on the route: that twin's turn, or null), and the look its `path` ({turns, step, done, line, lineAt}; `line` '' and
|
|
53
|
+
`lineAt` null when none is given). What an order's `route()` gives — a deck's own too — is checked (names, a
|
|
54
|
+
`step` of 0–5 s, a text `line`, a `lineAt` of 'x y' or null) and refused with the strategy's name and the fix,
|
|
55
|
+
and is only read, never written to (a frozen result is fine). `focusOverlay` writes the line; `focus.css` animates
|
|
56
|
+
the path only on a forward click, never under reduced motion (`--sd-glow` colours the pulse). A click without a
|
|
57
|
+
path is drawn byte for byte as 0.3.0 drew it.
|
|
58
|
+
|
|
3
59
|
## 0.3.0 — `footprint-storydeck`; the narration rules move into footprint-narration
|
|
4
60
|
|
|
5
61
|
- **The package is renamed `footprint-storydeck`** — the footprintjs family's name, as footprint-storyreel and
|
package/PostView.jsx
CHANGED
|
@@ -188,7 +188,7 @@ export default function PostView({ post }) {
|
|
|
188
188
|
) : null}
|
|
189
189
|
<span className="watch-lens-hint" title="Press Esc to return to Read" aria-hidden="true">Esc</span>
|
|
190
190
|
</div>
|
|
191
|
-
<SlideDeck steps={post.deckSteps} />
|
|
191
|
+
<SlideDeck steps={post.deckSteps} tour={post.tour} />
|
|
192
192
|
</>
|
|
193
193
|
) : null}
|
|
194
194
|
</div>
|
package/README.md
CHANGED
|
@@ -73,10 +73,55 @@ decision every click makes again, so it is data on the click, and the library do
|
|
|
73
73
|
| mover | `zoom` | the view closes in on the subject (`share`, `most`) |
|
|
74
74
|
| mover | `left` · `right` · `up` · `down` | the context steps aside that way, smaller (`scale`), and the subject grows into the room it leaves (`grow`, `fill`, `gap`, `align`; `move: 'aside'` keeps the subject where it is) |
|
|
75
75
|
| mover | `place` | named groups moved by the amounts you give (`groups: 'page floor: -90 -200 0.5 \| tl: 0 -230 1.1 @ 960 968'`): a layout you art-direct, still animated from wherever the last click left it |
|
|
76
|
+
| mover | `apart` | the exploded view: named groups fly apart along the vectors you give (`apart: 'lid: 0 -120 \| core: 160 0 \| base: 0 120 1.1'`), one spring each, `stagger` ms apart (35), and come back together on a later click without it — the last one out is the first one back |
|
|
76
77
|
| overlay | `blur` | everything outside the subject's frame is blurred; a frame (and a `label`) on it, or on the `rect`s you give |
|
|
78
|
+
| order | `path` | the subject comes on piece after piece along a route, as a request travels it — node, wire, node: each lit piece on it lights at its turn (`step` seconds apart, 0.35) and glows as it is reached; `route` names the order (default — and for an empty `route` — the click's `on`, or its `in`); a greyed piece on it takes no turn, and a route with no order at all (`on: ['*']`) is refused; a `line` ("agent → MCP → gateway") names the route, shown as the last turn starts. A deck's own `filter` on a path piece is not carried through the glow: it steps where the glow starts and ends |
|
|
77
79
|
|
|
78
80
|
`'left blur'` reads: the context steps aside to the left, greyed, under a blur, and the subject grows on
|
|
79
|
-
the right, framed.
|
|
81
|
+
the right, framed. `'path grey'` lights a route in turn and greys the rest. Bring your own strategy of any kind
|
|
82
|
+
(`strategies: { name: { kind, … } }`); what your own order's `route()` gives is checked like the built-in's — piece
|
|
83
|
+
names, a `step` of 0–5 s, a text `line`, a `lineAt` of `'x y'` — and refused with the fix when it is not.
|
|
84
|
+
|
|
85
|
+
**A path, on a diagram of an app.** An architecture slide is a route more often than a picture: who calls whom.
|
|
86
|
+
Write the click's `on` in the order a request travels it, wires included, and the path lights it that way:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<step data-on="agent link-mcp mcp link-gw gw" data-focus="path" data-focus-line="agent → MCP → gateway"></step>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Everything on a path keeps its turn: a piece lit from grey glides into colour and the glow in one animation (two
|
|
93
|
+
on one `filter` would flash), its ring (`hot`) comes .3 s after its turn, and a dashed twin stays until its solid
|
|
94
|
+
twin's turn and only then steps aside, so the wire is never missing. Under a new blur the whole path, line included,
|
|
95
|
+
waits .4 s for it, as what a click adds does.
|
|
96
|
+
|
|
97
|
+
**An exploded view, on a figure of parts.** A figure built of parts — an engine, a stack, a page and the layers
|
|
98
|
+
under it — explains itself best when the parts come apart: the audience sees what it is made of, then sees it whole
|
|
99
|
+
again. Name the parts as groups and the way each one leaves:
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<step data-focus="apart" data-focus-apart="lid: 0 -120 | core: 160 0 | base: 0 120 1.1"></step>
|
|
103
|
+
<step data-on="core"></step> <!-- no apart: the parts come back together, the core lit -->
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each group has one spring, its value 0 together and 1 apart, a little under critical damping, so a part settles with
|
|
107
|
+
a hint of weight instead of stopping dead. The groups set off `stagger` ms apart (`data-focus-stagger`, 35) — out in
|
|
108
|
+
the order written, back in reverse — so the last one out is the first one back. A click that turns the parts round
|
|
109
|
+
mid-way reschedules only the groups not yet at its target (those turning round, and those still waiting to set off),
|
|
110
|
+
in order, each a stagger after the one before and never sooner than a start it was already waiting for — so the
|
|
111
|
+
order is kept: among them the earlier groups always lead, and a click that keeps a group's target keeps its time.
|
|
112
|
+
The order is what the library keeps; where the parts go is yours — vectors that cross make paths that cross, so give
|
|
113
|
+
each part its own way out. The click after a close keeps the closing groups on their springs one click more, so a
|
|
114
|
+
quick click lets them settle from wherever they are. A line or connector that carries a group's name
|
|
115
|
+
(`data-k="wire core"`) moves with that group, keeping its place against it. `apart` is a mover, so it goes with a look (the rest
|
|
116
|
+
greyed), an overlay and an order, and a click that also asks for `place` is refused, naming the fix; across clicks
|
|
117
|
+
the two compose: the parts fly out from the layout `place` left, and back into it.
|
|
118
|
+
|
|
119
|
+
The springs run in `focusRuntime` (the Watch lens, `SlideDeck`, installs it; a built page inlines it), each deck on
|
|
120
|
+
the page its own, played as Web Animations of the same `translate`/`scale` every mover uses (so a piece's own
|
|
121
|
+
transform, an SVG group's `transform` attribute included, is never overwritten), which means the video renderer
|
|
122
|
+
steps them like any animation. Nothing of a spring is written on the piece: at rest the animation is over and the
|
|
123
|
+
piece is exactly as drawn — closed, the figure is its static art. A jump, a step back, print and reduced motion land
|
|
124
|
+
at once, as every focus does.
|
|
80
125
|
|
|
81
126
|
The plan is plain data (each piece's state, what changed since the last click, how far it moved), so a
|
|
82
127
|
renderer only draws it. `footprint-storydeck/focus-html` writes it into slide HTML at build time and
|
|
@@ -113,6 +158,56 @@ a piece (a dashed wire under a solid one) step aside while its twin is lit. A pi
|
|
|
113
158
|
from how it looked and where it stood; a blur that moves to another subject stays, and only its frame is
|
|
114
159
|
new; overlapping holes stay clear, and nothing outside the `area` is blurred.
|
|
115
160
|
|
|
161
|
+
## Tour: a deck that shows its point while nobody is using it
|
|
162
|
+
|
|
163
|
+
A deck left on a screen with nobody presenting it — at a booth, on a post's Watch lens, as a page's hero figure — is
|
|
164
|
+
mostly looked at by people who have not touched it yet. If it sits on its first slide, they never see what it is
|
|
165
|
+
for. So, left alone for a moment (1.4 s), it plays a short loop of **stops**, the clicks that make its point, and the
|
|
166
|
+
moment someone reaches for it, it hands itself over:
|
|
167
|
+
|
|
168
|
+
- it drives the **same targets a person would** — the deck's own `goTo`, so a stop one click on animates as that click
|
|
169
|
+
does (focus, `apart`'s springs and all) and a jump lands, as a person's jump does; there is no second animation to
|
|
170
|
+
keep in step;
|
|
171
|
+
- **accessibility first**: the moment someone uses it — a key, a tap, a click, a focus, a real move of the pointer
|
|
172
|
+
over it (looked at every 250 ms, so a resting mouse or the deck moving under it is no move, and a finger lifting off
|
|
173
|
+
is not someone leaving), or a move it did not make (the presenter window) — it hands itself over, and it does not
|
|
174
|
+
take the deck back soon: only after **45 s of quiet** (`resumeMs`), or once the deck has left sight and come back.
|
|
175
|
+
It then goes on from the stop they left it on, when that is a stop;
|
|
176
|
+
- **Escape**, or the small **"Stop tour"** control it shows while a tour can move in view (or the page's own element
|
|
177
|
+
marked `data-tour-stop`, which it uses instead), stops every tour for the rest of the page view (WCAG 2.2.2). Escape
|
|
178
|
+
is one key with three meanings on a StoryDeck page, each by what is running: while listen mode plays, it stops listen
|
|
179
|
+
mode, and only that; in `PostView`'s Watch lens it returns to Read; and wherever a tour runs it stops every tour on
|
|
180
|
+
the page — so in a touring Watch lens one Escape does both;
|
|
181
|
+
- it **waits while out of sight** — scrolled away (IntersectionObserver, reading the newest entry) or the page
|
|
182
|
+
hidden — and while listen mode plays the deck (the deck carries `data-listening`); under **reduced motion** there
|
|
183
|
+
is no tour.
|
|
184
|
+
|
|
185
|
+
The plan is plain data — the stops, each one's hold, the waits — that a person or an LLM can read, and that the
|
|
186
|
+
deck's notes can say in one line (`tourText`):
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { idleTour, tourPlan, tourText } from 'footprint-storydeck/tour';
|
|
190
|
+
|
|
191
|
+
idleTour(document.querySelector('deck-stage'), { stops: '3 4 5 9:6s', hold: '3s' }); // clicks as the deck counts them
|
|
192
|
+
tourText({ stops: '3 4 5 9:6s' });
|
|
193
|
+
// "Left alone for 1.4 s, it tours itself: slide 3 for 3 s, then slide 4 for 3 s, then slide 5 for 3 s, then slide 9
|
|
194
|
+
// for 6 s, and round again. A key, tap, click or move hands it over until 45 s of quiet; Escape or "Stop tour" stops it."
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Declared on the deck instead, for a page built as HTML: `<deck-stage data-tour="3 4 5 9:6s" data-tour-hold="3s">` (and
|
|
198
|
+
`data-tour-idle`, `data-tour-resume`), with `(${idleTour})()` once in the page; a deck whose `data-tour` it cannot
|
|
199
|
+
read is left out with a console warning naming the fix, and the others still tour. A stop is held 300 ms or more. In
|
|
200
|
+
React, the Watch lens takes the same spec — `<SlideDeck steps={…} tour="3 4 5 9:6s" />` — and `PostView` passes the
|
|
201
|
+
post's own `tour` (post.json), so a post opts in where its structure is written. A figure that is not a deck tours
|
|
202
|
+
through its own function: stops are its names, `go` is what a person's click calls
|
|
203
|
+
(`idleTour(figure, { stops: ['lid', 'core'], go: (part) => show(part) })`). The tour's state rides on the element as `data-tour-state` (`waiting`,
|
|
204
|
+
`touring`, `paused`, `hidden`, `still`, `done`), so a page can say "touring — touch to explore"; the control is
|
|
205
|
+
`.sd-tour-stop`, styled as weakly as can be, so a page restyles it.
|
|
206
|
+
|
|
207
|
+
It lives on the Watch deck, not in the Scroll or Read lenses: their figure follows the reader's scroll position, so a
|
|
208
|
+
tour there would move the page under the reader's eyes; a deck is a place a person steps through, and the tour steps
|
|
209
|
+
through it the same way.
|
|
210
|
+
|
|
116
211
|
## Narration: the deck tells itself, in your voice
|
|
117
212
|
|
|
118
213
|
A talk lives on after the room: someone opens the deck alone, or it becomes a video. The words were in the
|
|
@@ -184,6 +279,66 @@ await renderVideo({
|
|
|
184
279
|
});
|
|
185
280
|
```
|
|
186
281
|
|
|
282
|
+
## PowerPoint: the deck as a .pptx
|
|
283
|
+
|
|
284
|
+
Conferences often ask for the talk as a PowerPoint file. `footprint-storydeck/pptx` (Node, no ffmpeg) exports one
|
|
285
|
+
through the same browser port as the video, so a deck that can be filmed can be exported:
|
|
286
|
+
|
|
287
|
+
- **one slide per click** — the click's settled picture (made forward, its entrance played to the end, as the
|
|
288
|
+
video's held frame) full-bleed on a 16:9 slide; its written notes as the speaker notes, a paragraph per note;
|
|
289
|
+
its headline (or label) as the slide's title, in the title placeholder just above the page, so the outline, the
|
|
290
|
+
slide list and a screen reader read it and the audience does not see it; a fade between slides;
|
|
291
|
+
- **a narrated deck that runs by itself** — with clips (as the video takes them, or found in the voice cache), each
|
|
292
|
+
slide carries its clip, which plays as the slide starts, and moves on after the clip plus `gap` (a click without
|
|
293
|
+
a clip after `noclip` plus `gap`, the video's timing). Without clips no slide moves on by itself: the speaker clicks.
|
|
294
|
+
|
|
295
|
+
**What it is, honestly:** pictures plus notes plus sound. A slide's words and shapes are in its picture, not
|
|
296
|
+
native PowerPoint text or shapes, so they cannot be edited there. Turning a deck's HTML into native shapes is a
|
|
297
|
+
possible later step.
|
|
298
|
+
|
|
299
|
+
**Why a flowchart.** The export is a [footprintjs](https://github.com/footprintjs/footPrint) flowchart —
|
|
300
|
+
PlanClicks → GatherClips → CheckClips → TimeTheDeck → ShootPictures → CheckPictures → WriteParts → Zip →
|
|
301
|
+
CheckPackage → Verdict → Deliver — so every run leaves a record that explains it, to a person or an LLM
|
|
302
|
+
(`done.narrative`). Each check is a `decide()` that records its evidence: a clip whose file is gone, one PowerPoint
|
|
303
|
+
cannot play (not a .wav, .m4a or .mp3; an empty file; or bytes that do not begin as their kind of file — RIFF…WAVE,
|
|
304
|
+
ID3 or an MP3 frame, ftyp), one over `maxClip` (300 s), a click that says words with no clip;
|
|
305
|
+
a click without a 16:9 PNG; a package that does not read back as planned. A refusal names each click and its fix,
|
|
306
|
+
carries the record (`error.narrative`), and writes nothing to `out`. The clips are checked before a browser opens.
|
|
307
|
+
footprintjs is an optional peer dependency: StoryDeck does not install it (a browser-only install stays small), so
|
|
308
|
+
install it beside StoryDeck to export — `npm i footprintjs` — and an export without it is refused, saying so. It is
|
|
309
|
+
loaded only when an export runs; the deck's browser runtime never loads it.
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import { chromium } from 'playwright-core';
|
|
313
|
+
import { deckStageDriver } from 'footprint-storydeck/video';
|
|
314
|
+
import { exportPptx } from 'footprint-storydeck/pptx'; // and `npm i footprintjs`: an optional peer, needed to export
|
|
315
|
+
|
|
316
|
+
const done = await exportPptx({
|
|
317
|
+
url: 'file:///…/deck.html', steps, out: 'out/my-talk.pptx', // the steps the video takes; or `out` a folder → <name>.pptx
|
|
318
|
+
driver: deckStageDriver({ chromium }),
|
|
319
|
+
});
|
|
320
|
+
// done: { file, slides, voiced, runtime (s the deck runs by itself, or null), bytes, clicks, narrative }
|
|
321
|
+
// [Condition]: No rules matched, fell back to default "Every clip is there, playable and within maxClip": TimeTheDeck.
|
|
322
|
+
// …or, refused: exportPptx: a clip is not ready: click 12: its clip lasts 412 s, over maxClip (300 s) — split its notes across clicks, or raise maxClip
|
|
323
|
+
|
|
324
|
+
// From the deck's clicks and the voice cache instead of steps (give `voice` the `spell`/`say` the clips were voiced with):
|
|
325
|
+
await exportPptx({ url, deck: clicks.map(c => ({ label: c.label, notes: c.notes })), voice: { cache: 'voice/cache', profile }, out: 'out', driver });
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The file is written by hand, with no dependency: a small ZIP writer (`zlib`'s raw deflate and a CRC-32) and a
|
|
329
|
+
minimal PresentationML part set (ECMA-376) — content types, relationships, the presentation, one master and one
|
|
330
|
+
title-only layout, a theme for each master, the slides, a notes master and a notes page per slide, the media (a
|
|
331
|
+
sound said on two clicks is stored once, matched by its bytes) and the document properties. The pieces are
|
|
332
|
+
exported for pictures made elsewhere: `pptxParts({ slides })` gives the parts, `zip(parts)` the file, and
|
|
333
|
+
`checkPptx(bytes)` reads one back — the ZIP against its CRC-32s, every part well-formed and typed, every
|
|
334
|
+
relationship's target there, every `r:id` known, slide, master and layout ids in PowerPoint's ranges, shape ids
|
|
335
|
+
unique on each slide — and lists each slide's title, notes, pictures, sound and time. Each export runs that check
|
|
336
|
+
before it writes to `out`. The tests also run Info-ZIP's `unzip -t`; LibreOffice, where it is installed, opens
|
|
337
|
+
the file and prints it to PDF; and with `STORYDECK_OOXML_SCHEMAS` pointing at the ISO/IEC 29500 schemas, every part
|
|
338
|
+
is validated against them. Limits: a clip is embedded as it is (a .wav as voiced: about 3 MB a minute at 24 kHz
|
|
339
|
+
mono — convert to .m4a to shrink a long talk); no ZIP64, so a file stays under 4 GB; the slides advance by
|
|
340
|
+
themselves when the show's "use timings" is on (PowerPoint's default).
|
|
341
|
+
|
|
187
342
|
## Faster edits — what making one talk taught us
|
|
188
343
|
|
|
189
344
|
- **Cache by content, not by position.** A clip is keyed by its words and settings: inserting or removing a
|
|
@@ -262,7 +417,7 @@ const { theme, toggle, setTheme } = useTheme(); // flips an html class + persi
|
|
|
262
417
|
| Export | What |
|
|
263
418
|
|---|---|
|
|
264
419
|
| `PostView` | the lens controller (Read · Scroll · Watch toggle) |
|
|
265
|
-
| `BlogView` · `ScrollyView` · `SlideDeck` | the three lenses (take `sections` / `sections` / `steps`) |
|
|
420
|
+
| `BlogView` · `ScrollyView` · `SlideDeck` | the three lenses (take `sections` / `sections` / `steps`; `SlideDeck` also a `tour`) |
|
|
266
421
|
| `SlideFigure` | one slide rendered as a scaled static figure |
|
|
267
422
|
| `StoryDeckProvider` · `useBasePath` | inject deploy config (asset base path) |
|
|
268
423
|
| `useTheme` · `ThemeToggle` | headless theming + a default toggle |
|
|
@@ -272,10 +427,12 @@ const { theme, toggle, setTheme } = useTheme(); // flips an html class + persi
|
|
|
272
427
|
| `slugify` · `scopeDeckCss` | utilities |
|
|
273
428
|
| `planFocus` · `focusStep` · `readFocus` · `FOCUS` | focus: per click, what it is about and what happens to the rest (also `footprint-storydeck/focus`) |
|
|
274
429
|
| `readPieces` · `readStep` · `drawFocus` · `wrapStage` · `slideClass` · `focusOverlay` · `focusRuntime` | focus, written into slide HTML (also `footprint-storydeck/focus-html`; styles in `footprint-storydeck/focus.css`) |
|
|
430
|
+
| `idleTour` · `tourPlan` · `tourText` | tour: a deck (or a figure) that shows its point while nobody is using it, and its plan in data and in one line (also `footprint-storydeck/tour`) |
|
|
275
431
|
| `narrationText` · `writtenText` | narration: what a click says (also `footprint-storydeck/narration`); the rules beneath, sentences, captions and chapters are `footprint-narration`'s |
|
|
276
432
|
| `notesData` · `notesRuntime` · `listenRuntime` | narration in the page: notes, presenter window, listen mode (also `footprint-storydeck/narration-html`; styles in `footprint-storydeck/narration.css`) |
|
|
277
433
|
| `embedClips` | `footprint-storydeck/voice` (Node only): a deck's clips inside one page; the clip cache, the voice port and its local-kit adapter are `footprint-narration/voice`'s |
|
|
278
434
|
| `renderVideo` · `planVideo` · `deckStageDriver` · `ffmpegMajor` | `footprint-storydeck/video` (Node only, ffmpeg 5.0+): the deck as a narrated video, with captions and chapters |
|
|
435
|
+
| `exportPptx` · `pptxParts` · `zip` · `unzip` · `checkPptx` | `footprint-storydeck/pptx` (Node only): the deck as a PowerPoint file — a slide per click, its picture, notes and clip — through a footprintjs flowchart that records why |
|
|
279
436
|
|
|
280
437
|
Types ship beside the source in `index.d.ts` — hand-kept (there is no build to generate them from),
|
|
281
438
|
and here rather than in a consumer's shim, because a package's shape belongs in the package.
|
|
@@ -287,7 +444,7 @@ npm test # vitest
|
|
|
287
444
|
npm run test:cov
|
|
288
445
|
```
|
|
289
446
|
|
|
290
|
-
|
|
447
|
+
317 tests (and 2 that run where LibreOffice or the ECMA-376 schemas are found) · ~99% lines · coverage thresholds enforced (90% statements, 85% branches, 90% functions, 95% lines).
|
|
291
448
|
|
|
292
449
|
## License
|
|
293
450
|
|
package/SlideDeck.jsx
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useBasePath } from './context';
|
|
4
|
+
import { idleTour } from './tour';
|
|
5
|
+
import { focusRuntime } from './focusHtml';
|
|
4
6
|
|
|
5
7
|
// Slide (watch) lens: the deck slides (as raw HTML strings, in order) mounted into <deck-stage>
|
|
6
8
|
// (the deck's own web component, loaded from /deck-stage.js). We set innerHTML imperatively so
|
|
7
9
|
// React doesn't fight the runtime once it restructures the stage; remounts auto-upgrade.
|
|
8
|
-
|
|
10
|
+
//
|
|
11
|
+
// It installs focusHtml.js · focusRuntime (once a page), as a built deck inlines it: only a forward click
|
|
12
|
+
// animates, and apart's springs play.
|
|
13
|
+
//
|
|
14
|
+
// `tour` (tour.js · idleTour) makes it a deck nobody has to present: left alone for a moment it shows
|
|
15
|
+
// its stops through the deck's own goTo, and a key, tap, click or move hands it over. It is the
|
|
16
|
+
// readable spec only ('3 4 5 9:6s', or {stops, hold, idleMs, resumeMs, loop}), so it can come from JSON.
|
|
17
|
+
export default function SlideDeck({ steps, tour }) {
|
|
9
18
|
const host = useRef(null);
|
|
10
19
|
const [copied, setCopied] = useState(false);
|
|
11
20
|
const basePath = useBasePath();
|
|
21
|
+
const tourKey = tour ? JSON.stringify(tour) : '';
|
|
12
22
|
|
|
13
23
|
// deck-stage keeps the current slide in location.hash (#1, #2, …), so the URL already points at
|
|
14
24
|
// the visible slide — copying window.location shares that exact slide (same scheme as the blog).
|
|
@@ -21,6 +31,7 @@ export default function SlideDeck({ steps }) {
|
|
|
21
31
|
useEffect(() => {
|
|
22
32
|
const el = host.current;
|
|
23
33
|
if (!el) return;
|
|
34
|
+
focusRuntime();
|
|
24
35
|
const inner = (steps || []).join('\n');
|
|
25
36
|
el.innerHTML = `<deck-stage no-rail width="1920" height="1080">${inner}</deck-stage>`;
|
|
26
37
|
if (!document.querySelector('script[data-deck-stage]')) {
|
|
@@ -31,6 +42,14 @@ export default function SlideDeck({ steps }) {
|
|
|
31
42
|
}
|
|
32
43
|
}, [steps]);
|
|
33
44
|
|
|
45
|
+
// After the deck effect, so a new deck (new steps) gets a new tour; keyed by the spec's text, not its identity.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const stage = host.current?.querySelector('deck-stage');
|
|
48
|
+
if (!tourKey || !stage) return undefined;
|
|
49
|
+
const t = idleTour(stage, typeof tour === 'string' ? { stops: tour } : tour);
|
|
50
|
+
return () => t.stop();
|
|
51
|
+
}, [steps, tourKey]);
|
|
52
|
+
|
|
34
53
|
return (
|
|
35
54
|
<>
|
|
36
55
|
<div className="deck-shell deck-scope" ref={host} />
|
package/focus.css
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* its own entrance, say — wins without !important.
|
|
10
10
|
*
|
|
11
11
|
* Tokens (set them in your theme; each has a fallback): --sd-dim-opacity · --sd-hot · --sd-blur ·
|
|
12
|
-
* --sd-veil · --sd-accent (falls back to storydeck's --yellow) · --sd-ease · --sd-move-time
|
|
12
|
+
* --sd-veil · --sd-accent (falls back to storydeck's --yellow) · --sd-ease · --sd-move-time · --sd-glow (a path's
|
|
13
|
+
* pulse, falls back to the accent).
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/* a piece's state: on (in colour), dim (the context: greyed in place, or hidden, or kept), gone */
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
.sd-moved { translate: var(--sd-dx, 0px) var(--sd-dy, 0px); scale: var(--sd-s, 1); }
|
|
26
27
|
.sd-origin-tl { transform-origin: 0 0; }
|
|
27
28
|
.sd-stage { position: absolute; inset: 0; transform-origin: 0 0; translate: var(--sd-tx, 0px) var(--sd-ty, 0px); scale: var(--sd-ts, 1); }
|
|
29
|
+
/* apart: a piece its group's spring moves is marked .sd-apart and has no CSS motion — focusRuntime plays the spring as
|
|
30
|
+
Web Animations of the same translate/scale; at rest it is the plain style above (together: the piece as drawn) */
|
|
28
31
|
|
|
29
32
|
/* the blur overlay: everything outside the holes, in the area, blurred and darkened; a frame on the subject */
|
|
30
33
|
.sd-veil { position: absolute; left: 0; top: 0; pointer-events: none; -webkit-backdrop-filter: blur(var(--sd-blur, 7px)); backdrop-filter: blur(var(--sd-blur, 7px));
|
|
@@ -37,10 +40,16 @@
|
|
|
37
40
|
color: var(--sd-accent, var(--yellow, #facc15)); }
|
|
38
41
|
@media print { .sd-veil { display: none; } }
|
|
39
42
|
|
|
43
|
+
/* a path: its line, naming the route, in slide px (it shows once the route is done) */
|
|
44
|
+
.sd-path-line { position: absolute; pointer-events: none; white-space: nowrap; font: 600 22px/1 var(--mono, ui-monospace, monospace);
|
|
45
|
+
letter-spacing: .04em; color: var(--sd-accent, var(--yellow, #facc15)); }
|
|
46
|
+
|
|
40
47
|
/* motion: each change fills its own slot, so a piece that moves AND lights does both */
|
|
41
48
|
@property --sd-a-state { syntax: '*'; inherits: false; }
|
|
42
49
|
@property --sd-a-move { syntax: '*'; inherits: false; }
|
|
43
50
|
@property --sd-a-hot { syntax: '*'; inherits: false; }
|
|
51
|
+
@property --sd-a-path { syntax: '*'; inherits: false; }
|
|
52
|
+
@property --sd-turn { syntax: '*'; inherits: false; } /* a piece's moment on a path; never inherited, so 0 off the route */
|
|
44
53
|
@keyframes sdNone {}
|
|
45
54
|
@keyframes sdEnter { from { opacity: 0; scale: calc(var(--sd-s, 1) * .92); } }
|
|
46
55
|
@keyframes sdArrive { from { opacity: 0; } }
|
|
@@ -52,6 +61,12 @@
|
|
|
52
61
|
@keyframes sdStage { from { translate: var(--sd-tx0, 0px) var(--sd-ty0, 0px); scale: var(--sd-ts0, 1); } }
|
|
53
62
|
@keyframes sdVeilIn { from { opacity: 0; } }
|
|
54
63
|
@keyframes sdVeilOut { from { opacity: 1; } to { opacity: 0; } }
|
|
64
|
+
@keyframes sdPulse { 40% { filter: drop-shadow(0 0 14px var(--sd-glow, var(--sd-accent, var(--yellow, #facc15)))) brightness(1.2); } }
|
|
65
|
+
/* a piece that lights from grey at its turn: sdLight and sdPulse as ONE animation. Two on one filter swap their lists
|
|
66
|
+
(grayscale ↔ drop-shadow brightness) at a jump — a flash; here every keyframe lists the same functions in the same
|
|
67
|
+
order, so the filter glides from grey into colour and the glow, and back to the plain style (the implicit end) */
|
|
68
|
+
@keyframes sdLightPulse { from { opacity: var(--sd-dim-opacity, .32); filter: grayscale(1) drop-shadow(0 0 0 transparent) brightness(1); }
|
|
69
|
+
40% { filter: grayscale(0) drop-shadow(0 0 14px var(--sd-glow, var(--sd-accent, var(--yellow, #facc15)))) brightness(1.2); } }
|
|
55
70
|
@media screen and (prefers-reduced-motion: no-preference) {
|
|
56
71
|
.sd-enter { --sd-a-state: sdEnter .6s var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) var(--sd-delay, 0s) both; }
|
|
57
72
|
.sd-arrive { --sd-a-state: sdArrive .6s ease both; }
|
|
@@ -62,10 +77,21 @@
|
|
|
62
77
|
.sd-relook.sd-from-hide { --sd-a-state: sdArrive .6s ease both; }
|
|
63
78
|
.sd-relook.sd-from-keep { --sd-a-state: sdFade .7s ease both; }
|
|
64
79
|
.sd-move { --sd-a-move: sdMove var(--sd-move-time, 1s) var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) both; }
|
|
65
|
-
.sd-hot-in { --sd-a-hot: sdRing .6s var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) .3s both; }
|
|
80
|
+
.sd-hot-in { --sd-a-hot: sdRing .6s var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) calc(.3s + var(--sd-turn, 0s)) both; } /* on a path, .3s after its turn */
|
|
66
81
|
.sd-focus-new .sd-enter { --sd-delay: .4s; } /* what a click adds waits for its blur */
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
/* a path: everything on it has one moment, --sd-turn — its delay, and .4s more under a new blur, so the whole path
|
|
83
|
+
waits for the blur and then runs. A piece on the route enters, arrives or lights then, and glows as the route
|
|
84
|
+
reaches it (on every forward click); a piece stepping aside for its lit twin goes then; the line shows then. */
|
|
85
|
+
:is(.sd-path, .sd-twin-turn, .sd-path-line) { --sd-turn: var(--sd-path-delay, 0s); }
|
|
86
|
+
.sd-focus-new :is(.sd-path, .sd-twin-turn, .sd-path-line) { --sd-turn: calc(var(--sd-path-delay, 0s) + .4s); }
|
|
87
|
+
.sd-path { --sd-a-path: sdPulse .9s ease var(--sd-turn); }
|
|
88
|
+
.sd-path.sd-enter { --sd-a-state: sdEnter .6s var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) var(--sd-turn) both; }
|
|
89
|
+
.sd-path.sd-arrive { --sd-a-state: sdArrive .6s ease var(--sd-turn) both; }
|
|
90
|
+
.sd-path.sd-light { --sd-a-state: sdLightPulse .9s ease var(--sd-turn) backwards; --sd-a-path: sdNone 0s; } /* grey until its turn */
|
|
91
|
+
.sd-twin-turn.sd-leave { --sd-a-state: sdLeave .45s ease var(--sd-turn) both; }
|
|
92
|
+
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) :is(.sd-enter, .sd-arrive, .sd-light, .sd-fade, .sd-leave, .sd-relook, .sd-move, .sd-hot-in, .sd-path) {
|
|
93
|
+
animation: var(--sd-a-state, sdNone 0s), var(--sd-a-move, sdNone 0s), var(--sd-a-hot, sdNone 0s), var(--sd-a-path, sdNone 0s); }
|
|
94
|
+
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-path-line { animation: sdArrive .5s ease var(--sd-turn) both; }
|
|
69
95
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-stage.sd-move { animation: sdStage var(--sd-move-time, 1s) var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) both; }
|
|
70
96
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-veil-in-new { animation: sdVeilIn .7s cubic-bezier(.4, 0, .2, 1) both; }
|
|
71
97
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-frame.sd-new { animation: sdArrive .5s ease .35s both; }
|
package/focus.d.ts
CHANGED
|
@@ -53,11 +53,25 @@ export interface FocusContext {
|
|
|
53
53
|
readonly options: Readonly<Record<string, unknown>>;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A route, as an order strategy gives it — checked, and refused (naming the strategy and the fix) when it is not this:
|
|
58
|
+
* the names in turn (each one's LIT pieces take the next turn; a name with nothing lit takes none), the seconds between
|
|
59
|
+
* two turns (0–5), and the line that names it, with where it goes ('x y', slide px). The strategy's object is only read.
|
|
60
|
+
*/
|
|
61
|
+
export interface FocusRoute { readonly names: readonly string[]; readonly step: number; readonly line?: string; readonly lineAt?: string | null }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A mover's groups going by a spring each (apart), `stagger` ms apart (0–1000): out in the order the groups are given,
|
|
65
|
+
* back in reverse. Checked, and refused (naming the strategy and the fix) when it is not this; only read.
|
|
66
|
+
*/
|
|
67
|
+
export interface FocusSpring { readonly stagger: number }
|
|
68
|
+
|
|
69
|
+
/** A focus strategy: a look (how the context looks), a mover (where things go), an overlay (what lies over the slide) or an order (how the subject comes on). */
|
|
57
70
|
export type FocusStrategy =
|
|
58
71
|
| { readonly name?: string; readonly kind: 'look'; readonly look: 'grey' | 'hide' | 'keep' }
|
|
59
|
-
| { readonly name?: string; readonly kind: 'mover'; place(ctx: FocusContext): { stage?: FocusGroup; subject?: FocusGroup; context?: FocusGroup; groups?: { names: readonly string[]; g: FocusGroup | null }[] } }
|
|
60
|
-
| { readonly name?: string; readonly kind: 'overlay'; cover(ctx: { subjectOnSlide: FocusBox | null; canvas: { w: number; h: number }; options: Readonly<Record<string, unknown>> }): { rects: number[][]; label?: string; out?: number[][] } | null }
|
|
72
|
+
| { readonly name?: string; readonly kind: 'mover'; place(ctx: FocusContext): { stage?: FocusGroup; subject?: FocusGroup; context?: FocusGroup; groups?: { names: readonly string[]; g: FocusGroup | null }[]; spring?: FocusSpring } }
|
|
73
|
+
| { readonly name?: string; readonly kind: 'overlay'; cover(ctx: { subjectOnSlide: FocusBox | null; canvas: { w: number; h: number }; options: Readonly<Record<string, unknown>> }): { rects: number[][]; label?: string; out?: number[][] } | null }
|
|
74
|
+
| { readonly name?: string; readonly kind: 'order'; route(ctx: { names: readonly string[]; options: Readonly<Record<string, unknown>> }): FocusRoute };
|
|
61
75
|
|
|
62
76
|
export interface FocusDeck {
|
|
63
77
|
readonly pieces: readonly FocusPiece[];
|
|
@@ -85,10 +99,24 @@ export interface FocusPieceLook {
|
|
|
85
99
|
readonly hot: boolean;
|
|
86
100
|
readonly hotIn: boolean;
|
|
87
101
|
readonly move: FocusMove | null;
|
|
88
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Where it moved from, when it moved since the last click. (A leaving piece's `move` is where it stood.) A piece on
|
|
104
|
+
* its group's spring (`apart`) has the spring's other end: where it stands together as its group opens — kept while
|
|
105
|
+
* the group stays open — and where it stood apart as its group closes.
|
|
106
|
+
*/
|
|
89
107
|
readonly from: FocusMove | null;
|
|
90
108
|
/** It stepped aside for its lit twin. */
|
|
91
109
|
readonly twinOff: boolean;
|
|
110
|
+
/** Stepping aside for a lit twin on a path: the seconds until that twin's turn, when it goes (the first, for several); null when it goes at once (a lit twin off the route) or does not step aside on this click. */
|
|
111
|
+
readonly twinTurn: number | null;
|
|
112
|
+
/** On a path: the seconds its turn waits (0 for the first); null when it takes no turn — not on the route, or not lit. */
|
|
113
|
+
readonly path: number | null;
|
|
114
|
+
/**
|
|
115
|
+
* It goes by its group's spring on this click (apart): the group's index in the look's `apart.groups` — the page's
|
|
116
|
+
* focusRuntime moves it from `from` to `move`. Null for every other piece (one that enters, leaves, or is in no
|
|
117
|
+
* group that opens or closes; one whose group stays open but moved anyway glides as any mover's piece does).
|
|
118
|
+
*/
|
|
119
|
+
readonly apart: number | null;
|
|
92
120
|
}
|
|
93
121
|
|
|
94
122
|
export interface FocusLook {
|
|
@@ -97,6 +125,15 @@ export interface FocusLook {
|
|
|
97
125
|
readonly stage: { readonly move: { s: number; x: number; y: number } | null; readonly from: { s: number; x: number; y: number } | null } | null;
|
|
98
126
|
/** The blur: 'in-new' when the last click had none, 'in' while it stays, 'out' as it melts away; frameNew when its frame moved or appeared. */
|
|
99
127
|
readonly overlay: { readonly rects: readonly (readonly number[])[]; readonly label: string; readonly phase: 'in-new' | 'in' | 'out'; readonly frameNew: boolean } | null;
|
|
128
|
+
/** A path: how many turns, the seconds between two, when the last starts (done), and its line ('' for none) and where it goes (null: the area's bottom left); null when no lit piece took a turn. */
|
|
129
|
+
readonly path: { readonly turns: number; readonly step: number; readonly done: number; readonly line: string; readonly lineAt: string | null } | null;
|
|
130
|
+
/**
|
|
131
|
+
* apart: the groups that open on this click (to 1, in the order written), then those the last click opened that
|
|
132
|
+
* close now (to 0, was 1, in its order), then those the last click closed (to 0, was 0) — carried one click more, so
|
|
133
|
+
* a spring still closing settles from wherever it is; `was` is where the last click left a group (0 together, 1
|
|
134
|
+
* apart). Null when no group opens, stays open, closes or settles — as on every click of a deck without apart.
|
|
135
|
+
*/
|
|
136
|
+
readonly apart: { readonly stagger: number; readonly groups: readonly { readonly names: readonly string[]; readonly to: 0 | 1; readonly was: 0 | 1 }[] } | null;
|
|
100
137
|
readonly strategies: readonly string[];
|
|
101
138
|
/** The subject where it ends up, on the slide. */
|
|
102
139
|
readonly subject: FocusBox | null;
|
|
@@ -105,7 +142,12 @@ export interface FocusLook {
|
|
|
105
142
|
/** The state a click leaves, to start the next from. Opaque. */
|
|
106
143
|
export type FocusState = { readonly __focusState: true } & Record<string, unknown>;
|
|
107
144
|
|
|
108
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The built-in strategies. `apart` (a mover) is the exploded view: its option `apart` — data-focus-apart — is
|
|
147
|
+
* 'names: dx dy [scale] | …' (slide px; each group scaled around its own centre), `stagger` the ms between two
|
|
148
|
+
* groups (35); a later click without it brings the groups back together, the last one out first.
|
|
149
|
+
*/
|
|
150
|
+
export const FOCUS: Readonly<Record<'grey' | 'hide' | 'keep' | 'zoom' | 'left' | 'right' | 'up' | 'down' | 'place' | 'apart' | 'path' | 'blur', FocusStrategy>>;
|
|
109
151
|
export function readFocus(spec: FocusSpec | undefined | null, shared?: Readonly<Record<string, unknown>>): { strategy: string; options: Record<string, unknown> }[];
|
|
110
152
|
export function focusStep(prev: FocusState | null, click: FocusClick, deck: FocusDeck): { state: FocusState; look: FocusLook };
|
|
111
153
|
/** `end` is null only when there are no clicks and no `start`. */
|