diff --git a/packages/dashboard/app/components/GoalsView.css b/packages/dashboard/app/components/GoalsView.css index 923fad9cf3..3e18edd57d 100644 --- a/packages/dashboard/app/components/GoalsView.css +++ b/packages/dashboard/app/components/GoalsView.css @@ -295,8 +295,16 @@ The goal card is a 3-column flex row: .goals-card-main | .goals-card-actions | . align-items: stretch; } - .goals-linked-missions-link-button, + /* + FNXC:Goals 2026-06-27-00:00: + FN-7155: In this tablet column layout, the picker flex-basis maps to height instead of width. Reset flex so the mission picker stays at native select height while preserving full-width stacking. + */ .goals-linked-missions-picker { + flex: 0 0 auto; + width: 100%; + } + + .goals-linked-missions-link-button { width: 100%; } } @@ -321,8 +329,16 @@ The goal card is a 3-column flex row: .goals-card-main | .goals-card-actions | . border-top: calc(var(--space-xs) / 4) solid var(--border); } - .goals-linked-missions-link-button, + /* + FNXC:Goals 2026-06-27-00:00: + FN-7155: In this mobile column layout, the picker flex-basis maps to height instead of width. Reset flex so the mission picker stays at native select height while preserving full-width stacking. + */ .goals-linked-missions-picker { + flex: 0 0 auto; + width: 100%; + } + + .goals-linked-missions-link-button { width: 100%; } diff --git a/packages/dashboard/app/components/__tests__/GoalsView.css.test.ts b/packages/dashboard/app/components/__tests__/GoalsView.css.test.ts index 0a6d0f8157..e4253e8efb 100644 --- a/packages/dashboard/app/components/__tests__/GoalsView.css.test.ts +++ b/packages/dashboard/app/components/__tests__/GoalsView.css.test.ts @@ -6,6 +6,41 @@ function goalsBlocks(css: string): string { return blocks.join("\n"); } +function extractRuleBlock(css: string, selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`)); + return match?.[1] ?? ""; +} + +function extractAtRuleBlocks(css: string, atRule: string): string[] { + const blocks: string[] = []; + let start = css.indexOf(atRule); + + while (start !== -1) { + const open = css.indexOf("{", start); + if (open === -1) break; + + let depth = 1; + let cursor = open + 1; + while (cursor < css.length && depth > 0) { + if (css[cursor] === "{") depth++; + if (css[cursor] === "}") depth--; + cursor++; + } + + blocks.push(css.slice(open + 1, cursor - 1)); + start = css.indexOf(atRule, cursor); + } + + return blocks; +} + +function extractRuleBlockFromAtRule(css: string, atRule: string, selector: string): string { + return extractAtRuleBlocks(css, atRule) + .map((block) => extractRuleBlock(block, selector)) + .find((block) => block !== "") ?? ""; +} + describe("GoalsView CSS token guardrails", () => { it("uses tokens and contains mobile/focus rules", async () => { const css = await loadAllAppCss(); @@ -20,4 +55,17 @@ describe("GoalsView CSS token guardrails", () => { expect(css).toMatch(/\.goals-[^\n{]*:focus-visible/g); expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*\.goals-/); }); + + it("keeps the linked mission picker natural-height in column breakpoints", async () => { + const css = await loadAllAppCss(); + const basePickerBlock = extractRuleBlock(css, ".goals-linked-missions-picker"); + const mobilePickerBlock = extractRuleBlockFromAtRule(css, "@media (max-width: 768px)", ".goals-linked-missions-picker"); + const tabletPickerBlock = extractRuleBlockFromAtRule(css, "@media (min-width: 769px) and (max-width: 1024px)", ".goals-linked-missions-picker"); + + expect(basePickerBlock).toMatch(/flex\s*:\s*1\s+1\s+calc\(var\(--space-xl\)\s*\*\s*10\)/); + expect(mobilePickerBlock).toMatch(/flex\s*:\s*0\s+0\s+auto/); + expect(mobilePickerBlock).toMatch(/width\s*:\s*100%/); + expect(tabletPickerBlock).toMatch(/flex\s*:\s*0\s+0\s+auto/); + expect(tabletPickerBlock).toMatch(/width\s*:\s*100%/); + }); });