feat(web): rebuild the dashboard with React and Fluent UI - #277
Open
Menci wants to merge 582 commits into
Open
Conversation
This was referenced Jul 28, 2026
# Conflicts: # apps/web/__tests__/components/settings/SearchConfigSection_test.ts # apps/web/src/api/types.ts # apps/web/src/components/settings/ImportSection.vue # apps/web/src/components/settings/SearchConfigSection.vue # apps/web/src/pages/dashboard/settings.vue # packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts # packages/gateway/__tests__/control-plane/search-config/routes_test.ts # packages/gateway/__tests__/control-plane/search-usage/routes_test.ts # packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts # packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-grounding_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-web-iq_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/providers/web-iq_test.ts # packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts # packages/gateway/__tests__/repo/search-usage_test.ts # packages/gateway/src/control-plane/schemas.ts # packages/gateway/src/control-plane/search-usage/routes.ts # packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts # packages/gateway/src/data-plane/tools/web-search/config.ts # packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts # packages/gateway/src/data-plane/tools/web-search/operations.ts # packages/gateway/src/data-plane/tools/web-search/provider.ts # packages/gateway/src/data-plane/tools/web-search/providers/jina.ts # packages/gateway/src/data-plane/tools/web-search/providers/microsoft-grounding.ts # packages/gateway/src/data-plane/tools/web-search/providers/microsoft-web-iq.ts # packages/gateway/src/data-plane/tools/web-search/providers/web-iq.ts # packages/gateway/src/repo/sql.ts # packages/gateway/src/shared/web-search-providers.ts
Menci
marked this pull request as draft
July 29, 2026 08:27
# Conflicts: # apps/web/src/root.tsx # apps/web/src/theme.ts
WinUI holds a chromeless button's label at the primary text fill where Fluent runs it at the secondary one, and the layer said so by redefining the token Fluent reads. A custom property handed to the root reaches every descendant, and a button is free to hold more than a label: the usage summary puts a two-line tile inside one, and its caption -- which asks for the secondary fill by name -- was being answered with the primary, so the same caption read darker there than beside the identical tile on the performance page, which is not a button. Stated as a colour instead. It inherits too, but a descendant naming its own wins over it, which is the distinction that was missing.
Stating the chromeless foreground as a colour outranked what Fluent paints on a checked one, so a checked subtle button kept the accent fill and lost the on-accent label with it -- and a caller that hands that label down by inheriting from the root, as the usage summary does, got the near-black primary fill over accent blue. The rule now names the unchecked states only. A checked chromeless button paints itself from the accent family, which already carries its own label colour.
The tile was a ToggleButton, so choosing a metric filled it with the accent -- the heaviest mark WinUI has, and the right one for a switch that is on. Picking one item out of a set is not that. Every other selected thing in this dashboard says so the same way: the subtle fill it would take under the pointer, held, with an accent bar down its leading edge, and its text left alone. The fill and the bar are stated at the call site rather than in the layer, which paints a ToggleButton as WinUI paints one and is right to. The bar carries the indicator's 16px as the inset it leaves against a 36px row, so it grows with a tile that is taller than one.
Replacing the fill left the rest of it behind. The value inherited the on-accent label the layer sets on the root, so it stayed white over a pale fill, and the accent's elevation stroke stayed too -- its heavier bottom edge reading as a rule under the tile. The layer names the checked state at a specificity a call site cannot reach, which is right for a rule meant to hold across the dashboard, so undoing it here is what the escape hatch is for.
WinUI states the navigation indicator as a 16px rectangle, but it states it against a 36px item, so the length is a consequence of the room the item gives it rather than a value of its own. The list row and the usage tile already carry it that way; the navigation pill was the last one still naming a length, and a length cannot follow an item that grows. Every stretch in the animation is a multiple of the bar's own length, so that length is now derived from the item alongside them.
The bar's length is the room its item leaves it, and the layout has already worked that out. Recomputing it from the inset duplicated the arithmetic CSS had just done and would have drifted from it the moment either side moved.
Fluent animates its overlays from JavaScript through react-motion, so none of it was reachable from the stylesheet layer and the dialog was still entering Fluent's way: growing in from 0.85 while fading across the whole 250ms. WinUI does the opposite -- it settles down from 1.05, and its fade is a separate 83ms linear run, so the surface is already opaque while it is still moving. Hiding takes 167ms to showing's 250. The motion slot forbids `as`, so the animating component cannot be named directly; it does take a render function, which receives the props Fluent would have handed its own motion component. That is the seam. The duration and easing vocabulary moves to winui/motion.ts, which the token sheet now interpolates, so the CSS custom properties and the keyframe numbers are one value. The selection-indicator timing joins it from lib/, where it never belonged.
The backdrop was on Fluent's relaxed fade, 300ms, which outlasts even the dialog's own 250ms entrance -- the page was still darkening after the dialog had arrived and settled. WinUI gives the smoke layer its own keyframes and nothing but an opacity leg, 83ms linear, symmetric in both directions; the scale it pairs that with targets the dialog's background element, so the backdrop itself never moves. Measured in a browser: 0 to 1 in five linear steps ending at 89ms, and the same going out.
The ramp was transcribed once and then described twice, in two comments that contradicted each other: one still said Floway's brand fill stood in for it, the other stated the literals that had replaced that. The second is what the code does. The first is gone, and with it the reason the layer had for keying anything to Fluent's theme class -- no value here reads a Fluent variable any more, so the whole vocabulary sits on :root and the selected-text highlight takes the unmodified accent step WinUI actually names for it. Light1 and Dark3 complete the seven steps. That closes three deferrals the old comment justified: the accent text ramp's secondary and tertiary stops, the toast action's link colour, and the accent edge a focused text field draws, whose stop is the same step the accent fills take. The nav sheet's two remaining "out of reach" notes go too. The sidebar stopped forcing item fills and the layer implements the subtle ramp above them; the selected label came off semibold; and the pill has been on the accent fill since it became a measured element.
WinUI does not move a menu into place, it wipes it in: the presenter slides from half its own height while a clip slides the other way by exactly as much, pinning the visible window to the final layout box so only the content travels through it. A menu below its trigger starts as its own bottom half drawn in the top half of the box and grows down. Nothing fades in. Closing is the opposite kind of thing entirely -- a bare 83ms linear fade with no transform, which is why the two directions are written as an unmatched pair. The slide goes on `translate` rather than into `transform`, because `transform` is where the positioning engine already put the popover's coordinates; a keyframe naming it plays the whole reveal at the origin of the containing block instead. Measured: the clip and the offset agree to seven digits at every frame, and the positioning transform is untouched throughout. Popover keeps Fluent's motion. Its WinUI counterpart reads its timings out of the Windows theme at runtime, so there is nothing to transcribe.
Four faults, two causes. The chromeless selector was built as a comma-joined list and then had each state suffixed onto the joined string. A suffix attaches to a selector list's last item alone, so the pressed rule -- the last one written -- matched every subtle button in every state, and the rest rule above it never won. A subtle button's label sat at the secondary text fill at rest while the transparent button beside it, which the file declares to be the same WinUI style, sat at the primary one. The state is now appended per appearance. The same list carried no disabled exclusion, which is why a disabled chromeless button was pixel-identical to an enabled one; it now excludes both spellings of disabled and lets Fluent's disabled foreground through. The primary appearance had only its hover and pressed steps moved onto the accent, leaving the rest fill on the product's brand. WinUI states all three as one colour at 1.0, 0.9 and 0.8 opacity, so they cannot differ in hue -- and in dark the mismatch put the on-accent label, which is black against a light accent, on a dark brand fill. Stating the rest fill also settles the second half: a checked ToggleButton already read the accent, so the two sat side by side in different blues. Measured, both schemes: subtle rest #000000e4 / #ffffff, disabled #0000005c / #ffffff5d, and primary rest, primary hover at 90% and a checked toggle all resolving to #0067c0 / #4cc2ff.
Every fill and indicator rule in the sheet named NavItem alone, so a selected category row kept Fluent's opaque grey and Fluent's own pill -- 4x20 and fully rounded, in the brand foreground -- next to a WinUI item carrying a 3x16 accent bar. The fill ramp now names both. The pill stays a pseudo-element rather than being cleared the way an item's is. Clearing an item's is only right because the sidebar draws a measured one in its place to animate between items; a category opens and closes rather than navigating, so it is never an endpoint of that animation and has nothing to hand over to. Its colour has to be substituted, not declared. Fluent grows the pill in with a keyframe whose 100% stop reads the compound brand foreground, filled in both directions, and an animation outranks every normal rule in the cascade -- a background-color of ours was measured losing to it. The variable is declared on the pseudo-element itself, so it reaches the one declaration that reads it and nothing the row contains. Measured, both schemes: fill #9 / #ffffff0f, pill 3px at a 2px radius in #0067c0 / #4cc2ff.
The severity fill is the most visible thing an InfoBar has and it was the one part of the control never implemented: the bar was a pale wash of the brand ramp in all four severities, and its glyph was Fluent's tint rather than the system fill the validation glyph beside it on the same page already used. In dark that put WinUI yellow next to Fluent orange for the same idea. Both halves of the blocker are gone. The intent reaches the DOM as `data-winui-intent`, and the filled glyph the chokepoint swaps in is already WinUI's two-layer shape -- a disc with the symbol as negative space -- so only the colour was missing. What shows through the knock-out is a hole onto the bar's own wash where WinUI paints TextFillColorInverse; the two differ by a few percent of lightness and closing it would mean stacking an element behind a glyph that is already the right silhouette. The vocabulary gains the four SystemFillColor*Background steps and the success and attention hues. Attention is a step of the accent ramp rather than a literal, and not the step the accent fills take. Measured, both schemes: every bar and glyph resolves to its XAML value, and the stroke stays the card stroke every severity shares.
A link kept Fluent's brand foreground while the toast's link-shaped action had moved onto WinUI's accent TEXT ramp, so one screen carried two link colours. WinUI walks a hyperlink down that ramp -- primary, secondary on pointer, tertiary while pressed -- and it is a different ramp from the accent fill a button takes, darkened in light and lightened in dark so it reads as type rather than as a surface. The disabled step was already there; the three enabled ones join it. The check box was drawn at Fluent's 2px. Its sheet said the radius was "already ControlCornerRadius through the themed borderRadiusSmall", which the theme layer's own comment contradicts -- it leaves the small step on Fluent's 2px because no WinUI radius is that small -- and the browser sided with the theme. Stating 4px means naming the square shape so `shape="circular"` keeps its circle, which is one more axis Fluent resolves in JavaScript. That made four axes, so the three near-identical stamping functions collapse into one that takes the prop, the attribute, the default and the slots. Checkbox needs the slot list for the same reason Input does: its root is not its primary slot, and the stamp otherwise lands on the inner input. The request list's row separator named the neutral stroke, which maps onto the CARD stroke -- black at 10%, invisible against a dark page -- where every other row separator in the dashboard reads the divider. Its key selector hardcoded a 7px radius; WinUI has 4 and 8, and the selector heads a card, so it takes the card's. Measured, both schemes: link #003e92/#99ebff resting and #001a68/#99ebff on pointer, check box 4px, key selector 8px.
The drop zone predates the layer and sat entirely outside it: a plain button with no fui- class, so not one rule in winui/controls could reach it, holding Fluent brand colours, an ad-hoc 150ms transition and a 0.6 opacity for disablement. Neither library ships a drop target, so it is composed rather than transcribed. It is a button, and WinUI paints a chromeless button by stepping the subtle fill ramp; the outline takes the strong control stroke, the family an unchecked check box reads, because this is an outline that has to be seen rather than a surface edge; the radius is the overlay step the card around it takes; and dragging a file over it is the accepting state, so that one takes the accent. Disablement steps the fill and the foreground the way WinUI disables everything except a list item, instead of fading the element whole. The dashed pattern is the one thing with no WinUI provenance. It is the affordance itself and nothing in the corpus describes a drop target. The label and glyph drop their own foreground so the zone's disabled state actually reaches them, and the summary tiles take the card ramp's secondary step at the control corner rather than a hand-picked grey at a 6px radius WinUI has no counterpart for. Measured, both schemes: stroke #72 / #ffffff8b, radius 8px, rest transparent stepping to #9 / #ffffff0f, label #0000009e / #ffffffc5, 83ms on cubic-bezier(0, 0, 0, 1).
Each was removed in a live page and the computed value re-read after the
cascade settled, on every dashboard page, in both colour schemes, at two
viewports, and again with hover, active and focus forced on the element
and on six levels of its ancestors. None of the six moved a pixel.
Panel's radius map every card takes the overlay corner from the
layer, so the 8px entry restated it; the one
caller wanting 12px now says so itself, and
the prop goes with the map
sidebar item radius 4px, which is what the medium radius already
resolves to
tooltip icon colour `inherit !important`, which is what an icon
does anyway once the button states a colour
usage tile `!h-auto` height:auto is the initial value
usage graph opacity 1, already the initial value
request selector shadow none, and the layer flattens that elevation
Measured with transitions zeroed. Reading a computed value in the same
task as the CSSOM edit returns the pre-edit value while a transition is
running, which is what made an earlier pass of this call six more
declarations dead than really are.
Each was measured twice. First by dropping the importance from the live rule and re-reading the computed value on every dashboard page, in both colour schemes, at two viewports, with the states its own selector names forced through the debugger -- 81 rules carried importance and 26 of them turned out to change nothing without it. Then, after the edit, by diffing a full computed-style snapshot of every element on every page against the same snapshot taken with the change reverted: 28 properties, 20 page and scheme combinations, zero differences. Most were Uno utilities prefixed to beat a Griffel atom that does not exist -- widths, paddings, text alignment, overflow. The rest were the sidebar item's geometry and the usage chart's SVG overrides, which win on their own selector, and the usage tile's fill and label, whose attribute selector already outranks what it displaces. The tile's border colours keep theirs: those really are outranked, and the measurement says so.
A scan of the vocabulary against every var() in the app finds one declared property with no reader: SystemAccentColorLight1. It went in with the rest of the ramp, and unlike Dark3 -- which the accent text ramp's secondary step spends -- nothing has asked for it. The header's note about token blocks that must be declared where Fluent declares its theme goes too. There are no such blocks left.
The dashboard has been restyled onto WinUI 3 for a while and the file still described it as a Fluent web app with no mention of the layer, its scoping rules, or the two shims that make Fluent's JavaScript-resolved props and overlay motion reachable at all.
The sidebar's footer separator is reached through an inline style attribute, not through an `!important` declaration -- those came off when the measurements showed they changed nothing.
Each names a value the element already had: `tracking-normal` and `font-fui-regular` are the initial letter-spacing and weight, the three `leading-*` steps repeat the line-height their type ramp already sets, and `inline-block` loses to a display the same element is given elsewhere. Measured by removing all thirteen and diffing a full computed-style snapshot -- 60 page, scheme and viewport combinations, every element, 28 properties -- against the same snapshot of the unchanged tree. The two differ on one span, by 2px of text width, and two snapshots of the unchanged tree differ on that same span by the same amount: it is the harness's noise floor, not this change. Eight further candidates were tested the same way and put back. The sidebar footer's transparent fill, the two grid row templates and the chart height all moved real pixels -- the row templates let the document grow past the viewport -- and `font-mono` reaches twenty-one files where the measurement covered one.
The panel asked for 12px, which is not a radius WinUI has: the corpus states exactly two, ControlCornerRadius at 4 and OverlayCornerRadius at 8, and a card-shaped surface takes the second. The layer already gives every Card that value, so the override went rather than moving to a call site. Measured at both schemes: 12px to 8px on all four corners, with the padding and the fill unchanged.
The sheet's charter is to carry what still disagrees with WinUI after the theme layer re-points Fluent's tokens. These eight no longer disagree: the checked check box and radio under pointer and press, the disabled radio label, the ColorArea outline, and the tab foreground at rest and disabled -- the last two as one unit, since the disabled rule existed only to outweigh the rest rule. Each was adjudicated on its own, per page and per scheme, with the state its selector names forced through the debugger. Two sentinels separate the outcomes an earlier pass had conflated: an injected `!important` rule on the same selector proves the selector is matching in that state, and a sentinel on the candidate's own declaration proves which rule wins. Only where both showed does removal mean anything, and removal was then measured to change nothing in every such combination. Measured after the fact, both schemes: the tab foregrounds still resolve to TextFillColorSecondary and TextFillColorDisabled, the indicators to AccentFillColorDefault, the ColorArea to ControlStrokeColorDefault. The disabled link keeps its rule. It is redundant by the same test, but only because the accent and neutral disabled literals coincide, and unpicking it also unpicks a forced-colors guard that exists to outweigh it -- a path none of this measures.
`gap-[16px]` asked four panels for a 16px row gap and they render 12 -- Fluent's own card gap atom carries equal weight and comes later, so the value was never the one in effect. `max-[1100px]:min-h-0` is the same shape: the floor it tries to lift stays where it was. Removing either changes nothing, which the panels confirm at 12px afterwards; a later caller that actually wants those values can mark them important, which is what the neighbouring utilities on the same elements already do. The selector bar's rest foreground restates the primary text fill its items already inherit. Its pointer and pressed steps stay, because those do move the colour. Adjudicated per rule with the state its selector names forced, then re-measured on the elements: gaps 12px in both schemes, selector items still TextFillColorPrimary.
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.
Summary
Verification
pnpm run lintpnpm run typecheckpnpm run test— 429 files, 4,781 tests passedpnpm run test:agent-setup-installers— 105 passed, 4 skippedpnpm --filter @floway-dev/web run build— includes the Monaco lazy-chunk assertion