feat(remix_cli): add the open-code component workflow - #177
Open
leoafarias wants to merge 45 commits into
Open
Conversation
Distribute editable Theme and Button source through a first-party CLI modeled on shadcn. `remix init` writes remix.yaml and a managed UI barrel; `remix add <item>` renders bundled registry templates into the consumer's lib/, resolves dependencies, runs Mix generation, and analyzes the result. Remix keeps behavioral ownership; the consumer owns the authored and generated files. Includes the clean-sheet review remediation: - lib/remix_cli.dart is a marker library with no exports. The supported surface is the `remix` executable, so lib/src/ can be reshaped without a breaking change. - Runtime requirements (remix, mix_annotations) are satisfied only by `dependencies`. A dev_dependencies-only declaration, or the same package in both sections, fails preflight before any process runs or file is written, and names the section to move it to instead of rewriting pubspec.yaml. - init and add report the filesystem transition that actually happened: single-file init repairs are named individually, and `--overwrite` on a missing item reports "Added" rather than "Updated". - Installed Button source documents its own hit-target tradeoff instead of referencing repository-only CLEAN_SHEET.md. remix_cli is versioned independently: it is excluded from the lockstep Remix/Fortal release scope in version.yml and absent from publish.yml pending a separate release gate. Verified with `melos run ci`: analyzer, 57 remix_cli tests, and the open-code checker's hosted and current-checkout phases (89 behavior tests each) against a freshly assembled consumer app.
Walk through init, dry-run/diff preview, add, the three levels of customization, the overwrite contract, and the dependency-section rules. Registered under Introduction in the sidebar.
The tool's own usage output calls itself `remix`, but every example showed `dart run remix_cli:remix`. Add a "Running the CLI" section that states the binary name and both invocation forms once, then use `remix <command>` throughout. Add the two sections the guide was missing: - Configuration: the full remix.yaml schema-1 surface, validation rules for prefix and paths.ui, and the fact that neither can be changed after install (state is tracked by file path, so editing the prefix by hand leaves installed source untouched while new source renders differently). - Updating: the pub upgrade -> --diff -> --overwrite review loop, a state/flag matrix for reinstall behavior, and manual removal. Every command, output string, and error message in the guide is transcribed from a real run against a scratch Flutter package.
…lp output The Running-the-CLI block showed bare 'remix <command>' as the project-local form, which only works after global activation. Show the real 'dart run remix_cli:remix' invocation, fix the note's example to match, and include the trailing line of the actual --help output the block claims to reproduce. All three verified against the real CLI.
Registry expansion Phase 1, the architecture test: one simple form control and
one compound component, chosen to prove the item schema before batching the
rest of the catalog.
Both fit `registryDependencies / dependencies / devDependencies / files /
generated / exports` unchanged, with no edit to registry.dart or installer.dart.
`checkbox` ships two adapters (Checkbox, CheckboxGroupItem) and `tabs` three
(TabBar, Tab, TabView), each from one authored file and one generated part, so
the one-file/one-part shape already covers compound components.
One discovery the phase was designed to surface: `RemixTab.builder` is typed
`ValueWidgetBuilder<NakedTabState>`, and `NakedTabState` is not among the four
symbols `remix` re-exports from `naked_ui`. Forwarding it would drag
`package:naked_ui` into the consumer's generated file and break the installed
import boundary. Curated out with `@MixWidget(widgetParameters: .only({...}))`,
already available in the pinned mix_annotations, so no schema field was needed.
Recipe notes:
- Checkbox clamps `Tokens.radius` to 4px. That token is authored for 32-40px
controls; applied unclamped to a 16px box a pill radius draws a circle, which
reads as a radio button. Clamping keeps a theme asking for less rounding in
charge.
- Checkbox nests its hover fragment inside the checked fragment, because a
top-level hover fragment cannot know which fill it is dimming. Tabs does the
opposite and deliberately sets no fill when selected, so the hover fill and
the selected edge coexist.
- Tabs rings focus with a foreground decoration: `TabSpec` has no
containerEffects layer, and a real border would inset the label.
The checker now installs all three items, and keeps byte-exact snapshots for
two representative generated shapes: button (single adapter) and tabs (multi
adapter). Per-component behavior tests are the per-component guard.
Verified with the full open-code check: 58 remix_cli tests, 10 checker tests,
146 fixture behavior tests against hosted and current-checkout Remix, and the
playground dogfood, where a customized checkbox.dart survived `add` and showed
up in `add --diff`.
Registry expansion Phase 2: avatar, badge, callout, card, divider, icon_button, link, progress, skeleton, spinner, and toggle. Fourteen components now install as application-owned source, all on the item schema Phase 1 proved and the same fifteen theme tokens the MVP shipped. The planned `surface`/`surfaceForeground` token pair was **not** added. Card was the test for it and did not confirm the need: `background` plus a `border` hairline reads correctly in both shipped themes, which is what shadcn's own defaults do — its `--card` and `--popover` resolve to `--background` unless a theme chooses otherwise. Two tokens whose values equal two existing tokens would be ceremony, and every existing install would have to port them. Revisit at the overlay tier, where a floating surface can demonstrate a concrete failure. Two bugs the new tests caught, both worth stating: - The callout's destructive tone first set its sentence in `destructive`, which measures 4.1:1 on the dark theme's page — under the 4.5:1 WCAG floor for body copy. `destructive` is a fill color chosen to sit under `destructiveForeground`, not a text color. The tone now shows in the border and the glyph, where the floor is 3:1, and the sentence stays `foreground`. - Resolving a recipe is not the same as laying it out. The fixture now pumps the whole gallery at two viewport sizes, which caught a tab strip overflowing its container and a progress bar that shrink-wrapped to zero width. Neither was visible to a recipe-level assertion. Recipe notes: - Progress spans its parent and clips to its own radius, because progress is measured against the width of its container. - Divider takes a plain `Axis` rather than a local enum, and stretches along it with a FractionallySizedBox rather than offering length presets. - Link pins no font size: it is inline text and must take the scale of the paragraph around it. Its color is `foreground` with an underline, because this theme's `primary` is a neutral fill rather than a link hue. - Avatar clips rather than shapes, because Remix renders `backgroundImage` as a child of the container and not as part of its decoration. - `RemixTabBar` will not accept a scroll view as its child: Flutter's tab-bar semantics role requires every direct semantics child to be a tab. The strip now spans its container, and the recipe documents where the scroll view goes. The checker installs every item and derives its expected inventory from one item list, so a new item cannot be added without the boundary check covering it. The formatter-drift test now runs over every template at the long `Playground` prefix, not just Button. Verified with the full open-code check: 58 remix_cli tests, 10 checker tests, 217 fixture behavior tests against hosted and current-checkout Remix, both generated snapshots byte-exact, and the playground dogfood, where a customized card.dart survived `add` and showed up in `add --diff`.
…st gaps Applies the findings from a simplification pass and an adversarial review of the two preceding commits. The one real defect: avatar initials were set in `mutedForeground` on a `muted` surface, which measures 4.35:1 in the shipped light theme — under the 4.5:1 WCAG floor for text that size. It is the same defect that was caught and fixed in the callout one commit earlier, and avatar escaped only because nothing asserted its contrast. The fallback now takes `foreground`: initials name a person, so they are content, not a dim marker. So the fixture now measures the pairings instead of trusting the token names. A new group resolves each recipe's real spec across both themes and every state, composites the layers the way a reader sees them, and holds text to 4.5:1 and glyphs to 3:1. Reverting the avatar to `mutedForeground` fails it at exactly 4.35 with `reason: initials`, which is the check the previous commit was missing. Three assertions that read as proof and were not: - `a link with no callback is disabled by Remix` incremented a counter that the widget under test had no way to increment, so it restated its own setup. It now asserts the resolved disabled fragment is present on a callback-less link and absent on an enabled one, which is the recipe's actual claim. - The toggle's `focus-visible rings without moving the label` compared a padding that the focus fragment never touches. It now asserts the ring does not arrive as a box border, which is the thing that would move the label. - `every component item resolves theme first` checked only that each pinned name exists in the catalog. It now checks both directions. And the coverage gap behind them: three hand-maintained item lists, none of which asserted completeness against `registry.yaml`. An item added to the registry but forgotten in `tool/check_open_code.dart` was never installed, never rendered into the fixture, never behavior-tested, and never boundary checked — with every suite green. The checker now fails on that drift in either direction, before it creates the temporary app. De-duplication from the simplification pass: `_resolveStyle` and `_resolvedSpec` now delegate to the generic `_resolve` and `_resolvedSpecOf` they were copied from, and the single-use `_resolveCheckbox` wrapper is gone. One finding is deliberately *not* fixed here: `border` measures 1.26:1 against `background` in light and 1.91:1 in dark, which is below the 3:1 non-text floor for an unchecked checkbox, whose hairline is the only thing identifying it. That is inherited from the MVP theme, shadcn ships the same tradeoff, and every option is a theme decision with consequences for preserved installs. The options are written up in the expansion plan rather than settled by letting one component diverge. Verified with the full open-code check: 58 remix_cli tests, 12 checker tests, 229 fixture behavior tests against hosted and current-checkout Remix.
…ift seams
Registry expansion Phase 3 — radio, segmented_control, slider, switch,
textfield, and toggle_group — plus the drift work that reviewing the catalog
turned up. Twenty items now install as application-owned source.
## The `name:` argument was ceremony
Every template passed `@MixWidget(name: '{{typePrefix}}Divider', ...)`,
restating a value the generator already derives. `valuePrefix` lowercases only
the first character (project_config.dart:64), and the generator drops a
trailing `Style` and capitalises the first character — exact inverses, so
`{{valuePrefix}}XStyle` always yields `{{typePrefix}}X`. Regenerating without
it is byte-identical, and Fortal never passed it. Removed from all fourteen:
two names that had to agree, with nothing checking that they did.
That removal exposed a soft assertion. `both prefixes render every configured
public surface` searched the template text for `AcmeCheckboxGroupItem`, which
had been satisfied by the annotation; with the annotation gone it was satisfied
only by doc-comment prose elsewhere. It now pins the recipe *declaration* with
an anchored pattern, which is the same fact as the widget name and the thing a
comment cannot fake.
## The playground could rot silently
`apps/playground` holds a committed, CLI-installed copy of every item. Nothing
renders it, so a stale file compiles; and the ownership contract says a
consumer may edit installed source, so a forgotten resync and a deliberate
customization looked identical in review. Four items had diverged and it took
running fifteen diffs by hand to learn that all four were intentional.
`tool/check_open_code_dogfood.dart` now drives the CLI's own `add --diff` over
every item: each is either clean or named in a list with its reason, and a
named item that turns out to be clean fails too, so the list cannot outlive the
edit it describes. The list is down to one entry — the theme's indigo primary.
Repeating the same lesson in button, card, and checkbox cost three files that
could no longer be resynced and taught nothing the first one did not.
`generate:check` now covers the playground as well, so a `.g.dart` left behind
by a recipe edit fails instead of shipping green. The first thing it caught was
that the templates are *not* byte-identical to their installed form: the CLI
formats after rendering, and how the formatter wraps depends on how long the
prefix is. The dogfood check asks the CLI rather than re-rendering for that
reason.
The generated snapshots are prose-sensitive, because the generator copies the
recipe's doc comment into the adapter. That is now stated where the snapshots
are declared, so a comments-only diff is refreshed rather than investigated.
## Phase 3 findings
- `toggle_group` and `segmented_control` are a third adapter shape: the group's
spec carries the option's style as a field, so one `@MixWidget` styles the
container and every option, and options are data rather than widgets. The
schema did not change.
- `RemixSlider.semanticFormatterCallback` is the second naked_ui leak after
`RemixTab.builder`, curated out the same way.
- `textfield` generates two adapters through super-parameters, and needs
`package:flutter/gestures.dart` and `package:flutter/services.dart` for types
that appear only in the generated constructor.
- A focused text field asserts on an `Overlay` ancestor. The fixture's
deliberately minimal `WidgetsApp(builder: ...)` host has none, so the gallery
and the field tests supply one and the recipe documents the requirement.
Two more contrast failures, both caught by the group added last commit:
- The segmented control's unchosen label was `mutedForeground` on the `muted`
track: 4.35:1 in light. Every segment's label is now `foreground`, and the
chosen one is marked by its raised surface and a heavier weight — a cue that
survives where a colour difference would not.
- The text field's error helper was `destructive`: 4.10:1 on the dark page. The
outline carries the tone, where the floor is 3:1, and the message is
`foreground` at w500.
That is the third component to want a danger colour it could set text in, which
is the evidence D2 asked for before touching the token vocabulary. The options
are written up in the expansion plan; nothing was added here.
Verified with the full open-code check: 58 remix_cli tests, 12 checker tests,
276 fixture behavior tests against hosted and current-checkout Remix, both
generated snapshots byte-exact, the playground mirroring the registry, and
clean generation reproducing all 20 committed playground adapters.
Registry expansion Phase 4 — accordion, dialog, menu, popover, select, and tooltip. Twenty-six of the twenty-seven planned components now install as application-owned source; `disclosure` still postdates hosted 1.0.0-beta.6 and waits for the constraint bump, as the plan called for. The nested-spec shape the groups introduced in Phase 3 scales all the way: `MenuSpec` carries `item`, `checkboxItem`, `radioItem`, `submenuItem`, and `divider` as fields, and `SelectSpec` carries `trigger`, `content`, and `item`. A recipe that sets only `item` styles every row kind, which is what keeps a menu looking like one list rather than four. Still one `@MixWidget` per file, still no schema change — the item shape has now survived four phases and three distinct component architectures without gaining a field. The D2 token question was re-asked here, where the plan expected it to be answered, and the answer is still no. A floating panel is separated from the page by its outline and its shadow, and in dark mode the shadow does almost nothing — so the outline carries it alone at 1.91:1. That is a real problem, but a `surface` token does not fix it; a `border` that clears 3:1 does, which is already recorded as an open decision. `muted` as an overlay fill is worse than `background` on both pages. Findings: - `RemixAccordion.builder` is the third naked_ui leak after `RemixTab.builder` and `RemixSlider.semanticFormatterCallback`, curated out the same way. `RemixAccordionGroup.controller` looked like a fourth, but Remix already re-exports it as `RemixAccordionController` — the right fix on their side. - `AccordionStyler` forwards its box shorthand to `trigger`, not `container`, so the first draft's `.border(...)` outlined the clickable row instead of the section. Remix documents this in the spec; the recipe now reaches `container` by name and says why. The fixture caught it. - The tooltip is the one floating surface that is not `background`: a transient label is not a panel a reader can act in, and inverting it is what makes that difference legible without a second token. Verified with the full open-code check: 58 remix_cli tests, 12 checker tests, 301 fixture behavior tests against hosted and current-checkout Remix, both generated snapshots byte-exact, the playground mirroring the registry with its one declared customization, and clean generation reproducing all 26 committed playground adapters.
Four audits over the finished 26-component expansion: recipe correctness against the Remix API, test rigour by mutation, design-system consistency and accessibility, and the tooling and documentation. Everything below was independently verified before it was acted on; two reported findings did not survive checking and are called out at the end. ## Four accessibility failures, all recomputed from the token values - The switch had no outline on either box. `muted` on `background` is 1.09:1 in the light theme, so an off switch was a pale shape on a pale page holding an invisible thumb. The slider had already solved the identical problem for its thumb and the switch never got the same fix. Both boxes now carry a hairline in every state, so flipping it does not resize the control. - The segmented control's chosen segment was a 1.09:1 fill plus a font weight — and the weight was the cue I added in the previous commit while fixing a different contrast bug. It now carries an outline as well. - The select trigger tinted its box on hover and left the placeholder at `mutedForeground` on `accent`: 3.76:1, in both themes, and only while the pointer is on it. The content moves with the surface now. - The toggle told hover from on with `muted` versus `accent`, which are 1.155:1 apart — a state distinguished by colour alone, and by a colour difference most people cannot see. The on state now carries a `primary` outline. Because Flutter insets a container's content by its border widths, every state declares that outline and only its colour changes; `ghost` paints its copy in nothing. ## Two comments that were wrong about Remix The tooltip's `showDuration` and `dismissDuration` comments described each other's mechanism. Remix maps the first onto Naked UI's `touchDelay` — a long-press hold, nothing to do with hovering — and the second onto `dismissDelay`, which is the hover-exit grace the first comment claimed. The slider's doc promised a hover fragment the recipe did not have; it has one now. ## Test gaps, each proven by the mutation that used to pass - "`[style]` is merged last" is every recipe's headline promise and was verified for 6 of 26 components. Reversing badge's merge order passed all 301 tests. There are now 29 probes, one per published recipe, and that same mutation fails. - The accordion had no focus, disabled, precedence, or forwarding test — deleting two whole state fragments and breaking `focusNode` forwarding passed. It now has all four. - `toggle_group` and `segmented_control` never exercised `small` or `large`; setting those metrics to 999.0 passed. Both now have size tables. - Two counters asserted zero in tests that never triggered them. - "focus-visible rings without moving the label" still measured no position after being hardened once. It now measures the label's rect *and* the control's size — the label alone still missed a symmetric padding change, because a centred label does not move when the box grows on both sides. ## Deliberate absences, now documented rather than "fixed" The text field has no hover fragment: its affordance is the I-beam cursor Remix already sets, and a tint would compete with the focus ring a moment later. The accordion and segmented control carry no focus-ring offset, because their neighbours are flush and an outward ring would cross into them. Eight size enums that name a sub-44px control now disclose the hit-target tradeoff that button and icon_button already did. ## Tooling `CLEAN_SHEET.md` still said the catalog was fourteen components. Nothing checked it: `validate_docs.dart` covered three READMEs and none of the open-code documents. It now covers all three, and cross-references every registry item against every catalog document — removing one table row from docs/open-code.mdx now fails the check by name. ## Reported but not real The recipe audit's sweep for foreign types in generated adapters, mis-forwarded styler properties, and unwinnable state fragments came back clean, with `SelectSpec`'s non-obvious forward target checked explicitly. The tooling audit reported that no workflow runs `melos run ci`; `.github/workflows/ci.yaml` does exist and does run on PRs — what it delegates to is a pinned external workflow this checkout cannot read, which is recorded as an open question rather than a finding. And an estimate that the checker takes 15-20 minutes was out by 5x: measured, a full run with all 26 items takes 206 seconds, well inside the plan's budget. Verified with the full open-code check: 58 remix_cli tests, 12 checker tests, 341 fixture behavior tests against hosted and current-checkout Remix, both snapshots byte-exact, and the playground mirroring the registry.
Registry expansion Phase 5 — data_list and data_table — plus the design consistency work the specialist review left open. Twenty-eight of the twenty-nine catalog entries now ship; `disclosure` still postdates hosted 1.0.0-beta.6 and waits for the constraint bump, which was the plan's own stated blocker from the start. Phase 5 was written as demand-driven, and that gate is dropped deliberately: leaving two of twenty-eight out would have left the plan's Goal — every Remix component that exists in a hosted release — unmet on a guess about demand, after the other twenty-six proved the shape works. ## data_table is the one real exception to "templates are self-contained" `DataTableSpec` takes a `Style<CheckboxSpec>`, a `Style<IconButtonSpec>`, and a `Style<SelectSpec>`. A table's selection column, pager, and page-size control literally *are* those components. So `data_table` is the first item with `registryDependencies` beyond `theme`: it imports three sibling files and `add data_table` installs four items in dependency order. The schema already supported this and needed no change — the resolver's multi-dependency path had a unit test since the MVP but no real item until now. The alternative, restating three recipes inside a fourth, would produce a table whose controls drift away from the rest of the application, which is what D3 exists to prevent rather than to cause. ## `mutedForeground` on `muted` is 4.35:1, and this is the third component it bit Avatar initials, then the segmented control's unchosen label, and now the data table's column headers. The pairing reads as if it were designed for each other — the token is literally named for that surface — and it is under the 4.5:1 floor for text in the light theme. Header labels are `foreground` at w500 now, with the header's own `muted` surface doing the separating. `mutedForeground`'s own doc comment says so now, so the next person reaching for it is told before they write the line rather than by a test afterwards. ## The rest of the specialist review's design findings - The segmented control's type and icon scale were 13/14/14 and 14/16/16 while the button, toggle, and tab family used 14/14/16 and 16/16/18. Aligned; only the heights still differ, because a segment is inset inside its track and the *track* is what lines up with the button beside it. - The link has no pressed fragment. That is deliberate — a link replaces the page and the destination arriving is the feedback — and it now says so, because the checkbox's version of that rationale does not transfer. - `focusRing` and `mutedForeground` resolve to the same value in both shipped themes. Deliberate, now stated: a neutral ring reads as the platform talking rather than the brand. ## Process note The scratch consumer app used for iteration had itself drifted from the templates — a stale `segmented_control` there is why a stale test table reached the checker instead of being caught locally. Re-rendering every item before trusting that app is now part of the loop, which is the same lesson the playground dogfood check already encodes for the committed copy. Verified with the full open-code check: 58 remix_cli tests, 12 checker tests, 355 fixture behavior tests against hosted and current-checkout Remix, both snapshots byte-exact, the playground mirroring the registry with its one declared customization, and clean generation reproducing all 28 committed playground adapters. The whole check measures 238 seconds.
Two leftovers from the specialist review, both small. The text field and the select trigger keep a flat 12px horizontal inset across their sizes while the button scales 12/16/20 — flagged as an inconsistency, and it is deliberate. A button's padding gives its label room to breathe, so it grows with the control; a field's is the gutter before the text cursor, and widening it at a larger size only moves the caret away from the edge the reader clicked. Both recipes say so now, and the select points at the field. The dialog's description is `mutedForeground` on the panel, which clears 4.5:1 at 4.74 in the light theme — the thinnest margin of any pairing in the set, and the only one the contrast group did not measure. It does now, along with the title. Verified with the full open-code check: 357 fixture behavior tests against hosted and current-checkout Remix, the playground mirroring the registry.
A completeness pass over the finished catalog, checked mechanically rather than by recollection. The catalog itself is complete: diffing the registry's item set against `remix`'s own component directory gives 28 = 28, nothing missing and nothing extra. `disclosure` is not in hosted 1.0.0-beta.6 at all, so it is not a gap in the catalog — it is the constraint bump the plan named at the start. Cross-checking every `on…` fragment in every template against the states the suite actually drives found one real hole: six components declared a `disabled` fragment that nothing exercised — menu's trigger and item, radio, the segmented control, select's trigger and item, textfield, and the toggle group. A disabled fragment is the easiest thing in a recipe to declare and never look at again, because it is invisible in the gallery and the states around it are the ones a person exercises by hand. The new group enumerates rather than samples, and deleting either menu's or select's item fragment fails exactly its own probe. Two smaller things the same pass turned up: - The CLI's changelog still described a two-item registry. It now records the catalog, and that `add data_table` installs four items. - `open_code/README.md` was the one document missing the two host notes the others carry: the `Overlay` a focused text field needs, and the four behavioral Remix widgets that have no registry item because they carry no style. Dead code was checked too and there is none: the fixture's analysis options set `unused_element: error` and the checker analyzes the installed source with them, so an unused constant or helper in any of the 28 templates would already have failed the build. Verified with the full open-code check: 58 remix_cli tests, 12 checker tests, 365 fixture behavior tests against hosted and current-checkout Remix, both snapshots byte-exact, and the playground mirroring the registry.
Six hundred lines out, three hundred and thirty in, across 27 templates. The
shape this started from, and what it is now:
.onFocusVisible( .onFocusVisible(
SelectTriggerStyler().containerEffects( .containerEffects(
RemixBoxEffectsMix( .outline(
outline: BorderSideMix( .color(
color: Tokens.focusRing(), Tokens.focusRing(),
width: _focusRingWidth, ).width(_focusRingWidth)
strokeAlign: .strokeAlignInside, .strokeAlign(inside),
), ).outlineOffset(_offset),
outlineOffset: _focusRingOffset, ),
), )
),
)
Which forms are available was settled with the analyzer rather than from
memory, because the rule is narrower than it looks: a dot shorthand resolves
only against static members and constructors of the *declared* context type.
Three things therefore cannot be shortened and are left alone —
`BorderSide.strokeAlignInside`, whose parameter is a plain `double`;
`ContextVariant.widgetState`, which lives on a subtype of the `Variant` the
parameter declares; and `wrap`, `thumbColor`, and `trackColor`, which are
instance methods with no static counterpart. That last group is the one worth
remembering: not every fluent method is shorthand-able, so this cannot be
applied by pattern. The analyzer caught all five sites where a sweep had
assumed otherwise.
The border collapse is the largest single win. `BoxBorderMix.color` and
`.width` are defined as `BorderMix.all(...)`, so `.border(.color(x).width(y))`
*is* the all-sides form — proven by resolving both spellings in a real
`BuildContext` and comparing the `Decoration`, not by reading the source. The
seven per-side borders keep their explicit side: the data table's row rules
and footer want one rule, and `.color()` there would draw a box.
`.behindContent(.shadows([...]))` is deliberately not adopted. It needs the
layer statics added in the previous commit, and these templates compile
against hosted `remix ^1.0.0-beta.6`, where the analyzer rejects it. It
arrives with the same release that unblocks `disclosure`.
Templates are taken from the CLI's own formatted output at `Playground`, the
longest prefix the repository dogfoods, so the file a maintainer reads is the
file a consumer gets.
Verified: 58 remix_cli tests including the formatter-drift test that installs
every item and requires `add --diff` to come back clean, 2,727 remix tests,
the full open-code check at 365 fixture tests against hosted and checkout
Remix with both snapshots byte-exact, and the playground still mirroring the
registry with its one declared customization intact.
Driving the installed gallery in a browser turned this up: in the section that rethemes with `radius: Radius.circular(999)`, the data table's select-all checkbox is sliced into a wedge and the last row's checkbox is clipped to a sliver. The control is not just ugly, it is gone. The table is the only recipe here that both rounds its frame and clips to it. Only three templates clip at all — the avatar clips an image to a circle and the progress bar clips a fill to a pill, and neither has anything in the corner. The table does: the corner arc is carved out of the first and last rows, and the leading cell of both is the selection column. Past a certain radius the clip stops trimming the header fill and starts removing a checkbox. The clip is not optional. The header sits on `muted` and would otherwise square off the top two corners inside the rounded border. So the frame bounds its own radius instead, and the bound is geometry rather than taste: an arc of radius r has finished turning r from the corner, so a radius no larger than half the header's height is out of the way by the row's own content line. Rows are taller than the header, so the header binds. This clamps rather than hardcodes, which keeps the theme in charge in the other direction — `radius: Radius.zero` still gives a square table, and there is a test for that. It is the same move the checkbox recipe already makes at a different scale, for the same reason: a token authored for controls has to be interpreted by a recipe whose geometry it knows nothing about. With one radius token in the vocabulary, a consumer who wants pill buttons has no way to ask for anything else, so the recipe has to be the one that copes. Deliberately not changed: the menu and select panels, and the card, callout, dialog, and text area, all become lozenges at that radius and their content sits outside the arc. None of them clips, so nothing is lost — that is a theme asking for a shape and getting it, and bounding it would be this layer overriding a choice rather than surviving one. Verified with the full open-code check at 367 fixture tests, both snapshots byte-exact. The new test is discriminating: with the clamp removed the frame resolves to 999.0 against a bound of 20.0 and the test fails.
…talog with disclosure `remix 1.0.0-beta.7` is on pub.dev, which was the plan's one stated blocker. This closes everything that waited on it. **The constraint moves to `^1.0.0-beta.7`** in the registry, the CLI tests, the checker test, and the docs. The checker's first phase now resolves beta.7 from the hosted cache, so the guarantee is real rather than assumed. The scratch verification also exercised the CLI's own guard on the way: an app whose lockfile still held beta.6 was refused with "remix 1.0.0-beta.6 does not satisfy ^1.0.0-beta.7" before any file was written. **`disclosure` is the 29th item**, and the catalog now covers every component in the hosted release. The recipe is deliberately frameless: the accordion is this component's stacked sibling and draws a rule because its rows have neighbours to separate; a lone disclosure has none, so a frame would only box in whatever the caller placed it inside. The trigger is styled as a self-contained row target instead — menu-row padding, radius, and hover treatment, at the accordion's 44px height because it stands alone rather than in a dense list. The open trigger holds a `muted` fill, distinct from the `accent` hover, so a reader can tell an open section from a hovered one. `triggerBuilder` and `transitionBuilder` are curated out of the generated widget: both are typed by `package:naked_ui`, which this layer does not depend on. Its tests resolve through a real widget rather than `_resolve`, and not by preference: `onExpanded` reads the live `NakedDisclosureState`, so resolving the style outside a disclosure throws — the same constraint the checkbox's `onIndeterminate` already imposed, handled with the same `_disclosureSpec` pattern. The disabled-fade group grows its ninth probe. **`.behindContent(.shadows([_shadow]))`** replaces the spelled-out `RemixBoxEffectLayerMix(shadows: [...])` in menu and select — the two sites that were waiting for the beta.7 API. Also migrated `test:cli` to melos 8's `exec.command` form, which main's melos bump made mandatory for this branch's one remaining `run`+`exec` script. Worth recording: regenerating the playground against beta.7 produced byte-identical adapters for all 28 existing components — the release changed no generated output. Verified with the full open-code check: 58 CLI tests, 12 checker tests, 375 fixture behavior tests against hosted beta.7 and the current checkout, both snapshots byte-exact, docs validation over 21 consumer-facing Markdown files, and the playground mirroring the registry with its one declared customization intact.
Built a Playwright capture harness for the installed layer and reviewed all 29 components specimen by specimen — every variant, every size, rest/hover/focus/ pressed, both themes, 304 captures. The harness lives in `.context/` and is not part of the repository; what it found is here. **The select's option rows were inset 12px where the menu's are 8.** Same 32px row, same 4px panel, same kind of floating list. The recipe's own documentation claims the opposite — "the panel matches the menu's, so a select and a menu opened side by side do not read as two systems" — and the panels do match, the rows did not. One `_paddingX = 12.0` was serving both the trigger and the rows. 12 is right for the trigger, which is a field styled after the text field; a row in a dropdown is not a field. The rows now have their own constant at the menu's 8. **The accordion's row had no horizontal inset at all**, against 12 for every other row-like surface in the layer: the table's cells, the select's trigger, the callout, the disclosure. Upstream shadcn does leave its accordion trigger flush, but only because the item wrapping it supplies the inset — nothing wraps this one, so flush put the title hard against whatever contained it while the rule underneath still spanned the full width. Row and panel now take the same 12 as their neighbours. **The disclosure's focus ring wrapped the content as well as the trigger.** Keyboard focus is on the trigger, but an expanded disclosure's container is the trigger plus everything it revealed, so the ring claimed the content was focused and grew with it. It now rings the trigger alone via `foregroundDecoration`, which paints over the box rather than beside it — so the ring still takes no layout space and opening the section does not reflow the page. A plain border would have pushed the trigger's content in by two pixels on focus. **The disclosure's revealed content had no bottom inset**, leaving its last line against the container's own edge. Invisible while the container is undecorated, obvious under the focus ring, and wrong for any consumer who gives the container a fill. Both sides now. Two things the pass confirmed rather than changed: every one of the 29 reads the theme, with the only literal colors being transparent sentinels, shadow tints outside the fifteen-token vocabulary, and doc examples; and the segmented control's odd-looking 26/30/34 item heights are correct — plus twice the 3px track inset they are exactly the 32/36/40 the rest of the layer uses. Verified with the full open-code check: 375 fixture tests against hosted beta.7 and the checkout, both snapshots byte-exact, and the playground mirroring the registry.
Compared the layer against shadcn/ui numerically rather than by eye: its values come from the public registry, decoding Tailwind at one unit to four pixels; ours from resolving the installed specs in a real BuildContext and reading them back. Seventeen components have a direct counterpart. The controls already agree, in more detail than expected. Button heights are 32/36/40 against `h-8`/`h-9`/`h-10`; the small and medium insets are 12 and 16 against `px-3` and `px-4`; the text field and select trigger are 36 and 12 against `h-9 px-3`; the menu and select panels inset 4 against `p-1`; badge 8 against `px-2`; avatar 40 against `h-10`. That comparison also settles the row inset the previous commit changed on internal-consistency grounds alone. shadcn's `DropdownMenuItem` and its `SelectItem` both use `py-1.5` with 8px on the text side, differing only in which side reserves room for the indicator — 6 and 8, exactly where the fix landed. One value was genuinely wrong: the tooltip's vertical inset was 5. That is the only number in the layer off its own four-pixel grid, it carried no comment explaining the exception, and both shadcn and our own menu and select rows use 6. Now 6. Everything else that differs, differs consistently and stays. Every surface is one step tighter than shadcn — tooltip 8 against `px-3`, callout 12/10 against `px-4 py-3`, popover 12 against `p-4`, card 16 against `p-6`, dialog 20 against `p-6` — while the internal progression holds. That is one decision rather than five accidents, and it is what makes the layer read denser. Button large keeps 20 rather than shadcn's `px-8` at 32, which nearly doubles its own medium; ours steps 12/16/20 so the three sizes read as one family. The full comparison is written up in `.context/shadcn-compare/`, alongside the fetched registry items it was derived from. Verified with the full open-code check: 375 fixture tests against hosted beta.7 and the checkout, both snapshots byte-exact, and the playground mirroring the registry.
…hter
The measured comparison found every surface in this layer sitting one step
tighter than its shadcn counterpart. That was internally consistent — the
progression tooltip < callout < popover < card < dialog held — but consistency
was the only thing defending it. Nothing was written down about why a layer
that takes shadcn's token vocabulary, its control heights, and its row insets
should then run denser than it everywhere else, and being systematically
tighter is not a decision anyone made; it is five values that each drifted the
same direction.
tooltip 8/6 -> 12/6 (px-3 py-1.5)
callout 12/10 -> 16/12 (px-4 py-3)
popover 12 -> 16 (p-4)
card 16 -> 24 (p-6)
dialog 20 -> 24 (p-6)
The controls needed nothing: heights, button insets, field and trigger
geometry, row insets, and panel padding already matched exactly.
Two apparent mismatches turned out not to be. shadcn ships one checkbox and
one progress bar; this layer ships three sizes of each, and shadcn's single
value lands inside our scale rather than beside it — its 16px checkbox is our
small, its 8px track is our large. Collapsing our medium onto their one value
would flatten a scale with three real steps, so both stay.
Button large also stays at 20 rather than shadcn's `px-8`. That one is an
outlier inside shadcn's own system, nearly doubling its medium, where ours
steps 12/16/20 so the three sizes read as one family.
Verified with the full open-code check: 375 fixture tests against hosted
beta.7 and the checkout, both snapshots byte-exact, and the playground
mirroring the registry.
shadcn ships a size scale on exactly two components: `button` and `toggle`. This layer had thirteen. The other eleven were three numbers each with nothing anchoring them to the system they claim to follow, and they were the reason the checkbox and progress bar could not be aligned in the previous commit — their single value sat inside our scale rather than on it. Collapsed, each landing on shadcn's own value: checkbox and radio to 16 (`h-4`), progress to 8 (`h-2`), switch to 20 (`h-5`), avatar to 40 (`h-10`), the slider rail to 6 (`h-1.5`), and the text field, select trigger and tab to 36 (`h-9`) — which is also the button height they sit beside. The segmented control keeps 30 because its segment is inset inside the track by three on both sides, so the track lands on the same 36. The spinner has no shadcn counterpart and keeps its 20. `button`, `icon_button`, `toggle` and `toggle_group` keep their scales. `icon_button` is `button`'s `size=icon` as a component, so it inherits the same justification. This is a breaking change to the generated widgets: eleven of them lose their `size` parameter, and the committed `tabs` adapter snapshot records exactly that. A call site that needs a different weight now sets it through `style`, which is one line where it matters rather than an enum every consumer of the recipe has to read past. The data table composed `.small` variants of the checkbox and select for density; both are single-size now, which moves its selection checkbox by two pixels inside a 44px row and its page-size select by four inside a 40px footer. Both were checked and neither crowds. The fixture suite drops from 375 tests to 353 — the removed size permutations, not removed coverage. Every group that iterated a size map now asserts the one shipped value, and the four components that kept their scales still iterate theirs. Verified with the full open-code check: 353 fixture tests against hosted beta.7 and the checkout, both snapshots byte-exact, docs validation over 21 consumer-facing Markdown files, and the playground mirroring the registry.
Recaptured all 29 components after the size collapse — every variant, every state, both themes, 298 captures — and reviewed them one at a time. Three hover states were not doing anything a reader could see. **The radio's ring disappeared on hover.** `accent` over the `border` hairline is 1.09:1 in the shipped light theme, so tinting the disc erased its outline and left a hovered *empty* radio rendering as a solid grey dot — which is what a *chosen* one looks like. The hover fragment now darkens the ring to `mutedForeground` as well as filling, so the circle stays a circle. **The checkbox had the same bug** for the same reason, and takes the same fix. It reads less alarmingly — a grey square still looks like a box — but an unchecked checkbox with no visible edge under the pointer is the same defect. **The accordion had no perceptible hover at all.** Its fragment moved the two icons from `mutedForeground` to `foreground`, which at 16px next to a title already at full strength is invisible; hovered and resting rows were indistinguishable in the capture. It now underlines the title, which is what shadcn's own accordion trigger does (`hover:underline`). The title's weight stays reserved for the open state, so "the pointer is here" and "this section is open" remain different things. Two of these were only findable by looking. The specs resolve exactly what the recipes ask for, so no assertion was wrong — the colours were simply too close to see, which is the class of defect a test suite cannot report and a capture sheet shows immediately. The harness needed two fixes of its own to make that possible: specimens are now shrink-wrapped on both axes, because several components expand into whatever they are given and an unconstrained segmented control filled the viewport and captured as a slice of a stretched track; and the segmented control and toggle group are additionally width-constrained. Verified with the full open-code check: 353 fixture tests against hosted beta.7 and the checkout, both snapshots byte-exact, and the playground mirroring the registry.
Added specimens for the four components the capture harness had never covered — slider, tabs, popover and data table — which takes it to all 29, and found one real defect immediately. **The gallery's popover never opened.** `RemixPopover` opens on a tap of its own `child`, and the trigger was an `AcmeButton` with its own `onPressed`, which consumes that tap before the popover sees it. Nothing failed: the panel simply never appeared, and the only assertion covering it checked that the trigger *rendered*. The capture showed a button and no panel, which is what made it visible. The gallery now drives it from a `MenuController`, the recipe documents the trap with the working composition, and two tests cover both halves — a plain child opening on tap, and a button trigger needing the controller. The second is the one that would have caught this. Slider, tabs and the data table are all correct: the slider's range, thumb and focus ring are distinct; the tab bar's selected underline, hover fill and disabled state all render; and the table's select-all checkbox is intact in the corner, which is the radius clamp from an earlier commit still holding. Two harness fixes were needed to see any of it. The tabs specimen is now the bar alone rather than bar-plus-panel, because the driver puts the pointer at the specimen's centre and the centre of the taller composition landed on the panel — hover and focus captured as identical to rest. The popover specimen uses a tappable non-button child so the component's own behaviour is what the capture shows. Also recorded in the comparison notes: aligning `tabs` to shadcn would be wrong. shadcn has no segmented control — its `Tabs` *is* the pill-on-a-track control this layer already ships as `segmented_control`, down to the same 36px track. Matching it would leave two components rendering identically and differing only in name. Verified with the full open-code check: 355 fixture tests against hosted beta.7 and the checkout, both snapshots byte-exact, workspace analyze clean, and the playground mirroring the registry.
The registry's `remix: ^1.0.0-beta.7` admits every later beta, but the templates are only ever tested against one version. A consumer who upgraded landed on an untested beta with no signal, because lock verification only checked constraint membership. `add` now reports when the resolved `remix` is above the constraint's floor. The install still completes; `--dry-run` and `--diff` stop before `flutter pub get`, so they never print it. The floor is the tested version because the floor is now coupled to the release. `check_version_alignment` asserts the registry constraint floors at `packages/remix`'s version, `sync_registry_remix` is the writer that moves it, and `version.yml` runs that writer after `melos version` — melos cannot, because the registry is data, not a pubspec dependency. `check_open_code` proves a real consumer resolves that same version from pub.dev. Test fixtures derive both values from the bundled registry, so the next bump does not leave a dozen `does not satisfy` failures inside the release pull request.
The documented `flutter pub add dev:remix_cli` could not work: the package is not on pub.dev and `publish.yml` excluded it on purpose, behind a post-MVP release gate. This closes that gate. `publish.yml` gains a `remix_cli-v<version>` trigger and a fourth job, mirroring how `remix_ui_icons` was added. remix_cli depends on no other package here and is versioned independently, so its tag orders against nothing and cannot race the existing three jobs. The docs drop the "not yet published" qualifiers and describe the pub.dev install as the normal path. The job stays inert until the first `remix_cli-v*` tag exists. Creating the package on pub.dev and configuring automated publishing are manual bootstrap steps that follow this merge.
The catalog listed `orientation` in the Axes column for `divider`, next to `variant` and `size` on the rows that declare real enums. It is not one. The divider recipe accepts a plain Flutter `Axis` by explicit design — its own doc comment says so — and the generator emits named constructors only for a declared axis. `UiBadge` gets four, `UiButton` five; `UiDivider` gets none. The table implied a surface that is not there. Verified against the installed source: every row the table marks `variant` generates named constructors, and `divider` generates zero.
Restructure the standalone guide section by section. The guide now leads with the ownership model, then the six-step path, then the reference. - Replace the headline. The reader owns the recipe; Remix keeps the behavior, the accessibility, and the interaction states. - Show the render before the source in the hero, and use the real recipe entry point instead of a private helper. - Add four principles, an ownership table, an upstream-update answer, a six-card path overview, a three-scope customization ladder, a six-state sheet, and a provenance section. - Collapse five reference sections into one, and disclose the complete installed button.dart. Correct three inaccuracies: - theme_data.dart shows the real primary and focusRing values. - The --diff excerpt shows the real header a/proposed for a new file. - The one-call-site snippet matches the render beside it. Capture 12 new renders at a device pixel ratio of 2, from a specimen app built from the installed source. The 36 and 44 pixel pair measures 72 and 88 device pixels. Verify every command in a fresh Flutter 3.44.0 app, and check the guide at four viewport widths for overflow, broken images, contrast, and console errors.
The guide is about the command the reader types, so its title and brand now read `remix`. The header pills name both packages and their versions, because the guide contrasts them in the Update section. Correct the type system: - A table row header is prose, so it uses the text face. It keeps the monospace face only when it names an identifier. This fixes 34 headers. - The `.mono` class had no font-family rule, so the CLI status words rendered in the text face. It now applies the monospace face. Correct the mobile layout: - Every table stacks below 560 pixels. Six tables scrolled sideways before, which hid the last column. - A stacked cell puts the column name above the value, so a long column name no longer squeezes the value. - Code inside a stacked cell wraps. The two upgrade commands sit on two lines. Reduce the container styles: - The four principles use the hairline grid the guide already uses, not cards. - The scope rail is a 1 pixel hairline, like every other rule. Rewrite three result lines that shared one sentence template, and the Update subtitle. Match each contents entry to its heading. Verify at four viewport widths: no overflow, no broken or upscaled images, no contrast failure, and no console error.
A recipe is a declaration in source, not a rendered result, so a generator can read it. This section explains what that makes possible. - Map what the recipe already holds to what a generator can derive from it: the axes, the recipe matrix, the resolved styles, and the theme documents. - Show the Muse A2UI catalog entry a generator would emit, with the variant and size vocabulary marked, because it comes from the recipe enums. - Show the component capture document, with all six states as data. - Name the boundary: a generator derives mechanics, never meaning. Slot names, semantic roles, nested boundaries, and behavioral geometry stay authored. The section opens with a notice that it is a design preview. Provenance now scopes the evidence claim to the rest of the page, so the guide does not present an unbuilt generator as verified behavior. The two JSON shapes follow contracts that exist today.
…ject Dart Three defects a review found, each verified by running the CLI. A CRLF barrel was rejected. validateManagedBarrel and updateManagedBarrel split on the line feed and compared the marker exactly, so a barrel that Git checked out with core.autocrlf=true carried a trailing carriage return and matched nothing. A Windows consumer then saw every remix add fail, reporting a missing marker pair while both markers sat in the file. Both functions now parse without the terminator, and the rewrite keeps the terminator the file already used. The diff formatted the proposal with whichever Dart ran the CLI, while add formats with the project's Flutter SDK. A globally activated CLI on a different Dart could therefore report formatter-version differences that no install would produce, which breaks the promise that current is your file and proposed is the template. The diff now resolves the same toolchain as add. This makes --diff require Flutter 3.44 as add already does; a diff that predicts an install which cannot run has nothing to say. Bare `remix help` printed the usage twice. CommandRunner's help command printed it and returned null, and the null-result branch printed it again. `help` now takes the same path as --help and -h. The dogfood checker recorded only an exit code, which cannot be acted on from a CI log. It now carries stderr, where a missing Git or a refused preflight is named. Tests: LF and CRLF barrels, both parse and rewrite; bare help prints usage once; the diff test asserts the toolchain Dart instead of the host Dart, which is the assertion that would have caught the second defect.
- The step 3 card promised the proposed source from --dry-run. Only --diff shows source, so the card now names both modes. - The preview subtitle said both modes stop before any process. --diff runs a formatter and Git in a temporary directory. The section now states what each mode runs, and that neither writes to the project. - The update loop did not repeat the scope warning. One diff covers one item, so add button --diff can come back clean while the theme template moved. - The exit-code table did not record that a partial registry dependency blocks both previews, while a partially installed requested item still previews. - open-code.mdx read an empty diff as the only sign of being current. An edited recipe keeps the diff non-empty whether or not the template moved. CLEAN_SHEET.md said generation targets only the parts declared by resolved items. The installer unions every already-installed adapter, so adding one item cannot delete another item's generated part.
leoafarias
force-pushed
the
feat/open-code
branch
from
September 3, 2026 21:49
8795fec to
73666b2
Compare
5 tasks
leoafarias
force-pushed
the
feat/open-code
branch
from
September 4, 2026 19:28
ae10b93 to
5c974cf
Compare
5 tasks
leoafarias
marked this pull request as draft
September 4, 2026 20:11
Use the Windows Dart executable and resolved Pub workspace membership. Reject reserved prefixes before writes. Print command help once. Use literal package filters for source generation. Verify installation and edit preservation with the real SDK.
leoafarias
marked this pull request as ready for review
September 4, 2026 21:25
5 tasks
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.
Description
Add the project-local
remix_clifor installing editable Remix theme and component source.The application owns its recipes and generated adapters. Remix supplies component behavior.
The installer supports initialization, dependency installation, prefix rendering, previews, diffs, and explicit item overwrite.
Normal installation preserves application edits. The catalog includes core recipes, icons, and charts.
The playground uses the installed source, and fresh applications verify hosted and checkout Remix.
The registry requires published Remix beta.8 and includes the release changes from #182.
This is the base of the PR stack: #177, then Fortal #180, then Sidebar #178.
The Fortal and Sidebar changes are separate from this PR.
CI now runs for child PRs. Documentation identifies the pending first CLI publication.
The adversarial review fixed seven CLI defects: Windows SDK paths, setup output, dependency constraints, reserved prefixes, workspace discovery, duplicate help, and generation filters.
The installer now uses Pub package configuration and literal generation filters.
A real SDK test verifies installation and application edit preservation on Windows.
Related Issues
Related to #180 and #178.
Checklist
Validation
main.a56caca3b, including the real Windows SDK test.Breaking Change
The first CLI publication still requires package bootstrap. Complete the following stack before preparing the combined release.