oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

Product

<!-- impeccable:product-schema 1 -->

> How this file was written. Impeccable's init normally interviews > the user before writing anything here. This one was written from > repository evidence instead, during slice 1 and brought up to date at > the end of each slice since — most recently slice 3: the design > (docs/superpowers/specs/2026-09-07-slides-design.md, revision 1), the > plans (docs/superpowers/plans/2026-09-07-slice-1-live.md, > 2026-09-07-slice-2-editor.md and 2026-09-08-slice-3-export.md), > AGENTS.md, and the shipped code in internal/slides/ and > fixtures/. Facts traceable to a spec > ruling or to code are cited. Anything that is a reading rather than a > citation is marked [inferred — confirm] and should be corrected > rather than trusted.

Platform

web

Users

Primary: a member of one team, making and giving a talk. The instance boundary is the team — one deployed instance per team, not one multi-tenant app with teams inside it (spec §0.2, suite rulings 1 and 5). Sharing is therefore membership plus a link capability, not a permission matrix.

Secondary: an agent, driving a deck on a person's behalf. Not a metaphor and not a later feature — it was the only way to build a deck before the editor existed, and it is still a peer of it: an agent's ops and a person's keystrokes land in one document, in one order. An agent is its own principal, <name>~<operator email>, minted by a signed-in person and named in every write it makes (spec §1 ruling 7, §7.1). It never borrows a person's session, and the API never reads a session cookie.

Third: someone holding a share link, who has no account and gets a read-only deck (spec §5). If a talk is live, that same link offers "follow the presenter" (§3.4).

Access today is a magic link gated by SLIDES_ALLOWED_ADDRESSES (internal/slides/auth.go) — a per-instance allowlist, exactly Docs' shape, chosen because the suite home that will own identity is a skeleton (spec §1 ruling 8). Guest grants and edit links wait on that home (§5, §9).

[inferred — confirm] The team is assumed small enough that an address allowlist is the right admission mechanism. Nothing in the repo states a target team size.

Product Purpose

Slides is a presentation editor for the Oficina suite: a deck that looks like a Keynote deck on a desktop, reads like a document on a phone, is presented from a laptop and driven from a phone, exports to PDF, and is driveable by an agent through the same operations a person has (spec preamble).

Success is a deck that is genuinely the same deck everywhere it is seen — one block tree, rendered for a viewport rather than authored twice (§0.1). A phone does not get a cut-down deck; it gets the same deck reflowed.

Positioning

Three things a neighbouring presentation tool could not copy without rebuilding its foundations:

  1. One tree, one renderer, no second layout. The layout modes are two placements of the same blocks, and the phone layer is entries in the same tree (§2.3, §2.5). internal/slides/render is the only code that turns a slide into HTML, and it serves the viewer, the presenter view, the remote, and the editor's stage and thumbnails (§4.1) — the browser fetches that HTML rather than drawing its own. A product with a mobile view bolted on cannot make that claim afterwards, and neither can one whose editor renders slides a second time in JavaScript.
  2. An agent is a first-class author, badged as itself. Every write records person:<id> or agent:<id> (DeckUpdate.Actor, internal/slides/models.go; spec §6.2), and the agent's writes are attributed to the agent with its operator named, never laundered into the person's name. Slides is the first Oficina app to build that surface (§0.2).
  3. Presenting is a server-side session, so the room is one system. The presenter view, the projector, the phone remote and every audience follower read and write one record (§3.4, internal/slides/ presentation.go). A phone remote is not possible at all in a design where presenting is just a viewer with no session, which is the rejected alternative recorded in §1 ruling 5.

What is not shipped and must not be described as though it were: comments, version history, charts from Sheets, video and embeds, an MCP adapter, aspect ratios other than 16:9. §9 gives each a trigger rather than a date. And the first deployment has no object store bound, so image upload — which is built, and works against a bucket or a directory — answers 503 there and says so on the page, and an exported PDF is held in the instance's own memory for an hour instead of going to a bucket.

