diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 4a2fa4d..938f0b7 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1136,7 +1136,6 @@ export function Layout({ children, noPadding }: LayoutProps) { diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 2f881ac..4f69c9c 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -117,8 +117,6 @@ 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 { @@ -661,7 +659,7 @@ function ChatText({ text }: { text: string }) { // ─── Main component ─────────────────────────────────────────────────────────── -export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGuideProps) { +export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { const { user } = useCurrentUser(); const { t, language } = useTranslation(); const cvm = useCVMClient(); @@ -693,6 +691,9 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui // Draggable position state for minimized bubble const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null); const [isDragging, setIsDragging] = useState(false); + // Mirror of dragPos for the pointer-up handler (state may be stale in closures). + const dragPosRef = useRef(dragPos); + useEffect(() => { dragPosRef.current = dragPos; }, [dragPos]); const dragRef = useRef(null); const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null); const dragMoved = useRef(false); @@ -741,6 +742,15 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui dragStart.current = null; dragMoved.current = false; setTimeout(() => setIsDragging(false), 50); + // Drag it off the left edge to hide it — a tiny tab brings it back. + if (wasDrag) { + const x = dragPosRef.current?.x; + if (x != null && x <= 28) { + setHidden(true); + setDragPos(null); + return; + } + } // If this pointer sequence was a genuine tap (no real drag), open here. // A parallel onClick handler also opens as a reliable fallback for mobile // WebViews where the pointer-capture tap can be flaky on first interaction; @@ -748,7 +758,7 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui if (!wasDrag) { openPanel(); } - }, [openPanel]); + }, [openPanel, setHidden, setDragPos]); // Reliable open path: plain click. On mobile WebViews the pointer-capture tap // detection can miss the very first interaction after mount (the "1st tap does @@ -1549,10 +1559,8 @@ 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. + // A proactive tip is waiting (drives the unread dot). const hasUnread = !!message; - const compact = !!isFullBleed; // Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes useEffect(() => { @@ -1583,12 +1591,26 @@ Instructions: enableChat: true, }; - if (hidden) return null; + // Hidden by dragging to the left edge (or via the ✕ / panel Hide button). A + // tiny edge tab brings it back onto the screen. + if (hidden) { + return ( + + ); + } return ( <> - {/* ── Minimized bubble — visible unless the user hid the assistant ── */} - {!hidden && (!shouldShowPanel || isMinimized) && ( + {/* ── Minimized bubble — always visible, draggable ── */} + {(!shouldShowPanel || isMinimized) && (
- {/* Pulse only when there's an unread tip, never on the map, and never - under prefers-reduced-motion (the motion is the nuisance). */} - {hasUnread && !compact && ( - - )} + {/* Pulse like the original — always on, except under + prefers-reduced-motion. */} + {hasUnread && ( )} @@ -1702,15 +1722,6 @@ Instructions: > ⚡ Pro{proDaysLeft !== null ? ` · ${proDaysLeft}d` : ''} - ) : user ? ( - ) : null}
{/* Context + (owner-only) AI balance on one tidy line */} @@ -1765,6 +1776,21 @@ Instructions: {/* Body — scrolls internally so the card never outgrows the viewport */}
+ {/* Go Pro — a clean full-width strip (the inline header pill looked + cramped). Only for signed-in non-Pro users. */} + {!isPro && user && ( + + )} {/* Greeting — only shown before the user starts chatting, so the same wall of welcome text doesn't reappear on every open once a conversation is underway. */} diff --git a/src/i18n/batch3/sasquatchUi.ts b/src/i18n/batch3/sasquatchUi.ts index 9070510..c148c03 100644 --- a/src/i18n/batch3/sasquatchUi.ts +++ b/src/i18n/batch3/sasquatchUi.ts @@ -31,10 +31,12 @@ const sasquatchUi = { openGuideAria: 'Open Sasquatch guide', hideAria: 'Hide the Sasquatch assistant', hide: 'Hide assistant', + showAria: 'Show the Sasquatch assistant', activeUntil: 'Active until {{date}}', platformOwner: 'Platform owner', upgradeTitle: 'Upgrade to Sasquatch Pro', goPro: 'Go Pro', + goProBlurb: 'Personal AI · from 2k sats', aiBalanceTitle: 'Sasquatch AI credit balance (owner-only) — top up at {{provider}}', aiChip: '⚡ AI {{balance}}', minimizeToBubbleAria: 'Minimize to bubble', @@ -164,10 +166,12 @@ const sasquatchUi = { openGuideAria: 'Abrir la guía del Sasquatch', hideAria: 'Ocultar el asistente Sasquatch', hide: 'Ocultar asistente', + showAria: 'Mostrar el asistente Sasquatch', activeUntil: 'Activo hasta el {{date}}', platformOwner: 'Propietario de la plataforma', upgradeTitle: 'Mejora a Sasquatch Pro', goPro: 'Hazte Pro', + goProBlurb: 'IA personal · desde 2k sats', aiBalanceTitle: 'Saldo de crédito de la IA del Sasquatch (solo propietario) — recarga en {{provider}}', aiChip: '⚡ IA {{balance}}', minimizeToBubbleAria: 'Minimizar a la burbuja',