);
}
function SectionHeader({ marker, title, sub }) {
return (
{marker}
{title}
{sub ?
{sub}
:
null}
);
}
// ── Use Cases (instant relatability) ──────────────────────────
function UseCasesSection({ copy }) {
return (
{copy.useCases.map((item, i) =>
{item}
)}
);
}
// ── Hidden Mode (terminal easter egg via shake) ──────────────
function HiddenModeSection({ copy }) {
return (
{/* visual hint — phone with shake motion arrows */}
‹›
);
}
// ── Dev Note (personal letter) ───────────────────────────────
function DevNoteSection({ copy }) {
return (
{copy.devNoteTitle}
{copy.devNote.map((p, i) =>
{p}
)}
);
}
// ── Reference (compact command docs) ─────────────────────────
function ReferenceSection({ copy }) {
const [active, setActive] = React.useState(0);
const cmds = copy.refCmds;
// Below this width we keep the tab+single-phone UX; above it we lay out all
// three phones in a row so the section doesn't feel sparse on wide screens.
const [isNarrow, setIsNarrow] = React.useState(
typeof window !== 'undefined' ? window.innerWidth <= 900 : false
);
React.useEffect(() => {
const onResize = () => setIsNarrow(window.innerWidth <= 900);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return (
{isNarrow ?
{/* prev / next arrows beside the phone */}
{cmds.map((c, i) =>
)}
:
// Desktop: all three side by side, no tabs needed.
{cmds.map((c, i) =>
)}
}
{/* CTA below the showcase */}
);
}
function CommandBlock({ c, termPrefix, compact }) {
// Map the legacy `screen` value to the screenshot name used in /assets/img/screens.
const screenName =
c.screen === 'home' ? 'list' :
c.screen === 'new' ? 'add' :
'config';
return (
{/* phone frame with the matching screenshot */}
{/* copy */}
{c.summary}
{c.detail}
);
}
// Real-device screenshot pair, alternating to mimic the cursor blink.
// Renders directly into the phone-frame's viewport at full image height,
// anchored to the top so the relevant top of the screen stays visible when the
// frame is cropped.
function ScreenshotPair({ name }) {
const [frame, setFrame] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setFrame((f) => 1 - f), 530);
return () => clearInterval(id);
}, []);
return (
);
}
function PhoneFrame({ label, children }) {
// If children is a screenshot, skip the scale-down canvas; img fills the
// frame directly anchored to the top. Otherwise use the iPhone-resolution
// canvas (390×844 scaled) that the original JSX screen components expect.
const isScreenshot = children?.type === ScreenshotPair;
// Crop the bottom: phone is only ~2/3 visible. Bottom edge fades.
const PHONE_W = 260;
const VISIBLE_H = 380;
const fadeMask = 'linear-gradient(180deg, #000 0, #000 80%, transparent 100%)';
return (
{screens.map((s, i) => {
// Wrap-around: place each screen at its shortest signed offset
// from the active one so the next/prev neighbour visually
// appears even at the ends of the array.
const n = screens.length;
let offset = i - idx;
if (offset > n / 2) offset -= n;
else if (offset < -n / 2) offset += n;
return (
{s}
);
})}
{/* dots */}
{screens.map((_, i) => (
) : (
// Side-by-side (desktop)
)}
);
}
// ── Download ────────────────────────────────────────────────
function DownloadSection({ copy }) {
return (
{/* radial halo behind the section */}
{/* concentric pulse rings behind the dolphin */}
);
}
// ── Footer ────────────────────────────────────────────────────
function Footer({ copy }) {
const linkStyle = {
color: 'inherit', textDecoration: 'none',
borderBottom: `1px solid ${_HAIR}`, paddingBottom: 1,
transition: 'color 0.15s, border-color 0.15s'
};
// copy.footerCopyright looks like "while² · 2026 · daisandenki" — turn the
// trailing "daisandenki" segment into an X link.
const parts = copy.footerCopyright.split('daisandenki');
return (
);
}
// ── Mac (also-on-mac) ──────────────────────────────────────────
function MacSection({ copy }) {
return (
);
}
// ── AI Says (centered slideshow) ─────────────────────────────
function AISaysSection({ copy }) {
const cards = copy.aiSays;
const N = cards.length;
const [idx, setIdx] = React.useState(0);
const GAP = 24;
// Track viewport so we can keep the card narrow enough on phones for the
// adjacent cards to peek in from both sides.
const [cardW, setCardW] = React.useState(360);
React.useEffect(() => {
const update = () =>
setCardW(Math.min(360, Math.floor(window.innerWidth * 0.68)));
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
const STEP = cardW + GAP;
// Auto-advance: every 6s, move to the next card.
React.useEffect(() => {
const id = setInterval(() => setIdx((i) => (i + 1) % N), 6000);
return () => clearInterval(id);
}, [N]);
return (
{cards.map((r, i) => {
// Shortest-path wrap so the active card stays in view and the
// transition slides seamlessly when wrapping past the ends.
let offset = i - idx;
if (offset > N / 2) offset -= N;
else if (offset < -N / 2) offset += N;
const isActive = offset === 0;
const dist = Math.abs(offset);
return (