From 4aaa6869621c3f25482befb73d186fffbea10729 Mon Sep 17 00:00:00 2001 From: ShashiSubramanya Date: Thu, 27 Aug 2026 23:44:54 +0530 Subject: [PATCH 1/7] dark theme edits --- src/components/FloatingAssistant/index.scss | 217 +++++++++++++------- src/components/FloatingAssistant/index.tsx | 50 ++++- 2 files changed, 194 insertions(+), 73 deletions(-) diff --git a/src/components/FloatingAssistant/index.scss b/src/components/FloatingAssistant/index.scss index f8276f276..8901f566e 100644 --- a/src/components/FloatingAssistant/index.scss +++ b/src/components/FloatingAssistant/index.scss @@ -1,6 +1,83 @@ @import '../../assets/styles/variables.scss'; @import '../../assets/styles/highlight.scss'; +// ── Theme tokens ──────────────────────────────────────────────────────────── +// This widget mounts into its own React root appended directly to +// document.body (see gatsby-browser.js), outside #wrapper/#docsModal, so it +// can't inherit the site's data-theme custom properties from variables.scss. +// index.tsx mirrors the site's current theme onto this root's own +// data-theme attribute; these aliases are redeclared under that selector so +// they resolve against the dark values for every descendant (same reason +// variables.scss's ts-docs-radiant-aliases mixin is included twice). +#floating-assistant-root { + --fa-bg: #fff; + --fa-panel-bg: #fff; + --fa-surface: #F6F8FA; + --fa-surface-hover: #eef1f5; + --fa-border: #EAEDF2; + --fa-border-strong: #e3e6eb; + --fa-text: #1d232f; + --fa-text-secondary: #5f6368; + --fa-text-muted: #a5acb9; + --fa-accent: #2770ef; + --fa-accent-hover: #1a5fd4; + --fa-user-bubble-bg: #f1f4f8; + --fa-user-bubble-border: #e8eaed; + --fa-scrollbar: #d0d4db; + --fa-scrollbar-hover: #a5acb9; + --fa-scroll-btn-bg: #fff; + --fa-scroll-btn-border: #e0e4ea; + --fa-shadow: 0 4px 16px 0 rgba(25, 35, 49, 0.10); + --fa-shadow-hover: 0 8px 24px 0 rgba(25, 35, 49, 0.16); + --fa-panel-shadow: -4px 0 24px 0 rgba(25, 35, 49, 0.10); + --fa-input-border-idle: #d0d4db; + --fa-input-row-border: rgba(0, 0, 0, 0.06); + --fa-input-row-bg: rgba(255, 255, 255, 0.6); + --fa-fade-color: #fff; + --fa-code-inline-bg: #f1f4f8; + --fa-code-inline-border: #e3e6eb; + --fa-hover-overlay: rgba(0, 0, 0, 0.06); + --fa-panel-wash-1: rgba(255, 120, 160, 0.3); + --fa-panel-wash-2: rgba(160, 100, 255, 0.2); + --fa-panel-wash-3: rgba(100, 160, 255, 0.3); +} + +#floating-assistant-root[data-theme='dark'] { + --fa-bg: #26282C; + --fa-panel-bg: #1F2024; + --fa-surface: #26282C; + --fa-surface-hover: #2f333b; + --fa-border: #34363C; + --fa-border-strong: #34363C; + --fa-text: #e6edf3; + --fa-text-secondary: #9098a8; + --fa-text-muted: #8b949e; + --fa-accent: #71a1f4; + --fa-accent-hover: #abc7f9; + --fa-user-bubble-bg: #26282C; + --fa-user-bubble-border: #34363C; + --fa-scrollbar: #3a3f4b; + --fa-scrollbar-hover: #545b6b; + --fa-scroll-btn-bg: #2a3142; + --fa-scroll-btn-border: #3a4255; + --fa-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.4); + --fa-shadow-hover: 0 8px 24px 0 rgba(0, 0, 0, 0.5); + --fa-panel-shadow: -4px 0 24px 0 rgba(0, 0, 0, 0.4); + --fa-input-border-idle: #3a3f4b; + --fa-input-row-border: rgba(255, 255, 255, 0.08); + --fa-input-row-bg: rgba(31, 32, 36, 0.6); + --fa-fade-color: #1F2024; + --fa-code-inline-bg: #2a3142; + --fa-code-inline-border: #3a4255; + --fa-hover-overlay: rgba(255, 255, 255, 0.08); + // Same wash hues at much lower opacity — on the dark panel background the + // light-mode opacities read as a saturated, concentrated band instead of + // the soft diffused glow the reference design uses. + --fa-panel-wash-1: rgba(255, 120, 160, 0.10); + --fa-panel-wash-2: rgba(160, 100, 255, 0.07); + --fa-panel-wash-3: rgba(100, 160, 255, 0.10); +} + // ── Chip trigger ───────────────────────────────────────────────────────────── .floating-assistant__chip { position: fixed; @@ -12,19 +89,19 @@ justify-content: center; width: 56px; height: 56px; - background: #fff; + background: var(--fa-bg); border: 2px solid transparent; border-radius: 50%; cursor: pointer; background-clip: padding-box; - box-shadow: 0 4px 16px 0 rgba(25, 35, 49, 0.10); + box-shadow: var(--fa-shadow); transition: box-shadow 0.2s ease, transform 0.2s ease; outline: 2px solid transparent; // Gradient border using outline + pseudo-element on a wrapper isn't needed — // use a parent wrapper div for the gradient ring &:hover { - box-shadow: 0 8px 24px 0 rgba(25, 35, 49, 0.16); + box-shadow: var(--fa-shadow-hover); } svg { @@ -32,7 +109,7 @@ height: 26px; position: relative; z-index: 1; - color: #2770ef; + color: var(--fa-accent); } } @@ -47,7 +124,7 @@ background: linear-gradient(135deg, #8C62F5, #48D1E0, #2770EF); padding: 1px; z-index: 1001; - box-shadow: 0 4px 16px 0 rgba(25, 35, 49, 0.10); + box-shadow: var(--fa-shadow); cursor: pointer; .floating-assistant__chip { @@ -83,18 +160,18 @@ bottom: 0; width: 360px; background: - linear-gradient(180deg, transparent 0%, #fff 35%), + linear-gradient(180deg, transparent 0%, var(--fa-panel-bg) 35%), linear-gradient(90deg, - rgba(255, 120, 160, 0.3) 0%, - rgba(160, 100, 255, 0.2) 60%, - rgba(100, 160, 255, 0.3) 90% + var(--fa-panel-wash-1) 0%, + var(--fa-panel-wash-2) 60%, + var(--fa-panel-wash-3) 90% ), - #fff; + var(--fa-panel-bg); display: flex; flex-direction: column; overflow: hidden; z-index: 1002; - box-shadow: -4px 0 24px 0 rgba(25, 35, 49, 0.10); + box-shadow: var(--fa-panel-shadow); animation: fa-slide-in 0.25s ease-out forwards; &.closing { @@ -102,7 +179,7 @@ } &--conversation { - background: #fff; + background: var(--fa-panel-bg); } &--embedded { @@ -121,7 +198,7 @@ background: transparent; border: none; outline: none; - border-bottom: 1px solid #EAEDF2; + border-bottom: 1px solid var(--fa-border); } .floating-assistant__close-btn { @@ -133,16 +210,17 @@ justify-content: center; padding: 4px; border-radius: 4px; + color: var(--fa-text); transition: background 0.15s; - &:hover { background: #f1f4f8; } + &:hover { background: var(--fa-surface); } } .floating-assistant__title { font-size: 16px; font-weight: 600; font-family: 'Optimo-Plain', sans-serif; - color: #1d232f; + color: var(--fa-text); } @@ -160,12 +238,12 @@ &::-webkit-scrollbar { width: 4px; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { - background: #d0d4db; + background: var(--fa-scrollbar); border-radius: 2px; - &:hover { background: #a5acb9; } + &:hover { background: var(--fa-scrollbar-hover); } } scrollbar-width: thin; - scrollbar-color: #d0d4db transparent; + scrollbar-color: var(--fa-scrollbar) transparent; } // ── Messages fade + scroll-to-bottom ───────────────────────────────────────── @@ -183,7 +261,7 @@ left: 0; right: 0; height: 48px; - background: linear-gradient(to top, #fff 0%, transparent 100%); + background: linear-gradient(to top, var(--fa-fade-color) 0%, transparent 100%); opacity: 0; transition: opacity 0.2s ease; } @@ -203,17 +281,13 @@ display: inline-flex; align-items: center; justify-content: center; - background: #fff; - border: 1px solid #e0e4ea; + background: var(--fa-scroll-btn-bg); + border: 1px solid var(--fa-scroll-btn-border); border-radius: 50%; width: 28px; height: 28px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.1); - @media (prefers-color-scheme: dark) { - background: #2a3142; - border-color: #3a4255; - } } @keyframes fa-fade-in { @@ -255,12 +329,12 @@ font-size: 22px; font-weight: 700; font-family: 'Optimo-Plain', sans-serif; - color: #1d232f; + color: var(--fa-text); line-height: 1.3; letter-spacing: -0.4px; text-align: center; - span { color: #2770ef; } + span { color: var(--fa-accent); } } .floating-assistant__suggestions { @@ -272,10 +346,10 @@ } .floating-assistant__suggestion { - background: #F6F8FA; - border: 1px solid #EAEDF2; + background: var(--fa-surface); + border: 1px solid var(--fa-border); border-radius: 40px; - color: #1d232f; + color: var(--fa-text); cursor: pointer; font-size: 12px; font-family: 'Optimo-Plain', sans-serif; @@ -287,7 +361,7 @@ animation: fa-suggestion-in 0.3s ease-out both; opacity: 0; - &:hover { background: #eef1f5; } + &:hover { background: var(--fa-surface-hover); } } @keyframes fa-suggestion-in { @@ -320,7 +394,7 @@ .floating-assistant__timestamp { font-size: 11px; - color: #a5acb9; + color: var(--fa-text-muted); font-family: 'Optimo-Plain', sans-serif; } @@ -338,14 +412,14 @@ background: none; &:hover { - background: #d0d4db; + background: var(--fa-scrollbar); } &--send { - background: #2770ef; + background: var(--fa-accent); &:hover { - background: #1a5fd4; + background: var(--fa-accent-hover); } } } @@ -385,13 +459,13 @@ // User bubble — light gray rounded box .floating-assistant__user-bubble { - background: #f1f4f8; - border: 1px solid #e8eaed; + background: var(--fa-user-bubble-bg); + border: 1px solid var(--fa-user-bubble-border); border-radius: 10px; padding: 10px 14px; font-size: 13px; font-weight: 400; - color: #1d232f; + color: var(--fa-text); font-family: 'Optimo-Plain', sans-serif; line-height: 1.5; word-break: break-word; @@ -400,8 +474,8 @@ box-sizing: border-box; &--editing { - border-color: #2770ef; - background: #fff; + border-color: var(--fa-accent); + background: var(--fa-panel-bg); cursor: text; white-space: pre-wrap; } @@ -435,7 +509,7 @@ .floating-assistant__message-text { font-size: 13px; line-height: 1.6; - color: #1d232f; + color: var(--fa-text); word-break: break-word; font-family: 'Optimo-Plain', sans-serif; @@ -448,8 +522,8 @@ li { margin-bottom: 3px; } strong { font-weight: 600; } em { font-style: italic; } - a { color: #2770ef; text-decoration: underline; } - h1, h2, h3 { font-weight: 600; margin: 10px 0 4px; color: #1d232f; } + a { color: var(--fa-accent); text-decoration: underline; } + h1, h2, h3 { font-weight: 600; margin: 10px 0 4px; color: var(--fa-text); } h1 { font-size: 15px; } h2 { font-size: 14px; } h3 { font-size: 13px; } @@ -457,11 +531,11 @@ code { font-family: $font-family-code; font-size: 12px; - background: #f1f4f8; - color: #1d232f; + background: var(--fa-code-inline-bg); + color: var(--fa-text); border-radius: 3px; padding: 1px 5px; - border: 1px solid #e3e6eb; + border: 1px solid var(--fa-code-inline-border); } pre { @@ -486,7 +560,7 @@ gap: 5px; font-size: 13px; font-weight: 400; - color: #6b7280; + color: var(--fa-text-secondary); font-family: 'Optimo-Plain', sans-serif; cursor: default; user-select: none; @@ -513,12 +587,12 @@ border-radius: 4px; font-size: 11px; font-family: 'Optimo-Plain', sans-serif; - color: #a5acb9; + color: var(--fa-text-muted); transition: color 0.15s, background 0.15s; &:hover { - color: #1d232f; - background: #f1f4f8; + color: var(--fa-text); + background: var(--fa-surface); } &--copied { @@ -538,7 +612,7 @@ border-radius: 4px; opacity: 0.7; transition: opacity 0.15s, background 0.15s; - &:hover { opacity: 1; background: rgba(0,0,0,0.06); } + &:hover { opacity: 1; background: var(--fa-hover-overlay); } &.fa-feedback-btn--active { opacity: 1; } } @@ -552,7 +626,7 @@ .floating-assistant__loading-phase { font-size: 13px; - color: #5f6368; + color: var(--fa-text-secondary); font-family: 'Optimo-Plain', sans-serif; animation: fa-phase-in 0.4s ease-out forwards; } @@ -567,19 +641,19 @@ display: flex; align-items: center; gap: 6px; - background: #f1f4f8; + background: var(--fa-surface); border-radius: 8px; padding: 6px 8px 6px 10px; margin-bottom: 4px; - color: #5f6368; + color: var(--fa-text-secondary); - svg { flex-shrink: 0; color: #a5acb9; } + svg { flex-shrink: 0; color: var(--fa-text-muted); } } .floating-assistant__quote-text { flex: 1; font-size: 12px; - color: #5f6368; + color: var(--fa-text-secondary); font-family: 'Optimo-Plain', sans-serif; white-space: nowrap; overflow: hidden; @@ -649,8 +723,8 @@ .floating-assistant__input-row { padding: 8px 12px 8px; flex-shrink: 0; - border-top: 1px solid rgba(0, 0, 0, 0.06); - background: rgba(255,255,255,0.6); + border-top: 1px solid var(--fa-input-row-border); + background: var(--fa-input-row-bg); backdrop-filter: blur(8px); } @@ -658,7 +732,7 @@ display: flex; flex-direction: column; gap: 6px; - background: #fff; + background: var(--fa-bg); border-radius: 12px; padding: 10px 10px 8px; cursor: text; @@ -671,7 +745,7 @@ inset: 0; border-radius: 12px; padding: 1.5px; - background: #d0d4db; + background: var(--fa-input-border-idle); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); @@ -718,15 +792,15 @@ font-size: 14px; font-family: 'Optimo-Plain', sans-serif; background: transparent; - color: #1d232f; + color: var(--fa-text); min-height: 28px; max-height: calc(14px * 1.5 * 4); // 4 lines at font-size 14px, line-height 1.5 overflow-y: auto; line-height: 1.5; - &::placeholder { color: #a5acb9; } + &::placeholder { color: var(--fa-text-muted); } scrollbar-width: thin; - scrollbar-color: #d0d4db transparent; + scrollbar-color: var(--fa-scrollbar) transparent; } .floating-assistant__input-buttons { @@ -740,15 +814,15 @@ width: 30px; height: 30px; border-radius: 50%; - border: 1px solid #e3e6eb; - background: #fff; + border: 1px solid var(--fa-border-strong); + background: var(--fa-bg); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background 0.15s; - &:hover { background: #f1f4f8; } + &:hover { background: var(--fa-surface); } } .floating-assistant__send { @@ -756,7 +830,7 @@ height: 30px; border-radius: 50%; border: none; - background: #2770ef; + background: var(--fa-accent); cursor: pointer; display: flex; align-items: center; @@ -764,7 +838,8 @@ flex-shrink: 0; transition: background 0.15s; &:disabled { opacity: 0.4; cursor: not-allowed; } - &:not(:disabled):hover { background: #1a5ccc; } + &:not(:disabled):hover, + &:not(:disabled):active { background: var(--fa-accent-hover); } } .floating-assistant__stop { @@ -776,7 +851,7 @@ align-items: center; justify-content: center; flex-shrink: 0; - background: #2770ef; + background: var(--fa-accent); border:0px; color: #fff; transition: background 0.15s, border-color 0.15s; @@ -787,12 +862,12 @@ .floating-assistant__footer { padding: 6px 16px 12px; font-size: 11.5px; - color: #a5acb9; + color: var(--fa-text-muted); text-align: center; font-family: 'Optimo-Plain', sans-serif; a { - color: #2770ef; + color: var(--fa-accent); text-decoration: none; &:hover { text-decoration: underline; } } diff --git a/src/components/FloatingAssistant/index.tsx b/src/components/FloatingAssistant/index.tsx index 22edad90d..d117a5ad4 100644 --- a/src/components/FloatingAssistant/index.tsx +++ b/src/components/FloatingAssistant/index.tsx @@ -269,6 +269,52 @@ const FloatingAssistant: React.FC = () => { return () => window.removeEventListener('gatsby-route-update', handler as EventListener); }, []); + // This widget mounts into its own React root appended directly to + // document.body (see gatsby-browser.js), outside #wrapper/#docsModal, so + // it can't inherit the site's data-theme custom properties. Mirror the + // site's current theme onto our own root's data-theme attribute instead, + // so CSS here can key off #floating-assistant-root[data-theme='dark']. + useEffect(() => { + const themeRoot = document.getElementById('floating-assistant-root'); + const setTheme = (theme: 'dark' | 'light') => { + // Guard against redundant writes — themeRoot lives inside document.body, + // so an unconditional setAttribute here would itself be a mutation that + // a body-wide observer could pick back up, looping forever. + if (themeRoot && themeRoot.getAttribute('data-theme') !== theme) { + themeRoot.setAttribute('data-theme', theme); + } + }; + + let attrObserver: MutationObserver | null = null; + + // Watches the specific #wrapper/#docsModal element for data-theme flips + // (e.g. the user toggling dark mode) — scoped to that one element only, + // so it never observes themeRoot's own attribute writes above. + const watchThemedEl = () => { + const themedEl = document.getElementById('wrapper') || document.getElementById('docsModal'); + if (!themedEl) return; + setTheme(themedEl.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'); + attrObserver?.disconnect(); + attrObserver = new MutationObserver(() => { + setTheme(themedEl.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'); + }); + attrObserver.observe(themedEl, { attributes: true, attributeFilter: ['data-theme'] }); + }; + + watchThemedEl(); + + // #wrapper/#docsModal can be unmounted/remounted on client-side route + // changes — re-attach the attribute observer whenever that happens. + // childList-only, so this never fires from themeRoot's attribute writes. + const bodyObserver = new MutationObserver(watchThemedEl); + bodyObserver.observe(document.body, { childList: true, subtree: true }); + + return () => { + attrObserver?.disconnect(); + bodyObserver.disconnect(); + }; + }, []); + useEffect(() => { const handler = (e: CustomEvent<{ location: Location }>) => { setIsTutorialsPage(isTutorialsPath(e.detail.location.pathname)); @@ -455,7 +501,7 @@ const FloatingAssistant: React.FC = () => { SpotterCode @@ -685,7 +731,7 @@ const FloatingAssistant: React.FC = () => { aria-label="Reset conversation" disabled={messages.length === 0 && !input.trim() && !quotedText} > - + {isLoading ? (