feat: add InfoPopup with landing page content on first visit

Shows an informational popup with rich landing page content on first
visit, re-openable via a fixed info button (bottom-right corner).
Dismissible via X button, overlay click, or Escape key. Uses
localStorage to track first-visit state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-16 04:51:34 +00:00
parent ce05a51960
commit 77302e1a6e
3 changed files with 798 additions and 0 deletions

View File

@ -5,6 +5,8 @@ import { AuthProvider } from '@/components/AuthProvider'
import { PWAInstall } from '@/components/PWAInstall'
import { SubdomainSession } from '@/components/SubdomainSession'
import { Header } from '@/components/Header'
import InfoPopup from "@/components/InfoPopup"
import { LANDING_HTML } from "@/components/landing-content"
const inter = Inter({
subsets: ['latin'],
@ -53,6 +55,7 @@ export default function RootLayout({
{children}
<PWAInstall />
</AuthProvider>
<InfoPopup appName="rNotes" appIcon="📝" landingHtml={LANDING_HTML} />
</body>
</html>
)

View File

@ -0,0 +1,366 @@
"use client";
import { useState, useEffect, useCallback } from "react";
interface InfoPopupProps {
appName: string;
appIcon: string;
landingHtml: string;
storageKey?: string;
}
const CSS = `
/* ── CSS Variables (dark theme defaults) ── */
.info-popup-scope {
--rs-bg-surface: #16213e;
--rs-bg-page: #1a1a2e;
--rs-bg-hover: rgba(255,255,255,0.04);
--rs-primary: #e94560;
--rs-accent: #14b8a6;
--rs-text-primary: #eee;
--rs-text-secondary: #b0b0c0;
--rs-text-muted: #6b7280;
--rs-border: rgba(255,255,255,0.08);
--rs-border-subtle: rgba(255,255,255,0.05);
--rs-card-bg: rgba(255,255,255,0.03);
--rs-card-border: rgba(255,255,255,0.06);
--rs-gradient-brand: linear-gradient(135deg, #14b8a6, #22d3ee);
--rs-gradient-cta: linear-gradient(135deg, #14b8a6, #06b6d4);
--rs-btn-secondary-bg: rgba(255,255,255,0.06);
--rs-border-strong: rgba(255,255,255,0.15);
--rs-bg-surface-raised: rgba(255,255,255,0.06);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* ── Info button (fixed, bottom-right) ── */
.info-popup-trigger {
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
width: 44px; height: 44px; border-radius: 50%;
background: linear-gradient(135deg, rgba(20,184,166,0.15), rgba(79,70,229,0.1));
border: 1px solid rgba(20,184,166,0.3);
color: #14b8a6; font-size: 1.2rem; font-weight: 700;
cursor: pointer; display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
}
.info-popup-trigger:hover {
transform: scale(1.08);
box-shadow: 0 6px 28px rgba(0,0,0,0.4);
border-color: rgba(20,184,166,0.5);
}
/* ── Backdrop overlay ── */
.rapp-info-overlay {
position: fixed; inset: 0; z-index: 10000;
background: rgba(0,0,0,0.45);
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
animation: rapp-overlay-in 0.2s ease-out;
}
@keyframes rapp-overlay-in { from { opacity: 0; } to { opacity: 1; } }
/* ── Panel — centered modal ── */
.rapp-info-panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
z-index: 10001;
width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 64px);
background: var(--rs-bg-surface);
border: 1px solid rgba(20,184,166,0.25);
border-radius: 16px;
box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(20,184,166,0.1), 0 0 60px rgba(20,184,166,0.06);
display: flex; flex-direction: column; overflow: hidden;
animation: rapp-info-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes rapp-info-in {
from { opacity: 0; transform: translate(-50%, -48%) scale(0.95); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
/* ── Header with icon + name ── */
.rapp-info-panel__header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px;
background: linear-gradient(135deg, rgba(20,184,166,0.12), rgba(79,70,229,0.08));
border-bottom: 1px solid rgba(20,184,166,0.18);
}
.rapp-info-panel__header-left { display: flex; align-items: center; gap: 10px; }
.rapp-info-panel__icon { font-size: 1.5rem; line-height: 1; }
.rapp-info-panel__title {
font-size: 1.05rem; font-weight: 700; letter-spacing: 0.01em;
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.rapp-info-panel__close {
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
color: var(--rs-text-muted); font-size: 1.2rem; cursor: pointer;
border-radius: 8px; line-height: 1;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-info-panel__close:hover {
color: var(--rs-text-primary); background: rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.15);
}
/* ── Body ── */
.rapp-info-panel__body {
padding: 0; overflow-y: auto; flex: 1;
color: var(--rs-text-secondary); font-size: 0.92rem; line-height: 1.65;
}
.rapp-info-panel__body h1, .rapp-info-panel__body h2, .rapp-info-panel__body h3 {
color: var(--rs-text-primary); margin: 0 0 8px;
}
.rapp-info-panel__body h1 { font-size: 1.3rem; }
.rapp-info-panel__body h2 { font-size: 1.1rem; }
.rapp-info-panel__body h3 { font-size: 0.95rem; }
.rapp-info-panel__body p { margin: 0 0 10px; }
.rapp-info-panel__body a { color: var(--rs-primary); }
/* ── Panel-scoped overrides for rich landing content ── */
.rapp-info-panel__body .rl-hero { padding: 1.75rem 1.5rem 1.25rem; text-align: center; }
.rapp-info-panel__body .rl-hero .rl-heading { font-size: 1.5rem !important; }
.rapp-info-panel__body .rl-heading { font-size: 1.2rem; margin-bottom: 0.5rem; }
.rapp-info-panel__body .rl-tagline { font-size: 0.65rem; padding: 0.3rem 0.85rem; margin-bottom: 1rem; }
.rapp-info-panel__body .rl-subtitle { font-size: 1rem !important; margin-bottom: 0.75rem; }
.rapp-info-panel__body .rl-subtext { font-size: 0.9rem !important; margin-bottom: 1.25rem; line-height: 1.7; }
.rapp-info-panel__body .rl-hero .rl-subtext { font-size: 0.92rem !important; }
.rapp-info-panel__body .rl-section { padding: 1.5rem 1.25rem; border-top: 1px solid var(--rs-border-subtle); }
.rapp-info-panel__body .rl-section--alt { background: rgba(20,184,166,0.03); }
.rapp-info-panel__body .rl-container { max-width: 100%; }
.rapp-info-panel__body .rl-grid-2,
.rapp-info-panel__body .rl-grid-3,
.rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr 1fr !important; gap: 0.75rem; }
.rapp-info-panel__body .rl-card { padding: 1.15rem; border-radius: 0.75rem; }
.rapp-info-panel__body .rl-card h3 { font-size: 0.88rem; margin-bottom: 0.35rem; }
.rapp-info-panel__body .rl-card p { font-size: 0.82rem; line-height: 1.55; margin-bottom: 0; }
.rapp-info-panel__body .rl-icon-box { width: 2.5rem; height: 2.5rem; font-size: 1.25rem; border-radius: 0.6rem; margin-bottom: 0.65rem; }
.rapp-info-panel__body .rl-card--center .rl-icon-box { margin: 0 auto 0.65rem; }
.rapp-info-panel__body .rl-step__num { width: 2.25rem; height: 2.25rem; font-size: 0.75rem; margin-bottom: 0.5rem; }
.rapp-info-panel__body .rl-step h3 { font-size: 0.88rem; }
.rapp-info-panel__body .rl-step p { font-size: 0.82rem; }
.rapp-info-panel__body .rl-cta-row { margin-top: 1.5rem; gap: 0.625rem; display: flex; flex-wrap: wrap; justify-content: center; }
.rapp-info-panel__body .rl-cta-primary {
padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
border-radius: 10px; text-decoration: none;
background: var(--rs-gradient-cta, linear-gradient(135deg, #14b8a6, #06b6d4));
color: #fff; box-shadow: 0 4px 16px rgba(20,184,166,0.3);
transition: transform 0.15s, box-shadow 0.15s;
}
.rapp-info-panel__body .rl-cta-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(20,184,166,0.4); }
.rapp-info-panel__body .rl-cta-secondary {
padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
border-radius: 10px; text-decoration: none;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
color: var(--rs-text-secondary); transition: transform 0.15s, border-color 0.15s, color 0.15s, background 0.15s;
}
.rapp-info-panel__body .rl-cta-secondary:hover { transform: translateY(-1px); border-color: rgba(20,184,166,0.4); color: var(--rs-text-primary); background: rgba(20,184,166,0.08); }
.rapp-info-panel__body a[onclick*="startTour"],
.rapp-info-panel__body a[href*="tour"] { display: none; }
.rapp-info-panel__body .rl-badge { font-size: 0.65rem; padding: 0.15rem 0.5rem; }
.rapp-info-panel__body .rl-integration { padding: 1rem; border-radius: 0.75rem; gap: 0.75rem; }
.rapp-info-panel__body .rl-integration h3 { font-size: 0.88rem; }
.rapp-info-panel__body .rl-integration p { font-size: 0.82rem; }
.rapp-info-panel__body .rl-check-list li { font-size: 0.82rem; padding: 0.3rem 0; }
.rapp-info-panel__body .rl-divider { margin: 1rem 0; }
.rapp-info-panel__body .rl-divider span { font-size: 0.65rem; }
.rapp-info-panel__body .rl-back { padding: 1.25rem 0 1.5rem; }
.rapp-info-panel__body .rl-back a { font-size: 0.82rem; }
.rapp-info-panel__body::-webkit-scrollbar { width: 5px; }
.rapp-info-panel__body::-webkit-scrollbar-track { background: transparent; }
.rapp-info-panel__body::-webkit-scrollbar-thumb { background: rgba(20,184,166,0.2); border-radius: 9999px; }
.rapp-info-panel__body::-webkit-scrollbar-thumb:hover { background: rgba(20,184,166,0.35); }
/* ── Rich Landing CSS ── */
.rl-section { border-top: 1px solid var(--rs-border-subtle); padding: 4rem 1.5rem; }
.rl-section--alt { background: var(--rs-bg-hover); }
.rl-container { max-width: 1100px; margin: 0 auto; }
.rl-hero { text-align: center; padding: 5rem 1.5rem 3rem; max-width: 820px; margin: 0 auto; }
.rl-tagline {
display: inline-block; font-size: 0.7rem; font-weight: 700;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--rs-accent); background: rgba(20,184,166,0.1);
border: 1px solid rgba(20,184,166,0.2);
padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem;
}
.rl-heading {
font-size: 2rem; font-weight: 700; line-height: 1.15;
margin-bottom: 0.75rem; letter-spacing: -0.01em;
background: var(--rs-gradient-brand);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.rl-hero .rl-heading { font-size: 2.5rem; }
.rl-subtitle { font-size: 1.25rem; font-weight: 500; color: var(--rs-text-primary); margin-bottom: 1rem; }
.rl-hero .rl-subtitle { font-size: 1.35rem; }
.rl-subtext { font-size: 1.05rem; color: var(--rs-text-secondary); line-height: 1.65; max-width: 640px; margin: 0 auto 2rem; }
.rl-hero .rl-subtext { font-size: 1.15rem; }
.rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 640px) {
.rl-grid-2 { grid-template-columns: repeat(2, 1fr); }
.rl-grid-3 { grid-template-columns: repeat(3, 1fr); }
.rl-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) { .rl-grid-4 { grid-template-columns: repeat(4, 1fr); } }
.rl-card {
background: var(--rs-card-bg); border: 1px solid var(--rs-card-border);
border-radius: 1rem; padding: 1.75rem; transition: border-color 0.2s;
}
.rl-card:hover { border-color: rgba(20,184,166,0.3); }
.rl-card h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem; }
.rl-card p { font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6; }
.rl-card--center { text-align: center; }
.rl-step { display: flex; flex-direction: column; align-items: center; text-align: center; }
.rl-step__num {
width: 2.5rem; height: 2.5rem; border-radius: 9999px;
background: rgba(20,184,166,0.1); color: var(--rs-accent);
display: flex; align-items: center; justify-content: center;
font-size: 0.8rem; font-weight: 700; margin-bottom: 0.75rem;
}
.rl-step h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.25rem; }
.rl-step p { font-size: 0.82rem; color: var(--rs-text-secondary); line-height: 1.55; }
.rl-cta-row { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
.rl-cta-primary {
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
background: var(--rs-gradient-cta); color: white; font-size: 0.95rem; font-weight: 600;
text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
}
.rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.rl-cta-secondary {
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
color: var(--rs-text-secondary); font-size: 0.95rem; font-weight: 600;
text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
}
.rl-cta-secondary:hover { transform: translateY(-2px); border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
.rl-check-list { list-style: none; padding: 0; margin: 0; }
.rl-check-list li {
display: flex; align-items: flex-start; gap: 0.5rem;
font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.55; padding: 0.35rem 0;
}
.rl-check-list li::before { content: "\\2713"; color: var(--rs-accent); font-weight: 700; flex-shrink: 0; margin-top: 0.05em; }
.rl-check-list li strong { color: var(--rs-text-primary); font-weight: 600; }
.rl-badge {
display: inline-block; font-size: 0.65rem; font-weight: 700;
color: white; background: var(--rs-accent);
padding: 0.15rem 0.5rem; border-radius: 9999px;
}
.rl-divider { display: flex; align-items: center; gap: 0.75rem; margin: 1.5rem 0; }
.rl-divider::before, .rl-divider::after { content: ""; flex: 1; height: 1px; background: var(--rs-border-subtle); }
.rl-divider span { font-size: 0.75rem; color: var(--rs-text-muted); white-space: nowrap; }
.rl-icon-box {
width: 3rem; height: 3rem; border-radius: 0.75rem;
background: rgba(20,184,166,0.12); color: var(--rs-accent);
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; margin-bottom: 1rem;
}
.rl-card--center .rl-icon-box { margin: 0 auto 1rem; }
.rl-integration {
display: flex; align-items: flex-start; gap: 1rem;
background: rgba(20,184,166,0.04); border: 1px solid rgba(20,184,166,0.15);
border-radius: 1rem; padding: 1.5rem;
}
.rl-integration h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.35rem; }
.rl-integration p { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.55; }
.rl-back { padding: 2rem 0 3rem; text-align: center; }
.rl-back a { font-size: 0.85rem; color: var(--rs-text-muted); text-decoration: none; transition: color 0.2s; }
.rl-back a:hover { color: var(--rs-text-primary); }
.rl-progress { height: 0.5rem; border-radius: 9999px; background: var(--rs-border-subtle); overflow: hidden; }
.rl-progress__fill { height: 100%; border-radius: 9999px; background: var(--rs-accent); }
.rl-tier { display: flex; gap: 0.5rem; margin: 1rem 0; }
.rl-tier__item { flex: 1; text-align: center; border-radius: 0.5rem; border: 1px solid var(--rs-border-subtle); padding: 0.5rem; font-size: 0.75rem; }
.rl-tier__item--active { border-color: rgba(20,184,166,0.4); background: rgba(20,184,166,0.05); color: var(--rs-accent); }
.rl-tier__item--active strong { color: var(--rs-accent); }
.rl-zoom-bar { display: flex; flex-direction: column; gap: 0.5rem; }
.rl-zoom-bar__row { display: flex; align-items: center; gap: 0.75rem; }
.rl-zoom-bar__label { font-size: 0.7rem; color: var(--rs-text-muted); width: 1.2rem; text-align: right; font-family: monospace; }
.rl-zoom-bar__bar { height: 1.5rem; border-radius: 0.375rem; background: rgba(99,102,241,0.15); display: flex; align-items: center; padding: 0 0.75rem; }
.rl-zoom-bar__name { font-size: 0.75rem; font-weight: 600; color: var(--rs-text-primary); white-space: nowrap; }
.rl-zoom-bar__span { font-size: 0.6rem; color: var(--rs-text-muted); margin-left: auto; white-space: nowrap; }
@media (max-width: 600px) {
.rapp-info-panel {
top: auto; left: 8px; right: 8px; bottom: 8px;
transform: none; width: auto; max-height: 85vh;
animation: rapp-info-in-mobile 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes rapp-info-in-mobile {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.rapp-info-panel__body .rl-grid-2,
.rapp-info-panel__body .rl-grid-3,
.rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr !important; }
.rl-hero { padding: 3rem 1rem 2rem; }
.rl-hero .rl-heading { font-size: 2rem; }
.rl-section { padding: 2.5rem 1rem; }
}
`;
export default function InfoPopup({ appName, appIcon, landingHtml, storageKey }: InfoPopupProps) {
const key = storageKey || `${appName}-info-seen`;
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
try {
if (!localStorage.getItem(key)) {
setTimeout(() => setOpen(true), 800);
localStorage.setItem(key, "1");
}
} catch {}
}, [key]);
const close = useCallback(() => setOpen(false), []);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [open, close]);
if (!mounted) return null;
return (
<div className="info-popup-scope">
<style dangerouslySetInnerHTML={{ __html: CSS }} />
{/* Fixed info button */}
<button
className="info-popup-trigger"
onClick={() => setOpen(true)}
aria-label={`About ${appName}`}
title={`About ${appName}`}
>
&#9432;
</button>
{/* Modal */}
{open && (
<>
<div className="rapp-info-overlay" onClick={close} />
<div className="rapp-info-panel">
<div className="rapp-info-panel__header">
<div className="rapp-info-panel__header-left">
<span className="rapp-info-panel__icon">{appIcon}</span>
<span className="rapp-info-panel__title">{appName}</span>
</div>
<button className="rapp-info-panel__close" onClick={close} aria-label="Close">
&times;
</button>
</div>
<div
className="rapp-info-panel__body"
dangerouslySetInnerHTML={{ __html: landingHtml }}
/>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,429 @@
export const LANDING_HTML = `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rNotes</span>
<h1 class="rl-heading" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">(You)rNotes, your thoughts unbound.</h1>
<p class="rl-subtitle" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">Capture Everything, Find Anything, and Share your Insights</p>
<p class="rl-subtext">
Notes, clips, voice recordings, and live transcription &mdash; all in one place.
Speak and watch your words appear in real time, or drop in audio and video files to transcribe offline.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary" id="ml-primary">Open Notebook</a>
<a href="#transcription-demo" class="rl-cta-secondary">Transcribe</a>
<a href="#extension-download" class="rl-cta-secondary">Get Extension</a>
</div>
<p style="font-size:0.82rem;margin-top:0.5rem">
<a href="#" onclick="document.querySelector('folk-notes-app')?.startTour?.();window.__rspaceHideInfo?.();return false" style="color:var(--rs-primary,#06b6d4);text-decoration:none">
Start Guided Tour &rarr;
</a>
</p>
</div>
<!-- Live Transcription Demo -->
<section class="rl-section" id="transcription-demo">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Live Transcription Demo</h2>
<p class="rl-subtext" style="text-align:center">Try it right here &mdash; click the mic and start speaking.</p>
<div style="max-width:640px;margin:2rem auto;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:1rem;padding:1.5rem;position:relative">
<!-- Unsupported fallback (hidden by default, shown via JS) -->
<div id="transcription-unsupported" style="display:none;text-align:center;padding:2rem 1rem;color:#94a3b8">
<div style="font-size:2rem;margin-bottom:0.75rem">&#9888;&#65039;</div>
<p style="margin:0 0 0.5rem">Live transcription requires <strong>Chrome</strong> or <strong>Edge</strong> (Web Speech API).</p>
<p style="margin:0;font-size:0.85rem;color:#64748b">Try opening this page in a Chromium-based browser to test the demo.</p>
</div>
<!-- Demo UI (hidden if unsupported) -->
<div id="transcription-ui">
<!-- Controls -->
<div style="display:flex;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.25rem">
<button id="mic-btn" style="width:56px;height:56px;border-radius:50%;border:2px solid rgba(245,158,11,0.4);background:rgba(245,158,11,0.1);color:#f59e0b;font-size:1.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s" title="Start transcription">
&#127908;
</button>
<div style="text-align:left">
<div id="mic-status" style="font-size:0.9rem;color:#94a3b8">Click mic to start</div>
<div id="mic-timer" style="font-size:0.75rem;color:#64748b;font-variant-numeric:tabular-nums">00:00</div>
</div>
<div id="live-indicator" style="display:none;background:rgba(239,68,68,0.15);color:#ef4444;font-size:0.7rem;font-weight:600;padding:0.2rem 0.6rem;border-radius:9999px;text-transform:uppercase;letter-spacing:0.05em">
&#9679; Live
</div>
</div>
<!-- Transcript area -->
<div id="transcript-area" style="min-height:120px;max-height:240px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:0.5rem;padding:1rem;font-size:0.9rem;line-height:1.6;color:#e2e8f0">
<span style="color:#64748b;font-style:italic">Your transcript will appear here&hellip;</span>
</div>
</div>
<!-- Capability badges -->
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;justify-content:center;margin-top:1.25rem">
<span class="rl-badge" style="background:rgba(34,197,94,0.15);color:#22c55e">&#9679; Live streaming</span>
<span class="rl-badge" style="background:rgba(59,130,246,0.15);color:#3b82f6">&#127925; Audio file upload</span>
<span class="rl-badge" style="background:rgba(168,85,247,0.15);color:#a855f7">&#127909; Video transcription</span>
<span class="rl-badge" style="background:rgba(245,158,11,0.15);color:#f59e0b">&#128268; Offline (Parakeet.js)</span>
</div>
</div>
</div>
</section>
<!-- Transcription Demo Script -->
<script>
(function() {
var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
var ui = document.getElementById('transcription-ui');
var unsupported = document.getElementById('transcription-unsupported');
if (!SpeechRecognition) {
if (ui) ui.style.display = 'none';
if (unsupported) unsupported.style.display = 'block';
return;
}
var recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
var micBtn = document.getElementById('mic-btn');
var micStatus = document.getElementById('mic-status');
var micTimer = document.getElementById('mic-timer');
var liveIndicator = document.getElementById('live-indicator');
var transcriptArea = document.getElementById('transcript-area');
var isListening = false;
var timerInterval = null;
var seconds = 0;
var finalTranscript = '';
function formatTime(s) {
var m = Math.floor(s / 60);
var sec = s % 60;
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
}
function startTimer() {
seconds = 0;
micTimer.textContent = '00:00';
timerInterval = setInterval(function() {
seconds++;
micTimer.textContent = formatTime(seconds);
}, 1000);
}
function stopTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
}
micBtn.addEventListener('click', function() {
if (!isListening) {
finalTranscript = '';
transcriptArea.innerHTML = '';
recognition.start();
} else {
recognition.stop();
}
});
recognition.onstart = function() {
isListening = true;
micBtn.style.background = 'rgba(239,68,68,0.2)';
micBtn.style.borderColor = '#ef4444';
micBtn.style.color = '#ef4444';
micBtn.title = 'Stop transcription';
micStatus.textContent = 'Listening...';
micStatus.style.color = '#ef4444';
liveIndicator.style.display = 'block';
startTimer();
};
recognition.onend = function() {
isListening = false;
micBtn.style.background = 'rgba(245,158,11,0.1)';
micBtn.style.borderColor = 'rgba(245,158,11,0.4)';
micBtn.style.color = '#f59e0b';
micBtn.title = 'Start transcription';
micStatus.textContent = 'Click mic to start';
micStatus.style.color = '#94a3b8';
liveIndicator.style.display = 'none';
stopTimer();
};
recognition.onresult = function(event) {
var interim = '';
for (var i = event.resultIndex; i < event.results.length; i++) {
var transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript + ' ';
} else {
interim += transcript;
}
}
transcriptArea.innerHTML = finalTranscript +
(interim ? '<span style="color:#94a3b8">' + interim + '</span>' : '');
transcriptArea.scrollTop = transcriptArea.scrollHeight;
};
recognition.onerror = function(event) {
if (event.error === 'not-allowed') {
micStatus.textContent = 'Microphone access denied';
micStatus.style.color = '#ef4444';
}
};
})();
</script>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">What rNotes Handles</h2>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128221;</div>
<h3>Rich Text Notes</h3>
<p>Write with a full TipTap editor &mdash; formatting, code blocks, checklists, and embeds. Dual-format storage keeps Markdown portable.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127908;</div>
<h3>Voice &amp; Transcription</h3>
<p>Record voice notes with live transcription via Web Speech API. Drop in audio or video files and get full transcripts &mdash; all in the browser.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127991;</div>
<h3>Tagging &amp; Organization</h3>
<p>Tag freely, organize into notebooks, and search everything. Filtered views surface the right cards at the right time.</p>
</div>
</div>
</div>
</section>
<!-- Chrome Extension -->
<section class="rl-section rl-section--alt" id="extension-download">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Chrome Extension</h2>
<p class="rl-subtext" style="text-align:center">Clip pages, record voice notes, and transcribe &mdash; right from the toolbar.</p>
<div class="rl-grid-2" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.5rem;max-width:860px;margin:2rem auto">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128203;</div>
<h3>Web Clipper</h3>
<p>Save any page as a note with one click &mdash; article text, selection, or full HTML.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127908;</div>
<h3>Voice Recording</h3>
<p>Press <kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.8rem">Ctrl+Shift+V</kbd> to start recording and transcribing from any tab.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128275;</div>
<h3>Article Unlock</h3>
<p>Bypass soft paywalls by fetching archived versions &mdash; read the article, then save it to your notebook.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128268;</div>
<h3>Offline Transcription</h3>
<p>Parakeet.js runs entirely in-browser &mdash; your audio never leaves the device.</p>
</div>
</div>
<div style="text-align:center;margin-top:1.5rem">
<a href="/rnotes/extension/download" class="rl-cta-primary" style="display:inline-flex;align-items:center;gap:0.5rem">
&#11015; Download Extension
</a>
<p style="margin-top:0.75rem;font-size:0.8rem;color:#64748b">
Unzip, then load unpacked at <code style="font-size:0.75rem;color:#94a3b8">chrome://extensions</code>
</p>
</div>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-4" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Live Transcribe</h3>
<p>Speak and watch words appear in real time via the Web Speech API. No uploads, no waiting.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Audio &amp; Video</h3>
<p>Drop files and get full transcripts via Parakeet, running entirely in-browser. Supports MP3, WAV, MP4, and more.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Notebooks &amp; Tags</h3>
<p>Organize transcripts alongside notes, clips, code, and files. Tag, search, and filter across everything.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">4</div>
<h3>Private &amp; Offline</h3>
<p>Parakeet.js runs in-browser &mdash; audio never leaves your device. Works offline once the model is cached.</p>
</div>
</div>
</div>
</section>
<!-- Memory Cards -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Memory Cards</h2>
<p class="rl-subtext" style="text-align:center">
Every note is a Memory Card &mdash; a typed, structured unit of knowledge with hierarchy,
properties, and attachments. Designed for round-trip interoperability with Logseq.
</p>
<div class="rl-grid-3">
<!-- 7 Card Types -->
<div class="rl-card">
<div class="rl-icon-box">&#127991;</div>
<h3>7 Card Types</h3>
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-bottom:0.75rem">
<span class="rl-badge" style="background:rgba(245,158,11,0.2);color:#f59e0b">note</span>
<span class="rl-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">link</span>
<span class="rl-badge" style="background:rgba(34,197,94,0.2);color:#22c55e">task</span>
<span class="rl-badge" style="background:rgba(234,179,8,0.2);color:#eab308">idea</span>
<span class="rl-badge" style="background:rgba(168,85,247,0.2);color:#a855f7">person</span>
<span class="rl-badge" style="background:rgba(236,72,153,0.2);color:#ec4899">reference</span>
<span class="rl-badge" style="background:rgba(100,116,139,0.2);color:#94a3b8">file</span>
</div>
<p>Each card type has distinct styling and behavior. Typed notes surface in filtered views and canvas visualizations.</p>
</div>
<!-- Hierarchy & Properties -->
<div class="rl-card">
<div class="rl-icon-box">&#128194;</div>
<h3>Hierarchy &amp; Properties</h3>
<p>
Nest cards under parents to build knowledge trees. Add structured
<code style="font-size:0.8rem;color:rgba(245,158,11,0.8)">key:: value</code>
properties &mdash; compatible with Logseq's property syntax.
</p>
<div style="margin-top:0.5rem;font-family:monospace;font-size:0.75rem;color:#64748b;line-height:1.7">
<div><span style="color:#94a3b8">type::</span> idea</div>
<div><span style="color:#94a3b8">status::</span> doing</div>
<div><span style="color:#94a3b8">tags::</span> #research, #web3</div>
</div>
</div>
<!-- Data Source Integrations -->
<div class="rl-card">
<div class="rl-icon-box">&#128260;</div>
<h3>Import &amp; Export</h3>
<p>
Bring your notes from <strong>Logseq</strong>, <strong>Obsidian</strong>,
<strong>Notion</strong>, and <strong>Google Docs</strong>.
Export back to any format anytime &mdash; your data, your choice.
</p>
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.5rem">
<span class="rl-badge" style="background:rgba(34,197,94,0.2);color:#22c55e">Logseq</span>
<span class="rl-badge" style="background:rgba(139,92,246,0.2);color:#8b5cf6">Obsidian</span>
<span class="rl-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">Notion</span>
<span class="rl-badge" style="background:rgba(245,158,11,0.2);color:#f59e0b">Google Docs</span>
</div>
</div>
<!-- Dual Format Storage -->
<div class="rl-card">
<div class="rl-icon-box">&#128196;</div>
<h3>Dual Format Storage</h3>
<p>
Every card stores rich TipTap JSON for editing and portable Markdown for search, export, and interoperability.
Write once, read anywhere.
</p>
</div>
<!-- Structured Attachments -->
<div class="rl-card">
<div class="rl-icon-box">&#128206;</div>
<h3>Structured Attachments</h3>
<p>
Attach images, PDFs, audio, and files to any card with roles (primary, preview, supporting) and captions.
Thumbnails render inline.
</p>
</div>
<!-- FUN Model -->
<div class="rl-card">
<div class="rl-icon-box">&#10084;</div>
<h3>FUN, Not CRUD</h3>
<p>
<span style="color:#f59e0b;font-weight:600">F</span>orget,
<span style="color:#f59e0b;font-weight:600">U</span>pdate,
<span style="color:#f59e0b;font-weight:600">N</span>ew &mdash;
nothing is permanently destroyed. Forgotten cards are archived and can be remembered at any time.
</p>
</div>
</div>
</div>
</section>
<!-- Built on Open Source -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Built on Open Source</h2>
<p class="rl-subtext" style="text-align:center">The libraries and tools that power rNotes.</p>
<div class="rl-grid-4" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem">
<div class="rl-card rl-card--center">
<h3>Automerge</h3>
<p>Local-first CRDT for conflict-free real-time collaboration. Your notes sync across devices without a central server.</p>
</div>
<div class="rl-card rl-card--center">
<h3>Web Speech API</h3>
<p>Browser-native live transcription &mdash; speak and watch your words appear in real time.</p>
</div>
<div class="rl-card rl-card--center">
<h3>Parakeet.js</h3>
<p>NVIDIA&rsquo;s in-browser speech recognition. Transcribe audio and video files offline &mdash; nothing leaves your device.</p>
</div>
<div class="rl-card rl-card--center">
<h3>Hono</h3>
<p>Ultra-fast, lightweight API framework powering the rNotes backend.</p>
</div>
</div>
</div>
</section>
<!-- Your Data, Protected -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Your Data, Protected</h2>
<p class="rl-subtext">How rNotes keeps your information safe.</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128274;</div>
<h3>End-to-End Encryption</h3>
<span class="rl-badge">Coming Soon</span>
<p>All content encrypted before it leaves your device. Not even the server can read it.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128373;</div>
<h3>Zero-Knowledge Architecture</h3>
<span class="rl-badge">Coming Soon</span>
<p>The server processes your requests without ever seeing your data in the clear.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127968;</div>
<h3>Self-Hosted</h3>
<p>Run on your own infrastructure. Your server, your rules, your data.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">(You)rNotes, your thoughts unbound.</h2>
<p class="rl-subtext">Try the demo or create a space to get started.</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary">Open Notebook</a>
<a href="#transcription-demo" class="rl-cta-secondary">Transcribe</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>
`;