fix: product subdomain cleanup

This commit is contained in:
2026-07-23 20:52:49 -04:00
parent a6ef4d2955
commit 8e33af43a5
39 changed files with 1597 additions and 630 deletions

View File

@@ -1,4 +1,5 @@
import { ContactForm } from "~/components/ContactForm";
import SubdomainHeader from "~/components/SubdomainHeader";
/**
* InputHalo contact page (`inputhalo.freno.me/contact`).
@@ -16,5 +17,10 @@ import { ContactForm } from "~/components/ContactForm";
* `[InputHalo] Contact Request`.
*/
export default function InputHaloContactPage() {
return <ContactForm />;
return (
<>
<SubdomainHeader />
<ContactForm />
</>
);
}

View File

@@ -0,0 +1,134 @@
/**
* InputHalo per-subdomain downloads page — `inputhalo.freno.me/downloads`.
*
* Public browser path `/downloads`; vercel.json host rewrites serve this
* from the internal `/inputhalo/*` route prefix while keeping the URL clean.
*
* Mirrors the download surface already exposed on the InputHalo landing page:
* - Direct signed-S3 DMG download via `downloads.getDownloadUrl({ asset_name: "inputhalo" })`.
* - macOS App Store listing link (paid variant — currently a placeholder URL).
*/
import { A } from "@solidjs/router";
import { createSignal } from "solid-js";
import { PageHead } from "~/components/PageHead";
import SubdomainHeader from "~/components/SubdomainHeader";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import Button from "~/components/ui/Button";
import { useDarkMode } from "~/context/darkMode";
import { useSite } from "~/context/SiteContext";
import { api } from "~/lib/api";
import {
INPUTHALO_APP_STORE_URL,
INPUTHALO_ICON_DARK,
INPUTHALO_ICON_DEFAULT,
INPUTHALO_MIN_SYSTEM_VERSION,
performInputHaloDownload
} from "~/routes/inputhalo/download";
export default function InputHaloDownloadsPage() {
const site = useSite();
const { isDark } = useDarkMode();
const [loading, setLoading] = createSignal(false);
const iconSrc = () =>
isDark() ? INPUTHALO_ICON_DARK : INPUTHALO_ICON_DEFAULT;
const download = () => {
if (loading()) return;
setLoading(true);
performInputHaloDownload(
(input) => api.downloads.getDownloadUrl.query(input),
(url) => {
window.location.href = url;
},
() => {
alert("Failed to initiate download. Please try again.");
}
).finally(() => setLoading(false));
};
return (
<>
<PageHead
title="Download InputHalo"
description="Download InputHalo for macOS — menu bar app for keyboard, mouse, and scroll visualization."
/>
<SubdomainHeader />
<main class="bg-base relative min-h-screen w-full overflow-hidden px-4 pb-16">
<div
class="pointer-events-none fixed inset-0 z-0"
aria-hidden="true"
style={{
background: `radial-gradient(50% 40% at 50% 0%, ${site().brandColor}16 0%, transparent 70%)`
}}
/>
<div class="relative z-10 mx-auto flex max-w-2xl flex-col items-center pt-[12vh] text-center">
<img
src={iconSrc()}
alt="InputHalo app icon"
width={128}
height={128}
class="mb-6 h-28 w-28 rounded-[22%] object-cover shadow-2xl"
/>
<h1 class="mb-2 text-4xl font-bold tracking-tight">
Download InputHalo
</h1>
<p class="text-text/70 mb-10 max-w-md text-lg">
Show every keystroke, click, and scroll on macOS.
</p>
<div class="flex w-full flex-col items-center justify-center gap-8 sm:flex-row">
<div class="flex flex-col items-center gap-3">
<span class="text-subtext0 text-sm tracking-wider uppercase">
Direct download
</span>
<Button
variant="download"
size="lg"
loading={loading()}
onClick={download}
>
inputhalo.dmg
</Button>
<span class="text-subtext1 max-w-[240px] text-center text-xs">
macOS {INPUTHALO_MIN_SYSTEM_VERSION}+ · auto-updates via Sparkle
</span>
</div>
<div class="bg-surface0/40 hidden h-24 w-px sm:block" />
<div class="flex flex-col items-center gap-3">
<span class="text-subtext0 text-sm tracking-wider uppercase">
App Store
</span>
<A
class="transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={INPUTHALO_APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</A>
<span class="text-subtext1 max-w-[240px] text-center text-xs">
Paid App Store variant coming soon.
</span>
</div>
</div>
<p class="text-subtext0 mt-16 text-center text-sm">
<A
href="/"
class="text-text/80 hover:text-text underline underline-offset-4 transition-colors"
>
← back to InputHalo
</A>
</p>
</div>
</main>
</>
);
}

