Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions docs/guide/07-viewing.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,19 @@ automatically, polling about once a second. Reloading the page also reattaches t
latest consistent snapshot. Images, text/JSON,
audio, and video use generic previews; other file types can be opened in a new tab.

Run canvas cards also show the first available recorded image output. A count
indicates multiple distinct available images; select the card to inspect all outputs.
The canvas uses the verified artifact inventory and never evaluates author code to
produce thumbnails. Plan cards have no artifact previews, including steps marked
`done` by the planner. Updates preserve the canvas camera, selection and workflow navigation.
Run canvas cards also show the first available recorded image output. A step without
one shows the first frame of its first MP4 or WebM video. Resting the pointer on it
plays it muted and looping; click it to keep it playing with sound, or use its play
button. The video plays over the card, follows it as you pan and zoom, and stops when it
ends, on Escape, or when another card covers it; played again, it continues where it
stopped. Its controls keep their size at any
zoom (play, seek, time, mute and full screen, fewer on a small card); with the player
focused, Space or K plays and pauses, the arrow keys seek, M mutes and F goes full screen. A count indicates multiple distinct available images
(or videos); select the card to inspect all outputs. The canvas uses the verified
artifact inventory and never evaluates author code to produce thumbnails; a video's
first frame is read in the browser once its card comes into view. Plan cards have no
artifact previews, including steps marked `done` by the planner. Updates preserve the
canvas camera, selection and workflow navigation.

These automatic thumbnails use structured run data. The existing authoring `view`
declarations are booleans or HTML template paths; custom templates and structured
Expand Down
26 changes: 22 additions & 4 deletions spec/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ authored cells, rule 10 removes empty tracks and rule 11 sizes them.
left.

Rules 1–10 read no size, so a preview arriving never changes a cell. Footprints come from
declarations: ports, settings and kind. A step whose declared output ports include an image
reserves its preview area in both views, before any artifact exists.
declarations: ports, settings and kind. A step whose declared output ports include an image or a
video reserves its preview area in both views, before any artifact exists: a 16:9 frame as wide as
the card less its padding, in which the file is fitted whole. A recorded card
previews its first image output, or without one its first MP4 or WebM output, which shows its
first frame until it is played (§5.7).

