Simplify application copy and improve landing page spacing

This commit is contained in:
syntaxbullet
2026-10-25 14:42:39 +01:00
parent 36706c6734
commit 61001188b7
15 changed files with 53 additions and 127 deletions

View File

@@ -255,7 +255,7 @@ describe("homepage", () => {
test("signed-out visitors can try progress without calling private habit APIs", async () => {
await render("/");
expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm.");
expect(container.querySelector("h1")?.textContent).toBe("Track your habits");
expect(fetchMock.mock.calls.map(call => call[0])).toEqual(["/api/me"]);
await act(async () => buttonNamed("Increase glasses of water").click());
await settleDeferred();
@@ -278,7 +278,7 @@ describe("homepage", () => {
let resolve!: (response: Response) => void;
fetchMock.mockImplementation(Object.assign(() => new Promise<Response>(done => { resolve = done; }), { preconnect: fetch.preconnect }));
await render("/");
expect(container.textContent).toContain("Getting things ready");
expect(container.textContent).toContain("Loading account");
expect(container.querySelector("#welcome-title")).toBeNull();
await act(async () => resolve(new Response(null, { status: 401 })));
expect(container.querySelector("#welcome-title")).not.toBeNull();
@@ -298,7 +298,7 @@ describe("homepage", () => {
const f = connectAccount();
await render("/");
expect(container.textContent).toContain("Welcome back, alice.");
expect(container.textContent).toContain("Start with one habit.");
expect(container.textContent).toContain("No habits yet.");
await act(async () => buttonNamed("Add a habit +").click());
await settleDeferred();
const input = container.querySelector<HTMLInputElement>("form input")!;

View File

@@ -60,7 +60,7 @@ export function ArchivedHabits({ date, revision, onChanged, onExpired, onClose }
<div ref={archiveFocus} tabIndex={-1} aria-label="Archive status"><FormMessage>{notice}</FormMessage></div>
<FormMessage error>{error}</FormMessage>
{error && <Button variant="secondary" onClick={() => setAttempt(value => value + 1)}>Retry archive</Button>}
{loading ? <p className="ds-supporting-copy" role="status">Loading archive…</p> : !habits.length && <div className="ds-empty-panel"><Archive size={24} aria-hidden="true" /><div><p className="type-ui-heading">No archived habits.</p><p className="ds-supporting-copy">When you put a habit aside, its history will be waiting here.</p></div></div>}
{loading ? <p className="ds-supporting-copy" role="status">Loading archive…</p> : !habits.length && <div className="ds-empty-panel"><Archive size={24} aria-hidden="true" /><div><p className="type-ui-heading">No archived habits.</p></div></div>}
<div className="ds-record-list">
{habits.map(habit => <article key={habit.habitId} className="ds-record" aria-labelledby={`archive-title-${habit.habitId}`}>
<header className="ds-record-heading">

View File

@@ -40,7 +40,7 @@ export function CheckInEditor({ habitId, date, disabled, onSaved, onExpired, onB
} finally { saving.current = false; setBusy(false); onBusyChange?.(false); }
}
return <div className="ds-form-stack ds-check-in-controls" aria-label={`Check-in for ${date}`}>
<header className="ds-check-in-heading"><p className="ds-eyebrow">EDITING CHECK-IN</p><p className="type-ui-heading">{formatTrackingDate(date)}</p></header>
<header className="ds-check-in-heading"><p className="type-ui-heading">{formatTrackingDate(date)}</p></header>
<FormMessage error>{error}</FormMessage>
{!day ? error ? <Button onClick={() => setAttempt(value => value + 1)}>Retry check-in</Button> : <p role="status">Loading check-in…</p>
: day.future ? <p>Future check-ins cannot be edited.</p>

View File

@@ -114,9 +114,6 @@ export function CreateHabit({
}}
>
<div className="home-create-heading">
<p className="ds-eyebrow">
{editing ? "SHAPE YOUR RHYTHM" : "A LITTLE ROOM FOR SOMETHING GOOD"}
</p>
<Button
variant="text"
aria-label={editing ? "Close habit editor" : "Close new habit"}
@@ -127,21 +124,9 @@ export function CreateHabit({
</Button>
</div>
<h2 id="create-title">
{editing ? (
<>
Edit your <em>habit.</em>
</>
) : (
<>
Make it <em>yours.</em>
</>
)}
{editing ? "Edit habit" : "Create habit"}
</h2>
<p className="ds-muted">
{editing
? "Changes start today. Earlier targets and schedules stay as they were."
: "Start small. You don’t need a perfect plan."}
</p>
{editing && <p className="type-small">Changes apply from today. Earlier history is kept.</p>}
<form onSubmit={submit}>
<fieldset disabled={busy} className="home-form-fields">
<div className="ds-field">

View File

@@ -47,7 +47,6 @@ export function DeleteHabit({
if (!saving.current) onClose();
}}
>
<p className="ds-eyebrow">MAKE A LITTLE ROOM</p>
<h2 id="delete-title">
Delete this <em>habit?</em>
</h2>

View File

@@ -9,13 +9,12 @@ export function InstallApp() {
if (!enabled || installed) return null;
return <>
<Button variant="text" onClick={() => { if (installPrompt) void installApp(); else setHelp(true); }}>Install app</Button>
{help && <Modal id="install-app" size="compact" title="Keep Mina Habits close." onClose={() => setHelp(false)}>
{help && <Modal id="install-app" size="compact" title="Install app" onClose={() => setHelp(false)}>
<div className="ds-form-stack">
<p>Open Mina Habits from your home screen or dock.</p>
<p><strong>iPhone or iPad:</strong> open the Share menu, then choose Add to Home Screen.</p>
<p><strong>Mac Safari:</strong> choose File → Add to Dock.</p>
<p><strong>Chrome or Edge:</strong> use the install icon in the address bar, or Install app in the browser menu.</p>
<p className="ds-supporting-copy">Installation options depend on your browser. Your habits still need a connection to load and save.</p>
<p className="ds-supporting-copy">An internet connection is required to load and save habits.</p>
<div className="ds-actions"><Button onClick={() => setHelp(false)}>Got it</Button></div>
</div>
</Modal>}

View File

@@ -91,7 +91,6 @@ export function ShareProgress({ user, today, revision, onClose }: { user: Public
<div className={`ds-share-preview${thumbnail ? " ds-share-preview--thumbnail" : ""}`} aria-busy={!ready && !error}>
{error ? <div className="ds-share-placeholder" role="alert"><ImageIcon size={28} aria-hidden="true" /><p>{error}</p>{ids.length > 0 && <Button variant="secondary" onClick={() => setRenderAttempt(n => n + 1)}>Retry preview</Button>}</div> : ready ? <img src={ready.url} alt={ready.alt} /> : <div className="ds-share-placeholder" role="status"><ImageIcon size={28} aria-hidden="true" /><p>Creating your card…</p></div>}
</div>
<p className="ds-share-caption">Your preview matches the shared image.</p>
{ready?.avatarMissing && <p role="status" className="ds-share-caption">Your avatar couldn’t load. A neutral profile icon is used.</p>}
</section>
<div className="ds-share-controls">
@@ -112,13 +111,13 @@ export function ShareProgress({ user, today, revision, onClose }: { user: Public
</fieldset>
</details>
<div className="ds-share-destination"><span className="ds-share-destination-icon"><Hash size={20} aria-hidden="true" /></span><div><span className="ds-share-hint">Discord destination</span>
{connectionError ? <div role="alert"><p>{connectionError}</p><Button variant="text" disabled={busy} onClick={() => setConnectionAttempt(n => n + 1)}>Retry connection</Button></div> : connection?.connected ? <><a href={connection.channelUrl} target="_blank" rel="noreferrer">{connection.name}</a><p className="ds-share-hint">Posted by the Mina Habits bot.</p></> : <p className="ds-share-hint" role="status">{connection?.message ?? "Loading channel…"}</p>}
{connectionError ? <div role="alert"><p>{connectionError}</p><Button variant="text" disabled={busy} onClick={() => setConnectionAttempt(n => n + 1)}>Retry connection</Button></div> : connection?.connected ? <><a href={connection.channelUrl} target="_blank" rel="noreferrer">{connection.name}</a></> : <p className="ds-share-hint" role="status">{connection?.message ?? "Loading channel…"}</p>}
</div></div>
</div>
</div>
<footer className="ds-share-footer">
<div className="ds-share-footer-copy" aria-live="polite">
{sendError ? <p role="alert" className="ds-share-error">{sendError}</p> : sent ? <p>{sent.status === "sent" ? <>Shared successfully. {sent.messageUrl && <a href={sent.messageUrl} target="_blank" rel="noreferrer">View in Discord ↗</a>}</> : <>Delivery unconfirmed. Check {connection?.channelUrl ? <a href={connection.channelUrl} target="_blank" rel="noreferrer">your channel</a> : "your channel"}; this attempt won’t be resent.</>}</p> : <p>{sending ? "Sending your card to Discord…" : error ? "Check your card settings." : ready ? "Ready to share." : "Preparing preview…"}<span>{sending ? "This may take a moment." : "Only the details in your preview are shared."}</span></p>}
{sendError ? <p role="alert" className="ds-share-error">{sendError}</p> : sent ? <p>{sent.status === "sent" ? <>Shared successfully. {sent.messageUrl && <a href={sent.messageUrl} target="_blank" rel="noreferrer">View in Discord ↗</a>}</> : <>Delivery unconfirmed. Check {connection?.channelUrl ? <a href={connection.channelUrl} target="_blank" rel="noreferrer">your channel</a> : "your channel"}; this attempt won’t be resent.</>}</p> : <p>{sending ? "Sending your card to Discord…" : error ? "Check your card settings." : ready ? "Ready to share." : "Preparing preview…"}</p>}
</div>
<div className="ds-share-actions">
{ready ? <a className="ds-button ds-button--secondary" href={ready.url} download={`mina-habits-progress-${from}-${to}.png`}><Download size={17} aria-hidden="true" />Download</a> : <Button variant="secondary" disabled><Download size={17} aria-hidden="true" />Download</Button>}

View File

@@ -8,33 +8,30 @@ export function ContainerShowcase() {
return (
<section className="ds-section ds-container-showcase" id="containers" aria-labelledby="containers-title">
<SectionHeading number="CONTAINERS & LAYOUT" id="containers-title" title={<>A little structure. <em>Room to breathe.</em></>}>
Group related content with a quiet surface. Keep open layouts for long lists and calendars.
</SectionHeading>
<SectionHeading id="containers-title" title="Containers & layout" />
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>Two quiet surfaces</h3>
<h3>Card surfaces</h3>
<span className="ds-code">Card · soft / outlined</span>
</div>
<CardGrid>
<Card headingLevel={4} eyebrow="SOFT SURFACE" heading="A gentle grouping.">
<Card headingLevel={4} heading="Soft surface">
<p>A neutral fill gathers related content without adding another divider. Use it for summaries, guidance, and a small set of actions.</p>
</Card>
<Card headingLevel={4} variant="outlined" eyebrow="OUTLINED SURFACE" heading="A clear boundary.">
<Card headingLevel={4} variant="outlined" heading="Outlined surface">
<p>A fine border defines a standalone group on white. Useful when a form or a focused task needs its own space.</p>
</Card>
</CardGrid>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>A day, arranged simply</h3>
<h3>Bento layout</h3>
<span className="ds-code">CardGrid · bento</span>
</div>
<CardGrid layout="bento" aria-label="Interactive bento layout example">
<Card
headingLevel={4}
span="featured"
eyebrow="TODAY / YOUR OWN PACE"
heading={<>Small things, <em>adding up.</em></>}
heading="Today"
>
<div className="ds-card-summary" role="status">
<p className="type-display">{Number(water === 8) + Number(read)}<span className="type-title ds-muted"> / 2</span></p>
@@ -45,11 +42,11 @@ export function ContainerShowcase() {
<Counter label="bento glasses of water" value={water} target={8} onChange={setWater} />
<p className="type-small" role="status">{water === 8 ? "Complete for today" : `${8 - water} glasses to go`}</p>
</Card>
<Card headingLevel={4} eyebrow="A FEW PAGES" heading="Read a little" variant="outlined">
<Card headingLevel={4} heading="Read a little" variant="outlined">
<Checkbox label="Reading done" checked={read} onChange={(event) => setRead(event.target.checked)} />
</Card>
<Card headingLevel={4} span="wide" eyebrow="A GENTLE REMINDER" heading="There’s no catching up.">
<p>Come back to today. Your next small step is enough.</p>
<Card headingLevel={4} span="wide" heading="Demo">
<p>Changes to these examples are not saved.</p>
</Card>
</CardGrid>
<div className="ds-container-guidance">

View File

@@ -407,18 +407,7 @@ export function EditingWorkbench({
</div>
<div className="ds-edit-layout">
<aside className="ds-edit-context">
<span className="ds-eyebrow">
{tab === "Backfill progress"
? "A DATE, NOT A RESET"
: "MAKE IT YOURS"}
</span>
<h3>
{tab === "Habit settings"
? "Shape your rhythm."
: tab === "Tasks"
? "Small steps, clearly defined."
: "A missed log is not a missed day."}
</h3>
<h3>{tab}</h3>
<p>
{tab === "Habit settings"
? "Adjust a name, target, or schedule. Changes start today; your history stays as it was."

View File

@@ -39,7 +39,6 @@ export function PageLayout({
{children}
<footer className="ds-footer">
<span className="ds-wordmark">Mina Habits.</span>
<span>A little, every day.</span>
<InstallApp />
<Button
variant="text"

View File

@@ -68,7 +68,6 @@ export function WelcomePanel({
<h1 id="dashboard-title" className="type-display">
Welcome back, <em>{name}.</em>
</h1>
<p className="ds-welcome-subtitle">Make time for today.</p>
{children}
</div>
<div className="ds-welcome-calendar">
@@ -82,7 +81,7 @@ export function WelcomePanel({
<span className="ds-date-weekday">
{day.toLocaleDateString("en", { weekday: "long", timeZone: "UTC" })}
</span>
<span className="ds-date-caption ds-eyebrow">TODAY, AT YOUR OWN PACE</span>
<span className="ds-date-caption ds-eyebrow">TODAY</span>
</time>
) : (
<p className="ds-muted" role="status">Your day is loading…</p>

View File

@@ -13,7 +13,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png" />
<title>Mina Habits</title>
<meta name="description" content="A quiet place to track your habits and see your progress. Explore interactive habit calendars and connect with Discord with Mina Habits." />
<meta name="description" content="Track habits, check in, and view your progress." />
<script type="module" src="./frontend.tsx" async></script>
</head>
<body>

View File

@@ -36,7 +36,7 @@ export function DesignSystem() {
<DesignSystemTabs tabs={[
{ id: "foundations", label: "Foundations", content: (
<section className="ds-section ds-split-section" id="foundations">
<SectionHeading number="FOUNDATIONS" title="The essentials." />
<SectionHeading title="Foundations" />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Typography</h3>
@@ -44,14 +44,14 @@ export function DesignSystem() {
</div>
<div className="ds-type-specimen">
<span className="ds-type-display">
Small steps.
Regular type.
<br />
<em>Lasting rhythm.</em>
<em>Italic type.</em>
</span>
<p>Instrument Serif · Regular & italic · 72/81px display, 48/54px on small screens</p>
</div>
<div className="ds-body-specimen">
<span>Room to breathe. A clear next step.</span>
<span>Body text and controls.</span>
<p>System sans · 16/24px body & controls · 14/21px supporting text</p>
</div>
<ul className="ds-type-scale" aria-label="Shared typography scale">
@@ -131,7 +131,7 @@ export function DesignSystem() {
) },
{ id: "components", label: "Components", content: (
<section className="ds-section ds-split-section" id="components">
<SectionHeading number="COMPONENTS" title="Only what matters." />
<SectionHeading title="Components" />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Actions</h3>
@@ -180,7 +180,7 @@ export function DesignSystem() {
</div>
<div className="ds-component-row">
<div>
<h3>A little more</h3>
<h3>Counter</h3>
<span className="ds-code">Counter · editable total</span>
</div>
<Counter
@@ -245,7 +245,7 @@ export function DesignSystem() {
<section className="ds-section ds-split-section" id="calendar-states">
<SectionHeading
number="PROGRESS LANGUAGE"
title="Every shade counts."
title="Calendar states"
/>
<div className="ds-section-content">
<div className="ds-spec-heading">

View File

@@ -11,12 +11,10 @@ import { DiscordSignInButton } from "../components/DiscordSignInButton";
import { PageLayout } from "../components/design-system/PageLayout";
import {
Button,
ButtonLink,
Checkbox,
Counter,
SectionHeading,
} from "../components/design-system/primitives";
import { Card, CardGrid } from "../components/design-system/Card";
import { HabitChart } from "../components/design-system/HabitChart";
import { HABIT_COLORS } from "../components/design-system/calendar-model";
const HabitForm = deferred(() => import("../components/HabitForm").then(module => module.HabitForm));
@@ -56,17 +54,17 @@ export function Home() {
)}
{loading ? (
<section className="ds-section" aria-busy="true">
<p role="status">Getting things ready…</p>
<p role="status">Loading account…</p>
</section>
) : accountError ? (
<section className="ds-section">
<h1 className="type-section">{disconnected ? "A moment to reconnect." : "Let’s try that again."}</h1>
<Card headingLevel={2} heading={disconnected ? "Your habits will be here." : "Account unavailable"}>
<p>{disconnected ? "We can’t connect to Mina Habits. Check your internet connection to load your account and check in. We’ll try again automatically." : "Your account couldn’t be loaded."}</p>
<h1 className="type-section">{disconnected ? "Connection unavailable" : "Account unavailable"}</h1>
<div className="ds-form-stack">
<p>{disconnected ? "Check your connection. We’ll retry automatically." : "Your account couldn’t be loaded."}</p>
<div className="ds-actions">
<Button onClick={retry}>Try again</Button>
</div>
</Card>
</div>
</section>
) : user ? (
<CalendarCacheProvider key={`${user.id}:${user.timezone}`}><Dashboard user={user} onExpired={retry} /></CalendarCacheProvider>
@@ -83,39 +81,17 @@ function Welcome({ onSignIn }: { onSignIn: () => void }) {
const [read, setRead] = useState(false);
return (
<>
<section className="ds-section ds-split-section" aria-labelledby="welcome-title">
<div className="ds-section-heading">
<p className="ds-eyebrow">A LITTLE, EVERY DAY</p>
<h1 id="welcome-title" className="type-section">
Small steps.
<br />
<em>Lasting rhythm.</em>
</h1>
<section className="ds-section ds-landing-intro" aria-labelledby="welcome-title">
<h1 id="welcome-title" className="type-section">Track your habits</h1>
<p className="type-small">Sign in to save your habits and progress.</p>
<div className="ds-actions">
<DiscordSignInButton disabled={!online} onClick={onSignIn} />
</div>
<Card headingLevel={2} heading="Make room for what matters.">
<p>
Check in, count a little more, or work through a few tasks. See your progress grow, one
day at a time.
</p>
<div className="ds-actions">
<DiscordSignInButton disabled={!online} onClick={onSignIn} />
<ButtonLink href="#try-it">Try it below ↓</ButtonLink>
</div>
<p className="type-small">Sign in with your Discord account to save your habits.</p>
</Card>
</section>
<section className="ds-section" id="try-it" aria-labelledby="try-it-title">
<div className="ds-section-top">
<SectionHeading
number="TRY A CHECK-IN"
id="try-it-title"
title={
<>
A little progress. <em>Made visible.</em>
</>
}
/>
<span className="ds-demo-note">Example · September 4, 2026 · not saved</span>
<SectionHeading id="try-it-title" title="Try a check-in" />
<span className="ds-demo-note">Demo · changes are not saved</span>
</div>
<div className="ds-habit-chart-grid">
<HabitChart
@@ -146,22 +122,6 @@ function Welcome({ onSignIn }: { onSignIn: () => void }) {
</HabitChart>
</div>
</section>
<section className="ds-section" aria-label="Your own rhythm">
<CardGrid>
<Card heading="Your habits. Your pace." eyebrow="MAKE IT FIT">
<p>
Pick a check-in, a count target, or a task list. Repeat daily, on selected weekdays,
or at your own interval.
</p>
</Card>
<Card heading="See the days add up." eyebrow="KEEP PERSPECTIVE" variant="outlined">
<p>
Explore your calendar to see the progress behind each square. Days off stay distinct
from missed days.
</p>
</Card>
</CardGrid>
</section>
</>
);
}
@@ -374,16 +334,13 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
</span>
<span>
habits complete today
{today.completed === today.due && (
<span className="ds-welcome-complete">A little, all done.</span>
)}
</span>
</>
) : (
<p>
{today.habits.length
? "Nothing scheduled today. Enjoy a little breathing room."
: "Start with one habit. Your first small step starts here."}
? "Nothing scheduled today."
: "No habits yet."}
</p>
)}
</div>
@@ -457,18 +414,14 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
<div className="ds-section-top">
<SectionHeading
id="habits-title"
title={
<>
Today, <em>at your pace.</em>
</>
}
title="Habits"
/>
<div className="ds-action-row ds-habit-filters" role="group" aria-label="Show habits">
<Button variant={habitView === "today" ? "primary" : "text"} aria-pressed={habitView === "today"} onClick={() => setHabitView("today")}>Today · {today.due}</Button>
<Button variant={habitView === "all" ? "primary" : "text"} aria-pressed={habitView === "all"} onClick={() => setHabitView("all")}>All habits · {today.habits.length}</Button>
</div>
</div>
{habitView === "today" && today.habits.length > 0 && today.due === 0 && <p className="ds-supporting-copy">You’re all set for today. Choose All habits to view or edit your routines.</p>}
{habitView === "today" && today.habits.length > 0 && today.due === 0 && <p className="ds-supporting-copy">Choose All habits to view or edit your routines.</p>}
<div className="ds-habit-chart-grid ds-habit-list">
{today.habits.filter(habit => habitView === "all" || habit.due).map((habit) => (
<SavedHabit

View File

@@ -304,6 +304,13 @@
padding: 42px 0 56px;
border-top: 1px solid var(--ds-rule);
}
.ds-landing-intro > p {
margin-top: 16px;
}
.ds-landing-intro > .ds-actions {
margin-top: 24px;
padding-bottom: 0;
}
.ds-section-top {
display: flex;
justify-content: space-between;