Skip to content

chore: integration check for the follow-up stack (do not merge) - #339

Draft
leoafarias wants to merge 94 commits into
mainfrom
stack/follow-ups-integration
Draft

leoafarias wants to merge 94 commits into
mainfrom
stack/follow-ups-integration

Conversation

@leoafarias

@leoafarias leoafarias commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Do not merge. This branch checks that the open follow-up PRs work together. Each one still merges through its own PR.

It merges every PR head below into main (a3a8ad5) in the recommended merge order, then runs CI on the combined tree. When a component PR changes, this branch is rebuilt.

Merge order

  1. Registry, docs, and CI: chore(docs): update Next.js and dependencies with security fixes #321, test(naked_ui): run the test suite on the web #322, fix(registry): stop theme scopes rebuilding on unrelated MediaQuery changes #319, fix(registry): make every Fortal focus ring follow the focus ring tokens #320, then fix(registry): match Radix focus details in Fortal #328 (stacked on fix(registry): make every Fortal focus ring follow the focus ring tokens #320), fix(registry): keep equal Fortal themes from rebuilding token readers #325, ci: check independent open-code items in parallel #329, feat(registry): add the HeroUI preset #324.
  2. naked_ui: fix(naked_ui): keep focus semantics on a disclosure with a semanticLabel #317, fix(naked_ui): honor reduced motion in dialogs, tooltips, disclosures, and toasts #330, feat(naked_ui): let NakedMenu use the caller's control as its trigger #335, fix(naked_ui): scroll keyboard focus targets into view in overlays, tabs, toggle groups, and menubars #344, then fix(naked_ui): keep focus on the caller's control in a popover opened without tap #338 right before the naked_ui 1.3.0 release. That release raises remix's naked_ui floor to ^1.3.0.
  3. remix: feat(remix): add toast action placement and stack a narrow toast's action #318, fix(remix): honor reduced motion in animated components #327 (before fix(remix,registry): stop RemixSidebar rebuilding on unrelated MediaQuery changes #323), fix(remix,registry): stop RemixSidebar rebuilding on unrelated MediaQuery changes #323, feat(remix): expose the anchor width to select and menu overlays #326, feat(remix): add containerEffects to focusable parts #331, feat(remix)!: add label to RemixRadio and RemixSwitch #332, feat(remix)!: let a real button trigger RemixMenu #337 (stacked on feat(naked_ui): let NakedMenu use the caller's control as its trigger #335), fix(remix): keep outside widget state out of table headers, panel content, and sidebar slots #340, fix(remix): scroll a long RemixSelect list #341, then fix(remix): scroll a long RemixMenu #345 (stacked on fix(remix): scroll a long RemixSelect list #341). Remix 1.0.0-beta.14 follows.

Conflicts resolved here

Each conflict will come up again when the later PR is rebased onto main.

Cross-PR issues found here

These merge without a textual conflict but fail together:

Validation

Local, on 58c004b (Flutter 3.44.0):

  • melos bootstrap, then open-code:registry:build: no registry drift.
  • melos run ci:checks, ci:codegen, ci:cli, ci:flutter: pass. Test counts: remix 3038, registry_source 2282, naked_ui 1040, dashboard 84, demo 64, naked_ui_example 56, widgetbook_vanilla 38, playground 11, remix_ui_fonts 9, remix_ui_icons 4.

GitHub CI on this PR runs the open-code consumer legs (Vanilla, Fortal, HeroUI).

A NakedDisclosure with an explicit semanticLabel wrapped the trigger in
Semantics(excludeSemantics: true), which also dropped the trigger's focus
action and focused state from the semantics tree. Exclude only the visual
trigger content instead, so the label still replaces it while the
surrounding focus semantics survive.
…hanges

The Vanilla and Fortal theme scopes checked MediaQuery.maybeOf(context)
to decide whether to supply a MediaQuery from the view. maybeOf subscribes
to every MediaQuery field, so the scope and everything it builds rebuilt on
unrelated changes such as keyboard insets, once per frame while the keyboard
moves. Use getElementForInheritedWidgetOfExactType, which does not
subscribe. System mode still follows platformBrightness through its own
aspect lookup.

