Sasquatch: restore the pulse, hide by dragging left, cleaner Go Pro
- Restore the always-on blink/pulse (still motion-reduce:hidden for a11y). - Drag the bubble off the left edge to hide it; a tiny left-edge tab brings it back. (The ✕/panel/Settings Hide paths remain.) - Replaced the cramped inline "Go Pro" header pill with a clean full-width strip at the top of the panel body.
This commit is contained in:
parent
0e405eaa00
commit
d82ecf4adf
3 changed files with 54 additions and 25 deletions
|
|
@ -1136,7 +1136,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
<LazySasquatchGuide
|
<LazySasquatchGuide
|
||||||
message={currentMessage}
|
message={currentMessage}
|
||||||
onDismiss={dismissCurrentMessage}
|
onDismiss={dismissCurrentMessage}
|
||||||
isFullBleed={isFullBleed}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,6 @@ export interface GuideMessage {
|
||||||
interface SasquatchGuideProps {
|
interface SasquatchGuideProps {
|
||||||
message?: GuideMessage;
|
message?: GuideMessage;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
/** True on full-bleed routes (the map) — the bubble runs compact/no-pulse. */
|
|
||||||
isFullBleed?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
|
|
@ -661,7 +659,7 @@ function ChatText({ text }: { text: string }) {
|
||||||
|
|
||||||
// ─── Main component ───────────────────────────────────────────────────────────
|
// ─── Main component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGuideProps) {
|
export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { t, language } = useTranslation();
|
const { t, language } = useTranslation();
|
||||||
const cvm = useCVMClient();
|
const cvm = useCVMClient();
|
||||||
|
|
@ -693,6 +691,9 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui
|
||||||
// Draggable position state for minimized bubble
|
// Draggable position state for minimized bubble
|
||||||
const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null);
|
const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
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<HTMLDivElement>(null);
|
const dragRef = useRef<HTMLDivElement>(null);
|
||||||
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
|
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
|
||||||
const dragMoved = useRef(false);
|
const dragMoved = useRef(false);
|
||||||
|
|
@ -741,6 +742,15 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui
|
||||||
dragStart.current = null;
|
dragStart.current = null;
|
||||||
dragMoved.current = false;
|
dragMoved.current = false;
|
||||||
setTimeout(() => setIsDragging(false), 50);
|
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.
|
// 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
|
// A parallel onClick handler also opens as a reliable fallback for mobile
|
||||||
// WebViews where the pointer-capture tap can be flaky on first interaction;
|
// 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) {
|
if (!wasDrag) {
|
||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
}, [openPanel]);
|
}, [openPanel, setHidden, setDragPos]);
|
||||||
|
|
||||||
// Reliable open path: plain click. On mobile WebViews the pointer-capture tap
|
// 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
|
// 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.
|
// opt-in auto-show message (e.g. onboarding) — never automatically on / or /lab.
|
||||||
const shouldShowPanel = isVisible;
|
const shouldShowPanel = isVisible;
|
||||||
|
|
||||||
// A proactive tip is waiting (drives the pulse/dot), and the map route runs
|
// A proactive tip is waiting (drives the unread dot).
|
||||||
// the bubble in a quieter, compact form.
|
|
||||||
const hasUnread = !!message;
|
const hasUnread = !!message;
|
||||||
const compact = !!isFullBleed;
|
|
||||||
|
|
||||||
// Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes
|
// Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1583,12 +1591,26 @@ Instructions:
|
||||||
enableChat: true,
|
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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t('sasquatchUi.showAria')}
|
||||||
|
title={t('sasquatchUi.showAria')}
|
||||||
|
onClick={() => setHidden(false)}
|
||||||
|
className="fixed left-0 top-1/2 z-50 -translate-y-1/2 rounded-r-full border border-l-0 border-[#ccff00]/40 bg-black/70 py-3 pl-0.5 pr-1.5 transition-colors hover:bg-black/90"
|
||||||
|
>
|
||||||
|
<span className="block h-5 w-1.5 rounded-full bg-[#ccff00]/70" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ── Minimized bubble — visible unless the user hid the assistant ── */}
|
{/* ── Minimized bubble — always visible, draggable ── */}
|
||||||
{!hidden && (!shouldShowPanel || isMinimized) && (
|
{(!shouldShowPanel || isMinimized) && (
|
||||||
<div
|
<div
|
||||||
ref={dragRef}
|
ref={dragRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -1621,11 +1643,9 @@ Instructions:
|
||||||
alt="The Sasquatch"
|
alt="The Sasquatch"
|
||||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover pointer-events-none"
|
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover pointer-events-none"
|
||||||
/>
|
/>
|
||||||
{/* Pulse only when there's an unread tip, never on the map, and never
|
{/* Pulse like the original — always on, except under
|
||||||
under prefers-reduced-motion (the motion is the nuisance). */}
|
prefers-reduced-motion. */}
|
||||||
{hasUnread && !compact && (
|
|
||||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00] motion-reduce:hidden" />
|
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00] motion-reduce:hidden" />
|
||||||
)}
|
|
||||||
{hasUnread && (
|
{hasUnread && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -1702,15 +1722,6 @@ Instructions:
|
||||||
>
|
>
|
||||||
⚡ Pro{proDaysLeft !== null ? ` · ${proDaysLeft}d` : ''}
|
⚡ Pro{proDaysLeft !== null ? ` · ${proDaysLeft}d` : ''}
|
||||||
</span>
|
</span>
|
||||||
) : user ? (
|
|
||||||
<button
|
|
||||||
onClick={() => setProDialogOpen(true)}
|
|
||||||
className="ml-0.5 inline-flex items-center gap-1 rounded-full border border-[#ccff00]/40 bg-[#ccff00]/10 px-2 py-0.5 text-[9px] font-black uppercase tracking-wider text-[#ccff00] transition-colors hover:border-[#ccff00]/70 hover:bg-[#ccff00]/20"
|
|
||||||
title={t('sasquatchUi.upgradeTitle')}
|
|
||||||
>
|
|
||||||
<span aria-hidden>⚡</span>
|
|
||||||
{t('sasquatchUi.goPro')}
|
|
||||||
</button>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{/* Context + (owner-only) AI balance on one tidy line */}
|
{/* Context + (owner-only) AI balance on one tidy line */}
|
||||||
|
|
@ -1765,6 +1776,21 @@ Instructions:
|
||||||
|
|
||||||
{/* Body — scrolls internally so the card never outgrows the viewport */}
|
{/* Body — scrolls internally so the card never outgrows the viewport */}
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
|
||||||
|
{/* Go Pro — a clean full-width strip (the inline header pill looked
|
||||||
|
cramped). Only for signed-in non-Pro users. */}
|
||||||
|
{!isPro && user && (
|
||||||
|
<button
|
||||||
|
onClick={() => setProDialogOpen(true)}
|
||||||
|
className="mb-3 flex w-full items-center gap-2 rounded-xl border border-[#ccff00]/30 bg-[#ccff00]/10 px-3 py-2 text-left transition-colors hover:border-[#ccff00]/60 hover:bg-[#ccff00]/15"
|
||||||
|
title={t('sasquatchUi.upgradeTitle')}
|
||||||
|
>
|
||||||
|
<span aria-hidden className="text-sm">⚡</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block text-[11px] font-black uppercase tracking-wider text-[#ccff00]">{t('sasquatchUi.goPro')}</span>
|
||||||
|
<span className="block truncate text-[10px] text-[#ccff00]/60">{t('sasquatchUi.goProBlurb')}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Greeting — only shown before the user starts chatting, so the
|
{/* Greeting — only shown before the user starts chatting, so the
|
||||||
same wall of welcome text doesn't reappear on every open once
|
same wall of welcome text doesn't reappear on every open once
|
||||||
a conversation is underway. */}
|
a conversation is underway. */}
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,12 @@ const sasquatchUi = {
|
||||||
openGuideAria: 'Open Sasquatch guide',
|
openGuideAria: 'Open Sasquatch guide',
|
||||||
hideAria: 'Hide the Sasquatch assistant',
|
hideAria: 'Hide the Sasquatch assistant',
|
||||||
hide: 'Hide assistant',
|
hide: 'Hide assistant',
|
||||||
|
showAria: 'Show the Sasquatch assistant',
|
||||||
activeUntil: 'Active until {{date}}',
|
activeUntil: 'Active until {{date}}',
|
||||||
platformOwner: 'Platform owner',
|
platformOwner: 'Platform owner',
|
||||||
upgradeTitle: 'Upgrade to Sasquatch Pro',
|
upgradeTitle: 'Upgrade to Sasquatch Pro',
|
||||||
goPro: 'Go Pro',
|
goPro: 'Go Pro',
|
||||||
|
goProBlurb: 'Personal AI · from 2k sats',
|
||||||
aiBalanceTitle: 'Sasquatch AI credit balance (owner-only) — top up at {{provider}}',
|
aiBalanceTitle: 'Sasquatch AI credit balance (owner-only) — top up at {{provider}}',
|
||||||
aiChip: '⚡ AI {{balance}}',
|
aiChip: '⚡ AI {{balance}}',
|
||||||
minimizeToBubbleAria: 'Minimize to bubble',
|
minimizeToBubbleAria: 'Minimize to bubble',
|
||||||
|
|
@ -164,10 +166,12 @@ const sasquatchUi = {
|
||||||
openGuideAria: 'Abrir la guía del Sasquatch',
|
openGuideAria: 'Abrir la guía del Sasquatch',
|
||||||
hideAria: 'Ocultar el asistente Sasquatch',
|
hideAria: 'Ocultar el asistente Sasquatch',
|
||||||
hide: 'Ocultar asistente',
|
hide: 'Ocultar asistente',
|
||||||
|
showAria: 'Mostrar el asistente Sasquatch',
|
||||||
activeUntil: 'Activo hasta el {{date}}',
|
activeUntil: 'Activo hasta el {{date}}',
|
||||||
platformOwner: 'Propietario de la plataforma',
|
platformOwner: 'Propietario de la plataforma',
|
||||||
upgradeTitle: 'Mejora a Sasquatch Pro',
|
upgradeTitle: 'Mejora a Sasquatch Pro',
|
||||||
goPro: 'Hazte 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}}',
|
aiBalanceTitle: 'Saldo de crédito de la IA del Sasquatch (solo propietario) — recarga en {{provider}}',
|
||||||
aiChip: '⚡ IA {{balance}}',
|
aiChip: '⚡ IA {{balance}}',
|
||||||
minimizeToBubbleAria: 'Minimizar a la burbuja',
|
minimizeToBubbleAria: 'Minimizar a la burbuja',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue