Compare commits

...

11 Commits

Author SHA1 Message Date
3fe3516b9e nook helper 2026-09-22 22:22:13 -04:00
bef8f8414b utilization reduction 2026-09-22 16:38:43 -04:00
f3e9cdf0bd fix sentry + note email delivery 2026-09-06 13:53:49 -04:00
7ecc0f68d7 increase width cap 2026-09-06 01:08:52 -04:00
00a26a9e49 resized+compressed 2026-09-06 01:06:58 -04:00
163c68fcb8 new with temp demo 2026-09-05 20:02:10 -04:00
3149eba196 fix missing height 2026-09-05 17:20:12 -04:00
bc65789431 fix mobile overlap 2026-09-05 16:59:36 -04:00
21f1f07a99 fix download routing, fix layout shift 2026-09-05 16:45:40 -04:00
23f86681af better feature design 2026-09-05 16:06:27 -04:00
8492821f75 better fan ui 2026-09-05 15:08:36 -04:00
43 changed files with 964 additions and 155 deletions

1
.gitignore vendored
View File

@@ -34,3 +34,4 @@ Thumbs.db
# pygienium run-state and check artifacts # pygienium run-state and check artifacts
.pygienium/ .pygienium/
scripts/ scripts/
.cache

View File

@@ -10,21 +10,7 @@ export default defineConfig({
org: "mikefreno", org: "mikefreno",
project: "freno-dev", project: "freno-dev",
authToken: process.env.SENTRY_AUTH_TOKEN, authToken: process.env.SENTRY_AUTH_TOKEN,
telemetry: false, telemetry: false
sourcemaps: {
assets: [
{
type: "bundle",
path: "dist/client/assets/",
urlPrefix: "~/assets/"
},
{
type: "sourcemap",
path: "dist/client/assets/",
urlPrefix: "~/assets/"
}
]
}
}) })
], ],
build: { build: {

View File

@@ -11,7 +11,8 @@
"test:watch": "bun test --watch", "test:watch": "bun test --watch",
"test:coverage": "bun test --coverage", "test:coverage": "bun test --coverage",
"perf": "bun run scripts/perf-test.ts", "perf": "bun run scripts/perf-test.ts",
"perf:compare": "bun run scripts/perf-compare.ts" "perf:compare": "bun run scripts/perf-compare.ts",
"nook:grant": "NODE_ENV=production bun --env-file=.env scripts/grant-nook-license.ts"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.953.0", "@aws-sdk/client-s3": "^3.953.0",

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,21 @@
User-agent: * User-agent: *
Allow: / Allow: /
Allow: /blog Allow: /blog
Allow: /projects
Disallow: /login # Private / dynamic paths — not for indexing.
Disallow: /debug/ # Keeps crawlers off auth, admin, and API pages so they don't burn
# uncached SSR function invocations.
Disallow: /account
Disallow: /analytics
Disallow: /api/
Disallow: /blog/create
Disallow: /blog/edit
Disallow: /checkout
Disallow: /databaseMGMT Disallow: /databaseMGMT
Disallow: /debug/
Disallow: /error-test
Disallow: /login
Disallow: /success
Disallow: /test
Sitemap: https://www.freno.me/sitemap.xml Sitemap: https://www.freno.me/sitemap.xml

View File

@@ -722,3 +722,36 @@ a.hover-underline-animation:hover::after {
display: none; display: none;
} }
} }
/* ── Nook landing: fan card shake, builds with fan speed ───────── */
.fan-card-shake {
transform-origin: left center;
animation-name: fan-shake;
animation-duration: var(--fan-speed, 0.4s);
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
@keyframes fan-shake {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(calc(var(--fan-amp, 0deg) * -1)); }
}
/* Sputtering sparks off the fan card at max speed */
.fan-spark {
position: absolute;
border-radius: 9999px;
background: #ffd23f;
box-shadow: 0 0 4px 1px rgba(255, 140, 0, 0.9);
animation: fan-spark-fly var(--sd, 0.5s) ease-out var(--sdel, 0s) infinite;
}
@keyframes fan-spark-fly {
0% { transform: translate(0, 0) scale(1); opacity: 0; }
10% { opacity: 1; }
100% { transform: translate(var(--sx, 20px), var(--sy, 30px)) scale(0.4); opacity: 0; }
}
/* Demo pulse: battery bolt */
@keyframes nook-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}

View File

@@ -0,0 +1,29 @@
import { HttpHeader } from "@solidjs/start";
/**
* Renders edge-cache response headers for a page route.
*
* Sets `CDN-Cache-Control` so Vercel serves the rendered HTML from the edge
* (no origin re-render) for `maxAge` seconds, then stale-while-revalidate up
* to `staleSeconds`. `Cache-Control: public, max-age=0` keeps browsers
* revalidating, so visitors always get the latest version — only the CDN
* holds a cached copy. Function/CND-Cache-Control overrides the Vercel
* default, so repeated visits and bot crawls stop re-rendering at origin.
*/
export function EdgeCacheHeaders(props: {
/** Seconds the CDN may serve the response fresh. */
maxAge: number;
/** Seconds the CDN serves stale while revalidating (default: 1 day). */
staleSeconds?: number;
}) {
const stale = props.staleSeconds ?? 86400;
return (
<>
<HttpHeader name="Cache-Control" value="public, max-age=0" />
<HttpHeader
name="CDN-Cache-Control"
value={`public, s-maxage=${props.maxAge}, stale-while-revalidate=${stale}`}
/>
</>
);
}

View File