Regenerates the registry templates and the installed copies in the
workspace consumer apps.
RemixToast laid the message, action, and close button in one row, so a narrow
toast or large text squeezed the message into a sliver. Measure the width the
toast gets (parent constraints enforced by its style constraints, scaled by the
text scale) and, below the breakpoint, move the action and close button to
their own row under the message. Wider toasts keep the single row.
Fortal defines focusRingWidth and focusRingOffset, but the checkbox, radio,
switch, card and link outlines and the shared fortalFocusOutline hard-coded
width 2 and offset 2, so overriding the tokens changed nothing for them.
Outward rings (checkbox, radio, switch, link, classic and solid buttons) now
read both tokens and every outline reads the width token; inset rings keep
their -1 offset. Default token values are 2 and 2, so rendering is unchanged.

The registry templates and the demo and dashboard installed copies are
regenerated to match.
The first pass moved the Fortal outlines onto focusRingWidth and
focusRingOffset, but the select trigger and the slider thumb still drew their
rings from hard-coded box shadows.

The select trigger now uses fortalFocusOutline with Radix's inset -1px offset
(`outline: 2px solid focus-8; outline-offset: -1px`), replacing the outer plus
inset spread-1 shadow pair that painted the same band. The slider thumb keeps
Radix's 3px accent-3 halo and draws the ring as a token-width outline starting
where the halo ends (`0 0 0 3px accent-3, 0 0 0 5px focus-8`). The halo stays a
3px literal: it is not the 2px outline-offset of the other outward rings.

Default tokens give the same bands as before. Tests now cover every ring:
outlines (including select), the slider thumb, and the inside-border rings
(toggle, toggle group item, tabs, disclosure, accordion trigger).

Registry templates and the demo and dashboard installed copies are
regenerated to match.
The branch that supplies a MediaQuery when a scope sits above any had no
test, because View always provides one in apps and widget tests. Pump each
scope under a bare RawView and check it supplies a MediaQuery and follows
the view's brightness. Also tell preset authors which lookup avoids the
rebuild this PR fixes.
Add RemixToastActionPlacement (inline, below) as ToastSpec.actionPlacement
and the generated ToastStyler.actionPlacement. Being a spec field, a style
can switch it with a Mix variant such as onBreakpoint.

`below` puts the action under the title and description, aligned with
their start, and top-aligns the icon, message, and close button so the
close button stays at the trailing top corner. An inline toast that is too
narrow for one row at the current text scale uses the same arrangement;
the fallback keeps the existing width measurement and no longer moves the
close button, or a toast without an action, anywhere.

The stacked arrangement groups the message and action into one semantics
node and keeps widget-order focus traversal, so assistive technology and
Tab still reach the title, description, action, then close button.
Give the focus contract its own test that performs the semantics focus
action, keep the label test about replacement, wait a frame before reading
the controlled panel so the test also holds on the web, and drop the
activation test that could not detect this change. Clarify the comment.
An errored Fortal text field keeps an error-8 outline whether or not it is
focused, merged after the focus ring, so it is also the only ring drawn while
the field has focus. Its width was a literal 2, so overriding focusRingWidth
did nothing for errored fields. It now reads the token, keeping the -1 offset
and the inset shadow, so default rendering is unchanged.

The token test now also covers the segmented control item, the solid and soft
icon buttons, the disabled field's gray-8 ring and the errored field (idle and
focused), and checks that the inside-border rings use the focus colour. The
slider assertion that restated the offset and width checks is removed, and the
fortalFocusOutline doc comment now says which offsets follow the offset token.

Registry templates and the demo and dashboard installed copies are regenerated.
RemixToast is now a StatefulWidget with the same constructor. It keeps
the action and close button under GlobalKeys, so their elements, and the
keyboard focus on them, survive a switch between the inline and stacked
arrangements. Before, a resize, rotation, or text-scale change that
crossed the breakpoint or the narrow cutoff re-created both buttons;
focus fell to the route, the scope's focus-within state never cleared,
the countdown stayed paused, and Escape no longer reached the toast.

