fix(dashboard): remove the shared floating-window gutter and fix browser onboarding
Operator report: the first-run "Set Up AI" modal had extra space along its right edge, and it asked a browser visitor to connect a remote server and mentioned a native shell they do not have. Three defects, verified in a real container build at desktop and mobile widths: - FN-8015 reserved `margin-inline-end: var(--space-lg)` on the shared `.floating-window__body` so a hosted scrollbar cleared the east resize hot zones. One shared reservation every caller had to know about produced a recurring class of asymmetric-right-inset bugs instead: it was zeroed piecemeal five times (FN-8766, mobile task detail, FN-8722, FN-8702, every tablet window) while two callers leaned on it for their right inset and had to restore it whenever one of those predicates fired. Delete the gutter, its five overrides, and GitHub Import's borrowed-inset compensation. Where a scrollbar and a resize target actually collide, use FN-8766's outboard east targets. Trade-off accepted by the operator; the ratchet test now forbids the gutter in any stylesheet at any breakpoint. - The hosted Set Up AI modal did not fill its window: its standalone `height: min(85vh, ...)` rule ties on specificity with FloatingWindow's `height: 100%` and won on source order, leaving ~60px of dead window surface under the footer alongside the 16px gutter strip. Right gap 17px -> 1px, bottom gap 62px -> 1px. - The "Connect remote Fusion server" card keyed only on `desktopMode !== "local"`, and `desktopMode` is undefined on web, so every browser first-run led with a native-shell hand-off form. Now also requires `host !== "web"`. Verified: 165 dashboard test files / 5829 tests pass, eslint and `tsc -p tsconfig.app.json` clean, and a container built from this tree shows the panel flush at 1280x800 and as a symmetric full-screen sheet at 390x844 with no remote-server card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/fix-floating-window-gutter-and-onboarding.md
Normal file
7
.changeset/fix-floating-window-gutter-and-onboarding.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix uneven right/bottom space around floating windows and drop the remote-server prompt from browser onboarding.
|
||||
category: fix
|
||||
dev: Deletes FN-8015's shared `margin-inline-end` gutter on `.floating-window__body` plus its five piecemeal zeroing overrides and GitHub Import's borrowed-inset compensation; a scrollbar/resize-target collision is now fixed per-caller with FN-8766's outboard east handles. The hosted Set Up AI modal re-asserts `width/height: 100%` under `.floating-window--model-onboarding` (its standalone `85vh` rule tied on specificity and won on source order). The "Connect remote Fusion server" card now also requires `shellState.host !== "web"` — `desktopMode` is undefined in a browser, so web first-run showed a native-shell hand-off form.
|
||||
@@ -262,22 +262,19 @@ FNXC:ChatModal 2026-06-22-14:49:
|
||||
On mobile/narrow app viewports, opening Quick Chat should present the full Chat modal as a full-screen sheet instead of a small draggable desktop window. Scope this to the chat FloatingWindow and override the inline desktop geometry only at the mobile breakpoint; desktop pop-out behavior remains movable/resizable.
|
||||
*/
|
||||
/*
|
||||
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||
FN-8766 keeps the desktop Task Detail popup's visible header/content edge symmetric while
|
||||
preserving the shared body's scrollbar clearance for every other FloatingWindow consumer. This
|
||||
headerless host owns its close button, so remove only its visible body gutter and move its desktop
|
||||
east resize hit areas outside the painted shell; the scrollbar remains fully usable and the close
|
||||
control does not sit behind a resize target. Tablet retains its existing touch geometry and phones
|
||||
remain full-screen sheets below.
|
||||
FNXC:TaskDetailLayout 2026-08-17-23:47:
|
||||
FN-8766 gave the desktop Task Detail popup a symmetric painted edge by removing the shared body
|
||||
gutter here and moving its east resize hit areas OUTSIDE the painted shell, so the scrollbar stays
|
||||
usable and the header-owned close control does not sit behind a resize target. The shared gutter is
|
||||
now gone for every caller, so the local zeroing that used to accompany these rules is deleted; the
|
||||
outboard targets remain, and they are the pattern to copy wherever a hosted scrollbar and a resize
|
||||
hot zone actually collide. Tablet retains its existing touch geometry and phones remain full-screen
|
||||
sheets below.
|
||||
*/
|
||||
.floating-window--task-detail:not(.floating-window--tablet-viewport) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__body {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--e {
|
||||
right: calc(var(--space-sm) * -1);
|
||||
}
|
||||
@@ -498,32 +495,13 @@ all floating affordances so persisted desktop geometry cannot imply a draggable
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:MobileTaskPopups 2026-07-17-08:20:
|
||||
FN-8015 reserves `margin-inline-end: var(--space-lg)` on the shared
|
||||
`.floating-window__body` so a hosted scrollbar clears the desktop
|
||||
east/north-east/south-east resize hot zones. The mobile task-detail sheet is
|
||||
full-screen and hides every resize handle (above), so that gutter protects
|
||||
nothing and instead shifts the whole panel left — leaving an uneven right
|
||||
inset on every row. Zero it here so `.detail-body`'s own symmetric padding
|
||||
defines both insets equally. Desktop resize-handle clearance is untouched
|
||||
(this rule is mobile-only).
|
||||
FNXC:MobileTaskPopups 2026-08-17-23:47:
|
||||
The mobile task-detail and standalone GitHub Import sheets each carried a local
|
||||
`margin-inline-end: 0` to undo the shared body gutter, which left them shifted left with an uneven
|
||||
right inset on every row (FN-8722, and the mobile task-detail left-shift fix). The shared gutter is
|
||||
deleted, so both overrides are gone: `.detail-body` and the import shell's own symmetric padding
|
||||
now define both insets directly.
|
||||
*/
|
||||
.floating-window--task-detail .floating-window__body {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-08-02-02:47:
|
||||
FN-8722 makes the standalone GitHub Import host follow the full width-or-height
|
||||
sheet contract. Its shared body gutter exists only to clear desktop resize handles;
|
||||
once this predicate hides those handles, retaining the gutter shifts every shell
|
||||
region left. Do not broaden this rule to embedded Import Tasks or the independent
|
||||
detail window, and keep non-short tablet/desktop resize clearance intact.
|
||||
*/
|
||||
.floating-window--github-import .floating-window__body {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.floating-window--task-detail .task-detail-content--embedded,
|
||||
.floating-window--task-detail .task-detail-content--embedded > .modal-header {
|
||||
border-radius: 0;
|
||||
@@ -532,18 +510,6 @@ all floating affordances so persisted desktop geometry cannot imply a draggable
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/*
|
||||
FNXC:GitManagerMobileSpacing 2026-08-01-19:10:
|
||||
FN-8702 keeps the shared desktop resize-handle clearance for tablet and desktop windows.
|
||||
A standalone Git Manager phone sheet hides those handles, so only below the strict 768px
|
||||
boundary its body clears the obsolete inline-end gutter and aligns its header and close control.
|
||||
*/
|
||||
.floating-window--git-manager .floating-window__body {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:FloatingWindow 2026-07-15-18:20:
|
||||
Long titles (e.g. the GitHub import detail's "#2159 — <issue title>") must truncate with an ellipsis rather than hard-cut mid-word against the close button.
|
||||
@@ -581,34 +547,31 @@ Reintroducing flex here (e.g. to lay out an element title) re-breaks truncation
|
||||
FNXC:FloatingWindow 2026-06-22-20:45:
|
||||
Body is a flex host that lets its single child stretch to the full panel width/height (min-width/min-height:0 so a wide child cannot collapse the flex line, and the child's own overflow can engage).
|
||||
|
||||
FNXC:FloatingWindow 2026-07-15-00:00:
|
||||
FN-8015 / issue #2140 requires every shared FloatingWindow caller to keep its hosted vertical scrollbar clear of the right-edge and right-corner resize hot zones. Reserve the corner-width gutter on the body rather than specializing task detail: this applies equally when the body itself scrolls and when a headerless caller delegates scrolling to its full-size child. The panel border and its right handles remain available for desktop resizing, while the scrollbar is always inboard of the east, north-east, and south-east hit targets.
|
||||
FNXC:FloatingWindow 2026-08-17-23:47:
|
||||
THE SHARED BODY HAS NO INLINE-END GUTTER. FN-8015 / issue #2140 previously reserved
|
||||
`margin-inline-end: var(--space-lg)` here so a hosted vertical scrollbar stayed inboard of the east,
|
||||
north-east, and south-east resize hot zones. Operator removed it: one shared reservation that every
|
||||
caller had to know about produced a recurring class of asymmetric-right-inset bugs instead — the
|
||||
window's own surface showed through beside each child panel, so callers read it as unexplained
|
||||
padding on the right. It was zeroed piecemeal five separate times (desktop task detail FN-8766,
|
||||
mobile task detail, mobile GitHub import FN-8722, phone Git Manager FN-8702, and every tablet
|
||||
window), and the callers that instead LEANED on it for their right inset had to restore it locally
|
||||
whenever one of those predicates fired. FN-8630 / FN-8634 and the 2026-08-01 tablet fix are the same
|
||||
bug recurring.
|
||||
|
||||
The accepted trade-off: a hosted scrollbar now sits under the east/corner resize targets. Where that
|
||||
conflict is actually felt, fix it the FN-8766 way — move that caller's east targets outboard of the
|
||||
painted shell (`overflow: visible` plus negative `right` offsets on --e/--ne/--se) — rather than
|
||||
reintroducing a shared gutter. Do not re-add `margin-inline-end` to this rule.
|
||||
*/
|
||||
.floating-window__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin-inline-end: var(--space-lg);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
||||
Tablet windows drop FN-8015's inline-end scrollbar gutter. On tablet the gutter reads as a
|
||||
too-big, uneven right border inside Task Detail popups and the floating terminal (the inner
|
||||
surfaces already carry their own symmetric padding), and the mouse conflict FN-8015 solves —
|
||||
grabbing a hosted scrollbar thumb versus the east resize hot zone — does not apply to touch
|
||||
input, which never grabs scrollbar thumbs. This keys on tablet MODE (`.floating-window--tablet-viewport`,
|
||||
set from `useViewportMode()`), not `--touch-geometry`, so a non-touch window at tablet widths
|
||||
gets the same symmetric inset the operator sees on an iPad. Desktop keeps the FN-8015 gutter.
|
||||
Callers that relied on the gutter as their right inset must compensate under the same class
|
||||
(see `.github-import-detail-panel`).
|
||||
*/
|
||||
.floating-window--tablet-viewport .floating-window__body {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.floating-window__body > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -224,14 +224,15 @@ export function FloatingWindow({
|
||||
FNXC:ModalTouchGeometry 2026-08-01-04:23:
|
||||
NAMING CONTRACT — FloatingWindow has two distinct tablet markers; do not conflate them:
|
||||
- `floating-window--tablet-viewport`: the viewport MODE classifies as tablet (769-1024px
|
||||
width OR a known 768px touch tablet), touch or not. Pure styling surface — currently the
|
||||
FN-8015 gutter zeroing lives here.
|
||||
width OR a known 768px touch tablet), touch or not. Pure styling surface.
|
||||
- `floating-window--touch-geometry`: tablet AND touch-capable (`isTabletTouchViewport`) —
|
||||
enlarged 44px drag/resize targets only.
|
||||
A 900px non-touch window is `--tablet-viewport` but NOT `--touch-geometry`, and operators
|
||||
still see the FN-8015 scrollbar gutter as an uneven right inset there (third recurrence of
|
||||
the Task Detail right-padding bug — FN-8630/FN-8634 fixed only the `.modal-overlay` shells,
|
||||
while every tablet task popup and floating terminal renders through THIS host).
|
||||
A 900px non-touch window is `--tablet-viewport` but NOT `--touch-geometry`; the marker exists so
|
||||
such a window still gets tablet STYLING. It used to carry FN-8015's gutter zeroing, because that
|
||||
shared gutter read as an uneven right inset on tablet (third recurrence of the Task Detail
|
||||
right-padding bug — FN-8630/FN-8634 fixed only the `.modal-overlay` shells, while every tablet
|
||||
task popup and floating terminal renders through THIS host). The gutter is deleted outright as of
|
||||
2026-08-17, so no gutter zeroing hangs off this class any more.
|
||||
*/
|
||||
const isTabletViewportMode = viewportMode === "tablet";
|
||||
const initialGeometry = useRef<{ size: FloatingWindowSize; position: FloatingWindowPosition } | null>(null);
|
||||
|
||||
@@ -310,11 +310,15 @@ holds a 36px touch target without claiming a row of its own.
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-07-15-18:20:
|
||||
The detail panel is the FloatingWindow's only child and owns its own inset — without one the Preview header rendered flush against the window's left edge while FN-8015's resize gutter left a lone gap on the right, so the header read as mis-aligned on both the desktop window and the mobile sheet.
|
||||
padding-inline-end is deliberately 0: FN-8015 already reserves var(--space-lg) of inline-end margin on the shared .floating-window__body to hold a hosted scrollbar clear of the east/north-east/south-east resize hot zones, and that gutter supplies this panel's right inset. Matching padding-inline-start to the same token makes the inset symmetric without overriding the shared body (which would push the inner scroller back into the hot zone).
|
||||
If that body gutter ever changes token, change padding-inline-start with it.
|
||||
Padding lives on the panel rather than the pane content so header, toast, and scrolling body share one inset and the header divider is inset with them.
|
||||
FNXC:GitHubImport 2026-08-17-23:47:
|
||||
The detail panel is the FloatingWindow's only child and owns its own inset — without one the Preview
|
||||
header rendered flush against the window's left edge, so the header read as mis-aligned on both the
|
||||
desktop window and the mobile sheet.
|
||||
This padding is now symmetric on all four sides. It previously set `padding-inline-end: 0` and
|
||||
borrowed its right inset from FN-8015's shared `.floating-window__body` gutter; that gutter is
|
||||
deleted, so the panel supplies both insets itself and no longer needs a tablet-specific restore.
|
||||
Padding lives on the panel rather than the pane content so header, toast, and scrolling body share
|
||||
one inset and the header divider is inset with them.
|
||||
*/
|
||||
.github-import-detail-panel {
|
||||
display: flex;
|
||||
@@ -323,20 +327,9 @@ Padding lives on the panel rather than the pane content so header, toast, and sc
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: var(--space-lg);
|
||||
padding-inline-end: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-08-01-03:48:
|
||||
Tablet windows zero the shared FN-8015 body gutter (`.floating-window--tablet-viewport .floating-window__body`
|
||||
in FloatingWindow.css) because it rendered as an uneven right border on tablet. This panel's right
|
||||
inset was supplied by that gutter, so restore it locally to keep the panel's insets symmetric.
|
||||
*/
|
||||
.floating-window--tablet-viewport .github-import-detail-panel {
|
||||
padding-inline-end: var(--space-lg);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-07-15-23:25:
|
||||
Bottom action bar for the detail preview. Import/Close issue moved here from the pane header —
|
||||
|
||||
@@ -25,6 +25,29 @@
|
||||
height: min(85vh, calc(100dvh - 40px));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-08-17-23:47:
|
||||
Hosted in FloatingWindow, this modal MUST fill its host exactly — the window paints its own bordered
|
||||
surface, so any part the child does not cover shows through as unexplained blank space around the
|
||||
content. FloatingWindow.css already sets `width/height: 100%` for this caller, but the two standalone
|
||||
sizing rules above carry equal specificity (0,2,0 — the `:not([style*=…])` attribute test counts) and
|
||||
won last on source order, so the panel rendered at 85vh inside a taller window and left ~60px of dead
|
||||
surface under the footer, plus the matching right-edge strip the operator reported. Re-assert the fill
|
||||
under the host class with the same `:not()` guards, so a user-resized window still wins via its inline
|
||||
style. `resize`/`min-*` are the standalone dialog's affordances; inside the window the FRAME resizes,
|
||||
and a 640px floor would push content under the east edge in a narrow window.
|
||||
*/
|
||||
.floating-window--model-onboarding .model-onboarding-modal:not([style*="width"]),
|
||||
.floating-window--model-onboarding .model-onboarding-modal:not([style*="height"]) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.model-onboarding-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2029,14 +2029,20 @@ export function ModelOnboardingModal({
|
||||
|
||||
const aiProviders = authProviders.filter((provider) => provider.id !== "github");
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-03-07:20:
|
||||
* Show the "Connect remote Fusion server" card ONLY when not already connected to a remote server
|
||||
* (no active remote profile) — on any host, web included. Never show it in LOCAL desktop mode: a
|
||||
* local runtime is already the connected backend, so prompting for a remote server URL just confuses
|
||||
* first-run setup (the original report).
|
||||
* FNXC:Onboarding 2026-08-17-23:47:
|
||||
* Show the "Connect remote Fusion server" card ONLY inside a NATIVE SHELL (desktop or mobile app)
|
||||
* that has no active remote profile yet. Two hosts must never see it:
|
||||
* - LOCAL desktop mode: a local runtime is already the connected backend, so asking for a remote
|
||||
* server URL just confuses first-run setup (the original 2026-07-03 report).
|
||||
* - A PLAIN BROWSER (`host === "web"`): there is no native shell to connect at all. The browser
|
||||
* IS already talking to the server it loaded from, and the card's own copy ("Your native shell
|
||||
* needs an active remote profile before dashboard handoff can complete") describes machinery
|
||||
* the visitor does not have. The previous rule keyed only on `desktopMode !== "local"`, and
|
||||
* `desktopMode` is undefined on web, so every browser first-run opened Set Up AI with a remote
|
||||
* server form above the AI providers it was supposed to lead with (operator report).
|
||||
*/
|
||||
const showShellConnectionSetup =
|
||||
!shellState.activeProfileId && shellState.desktopMode !== "local";
|
||||
!shellState.activeProfileId && shellState.host !== "web" && shellState.desktopMode !== "local";
|
||||
const orderedAiProviders = [...aiProviders].sort(compareOnboardingProviders);
|
||||
const hasOauthProviders = orderedAiProviders.some((provider) => !provider.type || provider.type === "oauth");
|
||||
const providerSupportsApiKey = (provider: AuthProvider) => provider.type === "api_key";
|
||||
|
||||
@@ -4,11 +4,13 @@ FNXC:TaskDetailLayout 2026-08-01-04:23:
|
||||
SHELL NAMING MAP — read before fixing any Task Detail inset/padding bug. "Task Detail modal"
|
||||
is ONE surface with THREE shells, and only one of them lives in this file's `.modal-overlay`
|
||||
rules: (1) the fixed desktop modal (`.modal-overlay` + `.modal.task-detail-modal`, this file),
|
||||
(2) the tablet popup and desktop pop-out (FloatingWindow.css — class floating-window--task-detail,
|
||||
whose shared body element owns the FN-8015 inline-end gutter and the tablet-viewport zeroing),
|
||||
(2) the tablet popup and desktop pop-out (FloatingWindow.css — class floating-window--task-detail),
|
||||
and (3) the mobile full-screen sheet. The right-
|
||||
padding bug recurred across FN-8630, FN-8634, and the 2026-08-01 tablet fix precisely because
|
||||
fixes landed only in shell (1) while the reporter was looking at shell (2).
|
||||
As of 2026-08-17 shell (2) no longer carries FN-8015's shared inline-end body gutter (deleted for
|
||||
every caller — see FloatingWindow.css), so an uneven right inset there is now the child's own
|
||||
padding, not an inherited reservation.
|
||||
*/
|
||||
.task-detail-content {
|
||||
display: flex;
|
||||
|
||||
@@ -11,10 +11,11 @@ FN-6811 recurrence #6 tightened ownership of this scoped symbols face: every ter
|
||||
FNXC:TerminalLayout 2026-08-01-04:23:
|
||||
SHELL NAMING MAP — the terminal renders through FIVE shells and only some live in this file:
|
||||
docked/below panels and the mobile sheet are plain `.modal`/host elements styled here, but the
|
||||
FLOATING pop-out wraps `terminalContent` in FloatingWindow (FloatingWindow.css), whose shared
|
||||
body element owns the FN-8015 inline-end gutter and the tablet-viewport zeroing. An
|
||||
FLOATING pop-out wraps `terminalContent` in FloatingWindow (FloatingWindow.css). An
|
||||
inset/padding bug reported "on the terminal
|
||||
modal" in tablet view is almost certainly the FloatingWindow shell, not a rule in this file.
|
||||
The shared body no longer reserves FN-8015's inline-end gutter (deleted 2026-08-17 for every
|
||||
caller), so the pop-out's insets now come only from `terminalContent` itself.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: "Fusion Terminal Nerd Font Symbols";
|
||||
|
||||
@@ -149,24 +149,44 @@ describe("FloatingWindow", () => {
|
||||
expectFloatingWindowStructure("beta");
|
||||
});
|
||||
|
||||
it("keeps every shared floating-window scrollbar inboard of the right resize hot zones", () => {
|
||||
const bodyRule = floatingWindowCss.match(/(?:^|\n)\.floating-window__body\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
/*
|
||||
FNXC:FloatingWindow 2026-08-17-23:47:
|
||||
RATCHET: the shared body carries NO inline-end gutter, on any breakpoint, for any caller.
|
||||
FN-8015 reserved `margin-inline-end: var(--space-lg)` there so a hosted scrollbar cleared the east
|
||||
resize hot zones; operator removed it because one shared reservation every caller had to know about
|
||||
produced a recurring class of asymmetric-right-inset bugs (FN-8630, FN-8634, FN-8702, FN-8722,
|
||||
FN-8766, the 2026-08-01 tablet fix, and the Set Up AI onboarding report) — the window surface showed
|
||||
through beside each child panel. It was also being zeroed piecemeal in five places, so the "shared"
|
||||
default applied to a shrinking minority.
|
||||
|
||||
// The global scrollbar is 8px wide; the shared body reserves the 12px corner-handle gutter.
|
||||
This asserts the ABSENCE globally rather than per-caller: a reintroduction anywhere — base rule,
|
||||
a caller override, or inside any `@media` block — fails here. Where a scrollbar and a resize target
|
||||
genuinely collide, move that caller's east targets outboard (the FN-8766 pattern asserted below).
|
||||
*/
|
||||
it("keeps the shared floating-window body free of any inline-end gutter", () => {
|
||||
const bodyRule = floatingWindowCss.match(/(?:^|\n)\.floating-window__body\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
expect(bodyRule).toContain("overflow: auto;");
|
||||
expect(bodyRule).not.toMatch(/margin-inline-end\s*:/);
|
||||
|
||||
/*
|
||||
* No rule in ANY app stylesheet, at any breakpoint, may set an inline-end margin on the shared
|
||||
* body. Comments are stripped first: the FNXC notes explaining WHY the gutter is gone name the
|
||||
* property, and must not read as a reintroduction.
|
||||
*/
|
||||
const appCssWithoutComments = allAppCss.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
const bodyRulesEverywhere = [...appCssWithoutComments.matchAll(/[^{}]*\.floating-window__body[^{}]*\{[^}]*\}/g)].map((m) => m[0]);
|
||||
expect(bodyRulesEverywhere.length).toBeGreaterThan(0);
|
||||
for (const rule of bodyRulesEverywhere) {
|
||||
expect(/margin-inline-end\s*:/.test(rule), rule.slice(0, 120)).toBe(false);
|
||||
}
|
||||
|
||||
// Shared handles stay flush with the painted edge; only FN-8766's task-detail host moves outboard.
|
||||
expect(stylesCss).toContain("*::-webkit-scrollbar {");
|
||||
expect(stylesCss).toContain("width: 8px;");
|
||||
expect(bodyRule).toContain("overflow: auto;");
|
||||
expect(bodyRule).toContain("margin-inline-end: var(--space-lg);");
|
||||
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--e", "right: 0;")).toContain("right: 0;");
|
||||
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--ne", "right: 0;")).toContain("right: 0;");
|
||||
expect(cssRuleContaining(floatingWindowCss, ".floating-window__resize-handle--se", "right: 0;")).toContain("right: 0;");
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||
FN-8766 is the sole desktop exception: its header-owned close control requires a symmetric
|
||||
painted edge, so its gutter is removed only while its east resize targets move outboard.
|
||||
Every unrelated consumer keeps the shared scrollbar-clearance geometry.
|
||||
*/
|
||||
const desktopAppCss = stripAtMediaBlocks(allAppCss);
|
||||
for (const callerClass of [
|
||||
"floating-window--automation",
|
||||
@@ -178,71 +198,64 @@ describe("FloatingWindow", () => {
|
||||
]) {
|
||||
const rules = cssRulesForClass(desktopAppCss, callerClass);
|
||||
const rightHandleRules = rules.filter((rule) => /floating-window__resize-handle(?:--(?:e|ne|se))?/.test(rule));
|
||||
const bodyRules = rules.filter((rule) => rule.includes("floating-window__body"));
|
||||
|
||||
expect(rightHandleRules.some((rule) => /(?:right|width)\s*:/.test(rule)), callerClass).toBe(false);
|
||||
expect(bodyRules.some((rule) => /margin-inline-end\s*:/.test(rule)), callerClass).toBe(false);
|
||||
}
|
||||
|
||||
const desktopTaskBody = cssRuleContaining(
|
||||
desktopAppCss,
|
||||
".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__body",
|
||||
"margin-inline-end",
|
||||
);
|
||||
expect(desktopTaskBody).toContain("margin-inline-end: 0;");
|
||||
/*
|
||||
FNXC:TaskDetailLayout 2026-08-17-23:47:
|
||||
FN-8766's outboard east targets survive the gutter removal and are now the sanctioned remedy for a
|
||||
scrollbar/resize collision, so they stay pinned here.
|
||||
*/
|
||||
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--e", "right")).toContain("right: calc(var(--space-sm) * -1);");
|
||||
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--ne", "right")).toContain("right: calc(var(--space-lg) * -1);");
|
||||
expect(cssRuleContaining(desktopAppCss, ".floating-window--task-detail:not(.floating-window--tablet-viewport) .floating-window__resize-handle--se", "right")).toContain("right: calc(var(--space-lg) * -1);");
|
||||
|
||||
/*
|
||||
FNXC:MobileTaskPopups 2026-08-03-19:36:
|
||||
The full-screen task-detail sheet hides every resize handle, so the inherited gutter must
|
||||
remain zero and the desktop outboard-target host must resume clipping for sheet geometry.
|
||||
*/
|
||||
const mobileTaskDetailBody = cssRuleContaining(
|
||||
allAppCss,
|
||||
".floating-window--task-detail .floating-window__body",
|
||||
"margin-inline-end",
|
||||
);
|
||||
expect(mobileTaskDetailBody).toContain("margin-inline-end: 0;");
|
||||
expect(cssRuleContaining(allAppCss, ".floating-window--task-detail", "overflow: hidden !important;")).toContain("overflow: hidden !important;");
|
||||
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-08-01-03:48:
|
||||
Tablet contract: ALL tablet-mode floating windows zero the FN-8015 gutter (it read as a
|
||||
too-big uneven right border in Task Detail popups and the floating terminal — third
|
||||
recurrence of the right-padding bug because tablet renders through FloatingWindow, not
|
||||
`.modal-overlay`). Touch never grabs scrollbar thumbs, so the desktop hot-zone conflict
|
||||
does not apply. GitHubImport relied on the gutter as its right inset and must compensate
|
||||
under the same class.
|
||||
FNXC:GitHubImport 2026-08-17-23:47:
|
||||
The import detail panel borrowed its right inset from the gutter (`padding-inline-end: 0` plus a
|
||||
tablet-only restore). With the gutter gone it must own a symmetric inset itself, or its Preview
|
||||
header goes flush against the window edge — the bug the borrowed gutter originally papered over.
|
||||
*/
|
||||
const tabletBody = cssRuleContaining(
|
||||
floatingWindowCss,
|
||||
".floating-window--tablet-viewport .floating-window__body",
|
||||
"margin-inline-end",
|
||||
);
|
||||
expect(tabletBody).toContain("margin-inline-end: 0;");
|
||||
const tabletGitHubImportPanel = cssRuleContaining(
|
||||
allAppCss,
|
||||
".floating-window--tablet-viewport .github-import-detail-panel",
|
||||
"padding-inline-end",
|
||||
);
|
||||
expect(tabletGitHubImportPanel).toContain("padding-inline-end: var(--space-lg);");
|
||||
const importPanel = cssRuleFor(allAppCss, ".github-import-detail-panel");
|
||||
expect(importPanel).toContain("padding: var(--space-lg);");
|
||||
expect(importPanel).not.toMatch(/padding-inline-end\s*:/);
|
||||
|
||||
// Headerless and chat variants replace only body overflow; the inherited gutter remains intact for their inner scrollers.
|
||||
// Headerless and chat variants replace only body overflow.
|
||||
expect(cssRuleFor(floatingWindowCss, ".floating-window--headerless .floating-window__body")).toContain("overflow: hidden;");
|
||||
expect(cssRuleFor(floatingWindowCss, ".floating-window--chat.floating-window--headerless .floating-window__body")).toContain("overflow: hidden;");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-08-17-23:47:
|
||||
A FloatingWindow paints its own bordered surface, so a hosted child that does not fill it leaves
|
||||
blank window surface around the content — read by the operator as unexplained right/bottom padding
|
||||
on first-run Set Up AI. The onboarding modal's standalone sizing rules tie on specificity with the
|
||||
shared host fill and won on source order, so the fill is re-asserted under the host class.
|
||||
*/
|
||||
it("makes the hosted onboarding modal fill its floating window", () => {
|
||||
// The fill is one grouped rule (`:not([style*="width"])`, `:not([style*="height"])`), so match the block.
|
||||
const hostedFill = allAppCss.match(
|
||||
/\.floating-window--model-onboarding \.model-onboarding-modal:not\(\[style\*="width"\]\)[^{]*\{[^}]*\}/
|
||||
)?.[0] ?? "";
|
||||
expect(hostedFill).toContain("width: 100%;");
|
||||
expect(hostedFill).toContain("height: 100%;");
|
||||
expect(hostedFill).toContain("max-height: none;");
|
||||
// The window frame owns resizing once hosted; a 640px floor would push content under the east edge.
|
||||
expect(hostedFill).toContain("min-width: 0;");
|
||||
expect(hostedFill).toContain("resize: none;");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:FloatingWindow 2026-07-25-00:00:
|
||||
Regression guard for the landscape-tablet right-inset gap: the width-gated
|
||||
769-1024px carve-out let iPad Air/Pro landscape (1180-1366 CSS px) fall back to
|
||||
the desktop contract and keep FN-8015's body gutter, so the task pop-up's
|
||||
content stopped short of the right edge while the left edge stayed flush.
|
||||
Assert the input-device-gated block covers the same two declarations at ANY
|
||||
width, and that it stays scoped to task-detail so other floating-window callers
|
||||
(whose right resize handles remain live) keep their scrollbar clearance.
|
||||
the desktop contract, so the task pop-up's content stopped short of the right
|
||||
edge while the left edge stayed flush. Assert the input-device-gated block
|
||||
applies at ANY width and stays scoped to task-detail.
|
||||
The body-gutter half of this guard is retired: the shared gutter that produced
|
||||
the original right-inset gap is deleted outright (see the ratchet above), so
|
||||
there is no longer a desktop-vs-tablet gutter difference to police here.
|
||||
*/
|
||||
it("uses the tablet-touch discriminator instead of bare coarse-pointer suppression", () => {
|
||||
expect(floatingWindowCss).not.toContain("@media (pointer: coarse)");
|
||||
@@ -250,7 +263,6 @@ describe("FloatingWindow", () => {
|
||||
expect(floatingWindowCss).toContain("@media (max-width: 767.98px)");
|
||||
expect(floatingWindowCss).toContain(".floating-window--touch-geometry .floating-window__resize-handle");
|
||||
expect(floatingWindowCss).toContain("width: var(--modal-resize-touch-target);");
|
||||
expect(floatingWindowCss).toContain("margin-inline-end: var(--space-lg);");
|
||||
|
||||
const phoneBlock = mediaBlockFor(floatingWindowCss, "(max-width: 767.98px)");
|
||||
expect(cssRuleFor(phoneBlock, ".floating-window--task-detail .floating-window__resize-handle")).toContain("display: none;");
|
||||
|
||||
@@ -121,19 +121,21 @@ describe("GitHubImportModal", () => {
|
||||
const onImport = vi.fn();
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-08-02-02:47:
|
||||
This stylesheet guard complements the emitted-CSS Chromium geometry regression: only the
|
||||
standalone importer clears the shared gutter under FloatingWindow's canonical sheet predicate.
|
||||
FNXC:GitHubImport 2026-08-17-23:47:
|
||||
FN-8722 gave the standalone importer sheet a local `margin-inline-end: 0` to undo the shared
|
||||
FloatingWindow body gutter that otherwise shifted every shell region left. That shared gutter is
|
||||
deleted outright, so the override is gone with it and this guard inverts: the importer must NOT
|
||||
carry any body-gutter rule of its own, and the detail panel now owns a symmetric inset directly
|
||||
instead of borrowing the gutter for its right side (it previously set `padding-inline-end: 0`).
|
||||
*/
|
||||
it("clears the standalone sheet gutter without changing embedded or detail presentations", () => {
|
||||
const sheetStart = floatingWindowCss.indexOf("@media (max-width: 767.98px), (max-height: 480px)");
|
||||
const sheetEnd = floatingWindowCss.indexOf("@media (max-width: 767.98px) {", sheetStart + 1);
|
||||
const sheetStyles = floatingWindowCss.slice(sheetStart, sheetEnd);
|
||||
it("carries no body gutter override and owns a symmetric detail-panel inset", () => {
|
||||
expect(floatingWindowCss).not.toContain(".floating-window--github-import .floating-window__body");
|
||||
|
||||
expect(sheetStyles).toContain(".floating-window--github-import .floating-window__body {");
|
||||
expect(sheetStyles).toContain("margin-inline-end: 0;");
|
||||
expect(sheetStyles).not.toContain(".floating-window--github-import-detail .floating-window__body");
|
||||
expect(sheetStyles).not.toContain(".github-import-embedded");
|
||||
const importCss = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8");
|
||||
const detailPanel = importCss.match(/\.github-import-detail-panel\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
expect(detailPanel).toContain("padding: var(--space-lg);");
|
||||
expect(detailPanel).not.toMatch(/padding-inline-end\s*:/);
|
||||
expect(importCss).not.toContain(".floating-window--tablet-viewport .github-import-detail-panel");
|
||||
});
|
||||
|
||||
it("uses color-mix tokens for focus and selection surfaces", () => {
|
||||
@@ -3168,7 +3170,15 @@ describe("GitHubImportModal", () => {
|
||||
*/
|
||||
it("scopes the import detail FloatingWindow as the shared mobile full-screen sheet", () => {
|
||||
const source = readFileSync(resolve(__dirname, "../FloatingWindow.css"), "utf8");
|
||||
const phoneSheetMedia = source.match(/@media \(max-width: 767\.98px\), \(max-height: 480px\) \{([\s\S]*?)\n\}\n\n@media/)?.[1];
|
||||
/*
|
||||
* FNXC:GitHubImport 2026-08-17-23:47:
|
||||
* Terminate on the media block's OWN closing brace (a `}` at column 0 — every rule inside is
|
||||
* indented) rather than on a following `@media`. The old pattern assumed another at-rule came
|
||||
* next, so deleting the phone-only block that followed it (it held nothing but a
|
||||
* `margin-inline-end: 0` undo of the retired shared gutter) made this match fail and the whole
|
||||
* sheet-geometry contract silently unverifiable.
|
||||
*/
|
||||
const phoneSheetMedia = source.match(/@media \(max-width: 767\.98px\), \(max-height: 480px\) \{([\s\S]*?)\n\}\n/)?.[1];
|
||||
expect(phoneSheetMedia, "phone-sheet media query must exist").toBeTruthy();
|
||||
const declarationList = (ruleBody: string) => ruleBody.split(";").map((declaration) => declaration.trim()).filter(Boolean);
|
||||
const ruleDeclarations = (css: string, selector: RegExp, name: string) => {
|
||||
|
||||
@@ -550,6 +550,74 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-08-17-23:47:
|
||||
The "Connect remote Fusion server" card belongs to the NATIVE SHELL hand-off, so it must appear on
|
||||
exactly one host class. The old gate keyed only on `desktopMode !== "local"`, and `desktopMode` is
|
||||
undefined in a browser, so every web first-run led Set Up AI with a remote-server form and copy
|
||||
about a "native shell" the visitor does not have (operator report). Enumerated surfaces: plain web
|
||||
(never), desktop shell in local mode (never — a local runtime is already the backend), desktop
|
||||
shell without a profile (shown), mobile shell without a profile (shown), and any host that already
|
||||
has an active profile (never).
|
||||
*/
|
||||
describe("AI Setup step — remote Fusion server card host gating", () => {
|
||||
const remoteCardText = /Connect remote Fusion server/;
|
||||
|
||||
function renderWithShellState(state: Record<string, unknown>) {
|
||||
mockUseShellConnection.mockReturnValue({
|
||||
shellApi: { saveProfile: vi.fn() },
|
||||
ready: true,
|
||||
openConnectionManagerSignal: 0,
|
||||
state: { profiles: [], activeProfileId: null, ...state },
|
||||
saveProfile: vi.fn(),
|
||||
removeProfile: vi.fn(),
|
||||
setActiveProfile: vi.fn(),
|
||||
});
|
||||
return render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
}
|
||||
|
||||
it("never offers a remote server in a plain browser", async () => {
|
||||
renderWithShellState({ host: "web" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Anthropic")).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText(remoteCardText)).toBeNull();
|
||||
});
|
||||
|
||||
it("never offers a remote server to a local-mode desktop shell", async () => {
|
||||
renderWithShellState({ host: "desktop-shell", desktopMode: "local" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Anthropic")).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText(remoteCardText)).toBeNull();
|
||||
});
|
||||
|
||||
it("offers a remote server to an unconfigured native shell", async () => {
|
||||
const { unmount } = renderWithShellState({ host: "desktop-shell", desktopMode: "remote" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(remoteCardText)).toBeTruthy();
|
||||
});
|
||||
unmount();
|
||||
|
||||
renderWithShellState({ host: "mobile-shell" });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(remoteCardText)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("stops offering a remote server once a profile is active", async () => {
|
||||
renderWithShellState({ host: "desktop-shell", desktopMode: "remote", activeProfileId: "profile_1" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Anthropic")).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText(remoteCardText)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI Setup step", () => {
|
||||
it("shows OAuth providers with Login button", async () => {
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
@@ -906,7 +906,6 @@ describe("TaskDetailModal", () => {
|
||||
it("keeps the floating task header symmetric without sacrificing its resize targets", () => {
|
||||
const floatingCss = readFileSync(resolve(__dirname, "../FloatingWindow.css"), "utf8");
|
||||
const desktopTaskSelector = ".floating-window--task-detail:not(.floating-window--tablet-viewport)";
|
||||
const taskPopupBody = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__body`);
|
||||
const sharedBody = getExactCssRuleBlock(floatingCss, ".floating-window__body");
|
||||
const header = getExactCssRuleBlock(readDashboardStylesSource(), ".modal-header");
|
||||
const eastResize = getExactCssRuleBlock(floatingCss, `${desktopTaskSelector} .floating-window__resize-handle--e`);
|
||||
@@ -915,12 +914,13 @@ describe("TaskDetailModal", () => {
|
||||
const onRequestClose = vi.fn();
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailLayout 2026-08-03-19:36:
|
||||
The shared body reserves desktop scrollbar clearance. Task Detail moves only its resize
|
||||
hit areas outboard, so the embedded header retains matching tokenized edges.
|
||||
FNXC:TaskDetailLayout 2026-08-17-23:47:
|
||||
The shared body reserves NOTHING on its inline end any more — FN-8015's gutter is deleted for
|
||||
every caller, so this popup's symmetric edge no longer depends on a local zeroing that undoes
|
||||
it. Task Detail keeps its outboard resize hit areas (below), which is what lets the embedded
|
||||
header keep matching tokenized edges while the scrollbar stays grabbable.
|
||||
*/
|
||||
expect(sharedBody).toContain("margin-inline-end: var(--space-lg);");
|
||||
expect(taskPopupBody).toContain("margin-inline-end: 0;");
|
||||
expect(sharedBody).not.toMatch(/margin-inline-end\s*:/);
|
||||
expect(header).toContain("padding: var(--modal-padding);");
|
||||
expect(eastResize).toContain("right: calc(var(--space-sm) * -1);");
|
||||
expect(northEastResize).toContain("right: calc(var(--space-lg) * -1);");
|
||||
|
||||
Reference in New Issue
Block a user