Example: `seed → propose (2 takes) → warm, cool, mono, dusk, night → sheet`, and an island
`notes → index`.
Expand Down Expand Up @@ -255,13 +258,14 @@ including those that leave the deck.
| Input | Closed deck | Expanded deck |
|---|---|---|
| Hover, only where `(hover: hover)` | raise the card under the pointer, and its linked partner; leaving restores the resting card | nothing; everything under the veil is inert |
| Rest on a card's video, only where `(hover: hover)` and motion is not reduced | after a moment it plays over the card, muted and looping, with its controls, until the pointer leaves; pressing it keeps it playing, and a press on its picture turns its sound on | the same |
| Hover a bookmark row | raise that member, as hovering its card does | the camera glides to that member's card, which is outlined; leaving keeps the camera there |
| Click a bookmark row | select that member (a frame member: its first step), which then rests in front | select it, and the camera glides to its card |
| Still click on a card, a back card's visible rim or the badge | expand | select that member |
| Click a labelled control on the front card (`Open workflow`) | does what it says | does what it says |
| Click a labelled control on the front card (`Open workflow`, a video's play control) | does what it says | does what it says |
| Double-click | counts as one expand click | on a workflow card: open it |
| Click the veil | — | collapse one level; the selection is unchanged; a second deck needs a second click |
| Esc | with focus in the canvas: go back a scope (unchanged) | collapse one level, wherever focus is except in a text field; consumed only then |
| Esc | with focus in the canvas: go back a scope (unchanged) | collapse one level, wherever focus is except in a text field or a playing video; consumed only then |
| × | — | collapse one level |
| Tab | one stop per deck (the front card); back cards have `tabindex=-1` | each card, in grid order |
| Enter or Space | expand | select |
Expand All @@ -271,6 +275,20 @@ including those that leave the deck.
| Scope change | — | collapse at once; keep the pre-expand camera |
| A deck inside an expanded card | — | a click expands it one level deeper. Only the innermost level is interactive; the levels around it are its veil, so a click on them collapses one level and selects nothing |

A video plays over its card's preview, following the card as the camera and the deck move it, and
selects nothing. Resting on it plays it muted, without taking focus or replacing a video someone
played or pressed. It stops when it ends, when the pointer leaves one that is only hovered, on Esc
(focus returns to its play control) and as soon as anything covers its card: another card raised
over it, its deck resting with another card in front, or the veil of a deck it is not in; a covered
card's video does not start. Until the
video shows its own frame, the card keeps its first frame; the video fades in over it, and out
again when it stops. Resting on it or playing it again
continues where it stopped, unless it played to its end. Its
controls are the viewer's own and keep their size on screen at any zoom: play, seek, time, mute and
full screen, the time going first and then seek and full screen as the player gets narrow. While it
plays they fade once the pointer rests. With focus in the player, Space or K plays and pauses, the
arrow keys seek 5 seconds, M mutes and F goes full screen.

On collapse, focus returns to the deck's front card. Raising a card never moves the focused
element in the document. At most one deck per view is expanded, plus its nested levels. There are
no dedicated deck buttons, no subview and no URL that selects a member (§11).
Expand Down
41 changes: 41 additions & 0 deletions web/fx-web/src/canvas.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,47 @@
@media (prefers-reduced-motion: reduce) { .fx-canvas-shimmer-band { animation: none; } }
.fx-canvas-image-error { fill: #71717a; font: 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; pointer-events: none; }
.fx-canvas-image-count { fill: #a1a1aa; font: 10px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
/* A video card (video.ts): its first frame, a play control, and while it plays, one player laid over the frame. */
.fx-canvas-play { cursor: pointer; outline: none; }
.fx-canvas-play circle { fill: #18181bb3; }
.fx-canvas-play:hover circle, .fx-canvas-play:focus-visible circle { fill: #18181b; }
.fx-canvas-play:focus-visible circle { stroke: #2563eb; stroke-width: 2px; }
.fx-canvas-play svg { color: #fff; pointer-events: none; }
.fx-canvas-video.is-unavailable .fx-canvas-play { display: none; }
/* The video fades in over the first frame and play control, which hide once it has; they show again at once as it fades out. */
.fx-canvas-video .fx-canvas-node-image, .fx-canvas-video .fx-canvas-play { transition: visibility 0s; }
.fx-canvas-video.is-shown .fx-canvas-node-image, .fx-canvas-video.is-shown .fx-canvas-play { visibility: hidden; transition-delay: .15s; }
.fx-canvas-video.is-unavailable .fx-canvas-image-error { visibility: visible; }
.fx-canvas-player { position: absolute; box-sizing: border-box; overflow: hidden; border-radius: 6px; outline: none; container-type: inline-size; font: 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; transition: opacity .15s ease; }
.fx-canvas-player:focus-visible { box-shadow: 0 0 0 2px #2563eb; }
/* Until the video shows its frame the card's first frame shows through, and takes the pointer. */
.fx-canvas-player:not(.is-shown) { opacity: 0; pointer-events: none; }
@starting-style { .fx-canvas-player.is-shown { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
.fx-canvas-player { transition: none; }
.fx-canvas-video.is-shown .fx-canvas-node-image, .fx-canvas-video.is-shown .fx-canvas-play { transition-delay: 0s; }
}
.fx-canvas-player > video { display: block; width: 100%; height: 100%; object-fit: contain; }
.fx-canvas-player:fullscreen { border-radius: 0; background: #000; }
.fx-canvas-player.is-idle { cursor: none; }
/* Its controls (video-controls.ts) keep their screen size at any zoom; a small player keeps fewer. */
.fx-video-controls { position: absolute; right: 0; bottom: 0; left: 0; display: flex; align-items: center; gap: 2px; padding: 14px 4px 3px; background: linear-gradient(#0000, #0000008c); color: #fff; transition: opacity .2s; }
.fx-canvas-player.is-idle .fx-video-controls { opacity: 0; }
.fx-video-controls button { display: grid; flex: none; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 5px; background: none; color: inherit; cursor: pointer; }
.fx-video-controls button[hidden] { display: none; }
.fx-video-controls button:hover { background: #ffffff2e; }
.fx-video-controls button:focus-visible { outline: 2px solid #93c5fd; outline-offset: -2px; }
.fx-video-time { flex: none; padding: 0 3px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fx-video-seek { flex: 1; min-width: 0; height: 16px; margin: 0 4px; background: none; cursor: pointer; appearance: none; }
.fx-video-seek:disabled { cursor: default; }
.fx-video-seek:focus-visible { outline: 2px solid #93c5fd; outline-offset: 1px; border-radius: 2px; }
.fx-video-seek::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: linear-gradient(to right, #fff var(--fx-played, 0%), #ffffff80 var(--fx-played, 0%) var(--fx-buffered, 0%), #ffffff40 var(--fx-buffered, 0%)); }
.fx-video-seek::-moz-range-track { height: 3px; border-radius: 2px; background: linear-gradient(to right, #fff var(--fx-played, 0%), #ffffff80 var(--fx-played, 0%) var(--fx-buffered, 0%), #ffffff40 var(--fx-buffered, 0%)); }
.fx-video-seek::-webkit-slider-thumb { width: 10px; height: 10px; margin-top: -3.5px; border: 0; border-radius: 50%; background: #fff; appearance: none; }
.fx-video-seek::-moz-range-thumb { width: 10px; height: 10px; border: 0; border-radius: 50%; background: #fff; }
@container (max-width: 220px) { .fx-video-time { display: none; } }
@container (max-width: 150px) { .fx-video-seek, .fx-video-controls .fx-video-fullscreen { display: none; } .fx-video-controls { justify-content: space-between; } }
@container (max-width: 64px) { .fx-video-controls { display: none; } }
.fx-canvas-toolbar { position: absolute; bottom: 16px; left: 16px; display: flex; align-items: center; gap: 1px; padding: 3px; border: 1px solid #e4e4e7; border-radius: 9px; background: #fff; box-shadow: 0 2px 5px #00000008; }
.fx-canvas-toolbar button { min-width: 32px; height: 30px; padding: 0 8px; border: 0; border-radius: 5px; background: #fff; color: #3f3f46; font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; }
.fx-canvas-toolbar button:hover { background: #f4f4f5; }
Expand Down
Loading
Loading