From 8cdc41cc15d1d9f1cf5153b94cfb144609945fe7 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 14:20:46 +0700 Subject: [PATCH] Quieter Sasquatch bubble: no perpetual pulse, hideable, map-aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses "the floating Sasquatch flashing is annoying": - Pulse is now state-driven: the animate-ping halo shows only when there's an unread tip (message), never perpetually; the unread dot matches. - Honors prefers-reduced-motion (motion-reduce:hidden). - Map-aware: Layout passes isFullBleed, so on the map the bubble runs compact (smaller, no glow, no pulse) instead of competing with map controls. - Hideable: a ✕ on the FAB hides it (useSasquatchHidden, localStorage sasquatch-hidden, same-tab synced via an event), with a "Show Sasquatch assistant" toggle in Settings → Appearance to bring it back. --- AGENTS.md | 8 ++++++ src/components/Layout.tsx | 1 + src/components/SasquatchGuide.tsx | 45 +++++++++++++++++++++++++------ src/hooks/useSasquatchHidden.ts | 31 +++++++++++++++++++++ src/i18n/batch3/sasquatchUi.ts | 2 ++ src/i18n/en.ts | 3 +++ src/i18n/es.ts | 3 +++ src/pages/SettingsPage.tsx | 17 ++++++++++++ 8 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useSasquatchHidden.ts diff --git a/AGENTS.md b/AGENTS.md index edda9d5..7eeef24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -574,6 +574,14 @@ session with **no other memory of this project**. So: calls are suppressed so partial JSON never renders. Pollinations' `mistral` slug is dead (404); only `openai` (gpt-oss-20b) is live. `findCoaches` + `getCommunityStats` cover **all** Lab roles (kinds 35001–35014). + **Bubble visibility (2026-10-04):** the floating Sasquatch FAB no longer + pulses forever — the `animate-ping` halo shows **only when an unread tip + exists**, is `motion-reduce:hidden`, and is suppressed on **full-bleed routes** + (`isFullBleed` passed from `Layout`), where the bubble also runs **compact** + (smaller, no glow) so it stops competing with the map. A ✕ on the FAB hides + it (`useSasquatchHidden`, localStorage `sasquatch-hidden`, same-tab synced via + a `sasquatch:hiddenchange` event), and **Settings → Appearance** has a "Show + Sasquatch assistant" toggle to bring it back. - **ContextVM (CVM) server** (`cvm-server/`, Bun): standalone MCP-over-Nostr endpoint for *external* agents. Runs on this machine as a **system** service (`/etc/systemd/system/skatespots-cvm.service`, `sudo systemctl restart diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 938f0b7..4a2fa4d 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1136,6 +1136,7 @@ export function Layout({ children, noPadding }: LayoutProps) { diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index f8a456a..5185f3b 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { X, Minimize2, HelpCircle, Send, Loader2, Wifi, WifiOff } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useSasquatchHidden } from '@/hooks/useSasquatchHidden'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCVMClient, type SpotResult } from '@/hooks/useCVMClient'; import { usePersonalContext } from '@/hooks/usePersonalContext'; @@ -116,6 +117,8 @@ export interface GuideMessage { interface SasquatchGuideProps { message?: GuideMessage; onDismiss?: () => void; + /** True on full-bleed routes (the map) — the bubble runs compact/no-pulse. */ + isFullBleed?: boolean; } interface ChatMessage { @@ -658,7 +661,7 @@ function ChatText({ text }: { text: string }) { // ─── Main component ─────────────────────────────────────────────────────────── -export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { +export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGuideProps) { const { user } = useCurrentUser(); const { t, language } = useTranslation(); const cvm = useCVMClient(); @@ -675,6 +678,7 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { const [proDialogOpen, setProDialogOpen] = useState(false); const [isMinimized, setIsMinimized] = useLocalStorage('sasquatch-minimized', false); + const [hidden, setHidden] = useSasquatchHidden(); const [isVisible, setIsVisible] = useState(false); // Fetched only while the panel is open (owner-only). const aiStatus = useSasquatchAiStatus({ enabled: isOwner && isVisible && !isMinimized }); @@ -1545,6 +1549,11 @@ Instructions: // opt-in auto-show message (e.g. onboarding) — never automatically on / or /lab. const shouldShowPanel = isVisible; + // A proactive tip is waiting (drives the pulse/dot), and the map route runs + // the bubble in a quieter, compact form. + const hasUnread = !!message; + const compact = !!isFullBleed; + // Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes useEffect(() => { const panelOpen = shouldShowPanel && !isMinimized; @@ -1574,10 +1583,12 @@ Instructions: enableChat: true, }; + if (hidden) return null; + return ( <> - {/* ── Minimized bubble — always visible, draggable ── */} - {(!shouldShowPanel || isMinimized) && ( + {/* ── Minimized bubble — visible unless the user hid the assistant ── */} + {!hidden && (!shouldShowPanel || isMinimized) && (
The Sasquatch - - + {/* Pulse only when there's an unread tip, never on the map, and never + under prefers-reduced-motion (the mount animation is the nuisance). */} + {hasUnread && !compact && ( + + )} + {hasUnread && ( + + )} +
)} diff --git a/src/hooks/useSasquatchHidden.ts b/src/hooks/useSasquatchHidden.ts new file mode 100644 index 0000000..a6b7d09 --- /dev/null +++ b/src/hooks/useSasquatchHidden.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect } from 'react'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; + +const KEY = 'sasquatch-hidden'; +const EVENT = 'sasquatch:hiddenchange'; + +/** + * Whether the floating Sasquatch assistant is hidden. Device-local (like the + * other visibility prefs) and synced **within the same tab** via a custom event + * — `useLocalStorage` only syncs across tabs, so the Settings toggle and the + * guide itself need this bridge to stay in step. + */ +export function useSasquatchHidden(): [boolean, (value: boolean) => void] { + const [hidden, setHiddenRaw] = useLocalStorage(KEY, false); + + useEffect(() => { + const handler = (e: Event) => setHiddenRaw(!!(e as CustomEvent).detail); + window.addEventListener(EVENT, handler); + return () => window.removeEventListener(EVENT, handler); + }, [setHiddenRaw]); + + const setHidden = useCallback( + (value: boolean) => { + setHiddenRaw(value); + window.dispatchEvent(new CustomEvent(EVENT, { detail: value })); + }, + [setHiddenRaw], + ); + + return [hidden, setHidden]; +} diff --git a/src/i18n/batch3/sasquatchUi.ts b/src/i18n/batch3/sasquatchUi.ts index 51ef174..ad6b1be 100644 --- a/src/i18n/batch3/sasquatchUi.ts +++ b/src/i18n/batch3/sasquatchUi.ts @@ -29,6 +29,7 @@ const sasquatchUi = { // ── Bubble + header ── bubbleTitle: 'Press and drag to move · Tap to open', openGuideAria: 'Open Sasquatch guide', + hideAria: 'Hide the Sasquatch assistant', activeUntil: 'Active until {{date}}', platformOwner: 'Platform owner', upgradeTitle: 'Upgrade to Sasquatch Pro', @@ -160,6 +161,7 @@ const sasquatchUi = { // ── Bubble + header ── bubbleTitle: 'Pulsa y arrastra para mover · Toca para abrir', openGuideAria: 'Abrir la guía del Sasquatch', + hideAria: 'Ocultar el asistente Sasquatch', activeUntil: 'Activo hasta el {{date}}', platformOwner: 'Propietario de la plataforma', upgradeTitle: 'Mejora a Sasquatch Pro', diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 0da425e..dca6179 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -5072,6 +5072,9 @@ settingsUi: { myStashSub: 'Private gear inventory', relaysSub: 'Nostr relay connections', appearanceSub: 'Language and theme', + assistant: 'Assistant', + showSasquatch: 'Show Sasquatch assistant', + showSasquatchDesc: 'Show the floating Sasquatch button on the map and other screens.', notifications: 'Notifications', notificationsSub: 'Alerts for DMs, crews and zaps', coordinator: 'Coordinator', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index bb69d30..23bc589 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -5072,6 +5072,9 @@ settingsUi: { myStashSub: 'Inventario privado de material', relaysSub: 'Conexiones a relays de Nostr', appearanceSub: 'Idioma y tema', + assistant: 'Asistente', + showSasquatch: 'Mostrar el asistente Sasquatch', + showSasquatchDesc: 'Muestra el botón flotante del Sasquatch en el mapa y otras pantallas.', notifications: 'Notificaciones', notificationsSub: 'Avisos de MD, crews y zaps', coordinator: 'Coordinador', diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index b772eff..64a51c7 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,6 +1,8 @@ import { useSeoMeta } from '@unhead/react'; import { Link, useSearchParams } from 'react-router-dom'; import { EditProfileForm } from '@/components/EditProfileForm'; +import { Switch } from '@/components/ui/switch'; +import { useSasquatchHidden } from '@/hooks/useSasquatchHidden'; import { SkaterDirectoryCard } from '@/components/SkaterDirectoryCard'; import { ProfilePreviewCard } from '@/components/ProfilePreviewCard'; import { RelayListManager } from '@/components/RelayListManager'; @@ -609,6 +611,7 @@ function RelaysScreen({ onBack }: { onBack: () => void }) { function AppearanceScreen({ onBack }: { onBack: () => void }) { const { t } = useTranslation(); + const [sasquatchHidden, setSasquatchHidden] = useSasquatchHidden(); return (
@@ -619,6 +622,20 @@ function AppearanceScreen({ onBack }: { onBack: () => void }) {
+ + {/* Sasquatch assistant — the floating map bubble can be distracting; + let people turn it off and back on. Device-local. */} +
+

{t('settingsUi.settingsPage.assistant')}

+
+
+

{t('settingsUi.settingsPage.showSasquatch')}

+

{t('settingsUi.settingsPage.showSasquatchDesc')}

+
+ setSasquatchHidden(!v)} aria-label={t('settingsUi.settingsPage.showSasquatch')} /> +
+
+
🌙

{t('settingsUi.settingsPage.darkModeOnly')}