Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions site/app/components/before-after.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { AnimatePresence, motion, useInView } from "motion/react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { ScrollReveal } from "./scroll-reveal";

Expand Down Expand Up @@ -217,7 +217,13 @@ function CodePanel({
</div>
))}
</div>
<pre className="py-3 sm:py-5 px-3 sm:px-5 text-xs sm:text-sm font-mono leading-relaxed overflow-x-hidden flex-1">
<pre
// The gutter is a sibling, so scrolling here and not on the rounded shell
// keeps the line numbers pinned while the code moves. min-w-0 because a
// flex item is otherwise sized to its content: the panel would widen past
// a 375px viewport instead of scrolling inside it.
className="py-3 sm:py-5 px-3 sm:px-5 text-xs sm:text-sm font-mono leading-relaxed overflow-x-auto min-w-0 flex-1"
>
<code>
{lines.map((line, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: static code lines
Expand Down Expand Up @@ -269,6 +275,25 @@ export function BeforeAfter() {
const sectionRef = useRef<HTMLDivElement>(null);
const inView = useInView(sectionRef, { once: true, amount: 0.2 });
const [showAfter, setShowAfter] = useState(false);
/*
* The panel swap is a full-panel scale plus a brightness flash, and it also
* fires on its own after 2s — motion the reader never asked for. Under
* prefers-reduced-motion the duration drops to 0 so the panels still swap,
* they just do not travel.
*
* useReducedMotion rather than matchMedia in render: matchMedia does not exist
* on the server, so reading it during render would throw there and hydrate
* mismatched here.
*
* Two things the hook does NOT do, in motion@12: it does not react to the OS
* setting changing mid-session (it snapshots once via useState — upstream's own
* TODO), and it returns null on the server rather than a boolean. Neither bites:
* null is falsy, so SSR renders the full duration, and `transition` is never
* serialized into the DOM, so there is nothing for hydration to disagree about.
* If mid-session reactivity is ever wanted, it needs a real listener — do not
* assume the hook already provides one.
*/
const shouldReduceMotion = useReducedMotion();

// Auto-flip from Before to After after 2s of being in view
useEffect(() => {
Expand All @@ -278,7 +303,7 @@ export function BeforeAfter() {
}, [inView]);

return (
<section className="relative py-24 sm:py-32 px-6">
<section className="relative py-24 sm:py-32 safe-x">
<div className="max-w-5xl mx-auto" ref={sectionRef}>
{/* Section header */}
<div className="text-center mb-14">
Expand All @@ -304,11 +329,17 @@ export function BeforeAfter() {

{/* Animated toggle pills */}
<ScrollReveal delay={0.3}>
{/*
* The pills are a two-state toggle, not navigation: without aria-pressed a
* screen reader announces two ordinary buttons and never says which panel
* is currently on screen — including after the 2s auto-flip moves it.
*/}
<div className="flex items-center justify-center gap-2 mb-8">
<button
type="button"
aria-pressed={!showAfter}
onClick={() => setShowAfter(false)}
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all duration-300 ${
className={`focus-ring inline-flex min-h-[44px] items-center justify-center px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all duration-300 ${
!showAfter
? "bg-danger/10 border border-danger/30 text-danger/80 shadow-[0_0_20px_rgba(239,68,68,0.1)]"
: "border border-white/[0.06] text-white/30 hover:text-white/50"
Expand All @@ -318,15 +349,20 @@ export function BeforeAfter() {
</button>
<motion.span
animate={{ x: showAfter ? 4 : -4 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
transition={
shouldReduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 300, damping: 20 }
}
className="text-white/20 text-lg"
>
</motion.span>
<button
type="button"
aria-pressed={showAfter}
onClick={() => setShowAfter(true)}
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all duration-300 ${
className={`focus-ring inline-flex min-h-[44px] items-center justify-center px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all duration-300 ${
showAfter
? "bg-ut/10 border border-ut/30 text-ut shadow-[0_0_20px_rgba(52,211,153,0.1)]"
: "border border-white/[0.06] text-white/30 hover:text-white/50"
Expand All @@ -346,7 +382,7 @@ export function BeforeAfter() {
initial={{ opacity: 0, scale: 0.97, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 1.02, filter: "brightness(2)" }}
transition={{ duration: 0.5, ease: "easeInOut" }}
transition={{ duration: shouldReduceMotion ? 0 : 0.5, ease: "easeInOut" }}
className="relative rounded-xl border border-danger/20 overflow-hidden"
style={{
background: "rgba(239,68,68,0.02)",
Expand Down Expand Up @@ -377,7 +413,7 @@ export function BeforeAfter() {
initial={{ opacity: 0, scale: 0.97, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.97, y: -10 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
transition={{ duration: shouldReduceMotion ? 0 : 0.4, ease: "easeInOut" }}
className="relative rounded-xl border border-ut/20"
style={{
background: "rgba(52,211,153,0.03)",
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/built-for.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const useCases = [

export function BuiltFor() {
return (
<section className="relative py-20 sm:py-24 px-6">
<section className="relative py-20 sm:py-24 safe-x">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-12">
<ScrollReveal>
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/byok.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const providers = [

export function BYOK() {
return (
<section className="relative py-24 sm:py-28 px-6 border-t border-b border-white/[0.06]">
<section className="relative py-24 sm:py-28 safe-x border-t border-b border-white/[0.06]">
<div className="max-w-5xl mx-auto flex flex-col items-center gap-10 text-center">
<div className="flex flex-col gap-4 max-w-lg">
<ScrollReveal>
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/code-example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TypewriterCode } from "./typewriter-code";

export function CodeExample() {
return (
<section id="code" className="relative py-24 sm:py-32 px-6">
<section id="code" className="relative py-24 sm:py-32 safe-x">
<div className="max-w-5xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* Left: copy */}
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/copy-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function CopyCommand() {
<button
type="button"
onClick={handleCopy}
className="gradient-border group relative flex w-full items-center justify-between gap-4 rounded-xl border border-white/[0.06] px-5 py-3.5 cursor-pointer hover:border-ut/30 hover:shadow-[0_0_20px_rgba(52,211,153,0.1)] transition-all duration-200"
className="focus-ring gradient-border group relative flex w-full items-center justify-between gap-4 rounded-xl border border-white/[0.06] px-5 py-3.5 cursor-pointer hover:border-ut/30 hover:shadow-[0_0_20px_rgba(52,211,153,0.1)] transition-all duration-200"
style={{ background: "rgba(255,255,255,0.06)" }}
aria-label="Copy install command"
>
Expand Down
6 changes: 3 additions & 3 deletions site/app/components/cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ScrollReveal } from "./scroll-reveal";

export function CTA() {
return (
<section className="relative py-32 sm:py-40 px-6">
<section className="relative py-32 sm:py-40 safe-x">
<div className="max-w-[640px] mx-auto">
<div className="shimmer-border flex flex-col items-center gap-8 text-center p-8 sm:p-12 rounded-2xl border border-white/[0.04] bg-white/[0.01]">
<div className="flex flex-col gap-4">
Expand All @@ -28,7 +28,7 @@ export function CTA() {
<div className="flex flex-wrap items-center justify-center gap-3">
<a
href="#code"
className="inline-flex items-center justify-center gap-2.5 px-6 py-3 min-h-[44px] bg-ut text-brand-bg rounded-lg text-sm font-semibold hover:bg-ut/90 active:scale-[0.98] transition-all duration-150 shadow-[0_0_30px_rgba(52,211,153,0.3),0_0_80px_rgba(52,211,153,0.12)] hover:shadow-[0_0_40px_rgba(52,211,153,0.4),0_0_100px_rgba(52,211,153,0.2)]"
className="focus-ring inline-flex items-center justify-center gap-2.5 px-6 py-3 min-h-[44px] bg-ut text-brand-bg rounded-lg text-sm font-semibold hover:bg-ut/90 active:scale-[0.98] transition-all duration-150 shadow-[0_0_30px_rgba(52,211,153,0.3),0_0_80px_rgba(52,211,153,0.12)] hover:shadow-[0_0_40px_rgba(52,211,153,0.4),0_0_100px_rgba(52,211,153,0.2)]"
>
Start Trusting
</a>
Expand All @@ -37,7 +37,7 @@ export function CTA() {
href="https://github.com/usertools-ai/usertrust"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 px-6 py-3 min-h-[44px] border border-white/[0.12] rounded-lg text-sm font-medium text-white/80 hover:bg-white/[0.05] hover:text-white hover:border-white/20 hover:shadow-[0_0_20px_rgba(255,255,255,0.04)] transition-all duration-200"
className="focus-ring inline-flex items-center justify-center gap-2 px-6 py-3 min-h-[44px] border border-white/[0.12] rounded-lg text-sm font-medium text-white/80 hover:bg-white/[0.05] hover:text-white hover:border-white/20 hover:shadow-[0_0_20px_rgba(255,255,255,0.04)] transition-all duration-200"
>
<GitHubIcon className="w-4 h-4" />
View on GitHub
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function SpotlightCard({ children, className }: { children: React.ReactNode; cla

export function Features() {
return (
<section id="features" className="relative py-24 sm:py-32 px-6">
<section id="features" className="relative py-24 sm:py-32 safe-x">
<div className="max-w-5xl mx-auto flex flex-col gap-12">
{/* Header */}
<div className="flex flex-col gap-4 max-w-xl">
Expand Down
40 changes: 24 additions & 16 deletions site/app/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function Footer() {
return (
<footer className="relative">
<div className="h-px bg-gradient-to-r from-transparent via-ut/15 to-transparent" />
<div className="max-w-5xl mx-auto px-6 py-12">
<div className="max-w-5xl mx-auto safe-x py-12">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-8 mb-10">
{/* Brand column */}
<div className="col-span-2 sm:col-span-1 flex flex-col gap-3">
Expand All @@ -14,78 +14,86 @@ export function Footer() {
</p>
</div>

{/* Product */}
<div className="flex flex-col gap-2.5">
{/*
* Link rows are 44px tall for the touch target, and the column carries no
* `gap` because those rows already supply the separation — a gap on top of
* them stacks two spacings and doubles the footer's height on a phone.
*
* `items-end`, not `items-center`: `.animated-underline::after` is pinned to
* `bottom: -2px` of the anchor box, so centring the label inside a 44px row
* strands the hover underline 12px beneath the word it belongs to.
*/}
<div className="flex flex-col">
<span className="text-xs font-medium text-white/50 uppercase tracking-wider">
Product
</span>
<a
href="#features"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
Features
</a>
<a
href="#how"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
How it works
</a>
<a
href="#code"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
Quick start
</a>
<a
href="/docs"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
Docs
</a>
</div>

{/* Resources */}
<div className="flex flex-col gap-2.5">
<div className="flex flex-col">
<span className="text-xs font-medium text-white/50 uppercase tracking-wider">
Resources
</span>
<a
href="https://github.com/usertools-ai/usertrust"
target="_blank"
rel="noopener noreferrer"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
GitHub
</a>
<a
href="https://www.npmjs.com/package/usertrust"
target="_blank"
rel="noopener noreferrer"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
npm
</a>
<a
href="https://github.com/usertools-ai/usertrust/blob/master/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
License
</a>
</div>

{/* Company */}
<div className="flex flex-col gap-2.5">
<div className="flex flex-col">
<span className="text-xs font-medium text-white/50 uppercase tracking-wider">
Company
</span>
<a
href="https://usertools.ai"
target="_blank"
rel="noopener noreferrer"
className="animated-underline text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
className="focus-ring animated-underline flex min-h-[44px] items-end text-sm text-white/30 hover:text-white/70 transition-colors duration-200"
>
Usertools
</a>
Expand All @@ -100,23 +108,23 @@ export function Footer() {
href="https://usertools.ai"
target="_blank"
rel="noopener noreferrer"
className="text-white/30 hover:text-ut transition-colors duration-200"
className="focus-ring text-white/30 hover:text-ut transition-colors duration-200"
>
usertools.ai
</a>
</p>
<div className="flex items-center gap-4">
<a
href="#top"
className="text-xs text-white/20 hover:text-ut transition-colors duration-200"
className="focus-ring inline-flex min-h-[44px] items-center text-xs text-white/20 hover:text-ut transition-colors duration-200"
>
Back to top ↑
</a>
<a
href="https://github.com/usertools-ai/usertrust"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-white/20 hover:text-white/50 transition-colors duration-200"
className="focus-ring inline-flex min-h-[44px] items-center gap-1.5 text-xs text-white/20 hover:text-white/50 transition-colors duration-200"
>
<GitHubIcon className="w-3.5 h-3.5" />
Star on GitHub
Expand Down
9 changes: 8 additions & 1 deletion site/app/components/governance-receipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,14 @@ export function GovernanceReceipt() {
)}
</div>

<pre className="p-3 sm:p-5 text-xs sm:text-sm font-mono leading-relaxed overflow-x-hidden">
<pre
// The rounded shell above keeps overflow-hidden — it is what clips the
// content to the rounded border — so the scroll container has to be this
// element instead. At
// 375px the receipt's longest lines are otherwise clipped unreachably
// rather than merely off-screen.
className="p-3 sm:p-5 text-xs sm:text-sm font-mono leading-relaxed overflow-x-auto"
>
<code>
{RECEIPT_LINES.map((line, i) => {
const indent = INDENTS[i] ?? 0;
Expand Down
14 changes: 11 additions & 3 deletions site/app/components/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ export function Hero({ downloads, stars }: { downloads: string; stars: string })
return (
<section
ref={sectionRef}
className="relative min-h-screen flex flex-col items-center justify-start text-center px-6 pt-[18vh]"
// 100dvh, not 100vh: iOS Safari measures vh against the viewport with the
// toolbar retracted, so a 100vh hero is taller than what is on screen and
// the page jerks as the toolbar hides.
//
// No vh fallback, deliberately. dvh is Safari 15.4+, while the emitted
// stylesheet already uses @property and color-mix() — Tailwind v4's own
// floor, Safari 16.4+. Any engine that can parse this CSS supports dvh, and
// Lightning CSS strips a paired vh declaration as provably dead.
className="relative min-h-[100dvh] flex flex-col items-center justify-start text-center safe-x pt-[18dvh]"
style={{ zIndex: 1 }}
>
{/* Bliss background — scroll-fades */}
Expand Down Expand Up @@ -142,15 +150,15 @@ export function Hero({ downloads, stars }: { downloads: string; stars: string })
>
<a
href="#code"
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 min-h-[44px] bg-ut text-brand-bg rounded-lg text-sm font-semibold hover:bg-ut/90 active:scale-[0.98] transition-all duration-150 shadow-[0_0_20px_rgba(52,211,153,0.3),0_0_60px_rgba(52,211,153,0.1)]"
className="focus-ring inline-flex items-center justify-center gap-2 px-5 py-2.5 min-h-[44px] bg-ut text-brand-bg rounded-lg text-sm font-semibold hover:bg-ut/90 active:scale-[0.98] transition-all duration-150 shadow-[0_0_20px_rgba(52,211,153,0.3),0_0_60px_rgba(52,211,153,0.1)]"
>
Start Trusting
</a>
<a
href="https://github.com/usertools-ai/usertrust"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 min-h-[44px] bg-white/[0.06] border border-white/10 rounded-lg text-sm font-medium text-white/80 hover:bg-white/[0.10] hover:text-white transition-all duration-150"
className="focus-ring inline-flex items-center justify-center gap-2 px-5 py-2.5 min-h-[44px] bg-white/[0.06] border border-white/10 rounded-lg text-sm font-medium text-white/80 hover:bg-white/[0.10] hover:text-white transition-all duration-150"
>
View on GitHub
</a>
Expand Down
2 changes: 1 addition & 1 deletion site/app/components/how-it-works.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ArchAnnotation({
/* ------------------------------------------------------------------ */
export function HowItWorks() {
return (
<section id="how" className="relative py-24 sm:py-32 px-4 sm:px-6">
<section id="how" className="relative py-24 sm:py-32 px-4 sm:safe-x">
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="text-center mb-12 sm:mb-20">
Expand Down
Loading
Loading