View File

@@ -1,29 +1,23 @@
/**
* InputHalo landing page — net-new marketing page for inputhalo.freno.me
* (task 06).
* InputHalo landing page — net-new marketing page for inputhalo.freno.me.
*
* Routes:
* - vercel.json rewrites `inputhalo.freno.me/(.*)` → `/inputhalo/$1`, so
* this `src/routes/inputhalo/index.tsx` serves the subdomain root.
* InputHalo is a polished macOS menu bar app that visualizes mouse and
* keyboard input on screen: keyboard indicators, cursor halos, click ripples,
* scroll indicators, and sensitive-input detection. It is built with SwiftUI
* and distributed via direct DMG (Sparkle auto-updates) with a paid App Store
* variant planned.
*
* Sections:
* - Hero: app icon (dark/light variant via `useDarkMode`), title, tagline
* - Feature highlights (real-time tracking, menu bar, customizable, native)
* - Download: inline macOS DMG button (tRPC → signed S3 URL) + App Store link
*
* The download orchestration lives in the pure, unit-tested
* `./download.ts` helper — this component is a thin wrapper that supplies the
* real tRPC client + `window.location.href` redirect and the loading/error
* UX. The pattern mirrors `downloads.tsx` and the site-aware PageHead /
* nav-config split.
* The download orchestration stays in the testable pure `./download.ts` module;
* this component is a thin wrapper around it plus the landing-page UX.
*/
import { PageHead } from "~/components/PageHead";
import Button from "~/components/ui/Button";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import { glitchText } from "~/lib/client-utils";
import { useDarkMode } from "~/context/darkMode";
import { For, createSignal } from "solid-js";
import { A } from "@solidjs/router";
import { createSignal, onMount, onCleanup } from "solid-js";
import { PageHead } from "~/components/PageHead";
import SubdomainHeader from "~/components/SubdomainHeader";
import Button from "~/components/ui/Button";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import { useDarkMode } from "~/context/darkMode";
import { useSite } from "~/context/SiteContext";
import { api } from "~/lib/api";
import {
INPUTHALO_APP_STORE_URL,
@@ -33,9 +27,55 @@ import {
performInputHaloDownload
} from "./download";
const FEATURES = [
{
title: "Keyboard indicator overlay",
body: "Display the keys you press in a clean, configurable on-screen overlay — perfect for demos and tutorials."
},
{
title: "Mouse halo & click ripples",
body: "Add a subtle halo around your cursor and visual click feedback so viewers never lose track of your pointer."
},
{
title: "Scroll indicators",
body: "Show scroll direction and intensity in real time, useful for screencasts and accessibility demos."
},
{
title: "Sensitive input detection",
body: "InputHalo automatically hides visual feedback when you type into password or secure fields."
},
{
title: "Fully customizable",
body: "Choose colors, sizes, animations, position, and behavior to match your setup and brand."
},
{
title: "Native macOS menu bar app",
body: "Built with SwiftUI, runs quietly in the menu bar, and uses system accessibility APIs responsibly."
}
] as const;
const USE_CASES = [
{
title: "Streamers & creators",
body: "Let your audience see exactly what you're clicking and typing without cluttering your scene."
},
{
title: "Presenters & educators",
body: "Make keyboard shortcuts and cursor movement crystal clear during demos and recordings."
},
{
title: "Developers",
body: "Show input interactions in bug reports, design reviews, and pair-programming sessions."
},
{
title: "Accessibility",
body: "Visualize inputs to make workflows easier to follow for users who benefit from clear feedback."
}
] as const;
export default function InputHaloLanding() {
const site = useSite();
const { isDark } = useDarkMode();
const [titleText, setTitleText] = createSignal("InputHalo");
const [loading, setLoading] = createSignal(false);
const download = () => {
@@ -52,142 +92,196 @@ export default function InputHaloLanding() {
).finally(() => setLoading(false));
};
onMount(() => {
const interval = glitchText(titleText(), setTitleText);
onCleanup(() => clearInterval(interval));
});
const iconSrc = () => (isDark() ? INPUTHALO_ICON_DARK : INPUTHALO_ICON_DEFAULT);
const iconSrc = () =>
isDark() ? INPUTHALO_ICON_DARK : INPUTHALO_ICON_DEFAULT;
const brandColor = () => site().brandColor;
return (
<>
<PageHead
title="InputHalo"
description="Input visualization for mouse and keyboard — a macOS menu bar app for real-time input tracking and customization."
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."
/>
<div class="bg-base relative min-h-screen overflow-hidden px-4 pt-[15vh] pb-12 md:px-8">
{/* Subtle scanline effect */}
<div class="pointer-events-none absolute inset-0 opacity-5">
<SubdomainHeader />
<main
class="relative min-h-screen w-full overflow-x-hidden"
style={{ "--brand-color": brandColor() }}
>
{/* Soft pink halos in the background */}
<div
class="pointer-events-none fixed inset-0 z-0"
aria-hidden="true"
style={{
background: isDark()
? `radial-gradient(60% 50% at 80% 0%, ${brandColor()}20 0%, transparent 70%), radial-gradient(50% 40% at 20% 100%, ${brandColor()}16 0%, transparent 70%)`
: `radial-gradient(60% 50% at 80% 0%, ${brandColor()}16 0%, transparent 70%), radial-gradient(50% 40% at 20% 100%, ${brandColor()}12 0%, transparent 70%)`
}}
/>
{/* ─── Hero ───────────────────────────────────────────────── */}
<section class="relative z-10 flex flex-col items-center px-4 pt-24 pb-16 text-center md:pt-32">
<div
class="h-full w-full"
style={{
"background-image":
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px)"
}}
/>
</div>
<div class="relative z-10 mx-auto max-w-5xl">
{/* Hero Section */}
<div class="mb-16 text-center">
{/* App Icon — dark/light variant matches the Gaze pattern */}
<div class="mb-8 flex justify-center">
<img
src={iconSrc()}
alt="InputHalo app icon"
class="h-32 w-32 rounded-2xl shadow-lg transition-transform duration-200 hover:scale-105"
/>
</div>
{/* Title */}
<h1 class="text-text mb-4 font-mono text-4xl md:text-5xl">
<span class="text-red">{">"}</span> {titleText()}
</h1>
{/* Tagline (from appcast-template.xml) */}
<p class="text-subtext0 mb-2 text-xl italic">
Input visualization for mouse and keyboard
</p>
{/* Platform note */}
<span class="text-subtext1 font-mono text-sm">
macOS menu bar app · minimum macOS {INPUTHALO_MIN_SYSTEM_VERSION}
</span>
class="mb-8 flex h-28 w-28 items-center justify-center rounded-[1.75rem] shadow-2xl md:h-32 md:w-32"
style={{ color: brandColor() }}
>
<img
src={iconSrc()}
alt="InputHalo app icon"
width={128}
height={128}
class="h-full w-full rounded-[1.75rem] object-cover"
/>
</div>
{/* Feature Highlights */}
<div class="mb-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div class="border-overlay0 rounded-lg border p-6 transition-transform duration-200 hover:scale-105">
<h3 class="text-text mb-2 font-mono text-lg">
<span class="text-red">{">"}</span> Real-time Tracking
</h3>
<p class="text-subtext0">
Visualize every mouse click and keyboard press as it happens
</p>
</div>
<h1 class="text-5xl font-bold tracking-tight md:text-7xl">
InputHalo
</h1>
<p class="text-text/85 mt-4 max-w-2xl text-lg md:text-2xl">
Show every keystroke, click, and scroll.
</p>
<p class="text-text/70 mt-3 max-w-xl text-base md:text-lg">
A polished macOS menu bar app for input visualization — built for
streamers, presenters, developers, and anyone who wants their inputs
seen.
</p>
<div class="border-overlay0 rounded-lg border p-6 transition-transform duration-200 hover:scale-105">
<h3 class="text-text mb-2 font-mono text-lg">
<span class="text-red">{">"}</span> Menu Bar App
</h3>
<p class="text-subtext0">
Lightweight presence in your menu bar — access settings anytime
</p>
</div>
<div class="border-overlay0 rounded-lg border p-6 transition-transform duration-200 hover:scale-105">
<h3 class="text-text mb-2 font-mono text-lg">
<span class="text-red">{">"}</span> Customizable
</h3>
<p class="text-subtext0">
Personalize colors, styles, and behavior to match your workflow
</p>
</div>
<div class="border-overlay0 rounded-lg border p-6 transition-transform duration-200 hover:scale-105">
<h3 class="text-text mb-2 font-mono text-lg">
<span class="text-red">{">"}</span> Native macOS
</h3>
<p class="text-subtext0">
Built with Swift and SwiftUI for optimal performance
</p>
</div>
<div class="mt-10 flex flex-col items-center gap-4 sm:flex-row">
<Button
variant="download"
size="lg"
loading={loading()}
onClick={download}
>
download.dmg
</Button>
<A
class="my-auto transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={INPUTHALO_APP_STORE_URL}
target="_blank"
rel="noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</A>
</div>
<p class="text-text/50 mt-3 text-sm">
macOS {INPUTHALO_MIN_SYSTEM_VERSION}+ · auto-updates via Sparkle
</p>
</section>
{/* Download Section */}
<div class="border-overlay0 rounded-lg border p-6 md:p-8">
<h2 class="text-text mb-6 font-mono text-2xl">
<span class="text-red">{">"}</span> Download
{/* ─── Feature highlights ─────────────────────────────────── */}
<section class="bg-surface0/30 relative z-10 px-4 py-20">
<div class="mx-auto max-w-6xl">
<h2 class="text-text mb-12 text-center text-3xl font-bold md:text-4xl">
Made for showing your inputs
</h2>
<div class="flex flex-col gap-8 lg:flex-row lg:justify-around">
{/* DMG Download (tRPC → signed S3 URL) */}
<div class="flex flex-col items-center gap-3">
<span class="text-subtext0 font-mono text-sm">
platform: macOS ({INPUTHALO_MIN_SYSTEM_VERSION}+)
</span>
<Button
variant="download"
size="lg"
loading={loading()}
onClick={download}
>
download.dmg
</Button>
<span class="text-subtext1 text-xs">
# auto-updates via Sparkle
</span>
</div>
{/* App Store (paid — coming soon) */}
<div class="flex flex-col items-center gap-3">
<span class="text-subtext0 font-mono text-sm">
variant: paid (coming soon)
</span>
<A
class="transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={INPUTHALO_APP_STORE_URL}
target="_blank"
rel="noreferrer"
>
<DownloadOnAppStore size={50} />
</A>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<For each={FEATURES}>
{(feature) => (
<div class="border-surface0 bg-base/80 flex flex-col gap-3 rounded-2xl border-2 p-6 backdrop-blur-sm">
<div
class="mb-1 flex h-8 w-8 items-center justify-center rounded-full text-white"
style={{ background: brandColor() }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-5 w-5"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
</div>
<h3 class="text-lg font-semibold">{feature.title}</h3>
<p class="text-text/80 text-sm leading-relaxed">
{feature.body}
</p>
</div>
)}
</For>
</div>
</div>
</div>
</div>
</section>
{/* ─── Use cases ──────────────────────────────────────────── */}
<section class="relative z-10 px-4 py-20">
<div class="mx-auto max-w-6xl">
<h2 class="text-text mb-12 text-center text-3xl font-bold md:text-4xl">
Who it's for
</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<For each={USE_CASES}>
{(use) => (
<div class="border-surface0 hover:bg-surface0/30 flex flex-col gap-2 rounded-2xl border-2 p-6 transition-colors">
<h3
class="text-lg font-semibold"
style={{ color: brandColor() }}
>
{use.title}
</h3>
<p class="text-text/80 text-sm leading-relaxed">
{use.body}
</p>
</div>
)}
</For>
</div>
</div>
</section>
{/* ─── Download CTA ───────────────────────────────────────── */}
<section class="bg-surface0/30 relative z-10 px-4 py-20">
<div class="mx-auto max-w-4xl text-center">
<h2 class="text-text mb-4 text-3xl font-bold">
Ready to visualize your inputs?
</h2>
<p class="text-subtext0 mx-auto mb-10 max-w-2xl leading-relaxed">
Download the latest signed macOS build directly, or grab the App
Store version once it launches.
</p>
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
<Button
variant="download"
size="lg"
loading={loading()}
onClick={download}
>
download.dmg
</Button>
<A
class="my-auto transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={INPUTHALO_APP_STORE_URL}
target="_blank"
rel="noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</A>
</div>
<p class="text-subtext1 mt-6 text-xs">
Requires macOS {INPUTHALO_MIN_SYSTEM_VERSION} or later.
</p>
</div>
</section>
{/* ─── Footer ─────────────────────────────────────────────── */}
<footer class="border-surface0 relative z-10 border-t px-4 py-10">
<div class="text-text/60 mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 text-sm sm:flex-row">
<span>{site().displayName}</span>
<A
href="https://freno.me"
class="hover:text-text underline-offset-4 hover:underline"
>
freno.me
</A>
</div>
</footer>
</main>
</>
);
}

View File

@@ -24,6 +24,7 @@
*/
import { A } from "@solidjs/router";
import { PageHead } from "~/components/PageHead";
import SubdomainHeader from "~/components/SubdomainHeader";
export default function InputHaloPrivacyPolicy() {
return (
@@ -32,15 +33,15 @@ export default function InputHaloPrivacyPolicy() {
title="Privacy Policy"
description="Privacy policy for InputHalo, a macOS menu bar productivity app."
/>
<SubdomainHeader />
<div class="min-h-screen px-[8vw] py-[10vh]">
<div class="py-4 text-xl">InputHalo&apos;s Privacy Policy</div>
<div class="py-2">Last Updated: February 9, 2026</div>
<div class="py-2">Last Updated: July 23, 2026</div>
<div class="py-2">
Welcome to InputHalo (&apos;We&apos;, &apos;Us&apos;,
&apos;Our&apos;). Your privacy is important to us. This privacy
policy will help you understand our policies and procedures related
to the collection, use, and storage of personal information from our
users.
&apos;Our&apos;). Your privacy is important to us. This privacy policy
will help you understand our policies and procedures related to the
collection, use, and storage of personal information from our users.
</div>
<ol>
<div class="py-2">
@@ -52,17 +53,17 @@ export default function InputHaloPrivacyPolicy() {
<div class="-ml-6">(a) Collection of Personal Data:</div>{" "}
InputHalo is designed with privacy as a core principle. We
currently do not collect, store, or share any personal
information from our users. The app runs entirely on your
device as a menu bar utility and does not require any account
creation or data transmission to external servers.
information from our users. The app runs entirely on your device
as a menu bar utility and does not require any account creation
or data transmission to external servers.
</div>
<div class="pb-2">
<div class="-ml-6">(b) Local-Only Settings:</div> Any
preferences, configuration, or cached state InputHalo maintains
is stored locally on your device using standard macOS
mechanisms (such as the user defaults system). This data never
leaves your device and is not transmitted to us or to any
third-party service.
is stored locally on your device using standard macOS mechanisms
(such as the user defaults system). This data never leaves your
device and is not transmitted to us or to any third-party
service.
</div>
<div class="pb-2">
<div class="-ml-6">(c) Future Data Collection:</div> We may in