Design module: component palette, drag and drop, emitted source - #203
Open
pathscale wants to merge 2 commits into
Open
Design module: component palette, drag and drop, emitted source#203pathscale wants to merge 2 commits into
pathscale wants to merge 2 commits into
Conversation
added 2 commits
August 31, 2026 20:55
A Design module for composing @pathscale/ui components and reading the
source they emit.
One IR, two emitters, which was the H6 decision. `document.ts` holds a
tree edited by pure functions and knows nothing about a target;
`emit/tsx.ts` produces a plain Solid component importing from
@pathscale/ui, and `emit/layout.ts` produces the `.layout.tsx` and
`.recipe.ts` pair solid-layouts compiles, matching what chuzz already
runs in `apps/chuzz/frontend/local-ui`. Adding a third target is a file
and a line.
Artboards render in the app's own document, the other H6 decision. Hit
testing is `closest("[data-design-id]")`, so no bridge is involved, the
control socket sees every node, and ps-qa can address them by id. The
canvas takes pointer events in the capture phase and stops them there,
which is what lets you select a real Button instead of pressing it.
`dist/layouts.manifest.json` carries a name and a kind per component and
nothing a properties panel can read, so `catalog.ts` curates 22 entries
by hand. It is shaped like the recipe metadata upstream already has, and
each entry records the recipe it was transcribed from, so a generator can
replace it wholesale later. That generator has to land in UI and
solid-layouts, outside this package. It will also need to cross-check
`dist/index.d.ts`: the manifest is not an import list, and Kbd is in it
without being exported from the package root.
Everything drives by accessible name as well as by pointer, so the whole
module is reachable from ps-qa. Tests assert the emitted source as text,
since the source is the deliverable: one dropped Button emits exactly the
import and the element.
The pencil beside the gauge opens Design the same way the gauge opens Analytics: one tab, reused if already open. Five small edits outside `features/design/`, each the same shape the Analytics tab already uses: the `TabKind` union, its icon, the pane switch, the `openDesign` action, and the strip button.
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.
Implements H6. A Design tab where you compose
@pathscale/uicomponents onan artboard and read the source they emit, live.
The two decisions H6 said to ask about
What is emitted: one IR, both emitters.
document.tsis a tree edited bypure functions that knows nothing about a target.
emit/tsx.tsproduces aplain Solid component importing from
@pathscale/ui, pasteable as-is.emit/layout.tsproduces the.layout.tsxand.recipe.tspairsolid-layouts-librarycompiles, matching what chuzz already runs inapps/chuzz/frontend/local-ui. Both are on screen behind a toggle. A thirdtarget is a file and a line.
The two differ structurally, and the difference is not cosmetic: a layout
template has exactly one root element because
slot.roothas to landsomewhere, so it always wraps, while the TSX emitter returns a lone dropped
Button bare.
Where artboards render: the same document. Hit testing is
event.target.closest("[data-design-id]"), so no bridge is involved, thecontrol socket sees every node, and ps-qa can address each one by id. The
canvas takes pointer events in the capture phase and stops them there, which
is what lets a click select a real Button instead of pressing it, with no
pointer-events: nonelayer that would have broken the hit test it wasmeant to serve.
The metadata gap
dist/layouts.manifest.jsongives a name and a kind per component andnothing a properties panel can read, so
catalog.tscurates 22 entries byhand: 7 layout, 9 display, 6 form. It is shaped like the recipe metadata the
library already carries, and each entry records the recipe it was
transcribed from, so a generator can replace it wholesale. Two notes for
whoever writes that generator, both learned here:
recipe({ props, defaults }); older ones export aCLASSESconst whosenested keys are the same axes. It has to read both.
Kbdis in it and is not exportedfrom the package root. A curated entry for it typechecked as metadata and
failed to compile the moment the canvas tried to render it, so it was
dropped. Generating from the manifest alone would emit imports that do not
resolve; cross-check
dist/index.d.ts.Verification
Per
docs/ui-verification.md, the emitted source is asserted as text, whichis the deliverable and needs no renderer: dragging a Button onto an empty
canvas emits exactly the import and the element, and that is a string
comparison.
bun run typecheckcleanbun run lintclean, including both contract gatesbun run test:run: 426 passed, 44 files, 32 tests newbun run buildsucceedsEverything drives by accessible name as well as by pointer, so the palette,
the property rows, the target switch and the emitted source are all
reachable from ps-qa. The
.ronoutcomes for the Design tab are not in thisPR; the ui-control gate does not require them, because every input primitive
rendered on the canvas is one AgencyZero already imports and already covers.
Files outside
features/design/The ownership table gives this package
features/design/only. Five editsland elsewhere, each the same shape the Analytics tab already uses, and each
one line or one block: the
TabKindunion, its icon inTAB_ICON, the paneMatch, theopenDesignaction, and the strip button. Plus the two i18ncatalogues, appended to.
Not done
No layers tree, no multi-artboard, no persistence: the document lives for
the life of the tab. No prop metadata generator, which by H6's own reading
belongs in
UI/andsolid-layoutsand needs its owner's agreement.