The narrow fallback now subtracts the container margin from the parent
constraints before applying the style constraints, so a toast whose box
is below the cutoff stacks even when its parent is wide enough.

Reword the placement docs to what holds: the close button stays on the
first line after the message, `inline` keeps the action in the row only
while the message has room, and a toast with an inline action measures
its width and so does not support intrinsic sizing. The docs example
uses Breakpoint.maxWidth(768).
Author the HeroUI v3.2.6 look as a first-party preset that styles existing
Remix components instead of reimplementing them. Each component is a
top-level `@MixWidget(target: Remix<X>.new)` recipe returning a styler, or a
thin composition where Remix has no widget (Kbd, Surface, Typography,
groups, Breadcrumbs). Behavior, focus, keyboard, and semantics stay in Remix
and Naked UI; the preset supplies tokens, variants, and sizes only.

- theme: typed light/dark palette, tokens, effects, and a Vanilla-shaped
  `HeroUiTheme`
- 42 components covering the display, input, selection, overlay, and
  navigation families, plus tooltip, modal, accordion, disclosure, slider,
  toast, alert, progress, and table
- one worksheet per component under specs/heroui, derived from the pinned
  upstream CSS and measurements
- ADR 0001/0002 recording the reference-over-Remix doctrine, the approximations
  where stylers cannot express an upstream visual, and the general Remix API
  candidates proposed as separate changes
- recipe tests that assert measured upstream geometry and tokens in both
  color modes
Pin the upstream evidence the HeroUI recipes are measured against, and the
checker that keeps it honest.

- reference/heroui_3_2_6: tarball URLs and integrity, the compiled CSS and
  theme variables for the mapped families, browser-resolved tokens in both
  modes, rendered measurements (375 and 1024 px, hover/pressed/focus), the
  family map, and the upstream Apache-2.0 text
- tool/heroui_parity: `check.dart` verifies the source hashes, the token
  inventory, and the item mapping (`melos run heroui:parity:check`);
  `slim_measurements.dart` derives measurements.json from raw captures; the
  `chromium/` harness regenerates them
- raw Chromium captures stay out of the repository (.gitignore); only the
  derived measurements and tokens are committed
- `crypto` becomes a managed dev dependency of registry_source for the
  SHA-256 pins, and the workspace constraint is shared through melos
- melos scripts `heroui:parity:check` and `ci:open-code:heroui`, wired into
  `ci`, `ci:checks`, and the open-code check aggregates
Derive the HeroUI preset into `registry/heroui` and wire it through the
build, the consumer check, and CI like Vanilla and Fortal.

- build_registry: a `heroui` preset spec (`HeroUi` type word, `heroUi` value
  word) deriving the theme plus 42 component items into 51 templates
- registry/index.yaml lists `heroui`, so the registry workflow promotes it to
  registry-stable once the hosted smoke check passes; registry/icons.yaml
  gains `chevronRight` for the select and dropdown indicators
- check_open_code: `--preset heroui` with its own item list, dependency set,
  and import allow-list; the contract now carries per-preset foundation,
  independent, spec-styler, and dev-dependency sets, and rejects an `--item`
  the preset does not install. HeroUI installs need no consumer build.yaml
  and no consumer codegen
- open_code/heroui_fixture: the installed-app gallery, behavior tests, and
  the capture script used for side-by-side review
- CI: `heroui` joins the open-code matrix and the hosted smoke in the
  registry workflow; the fixture is excluded from root analysis
- validate_docs: Dart snippets that name `HeroUi*`/`heroUi*` compile against
  `package:registry_source/heroui.dart`
- tests: build_registry and check_open_code cover the third preset
Teach consumers and preset authors what the HeroUI preset is and the
doctrine it follows: a preset adds design decisions to Remix, never
behavior.

- skills/using-remix: a HeroUI reference (installed API, variants, sizes,
  approximations, motion) beside Vanilla and Fortal, plus evals
- skills/building-remix-design-system: the reference-parity guide
  (appearance-only fidelity from pinned CSS, resolved tokens, and measured
  geometry, with the extension ladder style, composition, general Remix API
  by ADR, defer), and the doctrine folded into the component, token, and
  registry references
