diff --git a/.changeset/fix-floating-window-gutter-and-onboarding.md b/.changeset/fix-floating-window-gutter-and-onboarding.md new file mode 100644 index 0000000000..fc0f00ccd5 --- /dev/null +++ b/.changeset/fix-floating-window-gutter-and-onboarding.md @@ -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. diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index 4c73392c14..057e4019e1 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -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 — ") 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; diff --git a/packages/dashboard/app/components/FloatingWindow.tsx b/packages/dashboard/app/components/FloatingWindow.tsx index 68dddf1096..0b846c4c77 100644 --- a/packages/dashboard/app/components/FloatingWindow.tsx +++ b/packages/dashboard/app/components/FloatingWindow.tsx @@ -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); diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index 37839d912a..286c5c249d 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -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 — diff --git a/packages/dashboard/app/components/ModelOnboardingModal.css b/packages/dashboard/app/components/ModelOnboardingModal.css index e6349fe7f6..250b9f7cfa 100644 --- a/packages/dashboard/app/components/ModelOnboardingModal.css +++ b/packages/dashboard/app/components/ModelOnboardingModal.css @@ -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; diff --git a/packages/dashboard/app/components/ModelOnboardingModal.tsx b/packages/dashboard/app/components/ModelOnboardingModal.tsx index 5a324e3b43..edcb77725b 100644 --- a/packages/dashboard/app/components/ModelOnboardingModal.tsx +++ b/packages/dashboard/app/components/ModelOnboardingModal.tsx @@ -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"; diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index e28d45fef3..883dfd71f4 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -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; diff --git a/packages/dashboard/app/components/TerminalModal.css b/packages/dashboard/app/components/TerminalModal.css index c77b5abc52..72ee4956ff 100644 --- a/packages/dashboard/app/components/TerminalModal.css +++ b/packages/dashboard/app/components/TerminalModal.css @@ -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"; diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx index 76e06a9b8a..62d9fe072a 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx @@ -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;"); diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index ff2fda8551..83582fd5d0 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -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) => { diff --git a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx index 894de330ac..f11b48a920 100644 --- a/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx @@ -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) { + 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(); + } + + 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(); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index 2e14a68200..8e66eb1be6 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -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);");