Skip to content

fix(web): correct the dashboard's stylesheet delivery - #280

Closed
Menci wants to merge 4 commits into
fluent-dashboardfrom
fluent-dashboard-fonts
Closed

fix(web): correct the dashboard's stylesheet delivery#280
Menci wants to merge 4 commits into
fluent-dashboardfrom
fluent-dashboard-fonts

Conversation

@Menci

@Menci Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Stacked on #277.

Summary

Two defects in how the dashboard's CSS reaches the browser.

Utility stylesheet generation. UnoCSS ran in per-module mode, which generates one sheet per module and leaves the bundler to concatenate them in module-graph order. That forfeits UnoCSS's rule ordering, which is a property of a single generate() call. Breakpoint variants landed ahead of the base utilities they override, and since a media query adds no specificity the base utility won at every width — 42 responsive rules across 25 files were inert, among them the sidebar collapse and the mobile navigation trigger.

per-module was itself a workaround: the Vite integration's default global mode emits nothing under React Router. It keys its vite:css-post handle by the top-level build.outDir, while React Router sets outDir only per environment (dist/client, dist/server) and opts into builder.sharedConfigBuild, which is what would otherwise re-resolve the config per environment. The lookup misses and the layer placeholder ships as the entire stylesheet — unocss/unocss#4990, open, and not specific to Vite 8 or Rolldown.

Generating through @unocss/postcss sidesteps that machinery entirely: one pass over the content set is one generate() call, so base utilities, lt-*, and ascending min-width breakpoints emit in that order. The plugin's cwd is pinned to the package, because it resolves both config discovery and the content.filesystem globs against the build process's working directory — without the pin, a build launched from the workspace root emits an empty sheet. The source-level transformers go with the Vite plugin, which was the only thing applying them; neither was reachable, since the source has no variant groups and no CSS files for the directive transformer.

Monospace face. Bundle Cascadia Code through Fontsource and publish it as the theme's fontFamilyMonospace, so code surfaces stop resolving to Courier on macOS. Route UnoCSS's font-mono and every remaining hardcoded monospace declaration through that one token, and override the Prism themes' own font declaration, which addresses the code block through pre[class*="language-"] and outranks a single-class rule. Drop the fontFamilyBase / fontFamilyNumeric overrides, which pinned the bare sans-serif (Helvetica on macOS) in place of Fluent's Segoe UI stack.

Verification

  • pnpm run lint
  • pnpm run typecheck
  • pnpm run test — 420 files, 4,772 tests passed
  • pnpm --filter @floway-dev/web run build
  • parsed the emitted stylesheet and cross-checked every responsive class against the source: 42 overridden before, 0 after
  • confirmed the computed layout at 500, 700, 900, and 1400px in a headless browser, and measured the rendered font-family and loaded @font-face against the running dashboard

Every code surface asked for the generic `monospace` family, which macOS
resolves to Courier — a face that shares neither metrics nor mood with the
rest of the dashboard. Bundle Cascadia Code through Fontsource, publish it
as the theme's monospace family, and route UnoCSS's `font-mono` plus the
remaining hardcoded declarations through that one token.

The Prism themes address the code block through `pre[class*="language-"]`,
which outranks a single-class rule, so those two declarations override the
theme outright.

Drop the base and numeric family overrides at the same time: they pinned
the bare `sans-serif`, which resolves to Helvetica on macOS, while Fluent's
own stacks lead with Segoe UI and fall back to the platform UI face.
The Vite integration's global mode emits nothing here: it keys its
vite:css-post handle by the top-level build.outDir, while React Router
sets outDir only per environment and opts into builder.sharedConfigBuild,
so the lookup misses and the layer placeholder ships as the entire
stylesheet (unocss/unocss#4990). Per-module mode sidestepped that by
generating one sheet per module, but concatenating those in module-graph
order put breakpoint variants ahead of the base utilities they override.
A media query adds no specificity, so the base utility won at every
width: 42 responsive rules across 25 files were inert, among them the
sidebar collapse and the mobile navigation trigger.

Generate through PostCSS instead. One pass over the content set is a
single generate() call, which is where the rule ordering guarantee lives
-- base utilities, then lt-*, then ascending min-width breakpoints. Pin
the plugin's cwd so config discovery and the content globs resolve
against this package rather than the build process's working directory;
without it a build launched from the workspace root emits an empty sheet.

The source-level transformers go with the Vite plugin, which was the only
thing applying them. Neither was reachable anyway: no variant groups
appear in the source, and there are no CSS files for the directive
transformer to process.

Checked the emitted sheet against every responsive class in the source
(42 overridden, now 0) and confirmed the computed layout at 500, 700,
900, and 1400px in a headless browser.
@Menci Menci changed the title feat(web): publish Cascadia Code as the dashboard's code face fix(web): correct the dashboard's stylesheet delivery Jul 28, 2026
@Menci

Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Codex agent: Consolidating this follow-up into #277 as requested; the branch remains available while its changes are merged and verified there.

@Menci Menci closed this Jul 28, 2026
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