Operating Context

  • One Go binary per instance, CGO_ENABLED=0, cross-compiled to linux/arm64, on the CARLOS platform, at slides.oficina.works for the first team (spec §6.3, Makefile).
  • The deck is a Yjs document held by a Rust yrs core compiled to wasm and run under wazero (spec §1 ruling 3; core/, internal/slides/core). The Go side persists updates in SQLite, asks the core for a JSON view, and renders that view. This is Docs' pattern, adopted rather than reinvented.
  • Routes shipped today (internal/slides/app.go): sign-in and the deck list, /decks/new, /d/{sqid} and its notes, settings, share and delete pages; the editor at /d/{sqid}/edit with its slide HTML twin, its .svg thumbnail twin, its update log, stream and presence, and its upload, register and asset routes; /d/{sqid}/export, the job's status page under it and the .pdf beside that; /s/{token} for a share link; /p/{session} and its notes, stream, remote and command; /r/{code} to pair a phone. The share, present and remote routes sit outside the session group on purpose — the projector, the phone and the audience each carry their own capability and have no session at all.
  • The browser holds a Yjs document too. The editor's bundle is committed, hashed, and built by hand from fixtures/ (node build-bundles.mjs); the app builds nothing at run time and CI builds nothing for it. fixtures/ is also a second implementation of the operation vocabulary, held to the Rust one by generated vectors (spec §2.8) — an operation changes on both sides or on neither.
  • Local development is ./scripts/run-dev.sh: localhost:8091, a scratch SQLite database that starts empty every run, magic links logged to the terminal rather than mailed.
  • The scriptless floor is a product requirement, not a fallback (spec §8). Signing in, listing, creating, viewing, presenting, reading notes, managing sharing and using the remote all work with JavaScript off; each remote button is a form that POSTs and redirects. The editor is the one stated exception, and it says so in a <noscript> that links the viewer — a boundary declared in the spec before it was built, not discovered afterwards.
  • Work happens on a worktree branch and reaches main only by squash merge, after review and with the gate green. The review record is an amadan branch discussion, not a GitHub PR — this repo is hosted on an amadan hub (AGENTS.md).

Capabilities and Constraints

Shipped in slice 1: magic-link sign-in with an admission allowlist and a users row per person; deck list, create, settings, delete, three sharing rungs; the Rust core with the deck schema, the operation set and the JSON view; the HTML renderer with eleven block types, seven flow templates, the canvas frame, the phone layer and six themes; the paged viewer and the notes page; read-only share links, hashed and revocable; presentation sessions with the fullscreen surface, presenter view, phone remote and audience follow; the drive API for creating and driving a deck.

Shipped in slice 2: the browser editor at /d/{sqid}/edit — slide strip, stage, inspector for the deck, the slide and the selected block, text edited in place, canvas drag and resize with snapping, the desktop/phone switcher whose phone-view edits become overrides rather than changes everyone sees, undo scoped to the person who made the change, and presence. Under it: the live-sync wire (an update log served by cursor, an SSE stream of advisory seqs and whole presence sets, periodic snapshots so opening a long-lived deck is not a full replay), image upload browser-to-bucket with SVG sanitising and a deck-scoped check on every byte served, and the JS half of the operation vocabulary with the vectors that hold it to the Rust one.

Shipped in slice 3: the layout engine (internal/slides/layout), which measures a slide into a paint list with the twelve Liberation faces embedded in the binary and served to every slide surface; the SVG serialiser over that paint list, and a thumbnail route on all four capabilities; and PDF export — krilla compiled to wasm32 and hosted under wazero beside the CRDT core (core/pdf, internal/slides/pdfcore), driven by a rastrillo job whose status page polls itself and works with scripts off: one 16:9 page per slide, the speaker notes as pages of their own when they are asked for, the text as embedded subset glyphs, the images inlined. The API twins are POST /api/v1/decks/{sqid}/export and its two GETs.

Deliberately not built yet (spec §9): charts from Sheets, video and embeds, an MCP adapter, 4:3 and custom aspect ratios, comments, and version history. Each has a named trigger in §9 rather than a date.

