From d45a99eaa36c1816270648bd81c7241a48a912e5 Mon Sep 17 00:00:00 2001 From: 0raclus Date: Mon, 11 May 2026 17:39:06 +0300 Subject: [PATCH] fix: black-button text contrast + Tutorial viewport clamping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related polish bugs: 1) Light theme had --color-text-inverse: #1a1a1a (same near-black as --color-text). Components that built a 'primary' button with bg: --color-text + color: --color-text-inverse rendered as black-on-black, invisible label. Fixed by hard-coding white (#fff) on the affected 5 surfaces (Tutorial Next, PairQueue accept, ExecutionReplay primary, ProfileSetup save + selected-thumb check stroke). Left the token alone so accent-bg consumers (MorphicTabs etc.) aren't affected. 2) Tutorial card popped below the viewport on steps whose target sat near the page bottom (and the 'Built with' last step in particular). Card position now clamps to viewport bounds — picks above/below based on available space, falls back to a bottom-pinned position when neither fits. Also adds maxHeight + overflowY:auto on the card so a future longer body can scroll without pushing the action buttons off-screen. --- src/components/ExecutionReplay.tsx | 2 +- src/components/PairQueuePanel.tsx | 2 +- src/components/ProfileSetup.tsx | 4 +- src/components/Tutorial.tsx | 62 ++++++++++++++++++++++++++---- 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/components/ExecutionReplay.tsx b/src/components/ExecutionReplay.tsx index bf00301..35da9fc 100644 --- a/src/components/ExecutionReplay.tsx +++ b/src/components/ExecutionReplay.tsx @@ -550,7 +550,7 @@ const styles: Record = { fontFamily: MONO, fontSize: 13, fontWeight: 700, - color: "var(--color-text-inverse)", + color: "#ffffff", background: "var(--color-text)", border: "1px solid var(--color-text)", borderRadius: 10, diff --git a/src/components/PairQueuePanel.tsx b/src/components/PairQueuePanel.tsx index be7f9f1..83555c4 100644 --- a/src/components/PairQueuePanel.tsx +++ b/src/components/PairQueuePanel.tsx @@ -381,7 +381,7 @@ const styles: Record = { fontFamily: MONO, fontSize: 13, fontWeight: 700, - color: "var(--color-text-inverse)", + color: "#ffffff", background: "var(--color-text)", border: "1px solid var(--color-text)", borderRadius: 6, diff --git a/src/components/ProfileSetup.tsx b/src/components/ProfileSetup.tsx index dba3370..579a67c 100644 --- a/src/components/ProfileSetup.tsx +++ b/src/components/ProfileSetup.tsx @@ -187,7 +187,7 @@ export const ProfileSetup: FC = ({ = { fontFamily: MONO, fontSize: 15, fontWeight: 700, - color: "var(--color-text-inverse)", + color: "#ffffff", background: "var(--color-text)", border: "1px solid var(--color-text)", borderRadius: 10, diff --git a/src/components/Tutorial.tsx b/src/components/Tutorial.tsx index 54ac1a8..77aeb77 100644 --- a/src/components/Tutorial.tsx +++ b/src/components/Tutorial.tsx @@ -179,18 +179,61 @@ export const Tutorial: FC = ({ if (!step) return null; // Card position — derived from rect when available, fallback centre. + // Clamped to the viewport so the buttons never disappear off-screen, + // even when the spotlighted target is near the page bottom (e.g. the + // "Built with" footer step) or near a viewport edge. + const CARD_WIDTH = 340; + // Slight over-estimate so the card always reserves enough room for + // a 4-line body plus all three action buttons (real measured height + // is ~246px on the longest step, so 270 gives a small safety margin). + const CARD_EST_HEIGHT = 270; + const MARGIN = 20; + const GAP = 16; + let cardLeft: number | string = "50%"; + let cardTop: number | string = "50%"; + if (rect) { + const spaceBelow = window.innerHeight - rect.bottom - GAP; + const spaceAbove = rect.top - GAP; + const preferTop = step.side === "top"; + if (preferTop && spaceAbove >= CARD_EST_HEIGHT + MARGIN) { + cardTop = rect.top - CARD_EST_HEIGHT - GAP; + } else if (!preferTop && spaceBelow >= CARD_EST_HEIGHT + MARGIN) { + cardTop = rect.bottom + GAP; + } else if (spaceBelow >= CARD_EST_HEIGHT + MARGIN) { + cardTop = rect.bottom + GAP; + } else if (spaceAbove >= CARD_EST_HEIGHT + MARGIN) { + cardTop = rect.top - CARD_EST_HEIGHT - GAP; + } else { + // Neither above nor below has enough room — pin near the + // bottom of the viewport so the action buttons stay visible. + cardTop = Math.max(MARGIN, window.innerHeight - CARD_EST_HEIGHT - MARGIN); + } + const idealLeft = rect.left + rect.width / 2 - CARD_WIDTH / 2; + cardLeft = Math.max( + MARGIN, + Math.min(window.innerWidth - CARD_WIDTH - MARGIN, idealLeft), + ); + // Final hard clamp: regardless of branch chosen above, never let + // the card extend past the viewport bottom. If the estimate was + // tight, the inner scroll (overflowY: auto) keeps the body + // readable while the action buttons remain visible. + if (typeof cardTop === "number") { + cardTop = Math.max( + MARGIN, + Math.min(cardTop, window.innerHeight - CARD_EST_HEIGHT - MARGIN), + ); + } + } const cardStyle: CSSProperties = { position: "fixed", zIndex: 401, - left: rect ? Math.max(20, Math.min(window.innerWidth - 360, rect.left)) : "50%", - top: rect - ? step.side === "top" - ? Math.max(20, rect.top - 140) - : rect.bottom + 16 - : "50%", + left: cardLeft, + top: cardTop, transform: rect ? undefined : "translate(-50%, -50%)", - width: 340, + width: CARD_WIDTH, maxWidth: "calc(100vw - 40px)", + maxHeight: `calc(100vh - ${MARGIN * 2}px)`, + overflowY: "auto", background: "var(--surface-raised)", border: "1px solid var(--color-stroke)", borderRadius: 14, @@ -338,7 +381,10 @@ const styles: Record = { fontFamily: "var(--font-mono)", fontSize: 12, fontWeight: 700, - color: "var(--color-text-inverse)", + // Fixed white — `--color-text-inverse` in light theme is the same + // near-black as `--color-text`, so using it here made the label + // invisible on the black button. + color: "#ffffff", background: "var(--color-text)", border: "1px solid var(--color-text)", borderRadius: 6,