feat(web): rebuild the dashboard with React and Fluent UI - #277
Open
Menci wants to merge 1067 commits into
Open
Conversation
This was referenced Jul 28, 2026
Menci
marked this pull request as draft
July 29, 2026 08:27
Menci
marked this pull request as ready for review
July 29, 2026 16:10
The rows read the shared danger text class and the memo did not name it, so a row rendered before the class resolved would hold the old value.
A mutation in this app disables what could start a second one and hides what would be edited underneath it. Three places did not. The proxy backoff reset had no in-flight state at all, so a second click posted a second reset while the first was still going. The alias dialog's Add target stayed live while every other control in the same form went disabled. The retention field had no disabled prop to be given one, so both key retention rows kept their combo box editable through a save. Also makes the upstreams catalog warning dismissible, which its counterpart on the aliases page already was. The two report the same condition and the page already holds its whole loader payload in state and reseeds it on refresh, so there was nothing to the difference but the bar not being asked.
The key dialog said "this retention does not parse" in three places: the retention row re-parsed its own draft to decide whether to show a message, the dialog schema re-tested the sentinel under a second wording that was never rendered anywhere, and the submit handler opened with a guard that the schema had already made unreachable. The control already emits `'invalid'` when its draft stops parsing, so that sentinel is now the whole of the rule: the row renders on it, the schema refuses the submit on it under the string the row actually shows, and the request body narrows it through an assertion rather than a silent early return. The second wording is gone from both locales.
Three of the alias dialog's rules were stated and then never shown. The per-target ones -- an empty model id, a fractional or negative reasoning budget, adaptive combined with a fixed budget -- were attached to a target path nothing read, and the announced-metadata ones to a path nothing read either, so Save was live, blocked, and silent. The one metadata rule that did render, the budget range, was written a second time inside the editor and could drift from the schema's copy. The array-level "at least one target" message carried no key at all and came out as zod's own English in secondary text. The rules now live once, in a validation module the dialog's schema and the editors both read: a target renders its message on its own row, each metadata number renders on its own field, and the array message is a real string in the colour every other failure wears.
The proxy dialog stated its rules twice and the two copies disagreed. The form derived a per-field flag for the name, URL, host, port, UUID, secret, path, server name, and public key; the save handler re-listed only the four fields every protocol has. A blank Shadowsocks password or a malformed UUID therefore reddened its own field and went to the gateway anyway, and the operator got a server rejection on top of an answer the dialog had already given. `proxyDraftIssues` now computes the whole set once. The form renders what it is handed, the dialog decides when to hand it over -- still on the save click, which the fields are there to answer -- and refuses the submit when the set is non-empty. Test reads the three entries it shares rather than keeping a third list.
The password form ran its schema twice: once through the resolver, which renders each rule on the field that broke it, and again inside the route action, which returned the same rule's message as an action failure and put it in the page-level bar. The second copy was unreachable -- the resolver gates the submit -- but it was the page's only other way to describe a field, and it described it in the wrong place. The action now does the call and nothing else, reading its three fields strictly rather than defaulting a missing one to empty. What it reports is the gateway's own prose, which is rendered as prose: passing it to `t` made i18next read the first colon as a namespace separator and return the tail of the sentence.
The custom-hex field reported a draft it could not parse in the tertiary text colour, which is the colour a hint wears everywhere else in the dashboard. The same draft is already refused in red by the editor section behind the popover, so the two halves of one rule were speaking in different voices; the field now uses the shared danger class every other inline validation message uses.
Consolidating the copy button onto one outcome left three kinds of residue, all of it created by that commit rather than found beside it. The hook still returned copiedTag and copyFailedTag. Nothing outside the module reads either any more -- the five destructures and two prop lists that did were all rewritten -- so they are its backing store now, not its surface. Two label families went with their last readers: dashboard.apiKeys.copy, whose three render sites now resolve common.copy.*, and dashboard.requests.copied, whose sole reader was the request detail's copy button. Both proven dead by reading the render sites rather than grepping the literal, since i18next resolves plural suffixes and computed paths a grep would miss. And three shapes the same run left slightly wrong: mono-label handed back a one-key sheet that all three callers immediately unwrapped, where the two other shared class hooks this work added return the string; and MaskedIcon and ScrollArea kept an empty-string className default that existed only to keep undefined out of an interpolated string, which mergeClasses now drops itself.
Moving the bucket and series shapes into charts left four aliases behind -- UsageBucket, ChartEntry, PerformanceBucket, PerformanceChartEntry -- each of which had been a real declaration and became a rename that carries nothing the imported type does not. The concept spanned two modules for the sake of a name. Consumers import ChartBucket and ChartSeries from where they live.
…source A review of every factual claim the run added found seven false ones. Five are about mechanism, and each was written confidently: - The text control's padding and border are stated in the controls dictionary, as 10,5,6,6 over a 1px border, not the 10,3,6,6 over 2px in the framework's legacy generic.xaml. This comment has now been wrong twice in opposite ways -- first that the keys were absent, then that they lived outside the corpus -- and both times because nobody opened CommonStyles/Common_themeresources.xaml. The arithmetic follows: WinUI puts the two fields at 33 and 34, both above their floors, so the departure to 34 is a 1px unification rather than a 2px one and neither number comes from a floor. - The radio dot's growth is a VisualState storyboard, and RadioButton declares no VisualTransition anywhere, so shipped WinUI keeps growing the dot with animations off. Our suppression is a departure, of the same kind as the expander's, and now says so. - The scrollbar has exactly one VisualTransition, Expanded to Collapsed. Only the contract is gated; the expansion keeps running. Suppressing both is one more than WinUI, and suppressing only its half would leave a bar that grows smoothly and vanishes instantly. - The combo box popup is reached by neither of the two gates this layer cites elsewhere: its storyboard is a VisualState's own, but SplitOpenThemeAnimation is a DynamicTimeline, so the VSM generates it in SteadyState mode instead. - A saturated yellow does reach 4.5 against white by darkening; every hue reaches black. The hue that forces desaturation is a saturated blue on the dark card, which is 1.39:1 at full value with its channels already at their limit. The test asserted the wrong one of the two and passed only on a rounding accident. Also: accordion and list declared their motion under no-preference while saying it was the layer's own, which is the other shape index.ts describes -- and Fluent ships no reduced-motion rule for either, where it does ship four each for tabs and switch. They take the clamp now, so the discriminator index.ts states is the one the layer follows. Smaller corrections: the danger foreground reads 1.74:1 in light and 1.17:1 in dark rather than 'roughly 1.9'; the badge card composites through the card fill rather than the layer fill, which is why it lands on #373737; four names, not five; the operator, not the user.
Round one of the review found them; each traces to a hunk from this session. The request row's new hover fill erased the selection fill. Both classes land on the same element, and a pseudo-class rule outranks a bare declaration, so pointing at the selected request replaced its brand fill with a 3.5% wash and made it read as deselected -- while the comment beside it claimed the opposite. The selected fill states itself per state now, and mergeClasses puts it last. The drop-down reveal clipped the popup's own elevation shadow. clip-path clips every painted thing, and the shadow is declared on the element the animation runs on, so it was suppressed for the whole 250ms and snapped in when the clip was dropped. The keyframes end outside the border box instead. The scrollbar track escaped the delay the same commit added to the thumb, so the channel appeared instantly while the pill waited 400ms -- defeating, for half the control, the reason the delay exists. WinUI begins the track's opacity at the same instants it begins the thumb's size. readableTone had two latent defects in its brightening direction. Its terminal saturation test ran after the loop body rather than before, so the last pass ran at a negative saturation, overflowed a channel past 255 and produced a seven-digit hex that invalidates the whole declaration it lands in. And it chose its direction by whether the surface was light, where the question is which extreme actually clears the floor: between luminance 0.183 and 0.5 white misses and black clears, so it searched the one direction that cannot arrive. Both are covered by tests now. And Save was dead on an untouched create form. The create loader opens on a prefilled blueprint, so the form is clean at first render -- but the two credential gates are submit-time schema issues, and Save was the only thing that could raise them, so 'import a credential first' had become unreachable. The no-op rule holds where the baseline is a saved record.
The search moves in two directions and gives up saturation in a loop, and its last defect was an input escaping it malformed -- a negative saturation overflowing a channel past 255 and producing a seven-digit hex. The property worth asserting is that no input can: 216 hues against 18 grey surfaces, every one a well-formed #RRGGBB.
…true Both introduced by the repair that moved readableTone's terminal test into the loop header. The Math.max(0, saturation) beneath it is the vestige of the test that used to run after the body: the header now guarantees what it clamps, so it is identity on every path -- instrumented, it fired zero times across 67 million inputs -- and a guard that cannot fire suggests the invariant above it is not trusted. The fallback comment claimed it was reached only for a surface no foreground clears. Both halves are false. Such a surface cannot exist, because the ratios a surface gives black and white multiply to exactly 21, so the larger is never below sqrt(21); and the line is reached routinely -- the saturation ladder steps by a tenth and only lands on zero when the colour's own saturation is a multiple of one, so for most hues it goes negative with the last candidate still short. And the contrast figure named the wrong surface: a saturated blue reads 1.44:1 on its own chip, which is what the caller resolves against and what the test asserts. 1.39 is its reading against the bare card.
Round two of the review. Gating Save on the dirty flag fixed a no-op press and opened something worse. The saving flag was cleared when the write returned, not when the save finished -- and what follows a create is a navigation whose loader probes the provider for its catalog, so the editor stays mounted and interactive for that whole round trip with a form the dirty gate no longer covers. A second click there posts a second create, and for the provider kinds with no credential gate it succeeds. The flag now means what its name says: cleared on the error branch and after an edit settles, left set through a create's hand-off, because the editor unmounts when the target route commits. The menu flyout clips its own elevation shadow exactly as the drop-down did, but it cannot take the same fix: it travels as well as clips, and beyond the travelling edge lies the element's own body rather than shadow, so a negative inset there lets the surface overshoot its final position mid-reveal. Only the three stationary edges go outside the box. Two dead things: the alias dialog refused an empty target array under a message no render path can produce -- the per-row delete is disabled on the sole row -- and the commit that added it added a locale key in both languages to serve it. And the proxy dialog's save callback listed a dependency rebuilt on every render, so its useCallback memoised nothing; it computes the draft's issues itself now. Five comments corrected: two still named the reduced-motion shape they had just been moved out of, two asserted the legacy dictionary's values after their citation was deleted, one leaned on a cross-reference to a rule that does not exist and claimed upstream intent without a source, one explained half of the mechanism it describes, and one stated a clearance as a round number without saying what it clears.
Round three of the review. The 32px the two reveal clips leave outside their border box was justified with two numbers that are not in the recipe they cite: shadow16 is "0 0 2px ambient, 0 8px 16px key", so its key term reaches 16px below the box and 8px to either side, and its ambient reaches 1px all round -- not 28 below and 20 sideways. The 28 belongs to shadow28. The literal is still right, with room to spare; the reason for it was invented, and the permalink one line down refutes it. Two smaller ones from the same round. The proxy dialog's save callback still did not memoise after being given its own issue computation: dialTimeout is a fresh literal every render too, so the repair swapped one unstable dependency for another. It reads the two values it needs. And the layer's one appeal to WCAG now carries the clause it appeals to, like every other claim here.
Round four. The clearance comment stated a reach convention with no source and then contradicted it in its own next sentence: under half-a-blur-radius, shadow28 reaches 28px, which is less than the 32px literal, so "a deeper elevation would need more" was refuted by the example given to support it. CSS Backgrounds 3 settles it. A blur radius paints a transition roughly twice its own length, centred on the offset edge, so a shadow spreads by about a full radius: shadow16's key term reaches 24px below and 16px to either side, its ambient 2px, and shadow28's key term 42px. 32px clears the first and not the second, which is what the aside claims. Both reveal comments now say it and cite the clause.
Both expanders could refuse a submit and put the reason behind a closed row. The upstream access control opens itself when its switch is thrown, so the error is normally on screen -- but an operator who collapses the row by hand and then unchecks every upstream gets a live Save that refuses with nothing visible to say why. The alias dialog's metadata section has the same shape. SettingsExpander takes a revealOn flag, and it is deliberately one-directional: rising, it opens the row, because the control that produced the message is in there too; falling, it does nothing, because closing the row while someone is still editing inside it takes their work off the screen mid-correction. Both sites key it on the form's own error state rather than on whether the value is currently invalid. A value is momentarily invalid while it is being typed, and a row that threw itself open at every keystroke would be worse than the thing this fixes; a refused submit is the moment the operator is owed an answer.
…bout Two things about the warning bar, both visible the moment the dialog opens. It said no target resolves to a model on this gateway, on a form that opens with one blank target row -- true by construction, and reporting the starting state as a fault. The warning waits until a target has actually been entered now; entering one that does not resolve still raises it, and a blank row alongside a real one no longer suppresses it. And it drew a single warning as a one-item bullet list, which is more furniture than one sentence carries. The lines are separate blocks instead, so several still stack rather than running together.
…freeze danger.ts argued neutral-at-rest from taste -- "resident red would shout from every row and break the rhythm of the list it sits in" -- for a rule the operator actually decided: he asked for the delete icon to go red and then settled it at hover. Stated as his; the mechanism note about `data-fui-focus-visible` is unchanged. The two playground opt-out notes described the frozen subtree as a fact about how it was built. It is his instruction that the hand-designed chat UI keeps its styling, which is what the boundary is for, so both notes now say so. text-input.css.ts described the red aria-invalid stroke as an affordance WinUI lacks and left it at that, which reads as a settled departure. The operator asked whether a mis-filled field should carry a red box at all and has not answered; the comment now records the question as open.
…hat UI The 2023 capture is the source of truth for this region and the operator declared it frozen. Two rules depart from it and neither said so plainly. The Balanced hover wash is transposed above the gradient because the capture's own order paints nothing; that is argued from the source and stays, but it is the one departure in these tokens and the operator was shown it and did not rule, which the comment now records. The composer's wash swap eases where the original steps. The rule read as though it only borrowed the layer's fill duration; it also changes the original's behaviour, and that second departure is now named as ours and unruled. Neither value moves.
`ResourceListActions` declares `appearance?: 'secondary' | 'subtle'`, and all three call sites pass `'subtle'`; its `onCreate` arm declares `createTrailingIcon`, which only ever appears beside `createTrigger` (`dashboard-providers-upstreams.tsx`), so that arm's copy is unexercised. Both are type-only. `defaultAgentSetupConfiguration(apiKeyId = '')` was passed an id by two suites and by no production call: the card calls it bare and `applyLocalAgentSetupChanges` stamps the real id later. The parameter goes and the suites spread the id in themselves, which is what the production path does.
The area-chart line reorder threw from inside a `requestAnimationFrame` callback when a line had no parent. Nothing can catch that -- the frame runs outside React, so it reaches `window.onerror` past every error boundary -- and the condition cannot arise, because `querySelectorAll` only returns descendants of the mounted host. The reorder now appends through the optional call the DOM type asks for and says why it never fires. The backup import's `handleFile` started a `FileReader` per dropped file with nothing stopping the previous one, so dropping a second file while the first was still reading left two reads racing and the later-finishing one won regardless of which file was dropped last. The reader is held in a ref, aborted at the top of the handler and on unmount; `abort()` raises neither `load` nor `error`, so a losing read reaches no state. Its confirm dialog also stops passing the cancel label that `ConfirmDialog` already defaults to.
…espace `model-info-badges.tsx` was generalised into `components/models/`, where its neighbours serve four feature areas, but its twenty-odd keys stayed under `dashboard.playground.badges.*`. The next surface that wants a badge row either reaches into a playground namespace or forks the strings. The subtree moves to `dashboard.models.badges.*` in the component, both locales and the enum-coverage suite. Every rendered string is byte-identical.
The one-line module is the vendor's own documented Vite workaround, not an accidental split, and carried nothing that said so. It now names the failure it avoids and permalinks the README section that prescribes it.
Three suffixes carried one idea and one of them carried two. `-data.ts` meant network I/O in `usage/usage-data.ts` and meant types plus URL state plus query building in `performance/performance-data.ts`; `-model.ts` and `-logic.ts` named modules that do the same kind of work as each other. The directory name was also repeated in the file name, where the path already says it. One rule: `-data.ts` is a module that talks to the network, `types.ts` is types, `*-styles.ts` is a shared `makeStyles` module, and everything else is named for the job it does. Under it: - `performance/performance-data.ts` -> `performance/overview.ts`. Every export builds the overview request or reads the overview response, which is what the name now says; it does no I/O, so it is not `-data`. - `performance/performance-chart-model.ts` -> `performance/plot.ts`, and its `PerformanceChartModel` -> `PerformancePlot`, matching `usage/types.ts`'s existing `ChartPlot`. - `usage/chart-model.ts` -> `usage/plot.ts`. - `playground/playground-logic.ts` -> `playground/request.ts`, which is what its API list, model gates, custom-JSON merge, fetch wrapper and generation options all serve. The three suites follow their modules. Renames only; no export moved between modules and no body changed.
The card exported four helpers -- two snippet builders and two option builders -- that nothing but its suite imported, while three helpers of the same size sat in modules of their own. That is two conventions in one directory, and a reader cannot predict which one a given helper follows. Both snippet builders are pure functions over a configuration, so they join the Codex credential snippets in `agent-setup.ts`, taking the attribution constant only they use. The two option builders wrap `buildAgentModelOptions` and join it in `agent-setup-models.ts`, so option building is one module. The card is now a component file that exports a component.
Three modules export a helper their own file is the only caller of, so an export-vs-reference scan reads them as dead surface. Each is reached from a suite instead, and now says so at the export.
`PerformanceUrlState` is exported and the one consumer that needs it wrote `ReturnType<typeof parsePerformanceUrlState>` instead, which names the parse function to say the shape it returns. The route names the type.
The duration, request-size and response-size icons were 18px, set when the text beside them was going to 14px. The text stayed at 12px, so the pairing has read mismatched since: an icon half again the height of the numeral it labels. They take 16px, which is the step the row's own text sits under rather than one chosen against a size the row never got.
A page kept on screen while it leaves was mounting again on the way out, so it faded out showing the state it had when it was entered rather than the state it was left in. On the API keys page that was visible as the selected key snapping back, and it also persisted: the selection effect re-ran with the reset value and rewrote localStorage. The cause is the frame's element type. A frame renders its page bare while it is current and wrapped in the frozen router contexts once it leaves, and React reconciles by type at a position, so the whole page unmounted and mounted again under the same key. Measured in Chrome: a DOM node tagged before navigating away is gone from the leaving frame. Every frame now goes through the same wrapper, the current one included, which hands back the values already in scope and leaves the tree it would be without them.
Nine buttons swap their icon for a Spinner while working. Fluent paints a spinner's arc from colorBrandStroke1, which the layer maps to the WinUI accent -- right for a ring standing on the page, wrong for one standing inside a button. On the accent-filled Export and Import buttons that put a blue arc on a blue fill beside a white label. WinUI states ProgressRing's Foreground as a Style setter rather than a fixed brush, so an instance overriding it is the specified behaviour, not a departure. `currentColor` is that override written once: white on the accent button, the neutral foreground on a secondary one, the disabled foreground while the host is disabled. Naming a brush instead would have been correct for one appearance and wrong for the others. The track needed nothing -- it is already ControlFillColorTransparent.
Editing, rotating, deleting or copying an API key also selected that key, which the Agent Setup card below the table then reconfigured itself for. Fluent raises a row's selection from a plain click: DataGridRow toggles its row from the row's own onClick and ListItem triggers its action from the item's, and a button inside either one carries its click up to them -- through a portal too, since the narrow layout's menu popover is a React child of the trigger it was opened from. Both keyboard paths went the same way, because activating a button by Enter or Space dispatches the click that bubbles. The command cluster now stops the click where it was handled, as settings-card.tsx's expander does with its action slot. The click only: DataGridRow selects on Space just when the target is not interactive and ListItem acts on a key only when it is the target itself, so keyboard activation never selected on its own, and arrow keys have to keep travelling for the grid's own cell navigation to answer them. Verified in Chrome across both layouts: commands no longer select by click, Enter or Space, while a click on the row body, Space on the radio and the same pair on a narrow item still do, and arrow travel through the row is unchanged.
The two modes sat side by side with a full sentence each, so the descriptions met in the middle of the row and the trailing radio read as the last word of the preceding sentence rather than as the start of its own option. They stack now, which is what a two-line option wants. The label pair was also its own invention: a semibold header over a tertiary caption with 2px between them. WinUI's RadioButton states the content size and states no weight, and ../components/ui/settings-card.tsx already builds this exact pair from the toolkit -- body regular over the caption in the secondary fill, no gap. The emphasis belongs in the fill step, not in a heavier face, and there is no reason for the dashboard to carry two spellings of one idiom.
The form derives both outcomes during render: a 401 is the gateway rejecting the credentials and lands on the password field, anything else is the attempt failing and lands on the dismissible bar. A second effect took the same response and pushed every failure onto the password field as well, so a gateway fault painted the same words twice -- in the bar and, one frame later, under the password input, where dismissing the bar could not clear them. It also re-raised a field error the user had already typed away whenever it re-ran against an unchanged response. The render-phase split already feeds the field through credentialError, so the effect is removed rather than narrowed.
… last pass left Appending the lines in reverse document order is an involution rather than a normalisation: the pass starts from the order its predecessor produced, so a resize or a data reload swaps the lines end for end. Wherever two boundaries coincide -- any bucket range where the upper series contributes nothing -- the two 2px strokes overlap and the last one painted wins, so the same data drew a differently coloured boundary before and after a resize. Sorting by the series index the path id carries reaches the same order from any starting order, and that order is the one the first pass already produced, so nothing on screen moves.
…e paint The rate inputs are keyed by metric, so selecting another pricing rule reuses the same input with a new value prop. Mirroring that prop into the draft from an effect meant the commit that opened the new rule was painted with the previous rule's numbers still in the fields, corrected only on the following frame. Taking it during render collapses the two into one commit. The focus guard is unchanged and stays a ref: it answers a question about the input the operator is in right now, and the render-phase adoption is skipped for exactly as long as that is true.
A visit that could not load the key list erased the stored Agent Setup selection. The loader answers with no id when the list did not arrive, and the effect mirroring that state onto localStorage then removed the entry -- so the selection was gone for good, including for the next visit that loaded fine. Measured in Chrome against a failing GET /api/keys: the entry was deleted, and it now survives and re-selects on the following successful load. The write moves to the selection itself. A mirror cannot tell a selection the operator cleared from one the page could not resolve; recording the act instead states which of the two happened. The reload path clears the entry only for a key that a successful reload no longer lists, which is a key that is gone rather than one that failed to arrive.
The loop was started from the Connect handler and only torn down by an effect, so a teardown that preserved state left the panel drawing a live user code, its verification link and the waiting spinner with nothing polling for the authorisation. Arming the first tick from `flow` puts the loop where the UI that depends on it already is: whatever renders the code schedules the tick, and the same effect stops it. Each tick also re-entered the closure the flow started in, so every request for the code's whole lifetime -- a quarter of an hour -- carried the upstream as it read when Connect was clicked. An operator fixing the proxy while the first attempts failed through the wrong one saw no effect until they restarted the flow. The re-arm now goes through the newest closure, so the request body is the upstream as it currently reads.
The save debounce listed the lease and the draft objects among its dependencies while reading each of them only as a presence, so every heartbeat -- which adopts a freshly issued lease -- restarted the window. An edit made shortly before a heartbeat therefore waited a second full debounce for a reason that had nothing to do with what was typed, and an operator editing across heartbeats could be deferred again and again. The effect now depends on whether a lease and a draft exist, and on the generation counters, which are what say an edit happened. The save reads `draftRef` and `leaseRef` when the timer fires, so nothing it sends depended on those identities.
Fluent's single-select calls onOptionSelect for a click on the option already selected, and both producers allocated unconditionally, so the guard comparing filters by identity saw a new query where the operator had picked the same one. Measured in Chrome against the performance page: opening the Upstream filter and clicking the selected option issued a second GET /api/performance/overview, which resets loadedAt and shifts the chart's bucket axis; re-picking the current Group by did the same and additionally cleared every series the operator had hidden. Both now issue nothing, while a genuine change still refetches. The no-op is recognised where the selection arrives rather than at the guard, so nothing downstream has to tell a repeated pick from a new one.
The zone's dashed stroke and its accent accepting state had no counterpart in microsoft-ui-xaml or in the Community Toolkit -- neither ships a file drop target, a drop-area brush, or a dashed border, and WinUI's only drag affordance is the 0.80 opacity a ListViewItem takes while it is being dragged. The sheet was therefore inventing a control rather than transcribing one, and the file picker it fronted is rebuilt on a shape both libraries do state.
Both monitor pages wrote their loaded-query ref before issuing the fetch, so a run torn down before it answered was remembered as the data on screen. The abort comes from useRefresh's teardown and the reload returns without writing any state, so a setup that follows finds the ref already matching and issues nothing: the controls read the new range while the chart, its bucket axis and the tables still show the previous query's data until the next minute's poll. Measured in Chrome with the usage route wrapped in <Activity> for the occasion: switching the range under a three-second latency and hiding then re-showing the subtree while that fetch was open left the control reading 7 Days with no further request; with the write moved next to the state the same run commits, the fetch is re-issued on the way back in. A plain range switch still costs exactly one request.
The toolkit's SettingsCard has a third variant this app did not carry: with IsClickEnabled the card becomes the ButtonBase it already derives from, takes the pointer ramp a plain row never wires up, and shows a 13px action glyph held 14 clear of what precedes it. The toolkit means it for a row that opens something, which is what a file picker is, so the variant is added beside the other two rather than rebuilt per page. Its pointer-over declarations are lifted out of the interactive sheet so the clickable row can assert that state rather than match it: during an HTML drag the user agent matches no :hover anywhere, and a row that accepts a drop has to answer while the drag is over it. Reusing the state the card already has for 'the pointer is on me' is what keeps the accepting state from being a colour nobody sources. SettingsSection is the group those rows are read in -- BodyStrong over the rows, 6 below the heading and 4 between them, which is the Gallery's settings section header over the spacing its own StackPanel states.
Every part of the page now comes from a row Windows already states. Export is a section heading over one SettingsCard: the telemetry toggle as its header and hint, a switch at the trailing edge, and the download beneath as the page's primary action -- the same free-standing action row the search provider page ends on. Import is the same section holding two rows. Before a file is chosen the row IS the picker, clickable and accepting a drop; the accepting state is the card's own pointer-over rather than a dashed accent border, since neither library ships a drop target to copy. Once a file is chosen the row reports it: the file name and size as the header, what the file holds as the description, and the picker handed to a standard button at the trailing edge -- the appearance both the Gallery and the toolkit put in that slot for an action that is not the page's primary one. The two import modes are a picker in a row rather than a stack of radios with hand-built two-line labels. That is how the Gallery's own settings page answers Light / Dark / Use system setting, with the one description carried by the card, so the card shows the chosen mode's sentence and 'Import Mode' is a card header rather than a form label standing in for a heading. The preview tiles go with it: the counts read as the same sentence the server answers with after the import, so one function now writes both and the operator is told what happened in the vocabulary they were shown.
This reverts commit a335526. The mode has no call site, which is why a dead-code sweep took it. But it has no call site on purpose: it was kept deliberately, over the same argument the sweep made for removing it. A prop nothing passes yet is not the same thing as a prop nothing wants.
…sits in" This reverts commit c9b31c0e79c33ff87c6e5b3ac5a02c9e63c3fb45. The settings row presents a setting -- a persisted piece of configuration the system remembers. Backup and restore holds no such thing: export and import are operations, and the toggle and the mode beside them are arguments to a run that is about to happen, alive only while the form is on screen. Dressing them as settings would tell the operator something false about what they are looking at, so the row this added has no caller and goes with the page that was going to be its only one.
Switching API keys re-renders the hook instead of unmounting it, so a 'load older' request issued for one key resolved into the next key's list: its rows were appended under the new key's heading, where opening one navigates to a record id the new key does not have and the detail fetch 404s, and its setError/setHasOlder decided the new key's list too -- a short stale page turned pagination off for a key that had more. Measured in Chrome against two dump-enabled keys of 240 records each, with the older-page request held open across the switch: the new key's list came back holding 300 rows, 100 of them the other key's. It now holds only its own, and pages on to all 240. A generation is taken when the key's accumulation is discarded and read back after the await, so the whole completion path is answered rather than only the append -- an abort alone would not do, since callApi reports a cancelled request as an ordinary failure and would paint 'The operation was aborted' under the new key. The abort is kept for the round trip it saves. The in-flight marker holds that generation instead of a boolean, so a request left behind by another key cannot hold the new key's pagination shut and a finished request releases only its own claim.
Export and import are operations, not settings: nothing on the page is remembered, and the toggle and the mode beside them are arguments to a run that has not happened yet. So the page is shaped like the one other page here whose content is a thing to be done -- a panel per operation, the parameters above the command, the command against the trailing edge -- rather than like a list of settings rows. Every parameter is a Field with one control in it, which is how every other parameter in this app is spelled, and which the WinUI layer already maps onto a control's Header over its Description presenter. That gives 'Import Mode' a real control header instead of a form label standing in for a heading, and it gives each option's sentence a home the layer already sources. The choice itself is ChoiceGroup, the SelectorBar this app already spells a two-way choice with in the alias dialog and the key form, so the two options no longer stack and there is no vertical gap left to get wrong. The telemetry parameter is a check box, per Microsoft's own rule that a value applied by a separate command is a check box rather than a switch. The file zone stops inventing a control. Neither library ships a drop target, so it is drawn as the bounded region inside a panel that ../ui/code-block.tsx already had to answer -- the solid ramp's page-canvas step framed in ControlStrokeColorDefault -- with the subtle button's ramp on it while it is the picker, and the accepting state asserting the pointer-over wash rather than an accent stroke nothing sources. Filled, the same region reports the file name over what the file carries and keeps accepting a replacement, with the standard button that re-opens the picker at its trailing edge. The preview tiles go with it: the counts read as the same sentence the server answers with after the import, so one function now writes both.
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