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',