Feature: Start on workshop space template - #167
Merged
Conversation
…e to be opened Two things the first real layout needed and the declaration could not say. `route` scopes a panel to a path segment. A section declaring its own meta.panels needs none of it, but a shell that routes *itself* has no sections to hang a declaration on — which is how every showcase template works, and is what a workshop template built that way would hit immediately. `open: false` places a panel without opening it, and it exists because opening a module's panel means invoking the action its launcher declares — which is not always "open a panel". The call module's is goToCall, and goToCall *joins a call* when there is not one. A template that placed the call window would have started a call on entering the space. The default stays "open", because a declared panel nobody can see is pointless; the opt-out is for the case where the module's own verb is bigger than the panel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…of it The seventh showcase template, and the first about *panels* rather than a content arrangement. The other six show that channels, boards, playlists and events are one container seen differently; this one shows that where the surfaces around the content sit is also data. A call is running. Down the left the transcript, and beneath it a readout of what extraction is making of it. On the right the call. In the middle a board of the records the call produced — tasks and events — draggable and joinable. Then two more routes over the same material: tasks by state, and the record of past calls. The panels change with the route, which is the point: a board wants a transcript beside it and a task list does not. It mints nothing. The transcript is the collection module-transcribe already writes, the cards are the TaskBlocks and EventBlocks extraction already makes from it. The template is arrangement over both — the claim this package exists to make, one level up from content. Three of the four kinds of meta.panels entry are exercised: `module` for the transcript and the call, `node` for the extraction readout (this template's own schema, reading interpretationStore), `route` because a showcase template routes itself and has no sections to hang a declaration on, and `open: false` on the call so placing it never joins one. The board seed narrows to what extraction produces — the collection's children are also every utterance, and a board of six hundred transcript fragments is not a board. `manual` layout parks anything unplaced in a grid, so a freshly extracted record arrives somewhere sensible rather than stacked at the origin. Also teaches the schema validator the two meta keys the last PR added: it refused `chromeReserve` and `panels` outright, which is the gap working as intended — the types knew about them and the validator did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e it was asked to Three bugs the first real layout found, all in the layer that ships this. **A template's panel never mounted.** slotRegistry had no change channel, and TemplateProvider read nodes() once when it built the shell schema. Contributions used to all arrive at boot — modules register before the first render — so reading once was enough for years. A template declaring panels registers its frames reactively, long afterwards, into a list nobody read again: the dock resolved geometry for a panel that never mounted, which looks exactly like the panel being broken rather than absent. So the registry announces, and the shell schema re-reads on it. The node needs a `type` for that to reach anything: a typeless node renders its children through an unmemoized fragment, so the read is untracked and a getter would never re-run, while a typed one goes through `createMemo(() => renderChildren(node.children))`. `display: contents` keeps the wrapper out of the layout — the same trick dockFrame uses. The node's own identity never changes and renderChildren maps with a reference-keyed For, so a new panel mounts alone and a call's live video stays where it was. **A declaration was silently outranked by unrelated history.** Placements were keyed by dock id alone, so a transcript dragged while trying out one template kept that position under every other one and beat whatever the next template declared for it. That is the three-rung chain working and scoped wrongly: a drag is a fact about this panel *in this interface*. Keyed on the declaring template now. An interface that declares nothing keeps the unscoped key, so a panel somebody positioned in an ordinary space stays where they put it — the scope exists to stop declarations being overruled by unrelated history, not to make every template forget. **The board's placeholder sat under the switcher.** The graph is meant to run under the floating bar — that is what a full-bleed view with chrome over it looks like, and its controls are at the other corners. Text centred in the same box is not; it reads as content that has slid underneath something, because it has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for coasys-we ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Workshop — an MVP space built out of panels
Branch:
feat/workshop-spaceoffdev. Three commits, 20 files, +962/−24. The consumer half of thepanel-layout work merged in #166, and the first template in the repo that declares
meta.panels.Summary
Workshop is the seventh showcase template, and the first about the surfaces around the content
rather than an arrangement of the content itself. The other six argue that channels, boards,
playlists and events are one container seen differently; this one argues that where the panels sit
is data too.
A call is running. Down the left, the transcript as everyone speaks, and beneath it a readout of what
extraction is making of it. On the right, the call. In the middle, a board of the records the call
produced — tasks and events — draggable and joinable. Two further routes read the same material
differently: tasks by state, and the record of past calls with their transcripts. The panels change
with the route, which is the whole point: a board wants a transcript beside it and a task list does
not.
It mints no content model, like the six before it. The transcript is the collection
@we/module-transcribealready writes; the cards are theTaskBlocks andEventBlocks extractionalready makes from it.
Building it found two things the declaration could not express, one gap the previous PR left, and
three bugs — two of them in the shell rather than in the template.
Changes
What the first real layout needed
routeonTemplatePanel— scopes a panel to a path segment. A section declaring its ownmeta.panelsneeds none of it, but a shell that routes itself has no sections to hang adeclaration on. That is not a showcase-package property, incidentally: any template may own,
delegate or mix its route table, and these six happen to own theirs.
open: falseonTemplatePanel— places a panel without opening it. Opening amodulepanelmeans invoking the action its launcher declares, and that is not always "open a panel": the call
module's is
goToCall, which joins a call when there is not one. Without this, entering thespace would have started a call.
The template
WorkshopTemplate.schema.ts, registered in the showcase index, the template registryCATALOGUEand the seed — it appears in the picker as "Workshop" beside Channels and Timeline.
Three routes filling the screen behind a floating pill switcher, which is centred on the content
through
--we-chrome-center-xso it does not drift when a panel displaces an edge, and whichdeclares
meta.chromeReserveso floating panels clear it.The board uses the
boardseed over the call's collection, narrowed withcontainsto whatextraction produces — the collection's children are also every utterance, and a board of six
hundred transcript fragments is not a board.
manuallayout parks unplaced records in a grid;onNodeDragEndwrites drops back.The extraction panel is template-authored — a
nodeentry readinginterpretationStore— whichis the proof that gap 3 of the plan works end to end.
Three bugs, found by using it
A template's panel never mounted.
slotRegistryhad no change channel andTemplateProviderread
nodes()once. Every contribution until now arrived at boot, so reading once was enough foryears; template panels register reactively, afterwards, into a list nobody read again. The dock
resolved geometry for a panel that never rendered — which looks like the panel being broken rather
than absent. The registry announces now, and the shell schema re-reads on it.
The subtle half: the shell schema had no
type, and a typeless node renders children through anunmemoized fragment, so a getter would never have been tracked. It needed
type: 'Column'withdisplay: contentsto reachcreateMemo(() => renderChildren(node.children)). BecauserenderChildrenmaps with a reference-keyed<For>and the node's identity never changes, a newpanel mounts alone and a running call's video stays put.
A declaration was silently outranked by unrelated history. Placements were keyed by dock id
alone, so a transcript dragged while testing columns in the previous PR kept that position under
every other template and beat whatever the next one declared. That is the three-rung chain working
and scoped wrongly. Keyed by the declaring template now; templates that declare nothing keep the
unscoped key, so a panel positioned in an ordinary space stays where it was put.
The board's placeholder sat under the switcher. The graph is meant to run under the floating
bar; centred text in the same box is not, and reads as content that has slid underneath.
A gap the previous PR left
The schema validator did not know
meta.chromeReserveormeta.panelsand refused both outright —the types knew and
zodSchemas.tsdid not. Caught the first time a template used them, which is thecheck working.
panels[].nodeis typed but not structurally recursed: this schema is what checks anode, so recursing would be a cycle, and the validator's own traversal walks it anyway.
Deliberately thin
The MVP list. None of these is a bug; all of them are work not done.
modules.transcribe.collectionId, which the module releases when the call ends — so the boardempties. There is no picker for a past call's board, which is the first thing this needs to be
useful beyond a live meeting, and the Record route already lists exactly the collections it would
choose from.
assignment, no "who owes what" beyond who wrote it — the design for that was never settled, so this
is a placeholder shaped like the eventual thing rather than the thing.
TaskBlockandEventBlock; thereadout queries
TaskBlock. An extracted event appears on the board and not in the panel besideit.
$querytakes one entity, so the fix is two loops or a hoisted pair — deliberate rather thanoverlooked.
view; a third was needed and past calls were the obvious candidate.
EXTRACTEDrestatesEXTRACT_CLASSESfrom transcribe, becausetemplates → modulesis asideways edge. If that list grows, the board silently stops showing the new kind.
Known issues
The extraction readout appears twice during a call on the board route: once in the workshop's
panel, once in transcribe's fixed chrome under the call bar. They are the same data — transcribe's
runningPasses/settledPasses/hasActivityare thin filters over the sameinterpretation.activity()thatinterpretationStoreexposes. This is what the plan's PR Dresolves; it is a behavioural change to transcribe's call UX that wants its own manual testing.
Placement scoping is per template, not per (view, dock) as the plan specified. A panel dragged
on one route keeps that position on the others within the same template. Enough to stop the bug
that prompted it; not the whole of what was designed.
Known follow-ups
mobile-plan.md.slotRegistrygaining a change channel is the second registry to grow one by hand. That is thearchitecture plan's priority 4 (one registry primitive) arriving as a bill rather than a proposal.
Test plan
Run and passing:
@we/app-shell— 729 tests, including new coverage for slot-registry notification (register,remove, and staying quiet on a no-op removal) and for declaration scope.
@we/schema-shared— 615 tests;validateclean across 33 schemas including the new template;role-auditclean.@we/primitives138;@we/module-transcribe80.pnpm validate:seedclean;generate-templateslists 8 templates.pnpm buildclean with no tracked-file drift — the CI gate that caught PR Feature: Panel layouts transcript surface #166.Verified by hand in the Electron app: the Workshop template appears in the picker, the board route
renders, and the panels resolve from the declaration. The three bugs above were all found this way,
which is the argument for having built the consumer at all.
Not yet verified, and wanting a real call with a second agent: