| new file mode 100644 |
| index 0000000..edd260a |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-09-07-slides-design.md |
| @@ -0,0 +1,538 @@ |
| +# 🤖 Slides — design |
| + |
| +Design, 2026-09-07. Direction approved by Paul in conversation; this is the |
| +written form. **Revision 1.** Drafted by the Slides session (Fable 5.1) |
| +from a brainstorm with Paul the same day. Every ruling in §1 attributed to |
| +Paul was given in that conversation. |
| + |
| +Slides is a presentation editor for the Oficina suite: a deck of slides |
| +that looks like a Keynote or PowerPoint deck on a desktop and reads like |
| +a document on a phone, edited by several people at once, presented from a |
| +laptop and driven from a phone, exported to PDF, and driveable by an agent |
| +through the same operations a person has. One instance per team, on the |
| +CARLOS platform, at `slides.oficina.works` for the first team. |
| + |
| +Built beside [Docs](https://amadan.net/oficina/docs), |
| +[Sheets](https://amadan.net/oficina/sheets) and the suite |
| +[doctrine](https://amadan.net/oficina/suite). Where a decision is already |
| +made over there, this spec consumes it rather than restating it (§0.2). |
| + |
| +--- |
| + |
| +## 0. The rules this app lives under |
| + |
| +### 0.1 The one rule |
| + |
| +**A slide is one block tree, rendered many ways.** The tree is the only |
| +truth: the editor edits it, the viewer renders it, the phone reflows it, |
| +the PDF prints it, the agent patches it. Nothing that matters to what a |
| +reader sees lives outside the tree, so there is never a "desktop version" |
| +and a "mobile version" of a deck, only one deck and a viewport. |
| + |
| +Everything below derives from it. The hybrid layout modes (§2.3) are two |
| +ways of placing the same blocks, not two kinds of slide. The phone |
| +overrides (§2.5) are entries in the tree, not a second file. The drive |
| +API (§7) is the inspector's operations with a JSON body. |
| + |
| +### 0.2 Consumed suite rulings |
| + |
| +This app cites the suite sharing doctrine |
| +(`oficina/suite`, `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`, |
| +revision 8) by ruling number and never restates it. |
| + |
| +| Ruling | Used for | |
| +|---|---| |
| +| 1, 5 | One instance per team; the first deploy is the **lone shape** (§6.1). | |
| +| 4, 12 | Identity and roster belong to the home; until the home exists (it is a skeleton, `oficina/home` `internal/home/app.go`), the lone instance keeps its own allowlist exactly as Docs does. | |
| +| 6 | No product-wide egress. A PDF leaves as a download the person asked for; nothing is sent anywhere. | |
| +| 9 | Every write names its real actor. An agent's write names the agent (§7). | |
| +| 15 | Three rungs (private, granted, team) plus a **read-only link capability**. A share link never edits (§5). | |
| + |
| +One ruling made by Paul during this brainstorm is suite-wide and is |
| +filed as an `oficina/suite` discussion rather than restated here as if it |
| +were this app's: **agents drive every Oficina app** as their own |
| +identified principal, `claude~paul` style, on behalf of a person, never |
| +as the person (2026-09-07). Slides is the first app to build the surface |
| +(§7). |
| + |
| +### 0.3 What this spec does not claim |
| + |
| +It does not claim pixel identity between the browser stage and the PDF. |
| +§4 says where they may differ and by how much. It does not claim the |
| +suite home exists. It does not claim a native remote exists; §3.4 |
| +designs the phone web page so a native app later speaks the same three |
| +calls. |
| + |
| +--- |
| + |
| +## 1. Settled decisions (don't relitigate casually) |
| + |
| +| # | Ruling | Rejected alternative | Who, when | |
| +|---|---|---|---| |
| +| 1 | A slide is a tree of typed blocks; the slide's container has a **layout mode**, `flow` or `canvas`. Same blocks, same editor, two placements. | Blocks only (no free placement; feels like Gamma, not Keynote). Canvas only (unreadable on phones). Two separate slide kinds (two models to maintain). | Paul, 2026-09-07 | |
| +| 2 | Phones get **reflow** for flow slides and **scale-to-fit** for canvas slides, with a per-slide switch to reflow a canvas slide in reading order. | Hand-authored second layout per slide. | Paul, 2026-09-07 | |
| +| 3 | Collab core is **Yjs in the browser, yrs under wazero on the server**, Docs' pattern and Docs' live-sync transport (SSE advisory, bytes over POST). Slides is the second implementation of live-sync. | Server-authoritative JSON with last-write-wins (two people in one text block clobber each other). | Paul, 2026-09-07 | |
| +| 4 | **One Go renderer produces HTML** for the viewer, presenter, remote and the editor's stage. **Export** uses a Go layout engine that emits SVG per slide, and PDF is SVG to PDF through a Rust module under wazero. No Chromium. | Extend Docs' hand-rolled PDF with boxes and images (a second layout engine to keep in step). Headless Chromium (no such platform capability; one static binary). | Paul approved the SVG-and-Rust export path 2026-09-07; the HTML-first split for the live surfaces is the session's refinement, recorded in §4.1. | |
| +| 5 | Presenting is a **server-side presentation session**: current slide, presenter, pairing code. The phone remote, the presenter view and audience follow all read and write that one record. | Present as a pure viewer with no session (no remote possible). | Paul, 2026-09-07 ("include plans for speaker notes, presentation mode, and a mobile remote control") | |
| +| 6 | The remote is a **phone web page** first; a native app later speaks the same calls. | Native first. | Paul, 2026-09-07 | |
| +| 7 | **Every write names its actor**, and an agent is its own principal with an operator, never the person (suite ruling 9 plus Paul's 2026-09-07 ruling). Agent tokens are minted in the app by a signed-in person, named `<agent>~<person>`, stored hashed. | Agents borrowing a person's session. | Paul, 2026-09-07 | |
| +| 8 | First deploy is the **lone shape**: magic-link sign-in with an address allowlist, exactly Docs' `internal/docs/auth.go` shape. | Waiting for the home. | Session, 2026-09-07, from the survey: no app signs in through the home; the home is a skeleton. | |
| +| 9 | Themes are a **built-in set of token bundles** over estilo's variables. A deck may override accent colour and the two typefaces. No per-block fonts. | Free font picker per block (the fastest route to an ugly deck). | Paul approved the model 2026-09-07 | |
| +| 10 | Bytes (images) live in the **platform object store**, referenced by `blobs.Ref`; never on the instance disk, never in a column beyond the 4 KiB inline rule. | SQLite blobs (the instance hibernates and its disk is ephemeral; rastrillo §2). | rastrillo rule, adopted | |
| +| 11 | Repo `oficina/slides`, module `amadan.net/oficina/slides`, scaffolded by `rastrillo new` v0.26.0 (day theme, topbar shell). | Spanish name (`presentaciones`); the suite names generic apps in English. | Session, by the naming precedent | |
| + |
| +--- |
| + |
| +## 2. The deck model |
| + |
| +### 2.1 The Y.Doc |
| + |
| +A deck is one Y.Doc. Top-level shared types: |
| + |
| +- `meta` — Y.Map: `title`, `theme` (theme id), `accent` (optional hex), |
| + `fonts` (optional `{heading, body}`), `aspect` (`16:9` only in v1; the |
| + key exists so `4:3` can arrive without a migration). |
| +- `slides` — Y.Array of Y.Map, one per slide, in deck order. |
| +- `assets` — Y.Map from asset id to `{ref, name, width, height, alt}`, |
| + where `ref` is the object store address (§2.7). |
| + |
| +### 2.2 A slide |
| + |
| +A slide Y.Map holds: |
| + |
| +- `id` — stable, random, 12 lowercase base32 characters, assigned at |
| + creation, never reused. The drive API addresses slides by it. |
| +- `layout` — `flow` or `canvas`. |
| +- `template` — for flow slides: `title`, `title-body`, `two-column`, |
| + `section`, `image-full`, `quote`, `blank`. Ignored on canvas slides. |
| +- `blocks` — Y.Array of block Y.Maps in reading order (flow) or creation |
| + order with a `z` (canvas). |
| +- `notes` — Y.XmlFragment, speaker notes, the same text schema as §2.6. |
| +- `background` — `{kind: theme | color | image, value}`. |
| +- `phone` — `inherit` (default) or `reflow`. Only meaningful on canvas |
| + slides. |
| +- `transition` — `none` or `fade`. |
| + |
| +### 2.3 Layout modes |
| + |
| +**Flow.** The template names regions (`heading`, `body`, `aside`, |
| +`media`), each block has a `region` prop, and blocks stack inside their |
| +region in array order. The renderer places regions with CSS grid, and |
| +the template's grid collapses to one column at the phone breakpoint, so |
| +reflow is CSS, not a second layout. |
| + |
| +**Canvas.** Each block has `box: {x, y, w, h}` as fractions of the 16:9 |
| +frame (0 to 1, four decimals) and `z`. The renderer positions blocks |
| +absolutely inside a frame that scales to the viewport width. On a phone |
| +the frame scales to fit, text and all, unless `phone` is `reflow`, in |
| +which case the renderer emits the blocks as a flow slide with the |
| +`blank` template in `z` then top-left reading order. |
| + |
| +Switching a slide's mode is an operation, not a conversion the person |
| +does by hand: flow to canvas assigns each block the box its region gave |
| +it; canvas to flow drops boxes and assigns regions by position. Both are |
| +lossy in one direction and say so in the inspector. |
| + |
| +### 2.4 Blocks |
| + |
| +A block Y.Map holds `id` (same scheme as slides), `type`, `props` |
| +(Y.Map), and for text-bearing types `text` (Y.XmlFragment). The v1 |
| +vocabulary, the "maximally viable" set: |
| + |
| +| Type | Props | Text | |
| +|---|---|---| |
| +| `heading` | `level` 1 to 3 | yes | |
| +| `text` | `size` (`s`, `m`, `l`), `align` | yes | |
| +| `list` | `style` (`bullet`, `number`), `size` | yes, one paragraph per item | |
| +| `quote` | `cite` | yes | |
| +| `image` | `asset`, `fit` (`cover`, `contain`), `alt` (required, may be empty only if `decorative` is true), `radius` | no | |
| +| `columns` | `count` 2 or 3, `children` (Y.Array of block ids laid out left to right, each a block in this slide's `blocks`) | no | |
| +| `code` | `language`, `size` | yes, plain, one paragraph per line | |
| +| `shape` | `kind` (`rect`, `ellipse`, `line`, `arrow`), `fill`, `stroke`, `strokeWidth`, `radius` | optional single-paragraph label | |
| +| `table` | `rows`, `cols`, `header` (bool), `cells` (Y.Array of Y.XmlFragment, row-major) | per cell | |
| +| `divider` | none | no | |
| +| `spacer` | `size` | no | |
| + |
| +Every block additionally carries `region` (flow) or `box` and `z` |
| +(canvas), and an optional `phone` override map (§2.5). |
| + |
| +Charts from Sheets, embeds and video are **not in v1** (§9). |
| + |
| +### 2.5 Phone overrides |
| + |
| +A block's `phone` Y.Map may hold: `hidden` (bool), `order` (integer, |
| +sorts before array order among siblings), `size` (a size step), and on |
| +canvas slides `box`. Nothing else. The desktop layout is always the |
| +base; the phone layer only adds. |
| + |
| +The editor's desktop/phone switcher (§3.2) writes to this map when the |
| +stage is in phone view. The renderer reads it when the viewport is a |
| +phone, and the print engine never reads it. |
| + |
| +### 2.6 Text |
| + |
| +Text is a ProseMirror document under a constrained schema: `paragraph` |
| +only at the block level; marks `bold`, `italic`, `code`, `link`, |
| +`highlight`. No nested blocks, no headings inside text (a heading is a |
| +block). Lists are `list` blocks whose paragraphs are items; a nested list |
| +is a second `list` block with `indent` 1. The same schema drives speaker |
| +notes and table cells. |
| + |
| +### 2.7 Assets |
| + |
| +An image is uploaded browser-to-bucket by presigned PUT (rastrillo |
| +`blobs`), then registered in `assets` with its `blobs.Ref`, dimensions |
| +and alt text. The server verifies the object exists and is an image |
| +before registering. Limits: 20 MiB per image, PNG, JPEG, WebP, SVG |
| +(SVG is sanitised on registration). Deleting the last block that uses an |
| +asset leaves the asset; a deck-level "unused assets" list lets a person |
| +remove them. There is no cross-deck asset library in v1. |
| + |
| +### 2.8 Fixtures are the contract |
| + |
| +The JS side generates Y.Doc updates for a fixed set of decks |
| +(`fixtures/data/*.json` describing the tree, `fixtures/generate.mjs` |
| +producing `*.yjs` updates). The Rust core must load each and produce the |
| +same JSON view (`slides_core_json_read`). Docs' rule, adopted unchanged. |
| + |
| +--- |
| + |
| +## 3. The editor, presenting, and the remote |
| + |
| +### 3.1 Editor |
| + |
| +A JS bundle built like Docs' (`fixtures/build-bundles.mjs`, esbuild, |
| +committed hashed output, embedded by `assets.go`), ProseMirror plus yjs, |
| +no framework. Three panes on a desktop: the **strip** (slide thumbnails, |
| +drag to reorder, keyboard reorder with a menu equivalent), the **stage**, |
| +and the **inspector**. On a phone the strip is a horizontal filmstrip |
| +under the stage and the inspector is a bottom sheet. |
| + |
| +The stage shows the server's HTML for the current slide (§4.1) with the |
| +editor bound to it: each text-bearing block is a ProseMirror view over |
| +its XmlFragment; canvas blocks get drag, resize handles, snap guides at |
| +the frame's thirds and other blocks' edges, and z-order from the |
| +inspector. Flow blocks reorder by drag or by the inspector's move |
| +buttons. |
| + |
| +The inspector is per selection: block props, slide props (layout, |
| +template, background, transition, phone behaviour), deck props (title, |
| +theme, accent, fonts). **Every inspector control is one drive operation |
| +(§7.2)** and the editor calls the same function the API handler calls, |
| +so a person and an agent can never do different things. |
| + |
| +Undo is per person (yjs `UndoManager`, scoped to that client's origin). |
| +Keyboard: the ordinary text shortcuts, plus `Cmd/Ctrl+Enter` for a new |
| +slide, `Cmd/Ctrl+D` duplicate slide, `Cmd/Ctrl+Shift+F` present. |
| + |
| +### 3.2 The desktop/phone switcher |
| + |
| +A three-state control on the stage: **desktop**, **phone**, **both**. |
| +Phone view renders the stage at 390 CSS pixels wide with the phone |
| +layer applied. Edits to layout while in phone view go to the `phone` |
| +override map (§2.5); edits to text go to the text (there is one text). |
| +A block with an override shows a small badge in both views with "reset |
| +to desktop" in its inspector. "Both" shows desktop and phone side by |
| +side, the phone read-only. |
| + |
| +### 3.3 Collab |
| + |
| +Docs' live-sync spec is adopted as written |
| +(`oficina/docs`, `docs/superpowers/specs/2026-09-01-live-sync-design.md` |
| +§3 to §7): `GET /d/{sqid}/stream` SSE emitting `event: update` with a |
| +seq; bytes by `GET|POST /d/{sqid}/updates`; the cursor+1 rule; broadcast |
| +on success only; the 15 s ping, 30 s write deadline, 5 minute TTL; |
| +hibernation-safe because the stream carries no bytes. Slides is the |
| +second implementation, which is the estilo rule's trigger: whatever |
| +Docs' hub and stream code turns out to need in both places is extracted |
| +that week, and the Slides spec records the extraction when it happens. |
| + |
| +Presence rides the same stream as a second event type, `presence`, |
| +carrying `{client, name, colour, slide, block}`; the server holds |
| +presence in memory only (it is not document state) and drops a client |
| +after two missed pings. The editor shows avatars on the strip |
| +thumbnails and a coloured outline on the block another person has |
| +selected. Docs owns the first build of the presence stack per estilo's |
| +ownership table; if Docs has not built it when Slides needs it, Slides |
| +builds it and Docs adopts. |
| + |
| +### 3.4 Presenting, speaker notes, the remote |
| + |
| +**Presentation session.** A row: `deck`, `presenter` (actor), `slide` |
| +(index), `blackout` (bool), `started_at`, `ended_at`, `code` (six |
| +characters, unambiguous alphabet), `remote_token_hash`. Created by |
| +`POST /d/{sqid}/present`, which returns the session sqid and, once, a |
| +remote capability URL. Ended explicitly or after 12 hours. One live |
| +session per deck per presenter. |
| + |
| +**Presentation mode.** `GET /p/{session}` is the fullscreen surface: |
| +the current slide filling the viewport, keyboard (arrows, space, `b` |
| +for blackout, `n` for notes, `Esc`), swipe, a thin progress bar, and |
| +the slide's transition. The page subscribes to the session's SSE stream |
| +(`GET /p/{session}/stream`, `event: slide` with the index and blackout |
| +state) and applies what it hears, so it is the same page whether the |
| +presenter's keyboard or the phone moved the deck. It works with scripts |
| +off as a page per slide with next and previous links (§8). |
| + |
| +**Presenter view.** `GET /p/{session}/notes` shows the current slide, |
| +the next slide, the notes, a clock and elapsed time, and the pairing |
| +code and its QR. Meant for a second window on a desktop or the |
| +presenter's phone. Its controls post the same commands as the remote. |
| + |
| +**Remote.** `GET /r/{code}` on a phone: enter the code or scan the QR, |
| +which carries the remote capability URL. The page shows notes, current |
| +and next slide thumbnails, and **next**, **previous**, **blackout**, |
| +plus **end**. Commands are `POST /p/{session}/command` with |
| +`{op: next | prev | goto | blackout | end, slide?}` and the remote |
| +capability as a bearer (or, scriptless, as a form field). The presenting |
| +browser applies the resulting `slide` event. Latency target under 300 ms |
| +on the same continent. The capability lives as long as the session. A |
| +native remote later speaks exactly `present`, `command` and the |
| +`stream`; nothing else is needed. |
| + |
| +**Audience follow.** A share link (§5) opened while a session is live |
| +for that deck shows "follow the presenter". Following subscribes the |
| +viewer to the session stream read-only; the viewer can detach and |
| +browse, and reattach. |
| + |
| +--- |
| + |
| +## 4. Rendering and export |
| + |
| +### 4.1 The HTML renderer |
| + |
| +One Go package, `internal/render`, turns a slide (from the Rust core's |
| +JSON view) into HTML: a `<section slides-slide>` with the template's |
| +grid or the canvas frame, blocks as semantic elements (`h1` to `h3`, |
| +`p`, `ul`, `ol`, `blockquote`, `figure`, `pre`, `table`), theme tokens |
| +as CSS custom properties on the section, and the phone layer as |
| +`data-phone-*` attributes that the stylesheet honours under the phone |
| +media query. It serves the viewer, the presenter, the remote thumbnails |
| +and the editor stage. |
| + |
| +The one rule's refinement recorded here: the session had said SVG would |
| +serve the scriptless viewer. HTML is the better choice for every live |
| +surface because it reflows, it is accessible without extra work, and it |
| +is what the editor must bind to. SVG remains for export and thumbnails, |
| +where a fixed geometry is the point. |
| + |
| +### 4.2 The layout engine and SVG |
| + |
| +`internal/layout` measures text with the theme's fonts (self-hosted |
| +TTFs embedded in the binary, metrics via `golang.org/x/image/font/sfnt`) |
| +and places blocks in a 1920 by 1080 frame, producing a paint list: |
| +rectangles, images, text runs with font, size, position. `internal/svg` |
| +serialises a paint list as SVG with text as `<text>` runs and images by |
| +reference. Thumbnails are the SVG scaled down, served as SVG. |
| + |
| +Divergence from the browser is confined to line breaking inside a text |
| +block and is small when fonts match, the Docs finding. It is not zero |
| +and this spec does not claim it is. |
| + |
| +### 4.3 PDF |
| + |
| +`core/` (Rust) gains a second module beside the CRDT: `slides_pdf`, |
| +wrapping `svg2pdf` with `usvg` and `fontdb` fed the same embedded fonts, |
| +compiled to wasm32 and run under wazero like the CRDT core. Input: one |
| +SVG per slide plus the fonts; output: one PDF, 1920 by 1080 points per |
| +page, with the speaker notes as an optional second page per slide |
| +("with notes" export). Images are embedded at their stored resolution |
| +capped at 2000 px on the long edge. |
| + |
| +If `svg2pdf` under wasm32 proves unworkable in the plan's spike, the |
| +fallback recorded now is a Go PDF writer over the paint list directly, |
| +reusing Docs' font subsetting (`oficina/docs` `internal/pdf`) by |
| +extraction to a shared module, not by copy. |
| + |
| +Export runs as a rastrillo job: `POST /d/{sqid}/export` starts it, the |
| +status page polls, the result is a presigned GET from the object store |
| +that expires in one hour. A deck over 200 slides is refused with a |
| +message. |
| + |
| +--- |
| + |
| +## 5. Sharing and links |
| + |
| +Rungs are suite ruling 15's: **private** (the creator), **granted** |
| +(named people on this instance), **team** (everyone on the instance can |
| +open it). Every deck starts private. Editing needs sign-in and a rung of |
| +`granted` or `team`; a `team` deck is editable by every member, which is |
| +the Docs reading and the one that suits a small team. |
| + |
| +A **share link** is a read-only capability, calendar's pattern |
| +(`oficina/calendar` calendar design §6): `GET /s/{token}` mounted |
| +outside the session group, only the token's SHA-256 stored, the URL |
| +shown once at creation, revocable from the deck's share page, with a |
| +view count. The link opens the viewer (§8) with the deck as it is now, |
| +not a snapshot. If a presentation session is live, the link offers |
| +follow (§3.4). |
| + |
| +The **present link** is the same capability with `?present=1`, opening |
| +the fullscreen viewer directly. |
| + |
| +An "edit link" does not exist; ruling 15 says an edit capability is a |
| +sign-in that mints a guest grant, which waits on the home. |
| + |
| +--- |
| + |
| +## 6. Identity and the instance |
| + |
| +### 6.1 Lone shape |
| + |
| +Sign-in is Docs' `internal/docs/auth.go` adopted whole: rastrillo/auth |
| +magic links, `SLIDES_ALLOWED_ADDRESSES` gating `POST /signin` in front |
| +of `Begin` and in `Authorize`, `SLIDES_ORIGIN`, `SLIDES_INSTANCE_KEY`, |
| +`SLIDES_SMTP_*`, each a startup refusal outside `-env development`. The |
| +same 303 for permitted and non-permitted addresses. This is the second |
| +copy of that file, which is the extraction trigger: the plan includes a |
| +task to propose `auth.go` to rastrillo or estilo as a package. |
| + |
| +The person row is `users(id, email, name, colour)` created on first |
| +sign-in. `sessions.Current(r).Subject` is the email, mapped to the row |
| +before any scoping, per the rastrillo §5 warning. |
| + |
| +### 6.2 Actors |
| + |
| +Every write records an **actor**: `person:<user id>` or |
| +`agent:<agent id>`. Presentation sessions, share links, exports, agent |
| +tokens and the deck's activity list all carry it. The Y.Doc update log |
| +carries it too (a column on the updates table, not in the CRDT). |
| + |
| +### 6.3 Deploy |
| + |
| +One static binary, `CGO_ENABLED=0`, `linux-arm64`, `make release` with |
| +the version stamped, `carlos deploy --app slides --account oficina` |
| +with the build probe on `/api/version`. `carlos store create` for the |
| +object store; `carlos domains attach slides.oficina.works`. Secrets |
| +`SLIDES_INSTANCE_KEY` and `SLIDES_SMTP_PASS` via `carlos secrets`, the |
| +rest via `carlos env`, mirroring Docs' set. |
| + |
| +--- |
| + |
| +## 7. The drive surface |
| + |
| +### 7.1 Principals |
| + |
| +A signed-in person mints an **agent token** from Settings, naming the |
| +agent (`claude`, `codex`); the app stores `agents(id, name, operator, |
| +token_hash, created_at, revoked_at)` and shows the token once. The |
| +agent's principal is `<name>~<operator email>`; every write it makes is |
| +badged with a robot mark and the operator's name in the activity list, |
| +following amadan's own convention. A revoked token fails closed. Tokens |
| +are `Authorization: Bearer` on the API only; the API never reads a |
| +session cookie and the HTML routes never read a bearer. |
| + |
| +### 7.2 Operations |
| + |
| +`POST /api/v1/decks/{sqid}/ops` takes a JSON array of operations and |
| +applies them in order inside one transaction against the Y.Doc through |
| +the Rust core, producing one update, which is then broadcast exactly as |
| +a browser's POST would be. Operations, each with the ids of §2: |
| + |
| +- deck: `set_meta` |
| +- slide: `add_slide` (after, layout, template), `remove_slide`, |
| + `move_slide`, `set_slide` (props), `set_notes` (text as Markdown |
| + subset, converted server-side), `duplicate_slide` |
| +- block: `add_block` (slide, type, props, text, after or box), |
| + `remove_block`, `set_block` (props, box, z, region), `set_text` |
| + (Markdown subset), `move_block`, `set_phone` (the §2.5 map) |
| +- asset: `register_asset` after a presigned upload obtained from |
| + `POST /api/v1/decks/{sqid}/uploads` |
| + |
| +Reads: `GET /api/v1/decks` (list), `GET /api/v1/decks/{sqid}` (the JSON |
| +view, with an `ETag` of the seq), `GET /api/v1/decks/{sqid}/slides/{id}.svg` |
| +(a thumbnail, so an agent can look at what it made), |
| +`GET /api/v1/decks/{sqid}/export.pdf`. `POST /api/v1/decks` creates. |
| + |
| +The editor's inspector calls the same `ops` function in-process through |
| +the JS side of yjs; the server's `ops` handler calls the Rust core. The |
| +fixtures (§2.8) include one file per operation so both sides are held |
| +to the same result. |
| + |
| +### 7.3 MCP |
| + |
| +An MCP server is a thin adapter over §7.2, deferred until a consumer |
| +asks for it by name, memoria's rule. The ops are designed so each maps |
| +to one tool with the same name. |
| + |
| +--- |
| + |
| +## 8. The scriptless floor |
| + |
| +With scripts off, a person can: sign in, list decks, create a deck |
| +(title and theme, from a form), open a deck as a paged viewer (one slide |
| +per page, next and previous links, notes below when the deck allows), |
| +present from a share link as the same paged viewer, read the presenter |
| +notes page, download a PDF, manage sharing, mint and revoke agent |
| +tokens, and use the remote (each button is a form that POSTs and |
| +redirects back). They cannot edit slide content; the editor is the one |
| +surface that needs the bundle, Docs' ruling. |
| + |
| +WCAG 2.2 AA with no exemptions, 24 by 24 targets, 320 px reflow (the |
| +canvas stage may scroll inside its own region in the editor, the estilo |
| +allowance), colour never alone, every visible string a `rastrillo.T` |
| +key. |
| + |
| +--- |
| + |
| +## 9. Deliberately not built yet |
| + |
| +| Not built | Trigger | |
| +|---|---| |
| +| Sign-in through the home; guest grants; edit links | The home's identity layer (home spec §5.3, §9). | |
| +| Charts from Sheets by S6 | Sheets' S6 server exists and a deck wants a live number. | |
| +| Video, embeds, audio | A person asks and the platform has a media store policy. | |
| +| Follow-the-presenter inside Meet | Meet v1 ships. | |
| +| Native remote | The web remote has been used in anger for a month. | |
| +| MCP | A consumer names it. | |
| +| 4:3 and custom aspect | A person asks; the key already exists. | |
| +| Cross-deck asset library, templates gallery beyond built-ins | Ten decks on one instance. | |
| +| Comments on slides | Docs' comment thread lands in estilo. | |
| +| Version history and named snapshots | Docs' snapshots design is adopted rather than reinvented. | |
| + |
| +--- |
| + |
| +## 10. Slices |
| + |
| +1. **Live at `slides.oficina.works`:** scaffold, sign-in, deck list, |
| + Rust core with the schema and JSON view, HTML renderer for both |
| + modes with themes, viewer, share links, presentation sessions with |
| + presenter view and phone remote, scriptless floor throughout, the |
| + drive API for create and ops, first deploy. |
| +2. **Editor and collab:** the bundle, live-sync adoption, presence, the |
| + switcher, undo, asset upload. |
| +3. **Export:** layout engine, SVG thumbnails, the PDF module, the export |
| + job. |
| +4. **Extractions:** `auth.go` and the live-sync hub to shared modules; |
| + suite discussion outcomes adopted. |
| + |
| +Slice 1 is what Paul asked to try first. It is a deck a person or an |
| +agent can build through the API and present from a phone before the |
| +editor exists, which is also the order that proves the model before the |
| +most expensive part is built on it. |
| + |
| +--- |
| + |
| +## 11. Testing |
| + |
| +- Rust core: unit tests per operation; fixture replay against the JS |
| + generated updates (`fixtures/verify-rust.mjs` pattern from Docs). |
| +- Renderer: golden HTML per template and per block type, both modes, |
| + both viewports; an axe pass in the browser drive. |
| +- Presentation sessions: a Go test driving two subscribers and a remote |
| + command, asserting both see the slide event within the deadline. |
| +- Share links: token stored hashed, revoked link 404s, live-session |
| + follow offered only while live. |
| +- Drive API: every op has a fixture; a bearer from a revoked agent is |
| + refused; the actor column names the agent. |
| +- Scriptless: the browser drive with JavaScript disabled walks sign-in, |
| + create, view, present, remote. |
| +- Deploy: `/api/version` reports the stamped version on the live host |
| + before the deploy is called done. |
| + |
| +--- |
| + |
| +## 12. Review record |
| + |
| +Revision 1: unreviewed. The review pass is the first item after slice 1 |
| +ships, so that the reviewer has a running instance and not a document. |