From 0bfc5dede6a1d677c856a2e64b7bfc17dfcdf4b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:04:39 +0000 Subject: [PATCH 01/69] =?UTF-8?q?feat(web):=20Sprint=203=20=E2=80=94=20404?= =?UTF-8?q?=20page,=20loading=20skeleton,=20philosophy=20section,=20next/l?= =?UTF-8?q?ink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - not-found.tsx: branded 404 with gradient "404", constellation copy, and links back to home/products - products/[slug]/loading.tsx: pulse-skeleton matching the product page layout (breadcrumb, metric strip, hero, features) - PhilosophySection: 2×2 animated card grid for the four Konjo values (ቆንጆ/根性/康宙/건조) with per-card hover glow in the value's accent color; placed between ConstellationMap and DesignPreview on the homepage - next/link migration: ProjectGrid, RelatedProducts, LiveTicker, Breadcrumbs, Footer — all internal links now use for client-side navigation with prefetch; external URLs kept as https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/Breadcrumbs.tsx | 5 +- apps/web/app/_components/Footer.tsx | 13 +- apps/web/app/_components/LiveTicker.tsx | 5 +- .../web/app/_components/PhilosophySection.tsx | 111 ++++++++++++++++++ apps/web/app/_components/ProjectGrid.tsx | 13 +- apps/web/app/not-found.tsx | 60 ++++++++++ apps/web/app/page.tsx | 2 + apps/web/app/products/[slug]/loading.tsx | 62 ++++++++++ .../products/_components/RelatedProducts.tsx | 5 +- 9 files changed, 258 insertions(+), 18 deletions(-) create mode 100644 apps/web/app/_components/PhilosophySection.tsx create mode 100644 apps/web/app/not-found.tsx create mode 100644 apps/web/app/products/[slug]/loading.tsx diff --git a/apps/web/app/_components/Breadcrumbs.tsx b/apps/web/app/_components/Breadcrumbs.tsx index a211fdd..8217f5b 100644 --- a/apps/web/app/_components/Breadcrumbs.tsx +++ b/apps/web/app/_components/Breadcrumbs.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { motion, useReducedMotion } from "motion/react"; import { ease } from "@konjoai/ui"; @@ -23,12 +24,12 @@ export function Breadcrumbs({ trail }: { trail: Crumb[] }) { transition={{ duration: 0.3, ease: ease.kanjo, delay: i * 0.07 }} > {c.href && !last ? ( - {c.label} - + ) : ( {c.label} )} diff --git a/apps/web/app/_components/Footer.tsx b/apps/web/app/_components/Footer.tsx index 680502c..cfa5498 100644 --- a/apps/web/app/_components/Footer.tsx +++ b/apps/web/app/_components/Footer.tsx @@ -1,3 +1,4 @@ +import Link from "next/link"; import { severity as sevColor } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; @@ -23,7 +24,7 @@ export function Footer() {
{PRODUCTS.map((p) => ( - {p.slug} - + ))}
@@ -56,18 +57,18 @@ export function Footer() { + ); +} diff --git a/apps/web/app/_components/TerminalSection.tsx b/apps/web/app/_components/TerminalSection.tsx index 64eebc1..eba4866 100644 --- a/apps/web/app/_components/TerminalSection.tsx +++ b/apps/web/app/_components/TerminalSection.tsx @@ -45,6 +45,7 @@ export function TerminalSection() { const sectionRef = useRef(null); const inView = useInView(sectionRef, { once: true, margin: "-80px" }); const [lines, setLines] = useState([]); + const [replayCount, setReplayCount] = useState(0); const timerRef = useRef | null>(null); const runRef = useRef(0); @@ -57,6 +58,7 @@ export function TerminalSection() { } const run = ++runRef.current; + setLines([]); function clear() { if (timerRef.current) clearTimeout(timerRef.current); @@ -71,7 +73,7 @@ export function TerminalSection() { function step(lineIdx: number, charIdx: number): void { if (lineIdx >= SCRIPT.length) { - schedule(() => { setLines([]); step(0, 0); }, END_MS); + // Loop once then stop — manual replay via button return; } @@ -99,13 +101,14 @@ export function TerminalSection() { } } - schedule(() => step(0, 0), 600); + schedule(() => step(0, 0), replayCount === 0 ? 600 : 120); return () => { clear(); ++runRef.current; }; - }, [inView, reduce]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inView, reduce, replayCount]); const lastLine = lines[lines.length - 1]; const isTypingCmd = @@ -113,6 +116,7 @@ export function TerminalSection() { return (
konjo — bash + {/* Terminal body */} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8bbabf9..1f7667a 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -7,6 +7,7 @@ import { KeyboardHelp } from "./_components/KeyboardHelp"; import { ToastProvider } from "./_components/ToastProvider"; import { CursorGlow } from "./_components/CursorGlow"; import { PageTitleEffect } from "./_components/PageTitleEffect"; +import { SectionDots } from "./_components/SectionDots"; import "./globals.css"; export const metadata: Metadata = { @@ -46,6 +47,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) +
{children}
From e5c8717a4b6fbc448bd331da698ef8f7b40ecd48 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:41:40 +0000 Subject: [PATCH 31/69] feat(web): command palette global actions + hero particle field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CommandPalette: supports global actions alongside product search — System status, Keyboard shortcuts, Scroll to portfolio, GitHub link. Union PaletteItem type (product|action); single cursor navigates both. Result count shows filteredProducts + filteredActions. - HeroParticles: canvas-based particle field (55 particles) behind the hero — breathing opacity, gentle drift, soft mouse repulsion within 90px. All animation via requestAnimationFrame outside React. Respects prefers-reduced-motion. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/CommandPalette.tsx | 248 ++++++++++++++------ apps/web/app/_components/Hero.tsx | 2 + apps/web/app/_components/HeroParticles.tsx | 143 +++++++++++ 3 files changed, 317 insertions(+), 76 deletions(-) create mode 100644 apps/web/app/_components/HeroParticles.tsx diff --git a/apps/web/app/_components/CommandPalette.tsx b/apps/web/app/_components/CommandPalette.tsx index 0e09a5e..7e18da0 100644 --- a/apps/web/app/_components/CommandPalette.tsx +++ b/apps/web/app/_components/CommandPalette.tsx @@ -6,6 +6,58 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { cn, ease, StatusBadge } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; +type RouterCtx = ReturnType; + +/** A global action item (non-product) in the palette. */ +type PaletteAction = { + id: string; + icon: string; + label: string; + description: string; + onRun: (ctx: { router: RouterCtx; close: () => void }) => void; +}; + +const GLOBAL_ACTIONS: PaletteAction[] = [ + { + id: "status", + icon: "◉", + label: "System status", + description: "Live health of all nine products", + onRun: ({ router, close }) => { close(); router.push("/status"); }, + }, + { + id: "keyboard", + icon: "⌨", + label: "Keyboard shortcuts", + description: "Open the shortcuts reference panel", + onRun: ({ close }) => { + close(); + document.dispatchEvent(new CustomEvent("konjo:open-keyboard")); + }, + }, + { + id: "portfolio", + icon: "↓", + label: "Scroll to portfolio", + description: "Jump to the nine-product grid", + onRun: ({ close }) => { + close(); + document.getElementById("projects")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, + }, + { + id: "github", + icon: "↗", + label: "KonjoAI on GitHub", + description: "Open github.com/konjoai in a new tab", + onRun: ({ close }) => { close(); window.open("https://github.com/konjoai", "_blank", "noreferrer"); }, + }, +]; + +type PaletteItem = + | { kind: "product"; data: (typeof PRODUCTS)[number] } + | { kind: "action"; data: PaletteAction }; + /** Wraps the first occurrence of `query` inside `text` with an accent highlight span. */ function Highlight({ text, query }: { text: string; query: string }) { if (!query) return <>{text}; @@ -22,8 +74,9 @@ function Highlight({ text, query }: { text: string; query: string }) { /** * Keyboard-driven command palette — Cmd+K / Ctrl+K opens, Esc closes. - * Also responds to the "konjo:open-palette" custom DOM event so SiteNav - * can trigger it without shared state. + * Supports product navigation and global actions. Responds to the + * "konjo:open-palette" custom DOM event so SiteNav can trigger it + * without shared state. */ export function CommandPalette() { const [open, setOpen] = useState(false); @@ -35,7 +88,6 @@ export function CommandPalette() { const reduce = useReducedMotion(); const router = useRouter(); - // Load recently viewed slugs from localStorage each time the palette opens useEffect(() => { if (!open) return; try { @@ -44,19 +96,25 @@ export function CommandPalette() { } catch { /* storage unavailable */ } }, [open]); - const filtered = PRODUCTS.filter( - (p) => - query === "" || - p.name.toLowerCase().includes(query.toLowerCase()) || - p.tagline.toLowerCase().includes(query.toLowerCase()), + const q = query.toLowerCase(); + const filteredProducts = PRODUCTS.filter( + (p) => !q || p.name.toLowerCase().includes(q) || p.tagline.toLowerCase().includes(q), + ); + const filteredActions = GLOBAL_ACTIONS.filter( + (a) => !q || a.label.toLowerCase().includes(q) || a.description.toLowerCase().includes(q), ); - // When no query, show recently viewed products at the top; rest below const showRecent = !query && recent.length > 0; const recentProducts = recent .map((slug) => PRODUCTS.find((p) => p.slug === slug)) .filter((p): p is (typeof PRODUCTS)[number] => !!p); - const displayList = showRecent ? PRODUCTS : filtered; + + // Flat list for keyboard cursor: recent products first (if visible), then filtered products, then actions + const displayList: PaletteItem[] = [ + ...(showRecent ? recentProducts : []).map((p): PaletteItem => ({ kind: "product", data: p })), + ...(showRecent ? PRODUCTS : filteredProducts).map((p): PaletteItem => ({ kind: "product", data: p })), + ...filteredActions.map((a): PaletteItem => ({ kind: "action", data: a })), + ]; useEffect(() => { setCursor(0); }, [query]); useEffect(() => { activeItemRef.current?.scrollIntoView({ block: "nearest" }); }, [cursor]); @@ -82,9 +140,15 @@ export function CommandPalette() { if (open) requestAnimationFrame(() => inputRef.current?.focus()); }, [open]); - function navigate(slug: string) { - setOpen(false); - router.push(`/products/${slug}`); + function close() { setOpen(false); } + + function runItem(item: PaletteItem) { + if (item.kind === "product") { + close(); + router.push(`/products/${item.data.slug}`); + } else { + item.data.onRun({ router, close }); + } } function handleKeyDown(e: React.KeyboardEvent) { @@ -95,10 +159,20 @@ export function CommandPalette() { e.preventDefault(); setCursor((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && displayList[cursor]) { - navigate(displayList[cursor].slug); + runItem(displayList[cursor]); } } + // Split display list back into sections for rendering (with labels) + const recentSection = showRecent ? recentProducts : []; + const productSection = showRecent ? PRODUCTS : filteredProducts; + const actionSection = filteredActions; + + // Base cursor offset for each section + const recentOffset = 0; + const productOffset = recentSection.length; + const actionOffset = productOffset + productSection.length; + return ( {open && ( @@ -117,7 +191,7 @@ export function CommandPalette() { setQuery(e.target.value)} onKeyDown={handleKeyDown} - placeholder="Search products…" + placeholder="Search products and actions…" className="text-konjo-mono min-w-0 flex-1 bg-transparent text-sm text-konjo-fg placeholder:text-konjo-fg-faint focus:outline-none" - aria-label="Search products" + aria-label="Search products and actions" aria-autocomplete="list" aria-controls="palette-list" /> @@ -147,9 +221,9 @@ export function CommandPalette() { - {filtered.length} + {filteredProducts.length + filteredActions.length} )} @@ -161,60 +235,58 @@ export function CommandPalette() {
    - {showRecent && ( + {/* Recently viewed */} + {recentSection.length > 0 && ( <>
  • Recently viewed
  • - {recentProducts.map((p, i) => ( -
  • - -
  • - ))} + {p.glyph} +
    +

    {p.name}

    +

    {p.tagline}

    +
    + + + + ); + })}
  • - - All products - + All products
  • )} - {filtered.length === 0 ? ( + + {/* Products */} + {productSection.length === 0 && actionSection.length === 0 ? (
  • - No products match “{query}” + No results for “{query}”
  • ) : ( - filtered.map((p, i) => { - const idx = showRecent ? recentProducts.length + i : i; + productSection.map((p, i) => { + const idx = productOffset + i; return (
  • +
  • + ); + })} + + )}
{/* Keyboard hints */}
- - ↑↓{" "} - navigate - - - {" "} - open - - - Esc{" "} - close - + ↑↓{" "}navigate + {" "}run + Esc{" "}close
diff --git a/apps/web/app/_components/Hero.tsx b/apps/web/app/_components/Hero.tsx index ea82ec4..8ef3125 100644 --- a/apps/web/app/_components/Hero.tsx +++ b/apps/web/app/_components/Hero.tsx @@ -4,6 +4,7 @@ import { motion, animate, useInView, useMotionValue, useSpring, useTransform, us import type { MotionValue } from "motion/react"; import { useState, useEffect, useRef } from "react"; import { ease } from "@konjoai/ui"; +import { HeroParticles } from "./HeroParticles"; const PHRASES = [ "High-performance AI infrastructure, built in the Konjo way.", @@ -218,6 +219,7 @@ export function Hero() { /> )} + {/* Scroll parallax wrapper — content rises slightly faster than the page scrolls */} diff --git a/apps/web/app/_components/HeroParticles.tsx b/apps/web/app/_components/HeroParticles.tsx new file mode 100644 index 0000000..66234c2 --- /dev/null +++ b/apps/web/app/_components/HeroParticles.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useReducedMotion } from "motion/react"; + +type Particle = { + x: number; + y: number; + vx: number; + vy: number; + r: number; + phase: number; + phaseSpeed: number; +}; + +const COUNT = 55; + +function startParticles( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, +): () => void { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + let w = 0; + let h = 0; + let rafId = 0; + let mx = -9999; + let my = -9999; + const particles: Particle[] = []; + + function resize() { + const parent = canvas.parentElement; + if (!parent) return; + w = parent.offsetWidth; + h = parent.offsetHeight; + canvas.width = w * dpr; + canvas.height = h * dpr; + canvas.style.width = `${w}px`; + canvas.style.height = `${h}px`; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + if (particles.length === 0) { + for (let i = 0; i < COUNT; i++) { + particles.push({ + x: Math.random() * w, + y: Math.random() * h, + vx: (Math.random() - 0.5) * 0.28, + vy: (Math.random() - 0.5) * 0.28, + r: 0.7 + Math.random() * 1.6, + phase: Math.random() * Math.PI * 2, + phaseSpeed: 0.006 + Math.random() * 0.009, + }); + } + } + } + + function onMouseMove(e: MouseEvent) { + const rect = canvas.getBoundingClientRect(); + mx = e.clientX - rect.left; + my = e.clientY - rect.top; + } + function onMouseLeave() { mx = -9999; my = -9999; } + + function frame() { + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, w, h); + + for (const p of particles) { + p.x += p.vx; + p.y += p.vy; + + if (p.x < -8) p.x = w + 8; + else if (p.x > w + 8) p.x = -8; + if (p.y < -8) p.y = h + 8; + else if (p.y > h + 8) p.y = -8; + + // Soft mouse repulsion within 90px + const dx = p.x - mx; + const dy = p.y - my; + const d2 = dx * dx + dy * dy; + if (d2 < 8100 && d2 > 0) { + const dist = Math.sqrt(d2); + const force = ((90 - dist) / 90) * 0.06; + p.x += (dx / dist) * force; + p.y += (dy / dist) * force; + } + + // Breathing opacity: 0.05 – 0.18 + p.phase += p.phaseSpeed; + const opacity = 0.05 + (Math.sin(p.phase) * 0.5 + 0.5) * 0.13; + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); + ctx.fillStyle = `rgba(124,58,237,${opacity.toFixed(3)})`; + ctx.fill(); + } + + rafId = requestAnimationFrame(frame); + } + + resize(); + window.addEventListener("resize", resize, { passive: true }); + document.addEventListener("mousemove", onMouseMove, { passive: true }); + document.addEventListener("mouseleave", onMouseLeave); + rafId = requestAnimationFrame(frame); + + return () => { + cancelAnimationFrame(rafId); + window.removeEventListener("resize", resize); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseleave", onMouseLeave); + }; +} + +/** + * Canvas-based drifting particle field rendered behind the hero content. + * Particles breathe opacity, drift gently, and softly repel on mouse proximity. + * All animation runs outside React's render cycle via requestAnimationFrame. + * Returns null under prefers-reduced-motion. + */ +export function HeroParticles() { + const canvasRef = useRef(null); + const reduce = useReducedMotion(); + + useEffect(() => { + if (reduce) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + return startParticles(canvas, ctx); + }, [reduce]); + + if (reduce) return null; + + return ( + + ); +} From c3cdc1fd14899b4859772456e03a2b24d7a94081 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:44:02 +0000 Subject: [PATCH 32/69] feat(web): toast progress bar, 1-9 product shortcuts, keyboard event wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ToastProvider: auto-dismiss progress bar that shrinks left-to-right over 3.2 s matching each toast's tone color; toast container is now overflow-hidden so the bar clips to rounded corners - ProjectGrid: 1-9 keyboard shortcuts navigate directly to products by index (e.g. press 2 → vectro); / shortcut still focuses search - KeyboardHelp: added "1–9 Go to product by #" shortcut to Navigate group - KeyboardHelp: now listens to konjo:open-keyboard custom DOM event so the CommandPalette "Keyboard shortcuts" action opens it programmatically https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/KeyboardHelp.tsx | 4 ++++ apps/web/app/_components/ProjectGrid.tsx | 25 +++++++++++++++++----- apps/web/app/_components/ToastProvider.tsx | 24 +++++++++++++++------ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/apps/web/app/_components/KeyboardHelp.tsx b/apps/web/app/_components/KeyboardHelp.tsx index febe4e3..edeb251 100644 --- a/apps/web/app/_components/KeyboardHelp.tsx +++ b/apps/web/app/_components/KeyboardHelp.tsx @@ -18,6 +18,7 @@ const SHORTCUTS: Shortcut[] = [ { keys: ["Esc"], label: "Dismiss / close", group: "Global" }, { keys: ["g", "h"], label: "Go to Home", group: "Navigate" }, { keys: ["g", "s"], label: "Go to Status", group: "Navigate" }, + { keys: ["1–9"], label: "Go to product by #", group: "Navigate" }, { keys: ["["], label: "Previous product", group: "Navigate" }, { keys: ["]"], label: "Next product", group: "Navigate" }, { keys: ["↑", "↓"], label: "Move selection", group: "Palette" }, @@ -73,9 +74,12 @@ export function KeyboardHelp() { } } + function onOpenEvent() { setOpen(true); } document.addEventListener("keydown", onKey); + document.addEventListener("konjo:open-keyboard", onOpenEvent); return () => { document.removeEventListener("keydown", onKey); + document.removeEventListener("konjo:open-keyboard", onOpenEvent); if (gTimer) clearTimeout(gTimer); }; }, [gMode, isInputFocused, router]); diff --git a/apps/web/app/_components/ProjectGrid.tsx b/apps/web/app/_components/ProjectGrid.tsx index e633284..7d9e099 100644 --- a/apps/web/app/_components/ProjectGrid.tsx +++ b/apps/web/app/_components/ProjectGrid.tsx @@ -2,6 +2,7 @@ import { useRef, useState, useEffect } from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { motion, AnimatePresence, useMotionValue, useSpring, useReducedMotion } from "motion/react"; import { ease, StatusBadge, severity as sevColor, cn } from "@konjoai/ui"; import { PRODUCTS, type Product } from "@/lib/products"; @@ -27,18 +28,32 @@ export function ProjectGrid() { const [filter, setFilter] = useState("all"); const [search, setSearch] = useState(""); const searchRef = useRef(null); + const router = useRouter(); useEffect(() => { function onKey(e: KeyboardEvent) { - if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return; const tag = (e.target as HTMLElement).tagName; - if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; - e.preventDefault(); - searchRef.current?.focus(); + const inInput = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT"; + + // / → focus search + if (e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey && !inInput) { + e.preventDefault(); + searchRef.current?.focus(); + return; + } + + // 1-9 → navigate to product by index (only when not in an input) + if (!inInput && !e.metaKey && !e.ctrlKey && !e.altKey) { + const num = parseInt(e.key, 10); + if (num >= 1 && num <= 9 && PRODUCTS[num - 1]) { + e.preventDefault(); + router.push(`/products/${PRODUCTS[num - 1].slug}`); + } + } } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); - }, []); + }, [router]); const filtered = PRODUCTS.filter((p) => { const matchStatus = filter === "all" || p.status === filter; diff --git a/apps/web/app/_components/ToastProvider.tsx b/apps/web/app/_components/ToastProvider.tsx index 4a22cd9..c24aa1e 100644 --- a/apps/web/app/_components/ToastProvider.tsx +++ b/apps/web/app/_components/ToastProvider.tsx @@ -12,12 +12,14 @@ interface Toast { tone: ToastTone; } -const TONE: Record = { - success: { border: "border-konjo-good/40", dot: "bg-konjo-good" }, - info: { border: "border-konjo-accent/40", dot: "bg-konjo-accent" }, - warn: { border: "border-konjo-warm/40", dot: "bg-konjo-warm" }, +const TONE: Record = { + success: { border: "border-konjo-good/40", dot: "bg-konjo-good", bar: "bg-konjo-good" }, + info: { border: "border-konjo-accent/40", dot: "bg-konjo-accent", bar: "bg-konjo-accent" }, + warn: { border: "border-konjo-warm/40", dot: "bg-konjo-warm", bar: "bg-konjo-warm" }, }; +const AUTO_DISMISS_MS = 3200; + let nextId = 0; /** Renders toast notifications fired via the `konjo:toast` custom DOM event. */ @@ -36,7 +38,7 @@ export function ToastProvider() { ).detail; const id = nextId++; setToasts((prev) => [{ id, message, tone }, ...prev].slice(0, 3)); - setTimeout(() => dismiss(id), 3200); + setTimeout(() => dismiss(id), AUTO_DISMISS_MS); } document.addEventListener("konjo:toast", onToast); return () => document.removeEventListener("konjo:toast", onToast); @@ -59,7 +61,7 @@ export function ToastProvider() { exit={reduce ? { opacity: 0 } : { opacity: 0, x: -20, scale: 0.95 }} transition={{ duration: 0.28, ease: ease.nehan }} className={cn( - "glass-konjo rounded-konjo flex items-center gap-3 border px-4 py-2.5 shadow-lg", + "glass-konjo rounded-konjo relative flex items-center gap-3 overflow-hidden border px-4 py-2.5 shadow-lg", TONE[t.tone].border, )} role="status" @@ -74,6 +76,16 @@ export function ToastProvider() { > × + {/* Auto-dismiss progress bar */} + {!reduce && ( + + )} ))}
From 3daccf00a20676eca84479eb1275b92789daf04f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:46:08 +0000 Subject: [PATCH 33/69] feat(web): constellation map auto-cycles through product metrics when idle When no node is hovered, the center hub automatically cycles through each product every 2.6 s showing its glyph, key metric, and label. User hover immediately takes priority and suspends the auto-cycle. This makes the constellation feel alive without requiring user interaction. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/ConstellationMap.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/web/app/_components/ConstellationMap.tsx b/apps/web/app/_components/ConstellationMap.tsx index 56b3265..1282729 100644 --- a/apps/web/app/_components/ConstellationMap.tsx +++ b/apps/web/app/_components/ConstellationMap.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { motion, useReducedMotion } from "motion/react"; import { ease, severity as sevColor } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; @@ -48,9 +48,26 @@ const EDGES: [number, number][] = [ /** Network graph of the nine KonjoAI products — hover to highlight connections. */ export function ConstellationMap() { const [active, setActive] = useState(null); + const [autoSlug, setAutoSlug] = useState(null); const reduce = useReducedMotion(); - const activeIdx = active ? SLUG_ORDER.indexOf(active) : -1; + // Auto-cycle through products when no node is being hovered + useEffect(() => { + if (reduce) return; + let i = 0; + const id = setInterval(() => { + if (!active) { + setAutoSlug(SLUG_ORDER[i % SLUG_ORDER.length]); + i++; + } + }, 2600); + return () => clearInterval(id); + }, [active, reduce]); + + // The "display" active node — user hover takes priority over auto-cycle + const displaySlug = active ?? autoSlug; + + const activeIdx = displaySlug ? SLUG_ORDER.indexOf(displaySlug) : -1; const connectedIdx = new Set( activeIdx >= 0 ? EDGES.filter(([a, b]) => a === activeIdx || b === activeIdx).flatMap(([a, b]) => [a, b]) @@ -142,16 +159,16 @@ export function ConstellationMap() { )} - {active ? ( + {displaySlug ? ( <> - - {PRODUCT_MAP[active].glyph} + + {PRODUCT_MAP[displaySlug].glyph} - - {PRODUCT_MAP[active].metric.value}{PRODUCT_MAP[active].metric.unit} + + {PRODUCT_MAP[displaySlug].metric.value}{PRODUCT_MAP[displaySlug].metric.unit} - {PRODUCT_MAP[active].metric.label.toUpperCase()} + {PRODUCT_MAP[displaySlug].metric.label.toUpperCase()} ) : ( @@ -169,9 +186,10 @@ export function ConstellationMap() { {/* Nodes */} {NODES.map(({ slug, x, y, product }, i) => { const col = sevColor[product.metric.severity]; - const isActive = slug === active; + const isActive = slug === displaySlug; const isConnected = connectedIdx.has(i); - const dim = activeIdx >= 0 && !isActive && !isConnected; + // Only dim when the user is actively hovering (not during auto-cycle) + const dim = !!active && !isActive && !isConnected; return ( Date: Sun, 14 Jun 2026 16:47:49 +0000 Subject: [PATCH 34/69] feat(web): magnetic hero buttons + grid spotlight hover effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hero: MagneticButton component with spring physics — CTAs drift subtly toward the cursor (0.28× factor) and spring back on mouse leave. Disabled under prefers-reduced-motion. - ProjectGrid: GridSpotlight wrapper dims non-hovered cards to 0.45 opacity when hovering the grid, restoring the hovered card to full opacity. Creates a cinematic spotlight focus effect on hover. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/Hero.tsx | 46 ++++++++++++++++++++---- apps/web/app/_components/ProjectGrid.tsx | 42 ++++++++++++++++++---- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/apps/web/app/_components/Hero.tsx b/apps/web/app/_components/Hero.tsx index 8ef3125..c0705de 100644 --- a/apps/web/app/_components/Hero.tsx +++ b/apps/web/app/_components/Hero.tsx @@ -1,7 +1,7 @@ "use client"; import { motion, animate, useInView, useMotionValue, useSpring, useTransform, useReducedMotion, useScroll, AnimatePresence } from "motion/react"; -import type { MotionValue } from "motion/react"; +import type { MotionValue, HTMLMotionProps } from "motion/react"; import { useState, useEffect, useRef } from "react"; import { ease } from "@konjoai/ui"; import { HeroParticles } from "./HeroParticles"; @@ -136,6 +136,40 @@ function FloatingGlyphs({ normX, normY }: { normX: MotionValue; normY: M ); } +type MagneticButtonProps = HTMLMotionProps<"a"> & { children: React.ReactNode }; + +/** Anchor that subtly drifts toward the cursor — premium magnetic CTA feel. */ +function MagneticButton({ children, ...props }: MagneticButtonProps) { + const ref = useRef(null); + const reduce = useReducedMotion(); + const rawX = useMotionValue(0); + const rawY = useMotionValue(0); + const x = useSpring(rawX, { stiffness: 250, damping: 22 }); + const y = useSpring(rawY, { stiffness: 250, damping: 22 }); + + function onMove(e: React.MouseEvent) { + if (reduce) return; + const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; + rawX.set((e.clientX - rect.left - rect.width / 2) * 0.28); + rawY.set((e.clientY - rect.top - rect.height / 2) * 0.28); + } + + function onLeave() { rawX.set(0); rawY.set(0); } + + return ( + + {children} + + ); +} + /** Counts from 0 to `stat.countTo` once the element enters the viewport. */ function AnimatedStat({ stat }: { stat: Stat }) { const ref = useRef(null); @@ -273,21 +307,21 @@ export function Hero() { transition={{ duration: 0.6, ease: ease.kanjo, delay: 0.4 }} className="mt-10 flex flex-wrap items-center justify-center gap-3" > - Explore the constellation - - + GitHub → - + )} -
    - - {filtered.map((p, i) => ( - - ))} - -
+ + {filtered.map((p, i) => ( + + ))} +
); } +/** + * Wraps the product grid with hover-spotlight behaviour: hovering any child + * dims the rest to 0.35 opacity, restoring to 1 on mouse leave. + */ +function GridSpotlight({ children }: { children: React.ReactNode }) { + const [hovered, setHovered] = useState(false); + return ( +
    setHovered(true)} + onMouseLeave={() => setHovered(false)} + data-hovered={hovered ? "true" : undefined} + > + {children} + {/* Spotlight: when grid is hovered, darken all but the card being hovered */} + +
+ ); +} + /** Single product card with entrance animation, 3-D tilt, and mouse-follow spotlight. */ function ProjectCard({ project, index }: { project: Product; index: number }) { const reduce = useReducedMotion(); From ef6c3963612cd8c2400b27b2f47e3f5251c4ed6a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:52:50 +0000 Subject: [PATCH 35/69] feat(web): ScrambleText component applied to all section headings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScrambleText reveals text with a left-to-right character scramble on scroll entry — flashes random Konjo-brand chars (◈◇◐✸▲◉) then progressively reveals the final text over 620ms. SSR-safe (initial render shows final text, no hydration mismatch). Disabled under prefers-reduced-motion. Applied to: The constellation, Built to outperform, Ship from the terminal, The portfolio — each with a small stagger delay so headings reveal progressively as the page is explored. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/BenchmarkSection.tsx | 10 +- apps/web/app/_components/ConstellationMap.tsx | 10 +- apps/web/app/_components/ProjectGrid.tsx | 10 +- apps/web/app/_components/ScrambleText.tsx | 91 +++++++++++++++++++ apps/web/app/_components/TerminalSection.tsx | 10 +- 5 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 apps/web/app/_components/ScrambleText.tsx diff --git a/apps/web/app/_components/BenchmarkSection.tsx b/apps/web/app/_components/BenchmarkSection.tsx index a533668..fc08caa 100644 --- a/apps/web/app/_components/BenchmarkSection.tsx +++ b/apps/web/app/_components/BenchmarkSection.tsx @@ -4,6 +4,7 @@ import { useRef, useState, useEffect } from "react"; import Link from "next/link"; import { motion, useInView, useReducedMotion, animate } from "motion/react"; import { ease } from "@konjoai/ui"; +import { ScrambleText } from "./ScrambleText"; type BenchmarkRow = { glyph: string; @@ -191,9 +192,12 @@ export function BenchmarkSection() {

benchmarks · measured on M2 Pro

-

- Built to outperform -

+

Four headline metrics against industry baselines. Every number is reproducible — each repo ships its own benchmark suite. diff --git a/apps/web/app/_components/ConstellationMap.tsx b/apps/web/app/_components/ConstellationMap.tsx index 1282729..682a34d 100644 --- a/apps/web/app/_components/ConstellationMap.tsx +++ b/apps/web/app/_components/ConstellationMap.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import { motion, useReducedMotion } from "motion/react"; import { ease, severity as sevColor } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; +import { ScrambleText } from "./ScrambleText"; // ─── layout constants ───────────────────────────────────────────────────────── @@ -90,9 +91,12 @@ export function ConstellationMap() {

Nine products · one design system

-

- The constellation -

+

Hover any node to trace its connections. Click to dive in.

diff --git a/apps/web/app/_components/ProjectGrid.tsx b/apps/web/app/_components/ProjectGrid.tsx index 8aabd75..24a65d8 100644 --- a/apps/web/app/_components/ProjectGrid.tsx +++ b/apps/web/app/_components/ProjectGrid.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"; import { motion, AnimatePresence, useMotionValue, useSpring, useReducedMotion } from "motion/react"; import { ease, StatusBadge, severity as sevColor, cn } from "@konjoai/ui"; import { PRODUCTS, type Product } from "@/lib/products"; +import { ScrambleText } from "./ScrambleText"; type StatusFilter = "all" | "operational" | "degraded" | "research"; @@ -75,9 +76,12 @@ export function ProjectGrid() { className="mb-8 flex flex-wrap items-end justify-between gap-6" >
-

- The portfolio -

+

(null); + const inView = useInView(ref, { once: true, margin: "-50px" }); + const reduce = useReducedMotion(); + const [displayed, setDisplayed] = useState(text); + const triggered = useRef(false); + const rafRef = useRef(0); + + useEffect(() => { + if (!inView || reduce || triggered.current) return; + triggered.current = true; + + const DURATION = 620; + + const delayTimer = setTimeout(() => { + // Briefly flash to random chars, then reveal L→R + setDisplayed( + text + .split("") + .map((c) => (c === " " ? " " : CHARS[Math.floor(Math.random() * CHARS.length)])) + .join(""), + ); + + let start: number | null = null; + function tick(ts: number) { + if (!start) start = ts; + const progress = Math.min((ts - start) / DURATION, 1); + + setDisplayed( + text + .split("") + .map((c, i) => { + if (c === " ") return " "; + if (i / text.length <= progress) return c; + return CHARS[Math.floor(Math.random() * CHARS.length)]; + }) + .join(""), + ); + + if (progress < 1) { + rafRef.current = requestAnimationFrame(tick); + } else { + setDisplayed(text); + } + } + + rafRef.current = requestAnimationFrame(tick); + }, delay); + + return () => { + clearTimeout(delayTimer); + cancelAnimationFrame(rafRef.current); + }; + }, [inView, reduce, text, delay]); + + return ( + + {displayed} + + ); +} diff --git a/apps/web/app/_components/TerminalSection.tsx b/apps/web/app/_components/TerminalSection.tsx index eba4866..2659d60 100644 --- a/apps/web/app/_components/TerminalSection.tsx +++ b/apps/web/app/_components/TerminalSection.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { motion, useReducedMotion, useInView } from "motion/react"; import { ease } from "@konjoai/ui"; +import { ScrambleText } from "./ScrambleText"; type TermKind = "cmd" | "out" | "gap"; @@ -131,9 +132,12 @@ export function TerminalSection() {

konjo-cli · open-source toolchain

-

- Ship from the terminal -

+

Every product ships a first-class CLI. Inference, retrieval, compression — composable and scriptable. From 5473f02883303d88ba440e9e3734ef9173547f60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 16:54:51 +0000 Subject: [PATCH 36/69] feat(web): status ribbon + live online count in hero badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StatusRibbon: dismissible site-wide alert when products are degraded/outage — appears below SiteNav, shows product name+status or degraded count, links to /status page, persists dismiss in sessionStorage. Animated height open/close. - OnlineCount: simulated live visitor count (28-64 range, seeded per session, drifts ±1-2 every 8-14s) shown in the hero badge line. Numbers animate vertically in/out with AnimatePresence. Adds social proof signal and reinforces the "live" aesthetic. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/Hero.tsx | 3 + apps/web/app/_components/OnlineCount.tsx | 69 +++++++++++++++++++++ apps/web/app/_components/StatusRibbon.tsx | 74 +++++++++++++++++++++++ apps/web/app/layout.tsx | 2 + 4 files changed, 148 insertions(+) create mode 100644 apps/web/app/_components/OnlineCount.tsx create mode 100644 apps/web/app/_components/StatusRibbon.tsx diff --git a/apps/web/app/_components/Hero.tsx b/apps/web/app/_components/Hero.tsx index c0705de..c012fbf 100644 --- a/apps/web/app/_components/Hero.tsx +++ b/apps/web/app/_components/Hero.tsx @@ -5,6 +5,7 @@ import type { MotionValue, HTMLMotionProps } from "motion/react"; import { useState, useEffect, useRef } from "react"; import { ease } from "@konjoai/ui"; import { HeroParticles } from "./HeroParticles"; +import { OnlineCount } from "./OnlineCount"; const PHRASES = [ "High-performance AI infrastructure, built in the Konjo way.", @@ -271,6 +272,8 @@ export function Hero() { aria-hidden /> v0.2 · Sprint 0.5 of the Konjo UI Initiative + · + (null); + const [prev, setPrev] = useState(null); + + useEffect(() => { + let base: number; + try { + const stored = sessionStorage.getItem("konjo:online"); + base = stored ? parseInt(stored, 10) : randInt(28, 64); + sessionStorage.setItem("konjo:online", String(base)); + } catch { + base = randInt(28, 64); + } + setCount(base); + + const id = setInterval(() => { + setCount((c) => { + if (c === null) return c; + const delta = randInt(-2, 2); + const next = Math.max(12, Math.min(99, c + delta)); + setPrev(c); + return next; + }); + }, randInt(8000, 14000)); + + return () => clearInterval(id); + }, []); + + if (count === null) return null; + + const dir = prev !== null && count > prev ? 1 : -1; + + return ( + + + + + {count} + + + online + + ); +} diff --git a/apps/web/app/_components/StatusRibbon.tsx b/apps/web/app/_components/StatusRibbon.tsx new file mode 100644 index 0000000..6b2fedb --- /dev/null +++ b/apps/web/app/_components/StatusRibbon.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { motion, AnimatePresence } from "motion/react"; +import { PRODUCTS } from "@/lib/products"; + +const DEGRADED = PRODUCTS.filter((p) => p.status === "degraded" || p.status === "outage"); + +/** + * Dismissible site-wide alert ribbon that appears below the nav when one or + * more products are degraded/outage. Hidden via sessionStorage once dismissed. + * Does not render if all systems are operational or research. + */ +export function StatusRibbon() { + const [dismissed, setDismissed] = useState(() => { + if (typeof window === "undefined") return false; + try { return !!sessionStorage.getItem("konjo:ribbon-dismissed"); } catch { return false; } + }); + + if (DEGRADED.length === 0 || dismissed) return null; + + function dismiss() { + try { sessionStorage.setItem("konjo:ribbon-dismissed", "1"); } catch { /* noop */ } + setDismissed(true); + } + + const first = DEGRADED[0]; + const isOutage = first.status === "outage"; + const borderColor = isOutage ? "border-konjo-hot/40" : "border-konjo-warm/40"; + const bgColor = isOutage ? "bg-konjo-hot/8" : "bg-konjo-warm/8"; + const textColor = isOutage ? "text-konjo-hot" : "text-konjo-warm"; + const dotColor = isOutage ? "bg-konjo-hot" : "bg-konjo-warm"; + + return ( + + {!dismissed && ( + +

+ + + {DEGRADED.length > 1 + ? `${DEGRADED.length} systems degraded` + : `${first.glyph} ${first.name} — ${first.status}`} + + + View status → + + +
+ + )} + + ); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 1f7667a..0d055d5 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,7 @@ import { ToastProvider } from "./_components/ToastProvider"; import { CursorGlow } from "./_components/CursorGlow"; import { PageTitleEffect } from "./_components/PageTitleEffect"; import { SectionDots } from "./_components/SectionDots"; +import { StatusRibbon } from "./_components/StatusRibbon"; import "./globals.css"; export const metadata: Metadata = { @@ -49,6 +50,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) +
{children}
From a375ec2b9a4a2f2533b23ce67dcf88408e5801c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:04:37 +0000 Subject: [PATCH 37/69] feat(web): animated sparklines, terminal copy, feed pause + ScrambleText parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move MiniSparkline to shared _components; update status page import - Add AnimatedMiniSparkline: pathLength 0→1 draw on mount, re-animates on card hover - ProjectCard tracks hover state and passes it to sparkline + colors by metric severity - Terminal cmd lines show a hover-reveal copy button; clipboard success fires toast - ActivityFeed pauses new events + aging on hover (uses ref to avoid stale closure) - PhilosophySection and DesignPreview headings now use ScrambleText (consistent with other sections) --- apps/web/app/_components/ActivityFeed.tsx | 16 +++- .../app/_components/AnimatedMiniSparkline.tsx | 77 +++++++++++++++++++ apps/web/app/_components/DesignPreview.tsx | 10 ++- apps/web/app/_components/MiniSparkline.tsx | 51 ++++++++++++ .../web/app/_components/PhilosophySection.tsx | 10 ++- apps/web/app/_components/ProjectGrid.tsx | 23 ++++-- apps/web/app/_components/TerminalSection.tsx | 52 ++++++++++--- .../status/_components/ProductStatusList.tsx | 2 +- 8 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 apps/web/app/_components/AnimatedMiniSparkline.tsx create mode 100644 apps/web/app/_components/MiniSparkline.tsx diff --git a/apps/web/app/_components/ActivityFeed.tsx b/apps/web/app/_components/ActivityFeed.tsx index 52004a8..3583aad 100644 --- a/apps/web/app/_components/ActivityFeed.tsx +++ b/apps/web/app/_components/ActivityFeed.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { motion, AnimatePresence, useReducedMotion } from "motion/react"; import { ease } from "@konjoai/ui"; @@ -43,6 +43,10 @@ export function ActivityFeed() { const [events, setEvents] = useState([]); const [poolIdx, setPoolIdx] = useState(0); const [totalCount, setTotalCount] = useState(0); + const [paused, setPaused] = useState(false); + const pausedRef = useRef(false); + + useEffect(() => { pausedRef.current = paused; }, [paused]); const pushEvent = useCallback(() => { setEvents((prev) => { @@ -54,10 +58,11 @@ export function ActivityFeed() { setTotalCount((n) => n + 1); }, [poolIdx]); - // Initial batch + recurring trickle + // Initial batch + recurring trickle — pauses when hovering useEffect(() => { pushEvent(); const id = setInterval(() => { + if (pausedRef.current) return; setEvents((prev) => prev.map((e) => ({ ...e, age: e.age + 1 }))); if (Math.random() > 0.3) pushEvent(); }, 2600); @@ -87,6 +92,11 @@ export function ActivityFeed() { Stream + {paused && ( + + paused + + )} {totalCount > 0 && ( setPaused(true)} + onMouseLeave={() => setPaused(false)} > {events.map((ev) => ( diff --git a/apps/web/app/_components/AnimatedMiniSparkline.tsx b/apps/web/app/_components/AnimatedMiniSparkline.tsx new file mode 100644 index 0000000..e85e3c1 --- /dev/null +++ b/apps/web/app/_components/AnimatedMiniSparkline.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { motion, useReducedMotion } from "motion/react"; + +function seed(slug: string): number { + return slug.split("").reduce((n, c) => n + c.charCodeAt(0), 0); +} + +interface AnimatedMiniSparklineProps { + slug: string; + width?: number; + height?: number; + color?: string; + hovered?: boolean; +} + +/** + * Client-side sparkline that draws its path into view on mount and re-draws + * on card hover — pathLength 0→1 with a spring easing for a premium feel. + */ +export function AnimatedMiniSparkline({ + slug, + width = 72, + height = 28, + color = "var(--color-konjo-good)", + hovered = false, +}: AnimatedMiniSparklineProps) { + const reduce = useReducedMotion(); + const s = seed(slug); + const pts = Array.from({ length: 8 }, (_, i) => { + const v = 97.5 + Math.sin((s + i * 2.7) * 0.6) * 2.1 + Math.cos((s * 0.3 + i) * 0.9) * 1.1; + return Math.max(92, Math.min(100, v)); + }); + const lo = Math.min(...pts); + const hi = Math.max(...pts); + const range = hi - lo || 1; + const pad = 3; + const coords = pts.map((v, i) => [ + (i / (pts.length - 1)) * width, + height - pad - ((v - lo) / range) * (height - pad * 2), + ] as const); + const d = coords.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" "); + + if (reduce) { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/apps/web/app/_components/DesignPreview.tsx b/apps/web/app/_components/DesignPreview.tsx index 00cfb1b..b3ea0e2 100644 --- a/apps/web/app/_components/DesignPreview.tsx +++ b/apps/web/app/_components/DesignPreview.tsx @@ -8,6 +8,7 @@ import { MetricsSection } from "./showcase/MetricsSection"; import { ComplianceSection } from "./showcase/ComplianceSection"; import { RankingsSection } from "./showcase/RankingsSection"; import { ShellSection } from "./showcase/ShellSection"; +import { ScrambleText } from "./ScrambleText"; type Block = { id: string; @@ -106,9 +107,12 @@ export function DesignPreview() { Live
-

- Live design system -

+

Every primitive, alive. Five sections streaming real-time data — the shared visual language powering all nine products.

diff --git a/apps/web/app/_components/MiniSparkline.tsx b/apps/web/app/_components/MiniSparkline.tsx new file mode 100644 index 0000000..d167018 --- /dev/null +++ b/apps/web/app/_components/MiniSparkline.tsx @@ -0,0 +1,51 @@ +/** Converts a slug string into a stable integer seed. */ +function seed(slug: string): number { + return slug.split("").reduce((n, c) => n + c.charCodeAt(0), 0); +} + +interface MiniSparklineProps { + slug: string; + width?: number; + height?: number; +} + +/** + * Deterministic 8-point uptime sparkline for a product — pure SVG, + * fully server-renderable, no JavaScript required. + */ +export function MiniSparkline({ slug, width = 72, height = 28 }: MiniSparklineProps) { + const s = seed(slug); + const pts = Array.from({ length: 8 }, (_, i) => { + const v = 97.5 + Math.sin((s + i * 2.7) * 0.6) * 2.1 + Math.cos((s * 0.3 + i) * 0.9) * 1.1; + return Math.max(92, Math.min(100, v)); + }); + const lo = Math.min(...pts); + const hi = Math.max(...pts); + const range = hi - lo || 1; + const pad = 3; + const coords = pts.map((v, i) => [ + (i / (pts.length - 1)) * width, + height - pad - ((v - lo) / range) * (height - pad * 2), + ] as const); + const d = coords.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" "); + + return ( + + + + ); +} diff --git a/apps/web/app/_components/PhilosophySection.tsx b/apps/web/app/_components/PhilosophySection.tsx index 355437a..c78e77d 100644 --- a/apps/web/app/_components/PhilosophySection.tsx +++ b/apps/web/app/_components/PhilosophySection.tsx @@ -3,6 +3,7 @@ import { useRef } from "react"; import { motion, useMotionValue, useSpring, useReducedMotion } from "motion/react"; import { ease } from "@konjoai/ui"; +import { ScrambleText } from "./ScrambleText"; const PILLARS = [ { @@ -130,9 +131,12 @@ export function PhilosophySection() {

Four words · one way

-

- The Konjo philosophy -

+

Each product, each component, each commit — measured against all four.

diff --git a/apps/web/app/_components/ProjectGrid.tsx b/apps/web/app/_components/ProjectGrid.tsx index 24a65d8..b4c5b2b 100644 --- a/apps/web/app/_components/ProjectGrid.tsx +++ b/apps/web/app/_components/ProjectGrid.tsx @@ -7,6 +7,7 @@ import { motion, AnimatePresence, useMotionValue, useSpring, useReducedMotion } import { ease, StatusBadge, severity as sevColor, cn } from "@konjoai/ui"; import { PRODUCTS, type Product } from "@/lib/products"; import { ScrambleText } from "./ScrambleText"; +import { AnimatedMiniSparkline } from "./AnimatedMiniSparkline"; type StatusFilter = "all" | "operational" | "degraded" | "research"; @@ -196,6 +197,7 @@ function ProjectCard({ project, index }: { project: Product; index: number }) { const reduce = useReducedMotion(); const cardRef = useRef(null); const spotRef = useRef(null); + const [cardHovered, setCardHovered] = useState(false); const rawX = useMotionValue(0); const rawY = useMotionValue(0); const rotateX = useSpring(rawX, { stiffness: 300, damping: 25 }); @@ -225,6 +227,7 @@ function ProjectCard({ project, index }: { project: Product; index: number }) { function handleMouseLeave() { rawX.set(0); rawY.set(0); + setCardHovered(false); } return ( @@ -247,6 +250,7 @@ function ProjectCard({ project, index }: { project: Product; index: number }) { style={{ rotateX, rotateY, transformPerspective: 800 }} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} + onMouseEnter={() => setCardHovered(true)} className="group glass-konjo rounded-konjo-lg relative overflow-hidden p-6 transition-colors duration-300" > {/* Mouse-following spotlight overlay */} @@ -313,11 +317,20 @@ function ProjectCard({ project, index }: { project: Product; index: number }) { {project.metric.label} - +
+ + +
{/* CTAs */} diff --git a/apps/web/app/_components/TerminalSection.tsx b/apps/web/app/_components/TerminalSection.tsx index 2659d60..5389e00 100644 --- a/apps/web/app/_components/TerminalSection.tsx +++ b/apps/web/app/_components/TerminalSection.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState, useEffect, useRef } from "react"; -import { motion, useReducedMotion, useInView } from "motion/react"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { motion, AnimatePresence, useReducedMotion, useInView } from "motion/react"; import { ease } from "@konjoai/ui"; import { ScrambleText } from "./ScrambleText"; @@ -47,9 +47,20 @@ export function TerminalSection() { const inView = useInView(sectionRef, { once: true, margin: "-80px" }); const [lines, setLines] = useState([]); const [replayCount, setReplayCount] = useState(0); + const [copiedCmd, setCopiedCmd] = useState(null); const timerRef = useRef | null>(null); const runRef = useRef(0); + const copyCmd = useCallback((text: string) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedCmd(text); + document.dispatchEvent(new CustomEvent("konjo:toast", { + detail: { message: "Command copied to clipboard", tone: "success" }, + })); + setTimeout(() => setCopiedCmd(null), 2000); + }).catch(() => {/* clipboard blocked */}); + }, []); + useEffect(() => { if (!inView) return; @@ -180,17 +191,34 @@ export function TerminalSection() { > {lines.map((line, i) => line.kind === "cmd" ? ( -

- - {line.text.slice(0, line.chars)} - {line.chars < line.text.length && ( - +

+

+ + {line.text.slice(0, line.chars)} + {line.chars < line.text.length && ( + + )} +

+ {line.chars === line.text.length && ( + + copyCmd(line.text)} + aria-label={`Copy command: ${line.text}`} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + className="shrink-0 rounded border border-konjo-line/30 px-1.5 py-0.5 text-[9px] uppercase tracking-widest text-konjo-fg-faint opacity-0 transition-all group-hover/line:opacity-100 hover:border-konjo-brand/40 hover:text-konjo-brand focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-konjo-accent" + > + {copiedCmd === line.text ? "✓" : "copy"} + + )} -

+
) : line.kind === "gap" ? (
) : ( diff --git a/apps/web/app/status/_components/ProductStatusList.tsx b/apps/web/app/status/_components/ProductStatusList.tsx index e77422b..7a80122 100644 --- a/apps/web/app/status/_components/ProductStatusList.tsx +++ b/apps/web/app/status/_components/ProductStatusList.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { motion } from "motion/react"; import { StatusBadge, severity as sevColor } from "@konjoai/ui"; import { ease } from "@konjoai/ui"; -import { MiniSparkline } from "@/app/status/_components/MiniSparkline"; +import { MiniSparkline } from "@/app/_components/MiniSparkline"; import { PRODUCTS } from "@/lib/products"; const BUILD_TIME = new Date().toISOString(); From 11c8fa3ffb65cb55d584980ec9d57709dfd5736a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:08:38 +0000 Subject: [PATCH 38/69] =?UTF-8?q?feat(web):=20product=20page=20overhaul=20?= =?UTF-8?q?=E2=80=94=20ScrambleText=20headings,=20live=20badge,=20sparklin?= =?UTF-8?q?es,=20prev/next=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProductDashboard: ScrambleText on "Live dashboard" heading + Live badge (matches DesignPreview) - ProductCodeSnippet: ScrambleText on "Quick start" heading - Product page: ScrambleText on "What it does" heading - RelatedProducts: ScrambleText on "More from KonjoAI" + MiniSparkline on each card metric - ProductNavHint: new floating prev/next product navigator (lg+ only) visible after scroll, shows glyph + name + keyboard shortcut hint [ / ] for each adjacent product --- apps/web/app/products/[slug]/page.tsx | 12 ++- .../_components/ProductCodeSnippet.tsx | 13 ++- .../products/_components/ProductDashboard.tsx | 19 ++++- .../products/_components/ProductNavHint.tsx | 82 +++++++++++++++++++ .../products/_components/RelatedProducts.tsx | 40 ++++----- 5 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 apps/web/app/products/_components/ProductNavHint.tsx diff --git a/apps/web/app/products/[slug]/page.tsx b/apps/web/app/products/[slug]/page.tsx index f277f68..34ec215 100644 --- a/apps/web/app/products/[slug]/page.tsx +++ b/apps/web/app/products/[slug]/page.tsx @@ -4,6 +4,7 @@ import { ProductHero, StatusBadge } from "@konjoai/ui"; import { Footer } from "@/app/_components/Footer"; import { Breadcrumbs } from "@/app/_components/Breadcrumbs"; import { AnimatedSection } from "@/app/_components/AnimatedSection"; +import { ScrambleText } from "@/app/_components/ScrambleText"; import { ProductDashboard } from "@/app/products/_components/ProductDashboard"; import { ProductMetricStrip } from "@/app/products/_components/ProductMetricStrip"; import { AnimatedFeatureGrid } from "@/app/products/_components/AnimatedFeatureGrid"; @@ -13,6 +14,7 @@ import { ProductCodeSnippet } from "@/app/products/_components/ProductCodeSnippe import { TrackVisit } from "@/app/products/_components/TrackVisit"; import { ShareButton } from "@/app/products/_components/ShareButton"; import { ProductKeyboardNav } from "@/app/products/_components/ProductKeyboardNav"; +import { ProductNavHint } from "@/app/products/_components/ProductNavHint"; import { PRODUCTS, PRODUCT_BY_SLUG } from "@/lib/products"; export function generateStaticParams() { @@ -54,6 +56,7 @@ export default async function ProductPage({ +
-

- What it does -

+
diff --git a/apps/web/app/products/_components/ProductCodeSnippet.tsx b/apps/web/app/products/_components/ProductCodeSnippet.tsx index 0bae5d8..3bcbd2a 100644 --- a/apps/web/app/products/_components/ProductCodeSnippet.tsx +++ b/apps/web/app/products/_components/ProductCodeSnippet.tsx @@ -1,9 +1,10 @@ "use client"; import { useState } from "react"; -import { motion } from "motion/react"; +import { motion, useReducedMotion } from "motion/react"; import { ease } from "@konjoai/ui"; import { toast } from "@/lib/toast"; +import { ScrambleText } from "@/app/_components/ScrambleText"; type Span = { text: string; kind: "kw" | "str" | "cmt" | "fn" | "num" | "plain" }; @@ -207,6 +208,7 @@ function HighlightedCode({ code }: { code: string }) { /** Per-product quick-start code snippet with syntax highlighting and a copy button. */ export function ProductCodeSnippet({ slug }: { slug: string }) { const [copied, setCopied] = useState(false); + const reduce = useReducedMotion(); const snippet = SNIPPETS[slug]; if (!snippet) return null; @@ -223,9 +225,12 @@ export function ProductCodeSnippet({ slug }: { slug: string }) { return (
-

- Quick start -

+ -

- Live dashboard -

+
+ + + + Live + +
diff --git a/apps/web/app/products/_components/ProductNavHint.tsx b/apps/web/app/products/_components/ProductNavHint.tsx new file mode 100644 index 0000000..8ebbf0d --- /dev/null +++ b/apps/web/app/products/_components/ProductNavHint.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence, useReducedMotion } from "motion/react"; +import { ease } from "@konjoai/ui"; +import { PRODUCTS } from "@/lib/products"; + +interface ProductNavHintProps { + currentSlug: string; +} + +/** + * Floating prev/next product navigator — appears after 480px of scroll. + * Provides a visible UI complement to the [ / ] keyboard shortcuts. + */ +export function ProductNavHint({ currentSlug }: ProductNavHintProps) { + const [visible, setVisible] = useState(false); + const router = useRouter(); + const reduce = useReducedMotion(); + + useEffect(() => { + const handler = () => setVisible(window.scrollY > 480); + window.addEventListener("scroll", handler, { passive: true }); + handler(); + return () => window.removeEventListener("scroll", handler); + }, []); + + const idx = PRODUCTS.findIndex((p) => p.slug === currentSlug); + if (idx === -1) return null; + + const prev = PRODUCTS[(idx - 1 + PRODUCTS.length) % PRODUCTS.length]; + const next = PRODUCTS[(idx + 1) % PRODUCTS.length]; + + function goTo(slug: string) { + router.push(`/products/${slug}`); + } + + return ( + + {visible && ( + +
+ + + + + +
+
+ )} +
+ ); +} diff --git a/apps/web/app/products/_components/RelatedProducts.tsx b/apps/web/app/products/_components/RelatedProducts.tsx index af30274..8019405 100644 --- a/apps/web/app/products/_components/RelatedProducts.tsx +++ b/apps/web/app/products/_components/RelatedProducts.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { motion, useReducedMotion } from "motion/react"; import { StatusBadge, severity as sevColor, ease } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; +import { ScrambleText } from "@/app/_components/ScrambleText"; +import { MiniSparkline } from "@/app/_components/MiniSparkline"; interface RelatedProductsProps { currentSlug: string; @@ -21,15 +23,12 @@ export function RelatedProducts({ currentSlug }: RelatedProductsProps) { return (
- - More from KonjoAI - + delay={80} + />
    {related.map((p, i) => { @@ -74,19 +73,22 @@ export function RelatedProducts({ currentSlug }: RelatedProductsProps) {

- - {metricDisplay} - {p.metric.unit} - - - {p.metric.label} - +
+ + {metricDisplay} + {p.metric.unit} + + + {p.metric.label} + +
+
From 7d7e668e8abd85e2fda1569532710379074f3881 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:10:49 +0000 Subject: [PATCH 39/69] feat(web): film-grain overlay + status dots in live ticker - Grain: animated SVG feTurbulence grain texture at 4.5% opacity, fixed full-page overlay; steps(2) animation gives film-grain feel; respects prefers-reduced-motion - LiveTicker: each product entry now shows a status color dot (good/warn/hot/violet) so the ticker communicates system health at a glance --- apps/web/app/_components/Grain.tsx | 13 +++++++++++++ apps/web/app/_components/LiveTicker.tsx | 14 +++++++++++++- apps/web/app/globals.css | 18 ++++++++++++++++++ apps/web/app/layout.tsx | 2 ++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/_components/Grain.tsx diff --git a/apps/web/app/_components/Grain.tsx b/apps/web/app/_components/Grain.tsx new file mode 100644 index 0000000..c2f2d05 --- /dev/null +++ b/apps/web/app/_components/Grain.tsx @@ -0,0 +1,13 @@ +/** + * Full-page noise grain overlay — adds subtle analog film texture. + * Pure CSS/SVG, no JavaScript. pointer-events-none so nothing is blocked. + * Animation respects prefers-reduced-motion via CSS. + */ +export function Grain() { + return ( +
+ ); +} diff --git a/apps/web/app/_components/LiveTicker.tsx b/apps/web/app/_components/LiveTicker.tsx index 908e2bb..04ae143 100644 --- a/apps/web/app/_components/LiveTicker.tsx +++ b/apps/web/app/_components/LiveTicker.tsx @@ -2,6 +2,13 @@ import Link from "next/link"; import { severity as sevColor } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; +const STATUS_COLOR: Record = { + operational: "var(--color-konjo-good)", + degraded: "var(--color-konjo-warm)", + outage: "var(--color-konjo-hot)", + research: "var(--color-konjo-violet)", +}; + /** Duplicated for seamless CSS marquee — scroll by 50% to loop invisibly. */ const TICKER_ITEMS = [...PRODUCTS, ...PRODUCTS]; @@ -26,9 +33,14 @@ export function LiveTicker() { key={`${p.slug}-${i}`} href={`/products/${p.slug}`} tabIndex={i >= PRODUCTS.length ? -1 : 0} - aria-label={`${p.name}: ${p.metric.label} ${val}${p.metric.unit}`} + aria-label={`${p.name}: ${p.metric.label} ${val}${p.metric.unit} — ${p.status}`} className="text-konjo-mono flex shrink-0 items-center gap-2.5 px-6 text-xs text-konjo-fg-muted transition-colors hover:text-konjo-fg" > + + From d65b081719287ba245aff14cf71ebe332e0f1d3e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:12:37 +0000 Subject: [PATCH 40/69] feat(web): palette shows live metrics + DesignPreview cursor spotlight - CommandPalette: each product entry now shows its live metric value (e.g. "42 tok/s") in the accent color alongside the status badge - DesignPreview: spring-smoothed radial cursor spotlight follows the mouse inside the glass panel, matching the ProjectCard interaction pattern --- apps/web/app/_components/CommandPalette.tsx | 22 ++++++++++-- apps/web/app/_components/DesignPreview.tsx | 39 +++++++++++++++++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/apps/web/app/_components/CommandPalette.tsx b/apps/web/app/_components/CommandPalette.tsx index 7e18da0..c9797a4 100644 --- a/apps/web/app/_components/CommandPalette.tsx +++ b/apps/web/app/_components/CommandPalette.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; -import { cn, ease, StatusBadge } from "@konjoai/ui"; +import { cn, ease, StatusBadge, severity as sevColor } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; type RouterCtx = ReturnType; @@ -267,7 +267,15 @@ export function CommandPalette() {

{p.name}

{p.tagline}

- +
+ + {Number.isInteger(p.metric.value) ? p.metric.value : p.metric.value.toFixed(1)}{p.metric.unit} + + +
); @@ -310,7 +318,15 @@ export function CommandPalette() {

- +
+ + {Number.isInteger(p.metric.value) ? p.metric.value : p.metric.value.toFixed(1)}{p.metric.unit} + + +
); diff --git a/apps/web/app/_components/DesignPreview.tsx b/apps/web/app/_components/DesignPreview.tsx index b3ea0e2..2620fd9 100644 --- a/apps/web/app/_components/DesignPreview.tsx +++ b/apps/web/app/_components/DesignPreview.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useRef, useCallback } from "react"; -import { motion, AnimatePresence, useReducedMotion } from "motion/react"; +import { motion, AnimatePresence, useReducedMotion, useMotionValue, useSpring } from "motion/react"; import { ease, cn } from "@konjoai/ui"; import { StreamSection } from "./showcase/StreamSection"; import { MetricsSection } from "./showcase/MetricsSection"; @@ -68,6 +68,24 @@ export function DesignPreview() { const [activeId, setActiveId] = useState("stream"); const active = BLOCKS.find((b) => b.id === activeId) ?? BLOCKS[0]; const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + const panelRef = useRef(null); + const spotX = useMotionValue(0); + const spotY = useMotionValue(0); + const smoothSpotX = useSpring(spotX, { stiffness: 80, damping: 20 }); + const smoothSpotY = useSpring(spotY, { stiffness: 80, damping: 20 }); + + function handlePanelMouseMove(e: React.MouseEvent) { + if (reduce) return; + const rect = panelRef.current?.getBoundingClientRect(); + if (!rect) return; + spotX.set(e.clientX - rect.left); + spotY.set(e.clientY - rect.top); + } + + function handlePanelMouseLeave() { + spotX.set(-999); + spotY.set(-999); + } const handleTabKeyDown = useCallback((e: React.KeyboardEvent, idx: number) => { let nextIdx: number | null = null; @@ -185,12 +203,29 @@ export function DesignPreview() { transition={{ duration: 0.28, ease: ease.nehan }} > + {/* Cursor spotlight overlay */} + {!reduce && ( + + )} From 38f859874adfddf3d25c268c1dc356e0395546ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:15:08 +0000 Subject: [PATCH 41/69] feat(web): colorized terminal output + macOS-dock magnification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TerminalSection: ColoredOutput colorizes "▶" in brand color, numbers+units in accent, [brackets] in violet, quoted strings in warm — output lines look like real ANSI terminals - FloatingDock: macOS-dock-style magnification — hovered button scales to 1.45×, adjacent button to 1.22×; spring physics via motion.button; glow ring on active button - Remove unused END_MS constant from TerminalSection --- apps/web/app/_components/FloatingDock.tsx | 65 ++++++++++++++++---- apps/web/app/_components/TerminalSection.tsx | 20 +++++- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/apps/web/app/_components/FloatingDock.tsx b/apps/web/app/_components/FloatingDock.tsx index 92249ae..3779598 100644 --- a/apps/web/app/_components/FloatingDock.tsx +++ b/apps/web/app/_components/FloatingDock.tsx @@ -6,31 +6,50 @@ import { motion, AnimatePresence, useReducedMotion } from "motion/react"; import { ease } from "@konjoai/ui"; import { PRODUCTS } from "@/lib/products"; +const DOCK_ITEMS = [ + { id: "palette", label: "⌘", tooltip: "Command palette ⌘K" }, + { id: "random", label: "⚄", tooltip: "Surprise me" }, + { id: "top", label: "↑", tooltip: "Back to top" }, +] as const; + +type DockId = typeof DOCK_ITEMS[number]["id"]; + interface DockButtonProps { label: string; tooltip: string; onClick: () => void; + onMouseEnter: () => void; + magnified: boolean; + adjacent: boolean; } -/** Single dock button with a right-anchored tooltip on hover. */ -function DockButton({ label, tooltip, onClick }: DockButtonProps) { +/** Single dock button with tooltip, hover magnification, and ambient glow. */ +function DockButton({ label, tooltip, onClick, onMouseEnter, magnified, adjacent }: DockButtonProps) { + const scale = magnified ? 1.45 : adjacent ? 1.22 : 1; return (
- {/* Tooltip — slides in from the right edge */} {tooltip} - +
); } @@ -38,10 +57,12 @@ function DockButton({ label, tooltip, onClick }: DockButtonProps) { /** * Floating action dock — appears after 400 px of scroll. * Provides quick access to the command palette, a random product, and scroll-to-top. + * Buttons magnify on hover in a macOS-dock-style cascade. */ export function FloatingDock() { const reduce = useReducedMotion(); const [visible, setVisible] = useState(false); + const [hovered, setHovered] = useState(null); const router = useRouter(); useEffect(() => { @@ -54,16 +75,22 @@ export function FloatingDock() { function scrollToTop() { window.scrollTo({ top: 0, behavior: reduce ? "instant" : "smooth" }); } - function openPalette() { document.dispatchEvent(new CustomEvent("konjo:open-palette")); } - function randomProduct() { const idx = Math.floor(Math.random() * PRODUCTS.length); router.push(`/products/${PRODUCTS[idx].slug}`); } + const handlers: Record void> = { + palette: openPalette, + random: randomProduct, + top: scrollToTop, + }; + + const ids = DOCK_ITEMS.map((d) => d.id); + return ( {visible && ( @@ -75,10 +102,24 @@ export function FloatingDock() { transition={{ duration: 0.3, ease: ease.nehan }} className="fixed bottom-6 right-6 z-40 flex flex-col items-center gap-2" aria-label="Floating navigation dock" + onMouseLeave={() => setHovered(null)} > - - - + {DOCK_ITEMS.map((item) => { + const idx = ids.indexOf(item.id); + const hIdx = hovered ? ids.indexOf(hovered) : -1; + const dist = hIdx >= 0 ? Math.abs(idx - hIdx) : 999; + return ( + setHovered(item.id)} + /> + ); + })} )} diff --git a/apps/web/app/_components/TerminalSection.tsx b/apps/web/app/_components/TerminalSection.tsx index 5389e00..b88d666 100644 --- a/apps/web/app/_components/TerminalSection.tsx +++ b/apps/web/app/_components/TerminalSection.tsx @@ -31,7 +31,23 @@ const SCRIPT: TermLine[] = [ const CHAR_MS = 36; const OUT_MS = 100; -const END_MS = 2800; + +/** Applies inline color spans to terminal output lines for a richer ANSI-like appearance. */ +function ColoredOutput({ text }: { text: string }) { + // Split into segments: numbers+units get accent color, ▶ gets brand color, quoted strings get warm + const segments = text.split(/(\d+\.?\d*(?:\s*(?:tok\/s|ms|MB|GB|dim|s|x|%|chunks?))\b|\▶|\[[^\]]+\]|"[^"]*")/g); + return ( + <> + {segments.map((seg, i) => { + if (seg === "▶") return {seg}; + if (/^\d+\.?\d*\s*(?:tok\/s|ms|MB|GB|dim|s|x|%|chunks?)/.test(seg)) return {seg}; + if (/^\[[^\]]+\]$/.test(seg)) return {seg}; + if (/^"[^"]*"$/.test(seg)) return {seg}; + return {seg}; + })} + + ); +} const CLI_FEATURES: { icon: string; title: string; desc: string }[] = [ { icon: "→", title: "Streaming", desc: "Token-by-token output with backpressure. Pipe anywhere." }, @@ -222,7 +238,7 @@ export function TerminalSection() { ) : line.kind === "gap" ? (
) : ( -

{line.text}

+

) )} {!isTypingCmd && ( From 09689b65e4595e09b63704db5c5ec6a14468f83c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:18:02 +0000 Subject: [PATCH 42/69] feat(web): activity feed accent borders + constellation hub crossfade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ActivityFeed: colored left-border accent on each event row using the event's dot color — adds visual rhythm and quick product identification at a glance - ConstellationMap: hub content now uses AnimatePresence with foreignObject for smooth y-slide crossfade between products during auto-cycle and user hover --- apps/web/app/_components/ActivityFeed.tsx | 6 ++ apps/web/app/_components/ConstellationMap.tsx | 63 ++++++++++++------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/apps/web/app/_components/ActivityFeed.tsx b/apps/web/app/_components/ActivityFeed.tsx index 3583aad..ca878f7 100644 --- a/apps/web/app/_components/ActivityFeed.tsx +++ b/apps/web/app/_components/ActivityFeed.tsx @@ -128,6 +128,12 @@ export function ActivityFeed() { transition={{ duration: 0.35, ease: ease.nehan }} className="glass-konjo rounded-konjo group relative overflow-hidden" > + {/* Colored left accent border */} +
- {displaySlug ? ( - <> - - {PRODUCT_MAP[displaySlug].glyph} - - - {PRODUCT_MAP[displaySlug].metric.value}{PRODUCT_MAP[displaySlug].metric.unit} - - - {PRODUCT_MAP[displaySlug].metric.label.toUpperCase()} - - - ) : ( - <> - - KonjoAI - - - 9 products - - - )} + {/* Hub label — foreignObject for smooth AnimatePresence crossfade */} + + + {displaySlug ? ( + + + {PRODUCT_MAP[displaySlug].glyph} + + + {PRODUCT_MAP[displaySlug].metric.value}{PRODUCT_MAP[displaySlug].metric.unit} + + + {PRODUCT_MAP[displaySlug].metric.label.toUpperCase()} + + + ) : ( + + KonjoAI + 9 products + + )} + + {/* Nodes */} From f95b8a887ad94325b741cbbdcaa2ccd2d9bd0ba1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 17:22:22 +0000 Subject: [PATCH 43/69] feat(web): add LiveDemo interactive inference section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a fully interactive AI streaming demo to the homepage — users type a prompt (or pick an example chip), press Run, and watch tokens stream at ~35 tok/s with live throughput, TTFT, and token-count metrics. Canned responses cover squish/inference, kyro/RAG, and the full KonjoAI stack. Status badge transitions through Awaiting → Streaming → Complete via AnimatePresence. Wires the section into page.tsx and adds "demo" to the SectionDots nav. https://claude.ai/code/session_01D2raHvxTxUpokiB7nLZnP9 --- apps/web/app/_components/LiveDemo.tsx | 260 +++++++++++++++++++++++ apps/web/app/_components/SectionDots.tsx | 1 + apps/web/app/page.tsx | 2 + 3 files changed, 263 insertions(+) create mode 100644 apps/web/app/_components/LiveDemo.tsx diff --git a/apps/web/app/_components/LiveDemo.tsx b/apps/web/app/_components/LiveDemo.tsx new file mode 100644 index 0000000..dcbb9f6 --- /dev/null +++ b/apps/web/app/_components/LiveDemo.tsx @@ -0,0 +1,260 @@ +"use client"; + +import { useState, useRef, useCallback, useEffect } from "react"; +import { motion, AnimatePresence, useReducedMotion } from "motion/react"; +import { ease } from "@konjoai/ui"; +import { ScrambleText } from "./ScrambleText"; + +// Pre-defined responses keyed by prompt substring +const CANNED: Array<{ match: RegExp; tokens: string[]; model: string }> = [ + { + match: /squish|inference|token|throughput/i, + model: "squish · mlx-4", + tokens: "squish is KonjoAI 's native inference engine . It targets Apple Silicon via the MLX framework , delivering 42 tok/s on M3 Max — 5.3× over the CPU baseline . The decode path uses Metal-accelerated kernels and avoids Python GIL contention by streaming output asynchronously . Each request reports p50 , p95 , and p99 latency with sub-millisecond overhead .".split(" "), + }, + { + match: /kyro|retrieval|rag|search|vector/i, + model: "kyro · rag", + tokens: "kyro is a hybrid RAG engine that fuses dense vector search with sparse BM25 retrieval , then re-ranks using a ColBERT cross-encoder . The result is NDCG@10 = 0.91 on the BEIR benchmark — 21 points above the vectorDB average . Semantic cache hits drop latency from 68 ms to 2.1 ms . kyro runs entirely on-prem ; your embeddings never leave your infrastructure .".split(" "), + }, + { + match: /konjo|product|portfolio|stack/i, + model: "squish · mlx-4", + tokens: "KonjoAI builds nine complementary AI products : squish for inference , kyro for retrieval , vectro for compression , kairu for latency optimization , miru for vision tracing , toki for adversarial safety , kohaku for episodic memory , lopi for agent orchestration , and drex for hybrid architecture training . Every product ships a first-class CLI , a benchmark suite , and a CLAUDE.md . One design system . Zero compromises .".split(" "), + }, +]; + +const DEFAULT_TOKENS = "I'm ready to answer questions about the KonjoAI stack — inference , retrieval , compression , memory , agents , and more . Type a prompt above and press Run to see a live response stream .".split(" "); + +const EXAMPLE_PROMPTS = [ + "How does squish achieve 42 tok/s?", + "Explain kyro's hybrid RAG pipeline", + "What is the KonjoAI product stack?", +]; + +const CHAR_DELAY = 28; // ms per token (simulates ~35 tok/s) + +/** + * Interactive AI inference demo — type a prompt, see simulated token streaming + * with live throughput and latency metrics. Powered (aesthetically) by squish. + */ +export function LiveDemo() { + const reduce = useReducedMotion(); + const [prompt, setPrompt] = useState(""); + const [streaming, setStreaming] = useState(false); + const [tokens, setTokens] = useState([]); + const [tokenIdx, setTokenIdx] = useState(0); + const [model, setModel] = useState("squish · mlx-4"); + const [elapsed, setElapsed] = useState(0); + const runRef = useRef(0); + const startRef = useRef(0); + + const run = useCallback(() => { + if (streaming) return; + const p = prompt.trim(); + if (!p) return; + const canned = CANNED.find((c) => c.match.test(p)); + const toks = canned?.tokens ?? DEFAULT_TOKENS; + const mdl = canned?.model ?? "squish · mlx-4"; + + setModel(mdl); + setTokens(toks); + setTokenIdx(0); + setElapsed(0); + setStreaming(true); + startRef.current = Date.now(); + runRef.current++; + }, [prompt, streaming]); + + useEffect(() => { + if (!streaming) return; + const run = runRef.current; + if (tokenIdx >= tokens.length) { + setStreaming(false); + setElapsed(Date.now() - startRef.current); + return; + } + const id = setTimeout(() => { + if (runRef.current !== run) return; + setTokenIdx((i) => i + 1); + setElapsed(Date.now() - startRef.current); + }, reduce ? 0 : CHAR_DELAY); + return () => clearTimeout(id); + }, [streaming, tokenIdx, tokens.length, reduce]); + + const displayed = tokens.slice(0, tokenIdx).join(" "); + const toksPerSec = elapsed > 0 ? Math.round((tokenIdx / elapsed) * 1000) : 0; + const latencyMs = 11.8; // simulated first-token latency + + function handleKey(e: React.KeyboardEvent) { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + run(); + } + } + + return ( +
+ +

+ squish · live inference · mlx-4 +

+ +

+ Ask anything about the KonjoAI stack. Responses stream at ~42 tok/s — the same throughput squish achieves on M3 Max. +

+
+ + + {/* Prompt bar */} +
+
+ {EXAMPLE_PROMPTS.map((ex) => ( + + ))} +
+
+