@@ -4,16 +4,16 @@ import { buildMainSiteUrl } from "~/lib/site-context";
export default function SubdomainFooter() { export default function SubdomainFooter() {
return ( return (
<footer class="border-surface0 bg-surface0 relative z-10 border-t py-8"> <footer class="border-surface0 bg-surface0 relative z-10 border-t py-8">
<div class="relative flex items-center text-sm"> <div class="relative flex flex-col items-center gap-2 text-sm sm:flex-row sm:justify-center">
<A <A
href={buildMainSiteUrl()} href={buildMainSiteUrl()}
class="text-text/60 hover:text-text/80 mx-auto text-center underline underline-offset-4 transition-colors" class="text-text/60 hover:text-text/80 text-center underline underline-offset-4 transition-colors"
> >
made with <span class="text-red-400">&lt;3</span> made with <span class="text-red-400">&lt;3</span>
</A> </A>
<A <A
href={buildMainSiteUrl("/downloads")} href={buildMainSiteUrl("/downloads")}
class="text-text/80 hover:text-text absolute right-4 underline underline-offset-4 transition-colors" class="text-text/80 hover:text-text underline underline-offset-4 transition-colors sm:absolute sm:right-4"
> >
see more products see more products
</A> </A>

View File

@@ -1,10 +1,17 @@
import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js"; import {
For,
createEffect,
createSignal,
onCleanup,
onMount,
type JSX
} from "solid-js";
import { import {
IslandPill, IslandPill,
IslandExpanded,
AgentDotGrid, AgentDotGrid,
STATUS, STATUS,
CALM, CALM,
WARM,
ACCENT, ACCENT,
CRITICAL, CRITICAL,
ModuleCard ModuleCard
@@ -13,9 +20,9 @@ import { Campfire, type CampfireState } from "./Campfire";
/** /**
* Detailed feature breakdowns: alternating copy + live-demo sections. * Detailed feature breakdowns: alternating copy + live-demo sections.
* Each demo animates when scrolled into view (IntersectionObserver + * Each demo stages the real interaction loop: attention flares, approval
* motion's animate), staging the real interaction loop: attention * resolves, fans throttle, the charge ceiling holds, meetings surface,
* flares, approval resolves, fans throttle, sessions stream. * the camera wakes, the hook installs.
*/ */
/* Scroll-triggered reveal: adds `in` once the element crosses 25%. */ /* Scroll-triggered reveal: adds `in` once the element crosses 25%. */
@@ -131,9 +138,15 @@ function FleetDemo() {
<div <div
class="rounded-xl px-4 py-3 text-center shadow-lg transition-all duration-500" class="rounded-xl px-4 py-3 text-center shadow-lg transition-all duration-500"
style={{ style={{
background: phase() === "flare" ? "rgba(231,167,98,0.12)" : "rgba(255,255,255,0.055)", background:
phase() === "flare"
? "rgba(231,167,98,0.12)"
: "rgba(255,255,255,0.055)",
"box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)", "box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)",
transform: phase() === "flare" ? "translateY(0) scale(1)" : "translateY(6px) scale(0.98)", transform:
phase() === "flare"
? "translateY(0) scale(1)"
: "translateY(6px) scale(0.98)",
opacity: phase() === "watch" ? 0.55 : 1 opacity: phase() === "watch" ? 0.55 : 1
}} }}
> >
@@ -191,7 +204,9 @@ function ApprovalDemo() {
<span <span
class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold text-white" class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold text-white"
style={{ style={{
background: allowed() ? "rgba(150,220,150,0.25)" : "rgba(234,179,8,0.25)" background: allowed()
? "rgba(150,220,150,0.25)"
: "rgba(234,179,8,0.25)"
}} }}
> >
{allowed() ? "allowed" : "approval"} {allowed() ? "allowed" : "approval"}
@@ -250,14 +265,25 @@ function ApprovalDemo() {
/* ── Demo 3: fans throttle up under load, then back down ──────────── */ /* ── Demo 3: fans throttle up under load, then back down ──────────── */
/* Spark pool: deterministic pseudo-random, count gated by fan speed. */
const sparkRand = (i: number, k: number) => {
const x = Math.sin(i * 127.1 + k * 311.7) * 43758.5453;
return x - Math.floor(x);
};
const SPARKS = Array.from({ length: 20 }, (_, i) => ({
sx: `${Math.round(10 + sparkRand(i, 1) * 26)}px`,
sy: `${Math.round(18 + sparkRand(i, 2) * 30)}px`,
sd: `${(0.4 + sparkRand(i, 3) * 0.25).toFixed(2)}s`,
sdel: `${(sparkRand(i, 4) * 1.2).toFixed(2)}s`,
size: sparkRand(i, 5) > 0.5 ? 3 : 2
}));
function FansDemo() { function FansDemo() {
const [rpm, setRpm] = createSignal(1270); const [rpm, setRpm] = createSignal(1270);
const [maxed, setMaxed] = createSignal(false); const [maxed, setMaxed] = createSignal(false);
onMount(() => { onMount(() => {
let frame = 0; let frame = 0;
const timers: ReturnType<typeof setTimeout>[] = []; const timers: ReturnType<typeof setTimeout>[] = [];
/* Linear ramp toward `target` — close enough to the app's spring
at marketing-frame rates, with zero easing-library surface. */
const ramp = (target: number, durationMs: number) => { const ramp = (target: number, durationMs: number) => {
cancelAnimationFrame(frame); cancelAnimationFrame(frame);
const start = rpm(); const start = rpm();
@@ -293,48 +319,265 @@ function FansDemo() {
{ rpm: Math.round(rpm() * 0.96), frac: (rpm() * 0.96) / 6000 } { rpm: Math.round(rpm() * 0.96), frac: (rpm() * 0.96) / 6000 }
]; ];
const shakeIntensity = () =>
Math.max(0, Math.min(1, (rpm() - 2500) / (6000 - 2500)));
/* 5 sparks when the shake starts, ~20 at max speed. */
const sparkCount = () =>
shakeIntensity() > 0 ? 5 + Math.floor(shakeIntensity() * 15) : 0;
return (
<div class="relative mx-auto w-full max-w-[320px]">
<ModuleCard
class="fan-card-shake relative z-10"
style={{
"--fan-amp": `${(shakeIntensity() * 2.6).toFixed(2)}deg`,
"--fan-speed": `${(0.4 - shakeIntensity() * 0.28).toFixed(3)}s`
}}
>
<div class="space-y-3 text-left">
<For each={fans()}>
{(f, i) => (
<div class="flex items-center gap-3">
<span class="text-[10px] font-medium text-white/50">
Fan {i() + 1}
</span>
<div
class="h-1 flex-1 overflow-hidden rounded-full"
style={{ background: "rgba(255,255,255,0.08)" }}
>
<div
class="h-full rounded-full"
style={{
width: `${Math.min(f.frac, 1) * 100}%`,
background: maxed() ? CRITICAL : CALM,
transition: "width 0.2s linear, background 0.4s"
}}
/>
</div>
<span
class="font-mono text-white"
style={{ "font-size": "13px", "font-weight": 500 }}
>
{Math.round(f.rpm)}
</span>
<span class="text-[9px] font-semibold text-white/50">RPM</span>
</div>
)}
</For>
<div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
<div class="flex items-center gap-2">
<span
class="rounded-md px-2 py-0.5 text-[11px] font-bold transition-colors"
style={{ color: maxed() ? CRITICAL : "rgba(255,255,255,0.7)" }}
>
{maxed() ? "MAX" : "Auto"}
</span>
<span class="text-[10px] text-white/40">
{maxed() ? "cooling session active" : "curve in control"}
</span>
</div>
</div>
</ModuleCard>
<div
class="pointer-events-none absolute z-0"
style={{
right: "6px",
bottom: "-2px",
opacity: sparkCount() > 0 ? "1" : "0",
transition: "opacity 0.3s"
}}
>
<For each={SPARKS.slice(0, sparkCount())}>
{(s) => (
<span
class="fan-spark"
style={{
width: `${s.size}px`,
height: `${s.size}px`,
"--sx": s.sx,
"--sy": s.sy,
"--sd": s.sd,
"--sdel": s.sdel
}}
/>
)}
</For>
</div>
</div>
);
}
/** Fake cursor dot: rests on a target, presses, exits. */
function FakeCursor(props: {
phase: "idle" | "press" | "live";
at: { x: number; y: number };
exit?: { x: number; y: number };
}) {
const p = () =>
props.phase === "press"
? { ...props.at, scale: 0.62, o: 1 }
: props.phase === "idle"
? { ...props.at, scale: 1, o: 1 }
: { ...(props.exit ?? { x: 106, y: 8 }), scale: 1, o: 0 };
return (
<span
class="pointer-events-none absolute z-10 rounded-full bg-white"
style={{
width: "9px",
height: "9px",
"box-shadow":
"0 1px 4px rgba(0,0,0,0.6), 0 0 0 4px rgba(255,255,255,0.18)",
left: `${p().x}%`,
top: `${p().y}%`,
opacity: p().o,
transform: `translate(-50%, -50%) scale(${p().scale})`,
transition:
"left 0.8s cubic-bezier(0.35,0,0.25,1), top 0.8s cubic-bezier(0.35,0,0.25,1), opacity 0.4s, transform 0.3s"
}}
/>
);
}
/* ── Demo 4: the charge ceiling holds, top-up overrules ───── */
function BatteryDemo() {
const [level, setLevel] = createSignal(62);
const [phase, setPhase] = createSignal<"charge" | "held" | "topup">("charge");
onMount(() => {
let frame = 0;
const timers: ReturnType<typeof setTimeout>[] = [];
const ramp = (target: number, durationMs: number) => {
cancelAnimationFrame(frame);
const start = level();
const startTime = performance.now();
const tick = (now: number) => {
const t = Math.min((now - startTime) / durationMs, 1);
setLevel(start + (target - start) * t);
if (t < 1) frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
};
const loop = () => {
setLevel(62);
setPhase("charge");
ramp(80, 2600);
timers.push(setTimeout(() => setPhase("held"), 2800));
timers.push(
setTimeout(() => {
setPhase("topup");
ramp(100, 2600);
}, 5200)
);
};
loop();
const interval = setInterval(loop, 9600);
onCleanup(() => {
clearInterval(interval);
timers.forEach(clearTimeout);
cancelAnimationFrame(frame);
});
});
const pct = () => Math.round(level());
const watts = () => (phase() === "held" ? null : pct() >= 100 ? 9 : 38);
const subline = () =>
phase() === "held"
? "charging paused — at the limit"
: pct() >= 100
? "topped up — re-arms on unplug"
: phase() === "topup"
? `${watts()} W · top-up past the limit`
: `${watts()} W · ${Math.max(1, Math.round((100 - pct()) * 1.5))} min to full`;
return ( return (
<ModuleCard class="mx-auto w-full max-w-[320px]"> <ModuleCard class="mx-auto w-full max-w-[320px]">
<div class="space-y-3 text-left"> <div class="space-y-3 text-left">
<For each={fans()}> <div class="flex items-center gap-2">
{(f, i) => ( <span class="text-[10px] font-semibold tracking-wide text-white/40 uppercase">
<div class="flex items-center gap-3"> Battery
<span class="text-[10px] font-medium text-white/50"> </span>
Fan {i() + 1} <span
</span> class="ml-auto rounded-md px-2 py-0.5 font-mono text-[10px] font-semibold transition-transform duration-300"
<div style={{
class="h-1 flex-1 overflow-hidden rounded-full" color: "rgba(255,255,255,0.75)",
style={{ background: "rgba(255,255,255,0.08)" }} background: "rgba(255,255,255,0.08)",
> transform: phase() === "held" ? "scale(1.12)" : "scale(1)"
<div }}
class="h-full rounded-full" >
style={{ Limit 80%
width: `${Math.min(f.frac, 1) * 100}%`, </span>
background: maxed() ? CRITICAL : CALM, </div>
transition: "width 0.2s linear, background 0.4s" <div class="flex items-baseline gap-1.5">
}} <span class="font-mono text-[22px] font-semibold text-white">
/> {pct()}%
</div> </span>
<span <span class="text-[11px] text-white/50">{subline()}</span>
class="font-mono text-white" </div>
style={{ "font-size": "13px", "font-weight": 500 }} <div
> class="relative h-[6px] overflow-hidden rounded-full"
{Math.round(f.rpm)} style={{ background: "rgba(255,255,255,0.08)" }}
</span> >
<span class="text-[9px] font-semibold text-white/50">RPM</span> <div
</div> class="h-full rounded-full"
)} style={{
</For> width: `${pct()}%`,
background: pct() > 80 ? WARM : CALM,
transition: "background 0.4s"
}}
/>
<span
class="absolute top-0 h-full w-px"
style={{ left: "80%", background: "rgba(255,255,255,0.5)" }}
/>
</div>
<div
class="flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors duration-500"
style={{
background:
phase() === "topup" ? "rgba(110,167,255,0.10)" : "transparent"
}}
>
<div class="flex-1">
<p class="text-[11px] font-semibold text-white/85">Top-up mode</p>
<p class="text-[9px] text-white/40">
charge past the limit while plugged in
</p>
</div>
<span
class="relative inline-flex h-[16px] w-[30px] items-center rounded-full transition-colors duration-300"
style={{
background:
phase() === "topup" ? ACCENT : "rgba(255,255,255,0.15)"
}}
>
<span
class="absolute h-[12px] w-[12px] rounded-full bg-white transition-all duration-300"
style={{ left: phase() === "topup" ? "16px" : "2px" }}
/>
</span>
</div>
<div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} /> <div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <svg
class="rounded-md px-2 py-0.5 text-[11px] font-bold transition-colors" width="10"
style={{ color: maxed() ? CRITICAL : "rgba(255,255,255,0.7)" }} height="13"
viewBox="0 0 10 13"
style={{
animation: watts()
? "nook-pulse 1.1s ease-in-out infinite"
: "none",
opacity: watts() ? 1 : 0.3
}}
> >
{maxed() ? "MAX" : "Auto"} <path
</span> d="M6.2 0 0.8 7h2.6l-1 5.2L7.8 5.4H5l1.2-5.4z"
<span class="text-[10px] text-white/40"> fill="#FFD23F"
{maxed() ? "cooling session active" : "curve in control"} />
</svg>
<span class="text-[10px] text-white/50">
{phase() === "held"
? "holding — the pack stays young longer"
: phase() === "topup"
? "topping up — re-arms when you unplug"
: "on the way to the ceiling"}
</span> </span>
</div> </div>
</div> </div>
@@ -342,77 +585,392 @@ function FansDemo() {
); );
} }
/* ── Demo 4: remote sessions streaming into the island ────────────── */ /* ── Demo 5: the slot counts down, then opens the day ──────── */
const REMOTE_TICKS: {
name: string; const WEEK_LETTERS = ["S", "M", "T", "W", "T", "F", "S"];
via: string; const DAY_EVENTS = [
state: keyof typeof STATUS; { time: "09:3009:45", title: "Standup", loc: "Daily", meeting: true },
line: string; { time: "15:3016:00", title: "Design review", loc: "4th floor", meeting: false }
}[] = [
{ name: "build-box", via: "ssh", state: "running", line: "You: fix flaky test" },
{ name: "gpu-rig", via: "tailscale", state: "running", line: "You: benchmark fp16" }
]; ];
function RemoteDemo() { function CalendarDemo() {
const [tick, setTick] = createSignal(0); const [mins, setMins] = createSignal(14);
const [today, setToday] = createSignal(new Date());
onMount(() => { onMount(() => {
setToday(new Date());
const interval = setInterval( const interval = setInterval(
() => setTick((t) => (t + 1) % REMOTE_TICKS.length), () => setMins((m) => (m <= 0 ? 14 : m - 1)),
2600 900
); );
onCleanup(() => clearInterval(interval)); onCleanup(() => clearInterval(interval));
}); });
const imminent = () => mins() <= 5;
const dialDays = () => {
const base = today();
return [-4, -3, -2, -1, 0, 1, 2, 3, 4].map((off) => {
const d = new Date(
base.getFullYear(),
base.getMonth(),
base.getDate() + off
);
return {
letter: WEEK_LETTERS[d.getDay()],
num: d.getDate(),
today: off === 0,
past: off < 0,
dots: (d.getDate() * 7 + d.getDay() * 3) % 4
};
});
};
return ( return (
<div class="flex flex-col items-center gap-4"> /* Fixed height: the day panel expands into reserved space, never
<IslandExpanded shifts the page (matters most on mobile's stacked layout). */
activeCount={2} <div class="flex h-[316px] flex-col items-center gap-4">
tabs={[ <IslandPill
{ icon: "🔥", label: "Agents", active: true }, campfire="running"
{ icon: "♪", label: "Music" }, right={
{ icon: "◌", label: "System" } <div class="flex items-center gap-1">
]} <svg width="10" height="8" viewBox="0 0 12 9" class="shrink-0">
<path
d="M0 2A1.4 1.4 0 0 1 1.4 0.6h5.6A1.4 1.4 0 0 1 8.4 2v5A1.4 1.4 0 0 1 7 8.4H1.4A1.4 1.4 0 0 1 0 7z M12 2.6 9 4.5l3 1.9z"
fill="rgba(255,255,255,0.75)"
/>
</svg>
<span
class="whitespace-nowrap font-mono text-[9.5px] font-semibold text-white/90"
style={{ color: imminent() ? "#ff9e42" : "rgba(255,255,255,0.9)" }}
>
{mins() === 0 ? "now" : `${mins()}m`}
</span>
</div>
}
/>
{/* the island opening onto the day panel */}
<div
class="w-full max-w-[380px] overflow-hidden rounded-[14px] bg-black transition-all duration-700"
style={{
"max-height": imminent() ? "240px" : "0px",
opacity: imminent() ? 1 : 0,
transform: imminent() ? "translateY(0)" : "translateY(-8px)"
}}
> >
<div class="space-y-2 text-left"> <div class="px-4 pt-3 pb-3 text-left">
<For each={REMOTE_TICKS}> <div class="mb-2 flex items-center">
{(host, i) => ( <span class="text-[11px] font-semibold text-white/50">
<div Calendar
class="rounded-[10px] p-2.5 transition-all duration-500" </span>
style={{ <span class="ml-auto flex h-4 w-4 items-center justify-center text-[12px] font-semibold text-white/50">
background: "rgba(255,255,255,0.055)", +
"box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)", </span>
opacity: i() === tick() ? 1 : 0.55, </div>
transform: i() === tick() ? "scale(1.01)" : "scale(1)" <div class="mb-2 flex gap-1">
}} <For each={dialDays()}>
> {(d) => (
<div class="flex items-center gap-2"> <div
<span class="flex flex-1 flex-col items-center gap-[3px]"
class="inline-block h-[7px] w-[7px] rounded-full" style={{ opacity: d.today ? 1 : d.past ? 0.55 : 0.8 }}
style={{ background: STATUS[host.state] }} >
/> <span class="text-[8px] font-semibold text-white/45">
<span class="text-[13px] font-semibold text-white"> {d.letter}
{host.name}
</span> </span>
<span <span
class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold text-white/75" class="flex h-[20px] w-[20px] items-center justify-center rounded-full text-[10px]"
style={{ background: "rgba(255,255,255,0.08)" }} style={{
background: d.today
? "rgb(255,158,66)"
: "rgba(255,255,255,0.06)",
color: "rgba(255,255,255,0.95)",
"font-weight": d.today ? 700 : 500
}}
> >
{host.via} {d.num}
</span>
<span class="flex h-[4px] gap-[2px]">
<For each={Array.from({ length: Math.min(d.dots, 3) })}>
{() => (
<span
class="rounded-full"
style={{
width: "3.5px",
height: "3.5px",
background: d.today
? "rgb(255,158,66)"
: "rgba(255,255,255,0.55)"
}}
/>
)}
</For>
</span> </span>
</div> </div>
<p class="mt-0.5 text-[11px] text-white/50">{host.line}</p> )}
</For>
</div>
<div
class="mb-1 h-px"
style={{ background: "rgba(255,255,255,0.06)" }}
/>
<For each={DAY_EVENTS}>
{(ev) => (
<div
class="flex items-center gap-2 rounded-md px-1.5 py-1.5 transition-colors duration-500"
style={{
background:
ev.meeting && imminent()
? "rgba(72,151,178,0.16)"
: "transparent"
}}
>
<div>
<p class="font-mono text-[10px] text-white/45">{ev.time}</p>
<p class="text-[11px] font-semibold text-white">{ev.title}</p>
{ev.loc && <p class="text-[10px] text-white/35">{ev.loc}</p>}
</div>
{ev.meeting && (
<span
class="ml-auto rounded-md px-2 py-0.5 text-[10px] font-semibold text-white transition-colors duration-500"
style={{
background: imminent() ? ACCENT : "rgba(255,255,255,0.10)"
}}
>
Open
</span>
)}
</div> </div>
)} )}
</For> </For>
<p class="pt-1 text-center text-[10px] text-white/40">
same island, same glance agents two networks away
</p>
</div> </div>
</IslandExpanded> </div>
</div> </div>
); );
} }
/* ── Demo 5: campfire states ───────────────────────────────────────── */ /* ── Demo 6: the lens only wakes for a click ──────────────── */
function CameraDemo() {
const [phase, setPhase] = createSignal<"idle" | "press" | "live">("idle");
let videoRef: HTMLVideoElement | undefined;
onMount(() => {
const timers: ReturnType<typeof setTimeout>[] = [];
const loop = () => {
setPhase("idle");
timers.push(setTimeout(() => setPhase("press"), 2600));
timers.push(setTimeout(() => setPhase("live"), 3000));
};
loop();
const interval = setInterval(loop, 8800);
onCleanup(() => {
clearInterval(interval);
timers.forEach(clearTimeout);
});
});
createEffect(() => {
if (phase() === "live" && videoRef) {
videoRef.currentTime = 0;
videoRef.play();
} else {
videoRef?.pause();
}
});
return (
<ModuleCard class="relative mx-auto w-full max-w-[300px]">
<div class="space-y-2.5 text-left">
<div class="flex items-center">
<span class="text-[11px] font-semibold text-white/50">Camera</span>
<span
class="ml-auto rounded-md px-2 py-0.5 text-[10px] text-white/60 transition-opacity duration-300"
style={{
background: "rgba(255,255,255,0.06)",
opacity: phase() === "live" ? 1 : 0
}}
>
FaceTime HD Camera
</span>
</div>
<div class="relative h-[120px] overflow-hidden rounded-lg">
{/* inert dashed start button */}
<div
class="absolute inset-0 flex flex-col items-center justify-center gap-2 transition-all duration-500"
style={{
border: "1px dashed rgba(255,255,255,0.14)",
"border-radius": "8px",
opacity: phase() === "live" ? 0 : 1,
transform: phase() === "press" ? "scale(0.96)" : "scale(1)"
}}
>
<svg width="18" height="14" viewBox="0 0 18 14">
<path
d="M2 2.8A1.8 1.8 0 0 1 3.8 1h2.2l1 1.4h4.2A1.8 1.8 0 0 1 13 4.2v6.6a1.8 1.8 0 0 1-1.8 1.8H3.8A1.8 1.8 0 0 1 2 10.8z"
fill="rgba(255,255,255,0.5)"
/>
<circle cx="7.5" cy="7.4" r="2.6" fill="rgba(0,0,0,0.35)" />
</svg>
<span class="text-[12px] font-semibold text-white/80">
Start camera
</span>
</div>
{/* live preview */}
<video
ref={videoRef}
src="/nook/cam-recording.mp4"
muted
playsinline
preload="metadata"
onEnded={() => setPhase("idle")}
class="absolute inset-0 h-full w-full object-cover transition-opacity duration-700"
style={{ opacity: phase() === "live" ? 1 : 0 }}
/>
<FakeCursor phase={phase()} at={{ x: 50, y: 50 }} />
</div>
<p class="text-[10px] text-white/40">
{phase() === "live"
? "live — stops the moment you leave the panel"
: "inert — the lens stays dark until you click"}
</p>
</div>
</ModuleCard>
);
}
/* ── Demo 7: one click copies, sets up, probes — pong ─────── */
const SSH_LINES = [
{ at: 1, text: "scp nook-hook gpu-rig:~/.nook/bin/" },
{ at: 2, text: "setup: bake NOOK_HOST, write ~/.nook/endpoint" },
{ at: 3, text: "nook-hook --probe → pong" }
];
function SshInstallDemo() {
const [step, setStep] = createSignal(0);
onMount(() => {
const timers: ReturnType<typeof setTimeout>[] = [];
const loop = () => {
setStep(0);
timers.push(setTimeout(() => setStep(1), 1600));
timers.push(setTimeout(() => setStep(2), 3000));
timers.push(setTimeout(() => setStep(3), 4200));
timers.push(setTimeout(() => setStep(4), 5400));
};
loop();
const interval = setInterval(loop, 11000);
onCleanup(() => {
clearInterval(interval);
timers.forEach(clearTimeout);
});
});
const cursorPhase = () =>
step() === 0
? ("idle" as const)
: step() === 1
? ("press" as const)
: ("live" as const);
return (
<ModuleCard class="relative mx-auto w-full max-w-[330px]">
<div class="space-y-2.5 text-left">
<div class="flex items-center gap-2">
<div class="min-w-0 flex-1">
<p class="text-[13px] font-semibold text-white">gpu-rig</p>
<p class="truncate font-mono text-[10px] text-white/40">
mike@192.168.64.3 · linux
</p>
</div>
<span
class="rounded-md px-2.5 py-1 text-[11px] font-semibold text-white transition-all duration-300"
style={{
background: step() === 0 ? ACCENT : "rgba(255,255,255,0.08)",
opacity: step() >= 4 ? 0 : 1,
transform: step() === 1 ? "scale(0.94)" : "scale(1)"
}}
>
{step() === 0 ? "Install hook" : "installing…"}
</span>
</div>
<div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
<div class="relative" style={{ "min-height": "86px" }}>
<div
class="space-y-1.5 transition-opacity duration-500"
style={{ opacity: step() >= 4 ? 0 : 1 }}
>
{step() === 0 && (
<p class="pt-2 text-center text-[10px] text-white/35">
one click: copy setup probe
</p>
)}
<For each={SSH_LINES}>
{(l) => (
<div
class="flex items-center gap-1.5 transition-all duration-500"
style={{
opacity: step() >= l.at ? 1 : 0,
transform:
step() >= l.at ? "translateY(0)" : "translateY(4px)"
}}
>
<span
class="font-mono text-[10px]"
style={{
color:
step() > l.at
? "rgba(150,220,150,0.9)"
: "rgba(255,255,255,0.45)"
}}
>
{step() > l.at ? "✓" : "…"}
</span>
<span class="truncate font-mono text-[10px] text-white/70">
{l.text}
</span>
</div>
)}
</For>
</div>
<div
class="absolute inset-0 flex flex-col justify-center transition-all duration-500"
style={{
opacity: step() >= 4 ? 1 : 0,
transform: step() >= 4 ? "translateY(0)" : "translateY(6px)"
}}
>
<div
class="rounded-[10px] p-2.5"
style={{
background: "rgba(255,255,255,0.055)",
"box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)"
}}
>
<div class="flex items-center gap-2">
<span
class="inline-block h-[7px] w-[7px] rounded-full"
style={{ background: STATUS.running }}
/>
<span class="text-[13px] font-semibold text-white">
gpu-rig
</span>
<span
class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold"
style={{
background: "rgba(110,167,255,0.22)",
color: "#9cc3ff"
}}
>
remote
</span>
</div>
<p class="mt-0.5 text-[11px] text-white/50">
You: benchmark fp16
</p>
</div>
<p class="pt-1.5 text-center text-[10px] text-white/40">
agents join the same dot grid
</p>
</div>
</div>
</div>
<FakeCursor phase={cursorPhase()} at={{ x: 90, y: 14 }} />
</ModuleCard>
);
}
/* ── Demo 8: campfire states ───────────────────────────────────────── */
function CampfireDemo() { function CampfireDemo() {
const states: CampfireState[] = [ const states: CampfireState[] = [
@@ -440,7 +998,9 @@ function CampfireDemo() {
> >
<Campfire state={state} pixel={3} /> <Campfire state={state} pixel={3} />
</div> </div>
<p class="text-center text-[11px] font-medium text-white/60">{labels[i()]}</p> <p class="text-center text-[11px] font-medium text-white/60">
{labels[i()]}
</p>
</div> </div>
)} )}
</For> </For>
@@ -458,7 +1018,8 @@ export default function FeatureBreakdowns() {
bullets={[ bullets={[
"12 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, and more", "12 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, and more",
"The campfire dances while work runs, simmers when all is calm", "The campfire dances while work runs, simmers when all is calm",
"Slot rankings — you choose which data earns the notch" "Slot rankings — you choose which data earns the notch",
"You don't need a notch — external displays get the same island, floating top-center"
]} ]}
> >
<FleetDemo /> <FleetDemo />
@@ -470,7 +1031,7 @@ export default function FeatureBreakdowns() {
body="Gated tool calls hold right in the panel. Allow, deny, or demand a reason. Walking away? The same request lands on your phone and your tap flies back to the agent." body="Gated tool calls hold right in the panel. Allow, deny, or demand a reason. Walking away? The same request lands on your phone and your tap flies back to the agent."
bullets={[ bullets={[
"Permission and question cards pin above the session list", "Permission and question cards pin above the session list",
"Push-to-phone over your own network — no cloud middleman", "Push-to-phone over your own network — no cloud middleman (coming soon)",
"Works across every hooked agent, local or remote" "Works across every hooked agent, local or remote"
]} ]}
reversed reversed
@@ -481,8 +1042,9 @@ export default function FeatureBreakdowns() {
<BreakdownSection <BreakdownSection
kicker="Cooling" kicker="Cooling"
title="Fans that answer to you, not the scheduler" title="Fans that answer to you, not the scheduler"
body="Live RPM for every fan, a privileged helper for the curve. MAX before a compile, aggressive when the render queue spikes, auto when you walk away." body="Live RPM for every fan, and the curve is yours. Apple's stock curve waits until you're already cooking — swap to Aggressive and it climbs earlier and harder, or slam MAX before the compile."
bullets={[ bullets={[
"Stock, Aggressive, and MAX — swap curves in one click",
"MAX and COOL sessions surface right in the pill", "MAX and COOL sessions surface right in the pill",
"CPU/GPU die temps tier-color before things get loud", "CPU/GPU die temps tier-color before things get loud",
"All local: your hardware data never leaves the device" "All local: your hardware data never leaves the device"
@@ -492,17 +1054,61 @@ export default function FeatureBreakdowns() {
</BreakdownSection> </BreakdownSection>
<BreakdownSection <BreakdownSection
kicker="Remote" kicker="Power"
title="SSH boxes join the same island" title="Wall power, on your terms"
body="Push the nook-hook to any Linux host — Tailscale or plain SSH with a reverse tunnel — and its agents appear beside your local ones, indistinguishable." body="Live watts from the adapter to the battery to the chassis — and a charge ceiling you set yourself. On recent macOS The Nook drives the system's own limiter; below that it enforces the limit itself. One slider, 50100%, any version."
bullets={[ bullets={[
"One-line install bakes the bridge endpoint for you", "One slider, 50100%, on any macOS version",
"Works for Claude Code, Codex, Gemini CLI and friends", "Hooks the system limiter where it exists, enforces elsewhere",
"Sessions from both machines share one dot grid" "Top-up mode charges past, then re-arms on unplug",
"Thermal protection pauses charging when the pack runs hot",
"Energy flow figure: adapter → battery → system, live watts"
]} ]}
reversed reversed
> >
<RemoteDemo /> <BatteryDemo />
</BreakdownSection>
<BreakdownSection
kicker="Your day"
title="Meetings come to the notch"
body="Today sits in a dial — days behind, days ahead, dots where things happen. The slot counts your next call down, and when it gets close the island opens the day on its own."
bullets={[
"Next meeting counts down right in the slot",
"Near a call, the island opens the day itself",
"Meeting links get an Open button in the row",
"Real calendars and reminders — no sync service"
]}
>
<CalendarDemo />
</BreakdownSection>
<BreakdownSection
kicker="Camera"
title="A lens that stays asleep"
body="A live camera preview in the island for the seconds before a call. It never wakes on its own: permission-gated, inert until you click Start, and dead the moment you leave the panel."
bullets={[
"Nothing watches until Start camera gets a click",
"Live only while the panel sits open",
"A device picker for every connected camera",
"Opt-in module — in no default panel"
]}
reversed
>
<CameraDemo />
</BreakdownSection>
<BreakdownSection
kicker="Remote"
title="Any SSH box, one click away"
body="Point at any SSH-reachable box and click once — the nook-hook installs itself and its agents appear beside your local ones, indistinguishable."
bullets={[
"One click: copy the hook, run setup, probe answers pong",
"Remote agents wear a blue badge in the same dot grid",
"Works for Claude, Codex, Pi and many more"
]}
>
<SshInstallDemo />
</BreakdownSection> </BreakdownSection>
<BreakdownSection <BreakdownSection

View File

@@ -46,7 +46,10 @@ function CollageCell(props: {
*/ */
function InkStage(props: { children: JSX.Element; class?: string }) { function InkStage(props: { children: JSX.Element; class?: string }) {
return ( return (
<div class={`flex justify-center rounded-xl py-5 ${props.class ?? ""}`} style={{ background: "#0d0d0f" }}> <div
class={`flex justify-center rounded-xl py-5 ${props.class ?? ""}`}
style={{ background: "#0d0d0f" }}
>
{props.children} {props.children}
</div> </div>
); );
@@ -153,7 +156,10 @@ function RemoteMock() {
<div class="space-y-1.5"> <div class="space-y-1.5">
<div <div
class="rounded-[10px] p-2.5" class="rounded-[10px] p-2.5"
style={{ background: "rgba(255,255,255,0.055)", "box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)" }} style={{
background: "rgba(255,255,255,0.055)",
"box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)"
}}
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
@@ -172,7 +178,10 @@ function RemoteMock() {
</div> </div>
<div <div
class="rounded-[10px] p-2.5" class="rounded-[10px] p-2.5"
style={{ background: "rgba(255,255,255,0.055)", "box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)" }} style={{
background: "rgba(255,255,255,0.055)",
"box-shadow": "inset 0 0 0 1px rgba(255,255,255,0.07)"
}}
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
@@ -184,7 +193,7 @@ function RemoteMock() {
class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold text-white/75" class="ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-semibold text-white/75"
style={{ background: "rgba(255,255,255,0.08)" }} style={{ background: "rgba(255,255,255,0.08)" }}
> >
tailscale ssh
</span> </span>
</div> </div>
<p class="mt-0.5 text-[11px] text-white/50">Ready</p> <p class="mt-0.5 text-[11px] text-white/50">Ready</p>
@@ -237,7 +246,7 @@ function GaugesMock() {
export default function FeatureCollage() { export default function FeatureCollage() {
return ( return (
<section <section
class="bg-base relative z-20 -mt-28 px-4 pb-16 pt-40 md:px-8" class="bg-base relative z-20 px-4 pt-40 pb-16 md:px-8"
id="feature-collage" id="feature-collage"
> >
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-5xl">
@@ -250,9 +259,7 @@ export default function FeatureCollage() {
<div class="grid grid-cols-1 gap-5 md:grid-cols-3"> <div class="grid grid-cols-1 gap-5 md:grid-cols-3">
{/* Hero: real collapsed pill on the island surface */} {/* Hero: real collapsed pill on the island surface */}
<div <div class="border-overlay1 bg-surface1 relative col-span-1 flex flex-col items-center overflow-hidden rounded-2xl border p-6 md:col-span-2">
class="border-overlay1 bg-surface1 relative col-span-1 flex flex-col items-center overflow-hidden rounded-2xl border p-6 md:col-span-2"
>
<div class="mb-5 self-start"> <div class="mb-5 self-start">
<p class="text-subtext1 mb-3 text-[11px] font-semibold tracking-[0.14em] uppercase"> <p class="text-subtext1 mb-3 text-[11px] font-semibold tracking-[0.14em] uppercase">
The island The island
@@ -261,9 +268,9 @@ export default function FeatureCollage() {
Every agent, one glance Every agent, one glance
</h3> </h3>
<p class="text-subtext0 max-w-sm text-sm leading-relaxed"> <p class="text-subtext0 max-w-sm text-sm leading-relaxed">
A pill tucked into your notch a campfire for your whole A pill tucked into your notch a campfire for your whole fleet
fleet on the left, live data on the right. Blue running, on the left, live data on the right. Blue running, green ready,
green ready, amber needs you. amber needs you.
</p> </p>
</div> </div>
<div class="my-10 flex w-full justify-center"> <div class="my-10 flex w-full justify-center">
@@ -304,7 +311,7 @@ export default function FeatureCollage() {
<CollageCell <CollageCell
kicker="Approvals" kicker="Approvals"
title="Unblock agents, anywhere" title="Unblock agents, anywhere"
body="Permission prompts surface as cards in the panel — Allow once, always, or deny. Away from the desk, push them to your phone and answer from there." body="Permission prompts surface as cards in the panel — Allow once, always, or deny. Away from the desk, push them to your phone and answer from there (coming soon)."
> >
<InkStage> <InkStage>
<div class="w-full max-w-sm px-3"> <div class="w-full max-w-sm px-3">
@@ -330,7 +337,7 @@ export default function FeatureCollage() {
<CollageCell <CollageCell
kicker="Remote" kicker="Remote"
title="Agents on other machines" title="Agents on other machines"
body="Run agents on Linux boxes and SSH servers — a one-line install pipes their sessions back to the same island, over Tailscale or a reverse tunnel." body="Run agents on Linux boxes and SSH servers — one click installs the nook-hook, and their sessions join the same island."
> >
<InkStage class="w-full"> <InkStage class="w-full">
<div class="w-full max-w-[280px] text-left"> <div class="w-full max-w-[280px] text-left">

View File

@@ -33,13 +33,18 @@ export const CARD_STROKE = "rgba(255,255,255,0.07)";
* One module-card surface shared by every mock panel — the app's * One module-card surface shared by every mock panel — the app's
* cardChrome(): rounded 12, hairline stroke, white-on-black fill. * cardChrome(): rounded 12, hairline stroke, white-on-black fill.
*/ */
export function ModuleCard(props: { children: JSX.Element; class?: string }) { export function ModuleCard(props: {
children: JSX.Element;
class?: string;
style?: Record<string, string>;
}) {
return ( return (
<div <div
class={`rounded-xl ${props.class ?? ""}`} class={`rounded-xl ${props.class ?? ""}`}
style={{ style={{
background: CARD_FILL, background: CARD_FILL,
"box-shadow": `inset 0 0 0 1px ${CARD_STROKE}` "box-shadow": `inset 0 0 0 1px ${CARD_STROKE}`,
...props.style
}} }}
> >
<div class="p-3">{props.children}</div> <div class="p-3">{props.children}</div>

View File

@@ -1,5 +1,5 @@
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { HttpStatusCode } from "@solidjs/start"; import { HttpHeader, HttpStatusCode } from "@solidjs/start";
import { useLocation, useNavigate } from "@solidjs/router"; import { useLocation, useNavigate } from "@solidjs/router";
import { createSignal, onCleanup, onMount, Show } from "solid-js"; import { createSignal, onCleanup, onMount, Show } from "solid-js";
import { TerminalErrorPage } from "~/components/TerminalErrorPage"; import { TerminalErrorPage } from "~/components/TerminalErrorPage";
@@ -90,6 +90,14 @@ export default function NotFound() {
description="404 - Page not found. The page you're looking for doesn't exist." description="404 - Page not found. The page you're looking for doesn't exist."
/> />
<HttpStatusCode code={404} /> <HttpStatusCode code={404} />
{/* Cache 404/fallback responses at the edge (Vercel caches 404s) so
bots/monitors that probe dead paths don't re-render the full page
on every request. Function headers override vercel.json here. */}
<HttpHeader name="Cache-Control" value="public, max-age=0" />
<HttpHeader
name="CDN-Cache-Control"
value="public, s-maxage=300, stale-while-revalidate=86400"
/>
<TerminalErrorPage <TerminalErrorPage
errorContent={errorContent} errorContent={errorContent}
quickActions={quickActions} quickActions={quickActions}

23
src/routes/api/health.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { APIEvent } from "@solidjs/start/server";
/**
* Lightweight uptime/monitoring probe.
*
* Returns a tiny 200 that the edge CDN caches (`CDN-Cache-Control`), so
* health checks (Sentry uptime monitors, external monitors, `curl`) cost a
* fraction of a rendered page instead of a full SSR response. Point monitors
* at `https://freno.me/api/health`.
*/
export async function GET(_event: APIEvent) {
return new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: {
"Content-Type": "application/json; charset=utf-8",
// Browser: always revalidate (tiny body, no reason to cache).
"Cache-Control": "public, max-age=0",
// Edge CDN: serve from cache for 1 min, then stale-while-revalidate
// for a day so repeated probe hits never touch the origin.
"CDN-Cache-Control": "public, s-maxage=60, stale-while-revalidate=86400"
}
});
}

View File

@@ -7,6 +7,7 @@ import {
useSearchParams useSearchParams
} from "@solidjs/router"; } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import { createAsync } from "@solidjs/router"; import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import AuthenticatedLike from "~/components/blog/AuthenticatedLike"; import AuthenticatedLike from "~/components/blog/AuthenticatedLike";
@@ -327,6 +328,7 @@ export default function PostPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<PageHead <PageHead
title={p().title.replaceAll("_", " ")} title={p().title.replaceAll("_", " ")}
description={ description={

View File

@@ -1,6 +1,7 @@
import { Show } from "solid-js"; import { Show } from "solid-js";
import { useSearchParams, A, query } from "@solidjs/router"; import { useSearchParams, A, query } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import { createAsync } from "@solidjs/router"; import { createAsync } from "@solidjs/router";
import PostSortingSelect from "~/components/blog/PostSortingSelect"; import PostSortingSelect from "~/components/blog/PostSortingSelect";
import TagSelector from "~/components/blog/TagSelector"; import TagSelector from "~/components/blog/TagSelector";
@@ -90,6 +91,7 @@ export default function BlogIndex() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<PageHead <PageHead
title="Blog" title="Blog"
description="Technical blog posts about web development, programming, and software engineering." description="Technical blog posts about web development, programming, and software engineering."

View File

@@ -3,6 +3,7 @@ import { useSearchParams } from "@solidjs/router";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import RevealDropDown from "~/components/RevealDropDown"; import RevealDropDown from "~/components/RevealDropDown";
import { ContactForm } from "~/components/ContactForm"; import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import { buildSubdomainUrl } from "~/lib/site-context"; import { buildSubdomainUrl } from "~/lib/site-context";
import { useSite } from "~/context/SiteContext"; import { useSite } from "~/context/SiteContext";
import NessaContactPage from "./nessa/contact"; import NessaContactPage from "./nessa/contact";
@@ -125,7 +126,9 @@ function MainContactPage() {
const viewer = () => searchParams.viewer ?? "default"; const viewer = () => searchParams.viewer ?? "default";
return ( return (
<ContactForm <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<ContactForm
subline={ subline={
<Show when={viewer() !== "lineage"}> <Show when={viewer() !== "lineage"}>
(for this website or any of my apps...) (for this website or any of my apps...)
@@ -134,6 +137,7 @@ function MainContactPage() {
> >
<LineageContactQuestions /> <LineageContactQuestions />
</ContactForm> </ContactForm>
</>
); );
} }

View File

@@ -1,4 +1,5 @@
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { createSignal, onMount, onCleanup, Switch, Match } from "solid-js"; import { createSignal, onMount, onCleanup, Switch, Match } from "solid-js";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
@@ -71,6 +72,7 @@ function MainDownloadsPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Downloads" title="Downloads"
description="Download The Nook, InputHalo, Gaze, Life and Lineage, Shapes with Abigail, and Cork. Available on macOS, iOS, and Android." description="Download The Nook, InputHalo, Gaze, Life and Lineage, Shapes with Abigail, and Cork. Available on macOS, iOS, and Android."

View File

@@ -1,4 +1,5 @@
import { ContactForm } from "~/components/ContactForm"; import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
/** /**
@@ -19,6 +20,7 @@ import SubdomainHeader from "~/components/SubdomainHeader";
export default function GazeContactPage() { export default function GazeContactPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<SubdomainHeader /> <SubdomainHeader />
<ContactForm /> <ContactForm />
</> </>

View File

@@ -11,6 +11,7 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
@@ -48,6 +49,7 @@ export default function GazeDownloadsPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Download Gaze" title="Download Gaze"
description="Download Gaze for macOS — menu bar app for eye and posture health." description="Download Gaze for macOS — menu bar app for eye and posture health."

View File

@@ -1,5 +1,6 @@
import { createSignal, For } from "solid-js"; import { createSignal, For } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
@@ -60,6 +61,7 @@ export default function GazeLanding() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Home" title="Home"
description="Gaze is a macOS menu bar app for eye and posture health — blink reminders, 20-20-20 breaks, posture check-ins, and customizable reminder intervals." description="Gaze is a macOS menu bar app for eye and posture health — blink reminders, 20-20-20 breaks, posture check-ins, and customizable reminder intervals."

View File

@@ -15,11 +15,13 @@
*/ */
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
export default function GazePrivacyPolicy() { export default function GazePrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for Gaze, a macOS eye health reminder app." description="Privacy policy for Gaze, a macOS eye health reminder app."

View File

@@ -1,8 +1,10 @@
import { Switch, Match, type JSX } from "solid-js"; import { Switch, Match, type JSX } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import { DarkModeToggle } from "~/components/DarkModeToggle"; import { DarkModeToggle } from "~/components/DarkModeToggle";
import { Typewriter } from "~/components/Typewriter"; import { Typewriter } from "~/components/Typewriter";
import { useSite } from "~/context/SiteContext"; import { useSite } from "~/context/SiteContext";
import { buildSubdomainUrl } from "~/lib/site-context";
import NessaLanding from "./nessa"; import NessaLanding from "./nessa";
import LineageLanding from "./lineage"; import LineageLanding from "./lineage";
import GazeLanding from "./gaze"; import GazeLanding from "./gaze";
@@ -56,6 +58,7 @@ export default function Home(): JSX.Element {
function MainHome(): JSX.Element { function MainHome(): JSX.Element {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Home" title="Home"
description="Michael Freno - Software Engineer based in Brooklyn, NY" description="Michael Freno - Software Engineer based in Brooklyn, NY"
@@ -101,6 +104,32 @@ function MainHome(): JSX.Element {
</Typewriter> </Typewriter>
<div class="pt-8 text-center"> <div class="pt-8 text-center">
<div class="pb-4">Some of my recent projects:</div> <div class="pb-4">Some of my recent projects:</div>
{/* The Nook */}
<div class="border-surface0 mb-2 flex w-full flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My macOS notch utility:</div>
<a
href={buildSubdomainUrl("nook")}
class="text-blue hover-underline-animation mx-auto w-fit"
>
The Nook
</a>
<div class="mx-auto w-full max-w-4xl overflow-hidden rounded-lg">
<video
src="/nook/demo-expansion.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="pt-2 text-left text-sm">
A native macOS island that lives in the notch: orchestration for
a fleet of coding agents, fan curve control, thermal gauges,
calendar and camera modules. One-time purchase - no
subscription, ever.
</div>
</div>
<div class="flex flex-col items-center gap-2 xl:flex-row xl:items-start xl:justify-center"> <div class="flex flex-col items-center gap-2 xl:flex-row xl:items-start xl:justify-center">
{/* FlexLöve */} {/* FlexLöve */}
<div class="border-surface0 flex w-full flex-col rounded-md border-2 p-4 text-center"> <div class="border-surface0 flex w-full flex-col rounded-md border-2 p-4 text-center">

View File

@@ -1,4 +1,5 @@
import { ContactForm } from "~/components/ContactForm"; import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
/** /**
@@ -19,6 +20,7 @@ import SubdomainHeader from "~/components/SubdomainHeader";
export default function InputHaloContactPage() { export default function InputHaloContactPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<SubdomainHeader /> <SubdomainHeader />
<ContactForm /> <ContactForm />
</> </>

View File

@@ -11,6 +11,7 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
@@ -51,6 +52,7 @@ export default function InputHaloDownloadsPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Download InputHalo" title="Download InputHalo"
description="Download InputHalo for macOS — menu bar app for keyboard, mouse, and scroll visualization." description="Download InputHalo for macOS — menu bar app for keyboard, mouse, and scroll visualization."

View File

@@ -13,6 +13,7 @@
import { For, createSignal } from "solid-js"; import { For, createSignal } from "solid-js";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
@@ -98,6 +99,7 @@ export default function InputHaloLanding() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Home" title="Home"
description="A polished macOS menu bar app that visualizes keyboard presses, mouse clicks, cursor halos, and scroll events on screen — for streamers, presenters, and developers." description="A polished macOS menu bar app that visualizes keyboard presses, mouse clicks, cursor halos, and scroll events on screen — for streamers, presenters, and developers."

View File

@@ -23,11 +23,13 @@
*/ */
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
export default function InputHaloPrivacyPolicy() { export default function InputHaloPrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for InputHalo, a macOS menu bar productivity app." description="Privacy policy for InputHalo, a macOS menu bar productivity app."

View File

@@ -1,4 +1,5 @@
import { ContactForm } from "~/components/ContactForm"; import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import { LineageContactQuestions } from "../contact"; import { LineageContactQuestions } from "../contact";
@@ -24,6 +25,7 @@ import { LineageContactQuestions } from "../contact";
export default function LineageContactPage() { export default function LineageContactPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<SubdomainHeader /> <SubdomainHeader />
<ContactForm> <ContactForm>
<LineageContactQuestions /> <LineageContactQuestions />

View File

@@ -33,6 +33,7 @@
* the form posts to the correct tRPC mutation (Lineage-branded email). * the form posts to the correct tRPC mutation (Lineage-branded email).
*/ */
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DeletionForm from "~/components/DeletionForm"; import DeletionForm from "~/components/DeletionForm";
import { import {
@@ -45,6 +46,7 @@ import {
export default function LineageDeletionPage() { export default function LineageDeletionPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead title={PAGE_META.title} description={PAGE_META.description} /> <PageHead title={PAGE_META.title} description={PAGE_META.description} />
<SubdomainHeader /> <SubdomainHeader />
<div class="pt-20"> <div class="pt-20">

View File

@@ -30,6 +30,7 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { createSignal, onMount, onCleanup } from "solid-js"; import { createSignal, onMount, onCleanup } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
@@ -71,6 +72,7 @@ export default function LineageDownloadsPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead title={PAGE_META.title} description={PAGE_META.description} /> <PageHead title={PAGE_META.title} description={PAGE_META.description} />
<SubdomainHeader /> <SubdomainHeader />

View File

@@ -11,6 +11,7 @@
*/ */
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import SimpleParallax from "~/components/SimpleParallax"; import SimpleParallax from "~/components/SimpleParallax";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
@@ -25,6 +26,7 @@ import {
export default function LineageLandingPage() { export default function LineageLandingPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead title={PAGE_META.title} description={PAGE_META.description} /> <PageHead title={PAGE_META.title} description={PAGE_META.description} />
<SubdomainHeader /> <SubdomainHeader />
<SimpleParallax> <SimpleParallax>

View File

@@ -17,11 +17,13 @@
*/ */
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
export default function LineagePrivacyPolicy() { export default function LineagePrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights." description="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights."

View File

@@ -1,4 +1,5 @@
import { ContactForm } from "~/components/ContactForm"; import { ContactForm } from "~/components/ContactForm";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
/** /**
@@ -20,6 +21,7 @@ import SubdomainHeader from "~/components/SubdomainHeader";
export default function NessaContactPage() { export default function NessaContactPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} />
<SubdomainHeader /> <SubdomainHeader />
<ContactForm /> <ContactForm />
</> </>

View File

@@ -24,6 +24,7 @@
* Acceptance: `nessa.localhost:3000/deletion` renders the deletion form. * Acceptance: `nessa.localhost:3000/deletion` renders the deletion form.
*/ */
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import DeletionForm from "~/components/DeletionForm"; import DeletionForm from "~/components/DeletionForm";
import { import {
@@ -36,6 +37,7 @@ import {
export default function NessaDeletionPage() { export default function NessaDeletionPage() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead title={PAGE_META.title} description={PAGE_META.description} /> <PageHead title={PAGE_META.title} description={PAGE_META.description} />
<SubdomainHeader /> <SubdomainHeader />
<div class="pt-20"> <div class="pt-20">

View File

@@ -12,6 +12,7 @@
*/ */
import { For, Show } from "solid-js"; import { For, Show } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import { useDarkMode } from "~/context/darkMode"; import { useDarkMode } from "~/context/darkMode";
import { useSite } from "~/context/SiteContext"; import { useSite } from "~/context/SiteContext";
@@ -59,6 +60,7 @@ export default function NessaLanding() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead {...NESSA_LANDING_META} /> <PageHead {...NESSA_LANDING_META} />
<SubdomainHeader /> <SubdomainHeader />

View File

@@ -29,11 +29,13 @@
*/ */
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
export default function NessaPrivacyPolicy() { export default function NessaPrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for Nessa, a community platform for clubs, challenges, and social features." description="Privacy policy for Nessa, a community platform for clubs, challenges, and social features."

View File

@@ -110,8 +110,8 @@ export default function NookCheckout() {
{error() && <p class="text-red mt-4 text-sm">{error()}</p>} {error() && <p class="text-red mt-4 text-sm">{error()}</p>}
<p class="text-subtext1 mt-4 text-xs"> <p class="text-subtext1 mt-4 text-xs">
Billed once through Stripe. License delivered immediately after Billed once through Stripe. License delivered immediately and by
payment. email after payment.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
import FeatureCollage from "~/components/nook/FeatureCollage"; import FeatureCollage from "~/components/nook/FeatureCollage";
import FeatureBreakdowns from "~/components/nook/FeatureBreakdowns"; import FeatureBreakdowns from "~/components/nook/FeatureBreakdowns";
@@ -8,7 +9,20 @@ import { createSignal, Show } from "solid-js";
import { useDarkMode } from "~/context/darkMode"; import { useDarkMode } from "~/context/darkMode";
import { useSite } from "~/context/SiteContext"; import { useSite } from "~/context/SiteContext";
const NOOK_DOWNLOAD_URL = "https://freno.me/api/downloads/TheNook-0.2.0.zip"; /* The appcast's first enclosure always names the latest release dmg. */
async function downloadNook() {
try {
const res = await fetch("/api/TheNook/appcast.xml");
const xml = await res.text();
const url =
xml.match(/<enclosure url="([^"]+\.dmg)"/)?.[1] ??
xml.match(/<enclosure url="([^"]+\.zip)"/)?.[1];
if (!url) throw new Error("no enclosure");
window.location.href = url;
} catch {
console.error("Could not resolve latest Nook download");
}
}
export default function NookLanding() { export default function NookLanding() {
const site = useSite(); const site = useSite();
@@ -44,6 +58,7 @@ export default function NookLanding() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Home" title="Home"
description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control." description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control."
@@ -55,7 +70,9 @@ export default function NookLanding() {
<SubdomainHeader /> <SubdomainHeader />
{/* ── Hero ─────────────────────────────────────────────────────── */} {/* ── Hero ─────────────────────────────────────────────────────── */}
<div class="relative flex min-h-screen flex-col overflow-hidden"> {/* -mt-14 pulls the hero under the sticky header; pt-14 re-clears
content, so the gradient fills the full viewport incl. the bar */}
<div class="relative -mt-14 flex min-h-svh flex-col overflow-hidden pt-14">
<div <div
class="fixed inset-0 z-0" class="fixed inset-0 z-0"
style={{ style={{
@@ -64,13 +81,13 @@ export default function NookLanding() {
: "radial-gradient(ellipse at top, #d7eef5 0%, #f5f5f5 70%)" : "radial-gradient(ellipse at top, #d7eef5 0%, #f5f5f5 70%)"
}} }}
/> />
<div class="relative z-10 flex min-h-screen flex-col items-center justify-center px-4 py-24 text-center"> <div class="relative z-10 flex flex-1 flex-col items-center justify-center px-4 py-12 text-center sm:py-24">
<img <img
src="/nook/icon.png" src="/nook/icon.png"
alt="The Nook App Icon" alt="The Nook App Icon"
height={128} height={128}
width={128} width={128}
class="mb-6 h-32 w-32 rounded-[22%] object-cover object-center shadow-2xl" class="mb-4 h-24 w-24 rounded-[22%] object-cover object-center shadow-2xl sm:mb-6 sm:h-32 sm:w-32"
/> />
<div <div
class="text-text/90 mb-6 rounded-2xl px-5 py-3 text-sm font-semibold tracking-wide backdrop-blur-sm" class="text-text/90 mb-6 rounded-2xl px-5 py-3 text-sm font-semibold tracking-wide backdrop-blur-sm"
@@ -81,7 +98,7 @@ export default function NookLanding() {
> >
The Nook The Nook
</div> </div>
<h1 class="text-text mb-4 text-5xl font-bold tracking-tight"> <h1 class="text-text mb-4 text-4xl font-bold tracking-tight sm:text-5xl">
Your Mac and Agents, under control Your Mac and Agents, under control
</h1> </h1>
<p class="text-subtext0 mb-2 max-w-xl text-xl"> <p class="text-subtext0 mb-2 max-w-xl text-xl">
@@ -97,7 +114,7 @@ export default function NookLanding() {
variant="download" variant="download"
size="lg" size="lg"
color={brandColor()} color={brandColor()}
onClick={() => (window.location.href = NOOK_DOWNLOAD_URL)} onClick={downloadNook}
> >
Download trial Download trial
</Button> </Button>
@@ -154,7 +171,7 @@ export default function NookLanding() {
variant="download" variant="download"
size="lg" size="lg"
color={brandColor()} color={brandColor()}
onClick={() => (window.location.href = NOOK_DOWNLOAD_URL)} onClick={downloadNook}
> >
Download trial Download trial
</Button> </Button>

View File

@@ -7,11 +7,13 @@
*/ */
import { buildMainSiteUrl } from "~/lib/subdomain-url"; import { buildMainSiteUrl } from "~/lib/subdomain-url";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader"; import SubdomainHeader from "~/components/SubdomainHeader";
export default function NookPrivacyPolicy() { export default function NookPrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for The Nook, a coding-agent orchestration and hardware control app." description="Privacy policy for The Nook, a coding-agent orchestration and hardware control app."

View File

@@ -1,5 +1,6 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
/** /**
* Main-site privacy policy — `freno.me/privacy-policy`. * Main-site privacy policy — `freno.me/privacy-policy`.
@@ -14,6 +15,7 @@ import { PageHead } from "~/components/PageHead";
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy" title="Privacy Policy"
description="Privacy policy for the freno.me blog and personal site, covering accounts, comments, and contact forms." description="Privacy policy for the freno.me blog and personal site, covering accounts, comments, and contact forms."

View File

@@ -1,9 +1,11 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Privacy Policy - Shapes with Abigail" title="Privacy Policy - Shapes with Abigail"
description="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data." description="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data."

View File

@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js"; import { onCleanup, onMount } from "solid-js";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
export default function Resume() { export default function Resume() {
let iframeRef: HTMLIFrameElement | undefined; let iframeRef: HTMLIFrameElement | undefined;
@@ -25,6 +26,7 @@ export default function Resume() {
return ( return (
<> <>
<EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Resume" title="Resume"
description="View Michael Freno's resume - Software Engineer." description="View Michael Freno's resume - Software Engineer."

View File

@@ -67,7 +67,7 @@ const latestAssets: Record<
> = { > = {
gaze: { prefix: "downloads/Gaze-", ext: ".dmg" }, gaze: { prefix: "downloads/Gaze-", ext: ".dmg" },
inputhalo: { prefix: "downloads/InputHalo-", ext: ".dmg" }, inputhalo: { prefix: "downloads/InputHalo-", ext: ".dmg" },
thenook: { prefix: "downloads/TheNook-", ext: ".zip" } thenook: { prefix: "downloads/TheNook-", ext: ".dmg" }
}; };
export const downloadsRouter = createTRPCRouter({ export const downloadsRouter = createTRPCRouter({