Skip to content

Appearance preview: genuinely pinned header, unified and larger - #284

Merged
ghbarker merged 1 commit into
feature/post-refactorfrom
fix/appearance-preview-pinned-large
Sep 8, 2026
Merged

ghbarker merged 1 commit into
feature/post-refactorfrom
fix/appearance-preview-pinned-large

Conversation

@ghbarker

@ghbarker ghbarker commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Why

Shield feedback after PRs #263/#271/#275/#279: "The live preview also needs changed, it should stay pinned at the top of the screen and change based on what is hovered/last selected. The current way is way too small."

Two problems in the current system:

  • The preview (AppearancePreviewCard inside AppearancePreviewHost) was only the first item in the scrolling category column — it scrolled away like anything else, not pinned to the screen at all.
  • A separate compact copy, AppearancePreviewDock, appeared as a Stack/Positioned/IgnorePointer overlay while a Screen-layouts row was hovered/focused, but fit its stage into a fixed 78px-tall box — the "way too small" the Shield feedback names.

What

  • Retired AppearancePreviewDock entirely. AppearancePreviewHost is now the ONE preview. It shows the theme stage when a Look is hovered on the strip, or a LayoutPreviewStage when a Screen-layouts row elsewhere in the pane is hovered/focused, in the same slot — driven by LayoutPreviewChannel's pointed/resting model, extended with pointLook/unpointLook/restLook so a Look candidate and a layout candidate are one notifier instead of two parallel systems (a local _candidate State plus the channel).
  • Genuine pinning per surface, replacing the old Stack overlay:
    • SettingsSpotlightShell (phone/tablet/desktop): a new pinnedHeaderBuilder callback lets settings_screen.dart supply the preview as a Column header above an Expanded scrolling body — both the wide (_buildCategoryBody) and compact (_buildCompactDetail) layouts.
    • SettingsTvLayout: _buildPane's Appearance branch is now Column([header, Expanded(scrollBody)]) instead of Stack([paneScroller, Positioned(dock)]), with both header and body kept inside the same Focus(onKeyEvent: _paneKey)/FocusTraversalGroup so DPAD bubbling (Left at the first chip → back to rail) is unchanged.
    • buildSettingsCategoryChildren gained includeAppearancePreview (default true): it always reserves pane node zero for the preview so indices never shift, but a shell that pins the header separately passes false and builds AppearancePreviewHost(focusNode: paneNodes[0]) itself.
  • Bigger stage, scaling with available width: new named constants kAppearancePreviewStageMaxWidth (390) / kAppearancePreviewStageAspectRatio (16:9) / kAppearancePreviewStageMaxHeight (≈219) in appearance_preview_card.dart, replacing the old dock's fixed 78px box (≈2.8x taller) — both ThemePreviewStage and LayoutPreviewStage are fitted into the same slot via the existing "fixed canvas, FittedBox" technique.
  • Overflow safety net: a pinned header sits in a non-flexible layout slot, so nothing bounds its height against extreme system text scale (200%+) or a short TV frame (960×540). Both shells wrap the header in a ConstrainedBox + SingleChildScrollView capped at (viewport height × 0.75).clamp(240, 520) — inert at normal scale (natural height stays under the cap), the escape hatch only engages under extreme conditions.

How verified

  • flutter test --no-pub on every touched test file — all pass except two pre-existing, unrelated golden failures (settings_spotlight_phone.png, settings_spotlight_tv.png — the Connections-category root dashboards, untouched by this change) that reproduce identically on origin/feature/post-refactor before this branch's changes; this repo's goldens are Windows-rendered and allowlisted against the Linux goldens job.
  • dart analyze on every changed file: 0 errors, only 3 pre-existing use_build_context_synchronously infos unrelated to this diff.
  • tool/check_layering.dart: 42/42 (unchanged ceiling, no new violations in touched files).
  • tool/analyze_baseline.json: unchanged; zero new diagnostics in any file this PR touches (the baseline-check script does surface unrelated UNAWAITED_RETURN_IN_TRY_BLOCK drift across ~10 pre-existing files elsewhere in the repo — environmental SDK/analyzer drift, not introduced here).

Regenerated goldens (Windows-rendered, [golden]-tagged tests, all pre-existing golden tests — no new golden tests added so no new allowlist entries needed):

  • test/goldens/settings_layout_options_phone.png
  • test/goldens/settings_spotlight_desktop.png
  • test/goldens/settings_spotlight_phone_appearance.png
  • test/goldens/settings_spotlight_tablet.png
  • test/goldens/settings_spotlight_tv_appearance.png

New/updated tests:

  • test/appearance_preview_dock_test.dart — rewritten for the pinned-header architecture: pinned position unchanged across DPAD scrolling (TV @ 960×540 and 1920×1080) and pointer scrolling (desktop @ 1280); the same AppearancePreviewHost State instance shows theme-stage content for a Look hover and layout-stage content for a Screen-layouts row hover with no duplicate preview; a flicker-regression test sweeping the pointer rapidly across the Look strip and a Screen-layouts row's chips (no exception, pumpAndSettle terminates, nothing under the pointer shifts); a size assertion against the new named constants.
  • test/appearance_preview_tv_strip_test.dart — the old "focusing the strip reveals the whole card" scroll-reveal test replaced with "the header stays fully on screen no matter how far the pane below it is scrolled" (there is nothing left to reveal — the header was never inside the scrolled body).
  • test/settings_spotlight_shell_test.dart — its Appearance fixture now supplies the preview through pinnedHeaderBuilder instead of embedding it as the first child of categoryBuilder's Column, matching production.
  • Verified unchanged (no source changes needed): appearance_preview_focus_node_release_test.dart, settings_appearance_preview_test.dart, settings_layout_options_test.dart, settings_appearance_groups_test.dart.

Risks

  • The overflow-safety cap (0.75 × viewport height, clamped 240–520) is a defensive heuristic, not derived from measured content — a future caption/chip change that grows meaningfully taller could reintroduce clipping at 540p; the cap is in one place per shell if it needs raising.
  • pinnedHeaderBuilder is a new, generic extension point on SettingsSpotlightShell — currently only Appearance uses it, but any category returning a non-null header now gets the same pinned treatment, so a future category with a tall header would need the same size discipline.
  • Per the environment notes for this change, I did not touch settings_catalog.dart or settings_page_registry.dart's category/group wiring beyond the minimal includeAppearancePreview parameter needed here, to avoid conflicting with the concurrent feature/settings-menu-reorg lane.

The live preview scrolled away with the rest of the Appearance pane
(only its own top-of-pane position, not the top of the screen) and the
compact dock shown while hovering a Screen-layouts row squeezed the
stage into a 78px-tall box — both flagged on the Shield.

Restructure each surface (SettingsSpotlightShell, SettingsTvLayout) so
the preview is a fixed header above an Expanded scrolling body, instead
of a Stack overlay riding on top of it. Retire AppearancePreviewDock:
AppearancePreviewHost is now the one preview, pinned directly, and
LayoutPreviewChannel is extended with pointLook/unpointLook/restLook so
a Look hover and a Screen-layouts row hover share the same
pointed/resting model instead of two parallel notifiers. The stage
slot scales with available width up to a new named constant, well past
the old dock's 78px and LayoutPreviewStage's own 320x180 design canvas.

Pane-node accounting for the TV two-pane shell is unchanged: the
preview still claims exactly node zero, whether or not
buildSettingsCategoryChildren inlines the widget itself
(includeAppearancePreview) or a shell builds it separately as the
pinned header.
@ghbarker
ghbarker merged commit 21735e7 into feature/post-refactor Sep 8, 2026
3 checks passed
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