- open_code: ADDING_PRESETS.md for authoring a first-party preset beside
  Fortal, and the PRESETS, README, and RELEASING updates
- docs/open-code.mdx, README.md, AGENTS.md, and the remix_cli README list the
  third preset and its catalog
Bump next to 16.4.1 and zod to 4.6.6 in apps/docs. The lockfile now
resolves sharp 0.35.5 (next's optional dependency) and source-map-js
1.2.2 (postcss, @tailwindcss/node), and no longer contains the vulnerable
versions.
The preset's remix floor follows the Vanilla catalog (^1.0.0-beta.13), and
the derived theme scope template picks up the non-subscribing MediaQuery
lookup already present in the authoring source.
Move flutter_test_config.dart into test/ so `flutter test --platform chrome`
can compile the suite; at the package root the web compiler cannot resolve it.

Fix the two web-only failures:
- integration_tests_test.dart launches a child process through dart:io, so
  mark it @teston('vm').
- Split the radio space and enter test. On the web Enter maps to
  ButtonActivateIntent, which RawRadio does not handle, so the enter half is
  skipped there.
Fortal's shadow tokens are lists, and a plain List equals only itself. MixScope
compares the old and new token maps value by value to decide whether to notify
its dependents, so every FortalTheme rebuild, even with identical settings,
notified every widget that reads a Fortal token. Vanilla's tokens are all
value-equal and were never affected.

buildFortalScopeTokens now returns its shadow lists as unmodifiable lists that
compare by value, so an equal theme builds an equal token map. The registry
template and the demo and dashboard installed copies are regenerated.
…des above it

Every theme scope installs a complete token set instead of merging with the
scope above it, in Vanilla and in Fortal alike, so a MixScope.inherit override
made above a nested theme does not apply below it. Say so next to the
description of nested scopes, and where to place the override instead.
RemixSelect and RemixMenu now wrap their overlay content in an internal
RemixAnchorScope that carries the trigger size. The new remixAnchorWidth
context token resolves to that width inside those overlays and to 0
elsewhere, so a style can size a panel from its trigger with
SelectContentStyler().minWidth(remixAnchorWidth()). Panels stay
content-sized unless a style opts in.
showNakedDialog, showNakedAlertDialog, and NakedTooltip now show and hide
at once when MediaQuery.disableAnimations is set or the platform reports
AccessibilityFeatures.reduceMotion, which iOS Reduce Motion sets without
setting disableAnimations. A dialog reads the setting when it opens and when
it is dismissed; a tooltip follows changes while mounted.
The open-code jobs are the pipeline's long pole, and the longest stretch in
them is the independent-item loop: eighteen throwaway apps, each doing its own
`pub get`, `remix init`, and `remix add` with a generator run, one after
another. The items share nothing, so they now go through a pool of at most
four workers (one per core below that, never fewer than two). `--jobs N` or
`REMIX_OPEN_CODE_JOBS=N` overrides the default; `--jobs` is rejected with
`--item`, which never reaches the pool.

Each item buffers its output and the logs are written in input order, so the
log does not depend on which install finished first. After a failure no more
items start, the running ones are awaited, and the run still fails with the
earliest failing item's message and every started item's log. `_runProcess`
takes an optional log, keeps the registry-harness redirect, and drains both
pipes while waiting for the exit code.

The pub cache key now also hashes `registry/*/registry.yaml`: the throwaway
apps resolve the dependencies those manifests declare, so a registry-only
change used to restore a cache that lacked them.
Add RemixReducedMotion, which reports whether MediaQuery.disableAnimations
or the platform's accessibility reduceMotion is set and rebuilds when either
changes. iOS Reduce Motion only sets reduceMotion, so reading
MediaQuery.disableAnimations alone missed it.

The select menu and accordion panel now open and close without a
transition, and RemixSpinner holds a static frame. Skeleton and sidebar,
which already followed MediaQuery.disableAnimations, now follow iOS Reduce
Motion too. Style animations declared with .animate(...) are driven by Mix
and are not affected.
…uery changes

RemixSidebar read MediaQuery.maybeOf(context)?.disableAnimations, which
subscribes the sidebar to every MediaQuery field, so it rebuilt on each
keyboard inset frame. Read only disableAnimations through
MediaQuery.maybeDisableAnimationsOf.

The Agent loading glyph read the same field the same way. Its preset
source, registry templates, and installed dashboard and playground copies
take the same one-line change. The Vanilla and Fortal SidebarLayout reads
stay: MediaQuery.removePadding in the same widget subscribes it to the
whole MediaQuery regardless.
Checked against the pinned @radix-ui/themes 3.3.0 CSS, five Fortal focus
treatments differed from Radix.

- The soft radio rings in accent-a8 (base-radio.css:165-168); the other
  variants keep focus-8.
- The soft select rings in accent-8 (select.css:430-433). Fortal resolves
  both tokens to the same step, so the rendering is unchanged unless the focus
  color is overridden.
- The slider thumb's own 1px edge stays above the focus halo
  (slider.css:88-93). The halo moves from the foreground layer to behind the
  thumb, which is where the thumb's decoration shadows sit above it.
- The dashboard accent swatch rings with Radix's ThemePanel outline
  (theme-panel.css:36-47): 2px off the edge, gray-12 once selected, and the
  panel's accent-9 while focused, so focus stays visible on the selected
  swatch. The ring is a foreground border and takes no layout space; the panel
  gains 4px of horizontal padding so the scroll views that host it do not clip
  the first column.
- A focused sortable data table header draws a focus-8 ring inside its cell,
  as a foreground border like the row divider, through the existing headerCell
  slot.

The errored text field keeps its current focus treatment, which needs a design
decision.

Registry templates and the demo and dashboard installed copies are regenerated.
Add NakedMenu.openOnTap (default true). When false the menu draws no
NakedButton and adds no tap, keyboard activation, or focus stop: the child
or builder is the caller's own control, and opening and closing go through
the controller. triggerFocusNode is passed only to RawMenuAnchor
.childFocusNode, and Semantics(expanded:) still merges onto the control's
node. Using it inside a NakedMenubar asserts.
Mirror RemixCheckbox.label on RemixRadio and RemixSwitch. The label is drawn
inside the control's pointer, hover, press, and focus target, excluded from
semantics, and used as the accessible name unless semanticLabel overrides it.
RadioSpec and SwitchSpec gain label and labelSpacing (default 8) with the
generated label styler helpers, and both widgets gain minimumTapTargetSize,
which defaults to Size.zero so existing layouts are unchanged.

semanticLabel becomes optional: one of label and semanticLabel is still
required, now enforced by an assertion.

The label layout moves into one unexported RemixLabeledControl shared by
checkbox, radio, and switch. A radio or switch with no label and no minimum
target keeps its widget tree.
Add containerEffects to the Tab, Toggle, ToggleGroup item, SelectMenuItem,
MenuItem, and MenuTrigger specs, and triggerEffects to Accordion and
Disclosure, whose containerEffects already covers the whole panel. Each
widget renders through RemixBoxAdapter or RemixFlexBoxAdapter, so an
outline paints outside the box without taking layout space.
…mer check

Each item a preset installs alone starts a fresh `remix init`, and an init
that resolves the registry from a branch makes one unauthenticated GitHub API
request (60 an hour). Marking all 42 HeroUI items independent raised the
hosted release check from 38 to 81 requests, so the next remix_cli release
verification could fail on the rate limit, and added a second pass of 42
throwaway installs to the HeroUI CI job.

HeroUI now installs alone only the items whose registry dependencies name
something besides the theme (14 of 42), derived from registry.yaml, which
includes every item that uses an icon. The combined install of every item is
unchanged, and Vanilla and Fortal keep their lists.

Also restores the unconditional build_runner step (every preset has generated
parts), names the shared source-selection test for every preset, and braces
a multi-line `if` in the test helper.
RemixSidebar published `disabled` over its whole panel, so a widget in
`header` or `footer` with an `onDisabled` style lit when the sidebar was
disabled, contradicting the `enabled` documentation. Header and footer now get
an empty scope; the sidebar's own regions still resolve `onDisabled`.

Also cover disabled accordion and disclosure content, document that the
content of a non-sortable data-table header resolves widget-state variants
against an empty state, and reword the changelog entries.
# Conflicts:
#	packages/remix/CHANGELOG.md
Scroll notifications from the option list bubbled to the host page, so a
Material 3 AppBar elevated while the list scrolled and stayed elevated after
the select closed. The notifications now stop at the options, including the
scroll metrics notifications that hosts also treat as scrolling.

The list also follows Flutter's own menus: no overscroll stretch, no bounce on
iOS and macOS, clamping at both ends, and the desktop scrollbar stays.

Document the scrolling and the maxHeight that caps it, and note in the
changelog that menuContainer's main-axis alignment and maximal main-axis size
no longer distribute free vertical space.
…lays

Home and End in an anchored overlay (NakedSelect, NakedMenu) requested focus
directly on the first or last item, which skipped the traversal policy's
requestFocusCallback. When the overlay content scrolled, End focused the last
item without revealing it, so End then Enter selected an item the user could
not see. Request focus through the policy, as the arrow keys already do.
# Conflicts:
#	docs/components/select.mdx
#	packages/remix/CHANGELOG.md
… accordion triggers

RemixSidebar drew destinations with Mix RowBox, so the containerEffects slot
that SidebarDestinationStyler (a ToggleStyler) gained never painted: a preset
that moves its toggle focus ring there lost the sidebar destination ring.
Both destination row layouts (settled and collapsing or expanding) now render
through RemixFlexBoxAdapter.

RemixAccordion painted triggerEffects only on its default trigger, while
RemixDisclosure also wraps a custom triggerBuilder widget. A custom
RemixAccordion.builder widget is now wrapped in triggerEffects too, without
applying the trigger box, so a preset ring shows around a custom trigger.
# Conflicts:
#	packages/remix/CHANGELOG.md
#	packages/remix/lib/src/components/accordion/accordion_widget.dart
#	packages/remix/lib/src/components/sidebar/sidebar.dart
…e groups, and menubars

NakedTabBar Home and End, NakedToggleGroup arrow keys, Home, and End, and
NakedMenubar Home and End requested focus directly on the target, which skipped
the traversal policy's requestFocusCallback and left a target inside a
scrollable off screen. Request focus through the policy's callback, as the
arrow keys in tabs and the overlay fix already do.
…ic sizing

The shared label layout chose between a flexible and a plain label inside a
LayoutBuilder, which cannot report intrinsic dimensions. A labeled control
asserted in debug and collapsed in release under IntrinsicWidth,
IntrinsicHeight, and Material AlertDialog content.

A Row already shrink-wraps a loose Flexible child when its width is
unbounded, so the layout is now a plain Row with a Flexible label. The label
still wraps in bounded width and takes its natural width otherwise.

Drop the intrinsic-sizing warning from the dartdoc, docs, and skill, and add
a Fixes entry for the released RemixCheckbox label.
A RemixMenu panel, and every submenu panel, rendered its items in a column
with no scroll view, so a list taller than the overlay or than a maxHeight on
the overlay style overflowed.

RemixFlexBoxAdapter gains a scrollable option that puts a contained vertical
scroll view between the box and its column. The box's padding, decoration,
constraints, and effects frame the scroll view. The scroll view does not attach
to the host's primary scroll controller, does not notify the host page, and
clamps at the ends. RemixMenu and RemixSelect both use it, which replaces the
select's private scroll structure.
Focusing through the traversal policy's default callback aligns the target to
the trailing edge of every ancestor scrollable, even when it is already
visible, which scrolled the page and shifted fully visible strip items.
requestFocusInView runs the policy callback with keepVisibleAtEnd and then
keepVisibleAtStart so a target is revealed from either side and left alone when
visible. The anchored overlay, tabs, toggle group, and menubar use it, and
MenubarTriggers.move now reveals the next trigger as well.
…n stack

# Conflicts:
#	packages/naked_ui/CHANGELOG.md
@leoafarias leoafarias mentioned this pull request Oct 27, 2026
4 of 5 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant