diff --git a/documentation/plans/examples-grid-plan.md b/documentation/plans/examples-grid-plan.md index dfd15ff97d..1e16ee3546 100644 --- a/documentation/plans/examples-grid-plan.md +++ b/documentation/plans/examples-grid-plan.md @@ -2,10 +2,12 @@ _Drafted 2026-09-28, from the fit conversation that produced principles §10 and §9.6, with Stages 1–8 built and #3704 (attribute slots, second form) -the last heavy feature. Status: DIRECTION AGREED in conversation; nothing -here is built. This page is the checklist for the example set as a whole; -the two flagships get their own plans (linked below) and this page does not -design them. Design record: +the last heavy feature. Status: the apps that already exist are as far as +they go (Handoff, 2026-10-12). The left column landed on #3717 in the +shapes review accepted. The flagship rebuilds, `board`, retiring `room`, +and the V5 decision are handed off. This page is the checklist for the +example set as a whole; the two flagships get their own plans (not written) +and this page does not design them. Design record: `documentation/server-components/server-components-principles.md` §10 (fit) and §9.6 (live mutations seed). The grid is the one in ["The Grand Unifying Architecture of Frontend"](https://dev.to/playfulprogramming/the-grand-unifying-architecture-of-frontend-bhk) @@ -96,6 +98,12 @@ SSR compile, so neither depends on the directive's level; a non-exported wrapper registers exactly as an exported one. The first example PR confirms with its build: passing with the `server-only` markers in place (pruning precedes resolution) and failing on a deliberate client import. +_Confirmed by `hackernews`' build (2026-09-29): both hold, the references +are named from their bindings (`getStory-`, the same in both twins), +and a module with the marker needs +`/// ` in +`vite-env.d.ts`, since TypeScript 6 rejects an undeclared side-effect +import (TS2882)._ ## The map @@ -125,17 +133,72 @@ on the left (`hackernews`, `notes`), and is its own comparison: the same primitives with the client owning the markup (`board`) and with the server owning it (`chat`). +## Handoff (2026-10-12) + +The examples that exist are finished to what the runtime can do today. No +new product, and no example waiting on G1, G2, or V1–V5. #3717 is that set. + +Landed, in the shape review accepted rather than the shape first written +here: + +- `hackernews` / `hackernews-spa` — authoring layout, server routes, + cacheable GETs. Comment collapse is native `
`, not a binding + slot. +- `notes` — React's split, single-flight mutations, occlusion specs. +- `todos` — the SPA control. The README names the coordinate. +- `todos-server` — forms, binding slots, server-rendered errors. Intent is + `aria-busy`, not an optimistic store. + +Kept, with no further pass: + +- `rendering`, `effect`, `sierpinski` — off-grid on purpose. +- `chat` — the streaming demo that stands in the bottom-right corner. One + generation, a fake model, no threads. Not the flagship. +- `room` — the live-declaration test bed, data and markup from one + declaration. It stays until the flagships exist. +- `migrating-element` — the canvas. A mini-player waits on V5. + +Handed off, in this order: + +1. `chat-flagship.md`, then the rebuild. Absorbs `room`'s `/`. +2. `board-flagship.md`, then `board`. Absorbs `room`'s `/live`. There is + no `board` today. +3. Delete `room` once both exist. +4. V5, then the `migrating-element` decision. +5. G1 and G2, if a later example needs text positions or the binding-slot + execution model. Nothing landed here does. +6. V1–V4, if `todos-server` is later brought to the optimistic toggle. + The landed example does not use that shape. + ## Per-example disposition -### `hackernews` — bottom-left, reads. KEEP; collapse → binding slot, layout, README. Blocked on G1, G2 +### `hackernews` — bottom-left, reads. LANDED (#3717). Built 2026-09-29, revised The front door: the simplest server component, navigation over server -markup, a single stateful client concern. Its layering moment is comment -collapse — client state on server-rendered elements deep in a tree, surviving -navigation. Today that is a `Toggle` client component wrapping a template -slot (`toggle={p => {p.children}}`); under §9.2.3's -placement principle a thread exists because the server has comments, so it -is server markup and the collapse is a binding slot. +markup, and no client code. Revised in review (2026-09-29): the collapse only +hides replies, which is native `
`, so a binding slot there was a +heavier way to do what the browser already does. Both twins now render the +same `
`, every route in this twin is a server route, and the client +ships the router and the loading boundary. A later revision dropped the +navigation dim: the current page stays up until the next is ready. The +binding-slot demos are +`todos-server` and `chat`'s copy button (`notes`' search field was one until +its shell moved to the client; see `notes` below). + +Closed (2026-09-30): the proof that no content is sent twice when a client +component places server content conditionally (occlusion: an unplaced region +ships as an `sc:region:` record, never as markup too) had no end-to-end test. +The early demo showed it live (`b3f48999e`, deep replies collapsed by +default) and `63cd06688` dropped it when the collapse became +`display: none`; the only spec, `frames-occlusion-client.spec.tsx`, fed the +client hand-written records. `test/server/frame-occlusion-document.spec.tsx` +now covers the server side (each excerpt once: markup where placed, a record +where not; late placement locked to records), and +`test/hydration/frame-occlusion-document.spec.tsx` the client (adopt, then +expand, collapse and re-expand with the network stubbed to throw; each text +on screen once). `notes`' sidebar excerpt is the live demo. + +The superseded binding-slot shape, as reviewed and first built: Target shape (reviewed 2026-09-29): the recursive `Comment` is a server component and the client never sees the tree — per comment with replies, one @@ -155,7 +218,27 @@ gains the `id` the data already carries; the README's "`$key` keeps it attached" claim becomes true (today no key is passed); the bundle check can grep `comment-children` too. -### `hackernews-spa` — top-left. KEEP; layout only +_As built:_ the shape above, with `StoryDefinition.id` also typed `number` +(the capture and the live API both carry numbers). The thread's markup is +byte-identical to the twin's with hydration and slot markers stripped +(feed and user pages too); 652 of the 1,406 comments have replies, so 652 +fills. In the browser the collapsed state keeps its element and text node +through a `revalidate("story")` refetch. The bundle check greps the client +JavaScript, since `app.css` carries both class names. + +Follow-up in review: the feeds and the user page are server routes +(`serverRouteComponent`, router 2.0.0-next.31), so they have no route +component and no `preload`. The feeds are one `/:type?` route filtered to +the five feed names, with `page` from a hand-written search schema; the SPA +twin mirrors the pattern and filter and parses `?page` itself. (The story +route kept its component for the fill until the move to `
`, when it +became a server route too.) The nav is outside `Loading`, +since it does no I/O. Router next.29 broke client navigation to the user +page: it sent a schema-less route's args as `{ params, search: undefined }`, +which the JSON argument check rejects, and nothing was logged. next.30 +(#615) fixed that, so every example that uses the router moved to next.31. + +### `hackernews-spa` — top-left. LANDED (#3717). Built 2026-09-29 The twin; exists only as the comparison. README names the coordinate. Takes the authoring layout: each route file's `query` carries its server function @@ -164,25 +247,55 @@ module-level `"use server"` file). Diffing the twins then shows the thesis at the route file: the same `getStory`, returning JSON in one and markup in the other, and client components in one only. -### `notes` — middle-left, the RSC coordinate. KEEP; authoring layout + README +### `notes` — middle-left, the RSC coordinate. LANDED (#3717). Built 2026-09-30 React's own server-components demo ported: client islands whose state -survives server updates around them, single-flight mutations by redirect, -the search field as the idiomatic binding slot. Its layering moment is the -editor keeping its draft while the sidebar list refreshes around it — the -"shared client state preserved" line the HTML-partial tools cannot cross. -Behavior unchanged; the code moves to the authoring layout (queries and -actions inline in the files that use them, `server/` for `db.ts`) and the -README is repositioned. The overlap with `chat` (both are -sidebar + viewer + mutations) is intentional: opposite sides of the grid, +survives server updates around them, and single-flight mutations by +redirect. Its layering moment is the editor keeping its draft while the +sidebar list refreshes around it — the "shared client state preserved" line +the HTML-partial tools cannot cross. The code is in the authoring layout +(queries and actions inline in the files that use them, `server/` for +`db.ts`) and the README names the coordinate. The overlap with `chat` (both +are sidebar + viewer + mutations) is intentional: opposite sides of the grid, different audience. -### `todos` — top-left. KEEP as is +Revised in review (2026-09-30), toward React's own shape: + +- The shell is a client component, as React's `App.js` is. Only the sidebar + list and the note preview are server components. The server shell had + existed only to host a binding slot: once the shell was on the client, + the search field was a client component, as it is in the demo. Finding: + a binding slot needs a position inside server markup, and in this app the + only such positions are the per-note ones `SidebarNoteContent` already + owns. +- The editor's data is a plain query returning the note (`getNoteEdit`). + Before, it was a server component used as a data loader, filling the + editor slot with raw text. +- The excerpt mounts only while expanded, as in the demo, so collapsed + excerpts ship once as records (the occlusion specs above). +- `router.tsx` holds the route table, the root preload and `getNoteList`, + and `server-config.ts` passes that Router to the flight collector. This is + the fullstack template's layout. The collector takes a router instance + because not every app uses Solid Router; each router template does this + wiring itself. + +### `todos` — top-left. LANDED (#3717). Built 2026-09-30 The SPA control: optimistic store over `refresh`, client-held API mock. -Twin of `todos-server`; also the pedagogical control for `board`. +Twin of `todos-server`; also the pedagogical control for `board`. No server +side, so the authoring layout does not apply. The pass: the README names the +coordinate and the twin; Vite 8, as in the other grid examples; the error +map moves into `createTodos`; an unused `TodoActions` type goes; both +checkboxes use `onChange`. The `todos.ts` header keeps its instruction (read +it for the layering, not the syntax), which exists because agents reading +the example missed the layering, but it is no longer phrased as a rebuke. -### `todos-server` — bottom-left, writes. RESHAPE +### `todos-server` — bottom-left, writes. LANDED (#3717), revised + +Shipped as forms with binding slots and server-rendered errors. A +submitting row shows intent with `aria-busy`, not an optimistic store. +The optimistic-toggle target below was not built, and V1–V4 do not block +the example that shipped. Today: the §9.2.3 acceptance gate — seven client-owned positions per row, an intent record, a client error map, multi-flight `refresh`. Principles §10 @@ -206,7 +319,22 @@ README may describe it as the increment). Bulk actions stay plain forms. Pending feedback, if the router marks a submitting claimed form (`aria-busy` / `data-pending`), is CSS only — see V4. -### `chat` — bottom-right FLAGSHIP. REBUILD (own plan) +**Revised 2026-09-30, built (pending review of the feel).** Dropping the +bulk optimism left nothing a form could not do, and binding slots exist to +add back exactly that optimism, so the example has to keep it to prove the +point. Built shape: every control a form (`.with()` for rows), single-flight +responses; two binding slots — `row` (class, hidden, the toggle's value and +`aria-pressed`, retry title and handler) and `list` (empty, toggle-all +state and value, clear's visibility, the count as a text position, the bulk +error) — fed by one intent store each action's `onSubmit` writes. +Cross-element optimism (toggle-all, clear-completed, the count) is the +layering moment. Failures are returned `{ error }` values read through +`useSubmissions`, each action's `onSettled` superseding earlier answers to +the same question; the no-JS path reads the same list from the router's +flash cookie. Add is not optimistic (a busy form, the error with a retry). +The pending-row template slot is gone. + +### `chat` — bottom-right FLAGSHIP. STANDING DEMO; rebuild handed off Plan: `documentation/plans/chat-flagship.md` (to write first). Realistic AI chat: threads durable and addressable; generation as a job that outlives @@ -221,7 +349,7 @@ retry. Absorbs `room`'s `/` page. The `usage` projection goes (token usage is a number on the finished message), which removes the container tier's only example — flagged, consistent with principles §10.5. -### `board` — top-right FLAGSHIP. NEW (own plan) +### `board` — top-right FLAGSHIP. HANDED OFF (not built) Plan: `documentation/plans/board-flagship.md` (to write first). Pure data tier, no frames: one board of lists and cards as a `live` source, reconciled @@ -238,14 +366,14 @@ it). `board` is also the top-right form of `chat`: a live thread read by client components into an optimistic store is this architecture with a transcript instead of lists, which is why no separate `chat-spa` exists. -### `room` — RETIRE +### `room` — STAYS; retirement handed off with the flagships Stage 8's test bed. `/` becomes `chat`'s thread; `/live` becomes `board`. The chaos switch and status pills were apparatus; connection state stays visible in both flagships through `onstatus`, as an affordance the apps would have anyway. Delete once both flagships exist. -### `migrating-element` — CONDITIONAL on V5 +### `migrating-element` — STAYS as the canvas; the decision is handed off with V5 Today a canvas, because the README records that `