Binding technical constraints:

  • A slide is one block tree, rendered many ways (§0.1). Nothing that matters to what a reader sees may live outside the tree.
  • Every write names its real actor, person:<id> or agent:<id> (§6.2). An agent's write names the agent and its operator, never the person (suite ruling 9, plus Paul's 2026-09-07 ruling that agents drive every Oficina app as their own identified principal).
  • Not-found and not-yours both answer 404, never 403 (internal/slides/decks.go), so a stranger holding a sqid cannot learn whether it is real.
  • Only the SHA-256 of a share link is stored, so the URL is shown exactly once and the page says so at that moment (share.go, §5).
  • Every user-visible string is a catalog key resolved through rastrillo.T (internal/slides/locales/en.toml, 339 keys today). Twelve base locales are the suite target; one, en, exists. A hardcoded English string is a bug — including in the editor's JavaScript, which reads its strings off data-text-* attributes the template renders from the catalog. Deck titles and timestamps are data, not authored copy, and are correctly not keys.
  • The document is the log, and presence is not in it. Every write is a row with an actor; a snapshot is a shortcut and never the truth. Presence lives in memory, reaches no log, is in no snapshot, and is gone when the process restarts (spec §3.3).
  • The browser is not a second renderer. The editor's stage shows the server's HTML for the current slide and rebinds after a structural change; building slide markup in JavaScript would recreate exactly the divergence §0.1 exists to prevent.
  • href is stored as the one-key map {"href": url} and the marks are named b, i, c, h, href (§1 ruling 12, §2.6). Forced by y-prosemirror, which has no hook to map mark names to attribute names and deletes a text node when a read throws — so the wrong shape loses the words, not just the link.
  • *Markup is the bare `rst- attribute grammar**, never class="rst-"; this app's own attributes are slides-`.
  • Themes are a built-in set of token bundles, six of them (paper, ink, sunset, forest, slate, candy — internal/slides/render/themes.go). A deck may override accent and the two typefaces; there are no per-block fonts, which §1 ruling 9 calls the fastest route to an ugly deck.
  • CGO_ENABLED=0 everywhere, and the committed wasm artifact must match a fresh build or the gate fails.

Brand Commitments

  • Name: Slides, in the oficina namespace at amadan.net/oficina/slides, hosted at slides.oficina.works for the first team (§1 ruling 11). English, not Spanish — the suite names generic apps in English.
  • The design system is rastrillo's, with estilo (amadan.net/oficina/estilo) as the suite variant. Vendor tokens rather than copying files by hand.
  • A slide is not themed chrome. The deck carries its own theme tokens as CSS custom properties on each <section slides-slide> (render/render.go); the app's chrome follows the design system and the slide does not follow the reader's preferences. [inferred — confirm] The spec does not spell this out for Slides the way Docs' §10.2 does for the page canvas; it is the reading the renderer implements.
  • The empty state is a designed screen, not a blank page, and says what the screen is for: "A deck is a set of slides you write, present and share. Start one and it will show up here." (slides.decks.empty.body).
  • Voice, as evidenced by the shipped catalog: plain, unhedged, second person, no exclamation marks, no product cheer. Headings carry no full stop ("No decks yet"); errors state what happened and what helps ("That link has expired or was already used. Enter your email below for a new one."); a warning goes in front of the button rather than in a help page ("Copy this now — it is not shown again").
  • What does not exist and must not be invented: no customers, no testimonials, no benchmarks, no pricing, no usage numbers, no launch date, no press. There is one instance and it is not live yet.

Evidence on Hand

  • Spec: docs/superpowers/specs/2026-09-07-slides-design.md, revision 1, drafted 2026-09-07 from a brainstorm with Paul the same day. It is unreviewed: §12 records that the review pass is the first item after slice 1 ships, so the reviewer has a running instance rather than a document. Treat its rulings as settled and its prose as unaudited. One amendment has landed since: ruling 12 and the rewritten §2.6, added during slice 2 when the fixtures proved the old href shape did not survive y-prosemirror.
  • Plans: docs/superpowers/plans/2026-09-07-slice-1-live.md (eight tasks) and 2026-09-07-slice-2-editor.md (seven), one slice each.
  • Consumed rulings: the suite sharing doctrine (oficina/suite, revision 8) by number — 1, 4, 5, 6, 9, 12, 15 — never restated here (§0.2). Docs' live-sync design is adopted as written for slice 2 (§3.3).
  • API reference: docs/api.md, every endpoint and every operation with an example, written for a person and an agent both.
  • The export, end to end: internal/slidestest/export_test.go walks the button, the job, the polled fragment, the SSE stream and the file, and asserts the first heading is extractable with pdftotext when the machine has one (visibly skipped when it does not). internal/slides/export_test.go does the same at the pipeline's own level, recovering every heading from the PDF's glyph runs through each font's ToUnicode CMap.
  • Sample content: internal/slides/sample.go seeds one deck on first sign-in, covering each flow template, a canvas slide and a phone-reflow slide — which is also the fastest way to see what the renderer does.
  • The contract between the two implementations: fixtures/data/*.json are operation batches, fixtures/vectors/ is what the JS side makes of them, and internal/slides/core/vectors_test.go requires the Rust core to agree. Five vectors between them exercise every operation in §7.2.
  • The gate: .amadan/ci.d/10-gate, run by CI and by hand, and .amadan/README.md for what it needs. Since slice 2 it also runs the JS toolchain — npm ci, node --test, the vectors regenerated and the bundle rebuilt, both diffed against what is committed — which is what makes the two claims above checkable rather than remembered.
  • The browser drive: internal/slidestest/browser_test.go walks the editor in a real Chromium and fails on any console error, failed request or JS value that reached the screen. It is deliberately outside the gate: this project's sandbox will not start a Chromium, and a gate that ran it would report "could not see" as green.

Product Principles

  1. State the degradation rather than let it be discovered. The spec refuses to claim pixel identity between the browser and the PDF and says where they may differ (§0.3, §4.2); the share page says the URL will not be shown again at the moment it is shown; the editor's image field, on an instance with no object store, says so on the page instead of waiting for somebody to choose a file and hit a 503.
  2. One tree, one renderer. Any second representation of a deck — a mobile layout, a print document, an editor's own idea of a slide — is a design mistake before it is a maintenance cost, because the two will diverge and the divergence will reach a reader.
  3. A capability is a capability, and nothing more. A share link never edits (suite ruling 15). An edit link does not exist, because an edit capability is a sign-in that mints a guest grant, and that waits on the home (§5).
  4. An agent is a principal, never a costume. It has its own token, its own name, its own revocation, and its own badge in the activity list (§7.1). The alternative — an agent borrowing a person's session — is recorded as rejected, not overlooked.
  5. The scriptless floor is the floor, not a fallback. A screen that only works with JavaScript is a screen that has not been finished (§8).
  6. A string a person can read is a catalog key. Localisation is a structural property of the app, not a phase.

Accessibility & Inclusion

WCAG 2.2 AA, no exemptions (spec §8, restated in AGENTS.md). Four requirements bind concrete decisions here:

  • 24×24 minimum target size, which the phone remote's buttons are sized for — it is a control someone uses in the dark, one-handed, while talking.
  • 320px reflow. The one allowance is estilo's: the canvas stage may scroll inside its own region in the editor (§8). Nothing in the viewer uses it. Below 1100px the editor's three panes stack — the strip becomes a horizontal filmstrip and the inspector a <details> at the foot — rather than scrolling the page sideways.
  • Canvas resize handles are pointer-only, and the keyboard has a real equivalent rather than a smaller version of the same thing: the selected block's overlay is focusable and named, arrows nudge, Shift nudges further, Alt resizes, and the inspector's Left/Top/Width/Height fields do it by number.
  • Colour never carries meaning alone, so a deck's sharing rung shows a text label beside its tone and never a bare colour.
  • alt on every image. The model makes it structural rather than advisory: alt is a required prop on an image block and may be empty only when decorative is true (§2.4).

The phone is not a degraded case. Reflow for flow slides and scale-to-fit for canvas slides, with a per-slide switch to reflow a canvas slide in reading order (§1 ruling 2), exists so that reading a deck on a phone is a supported way to read it.

Twelve base locales are the target set; today exactly one exists (en). That is a commitment, not a shipped state, and this file should not be read as though eleven more catalogs are sitting in the repo.