diff --git a/src/App.test.tsx b/src/App.test.tsx
index 3ba3f08..f6f88b7 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -126,7 +126,7 @@ describe("homepage", () => {
await f.request(`/habits/${h.id}`, "DELETE");
await render("/");
await act(async () => buttonNamed("Archived habits").click());
- expect(container.textContent).toContain("Restore Reading");
+ expect(buttonNamed("Restore Reading")).toBeDefined();
await act(async () => buttonNamed("View history").click());
expect(container.querySelector('[aria-label="2026-09-03: 1 of 1 completion · Complete"]')).not.toBeNull();
await act(async () => buttonNamed("Restore Reading").click());
diff --git a/src/components/AccountSettings.tsx b/src/components/AccountSettings.tsx
index f9b97d4..33b58dc 100644
--- a/src/components/AccountSettings.tsx
+++ b/src/components/AccountSettings.tsx
@@ -3,7 +3,8 @@ import { useId, useRef, useState } from "react";
import type { PublicUser } from "../shared/user";
import { habitRequest } from "../lib/dashboard";
import { Button, ButtonLink } from "./design-system/primitives";
-import { Card } from "./design-system/Card";
+import { Download, Trash2 } from "lucide-react";
+import { FormMessage, SettingRow, WorkspacePanel } from "./design-system/WorkspacePanel";
import { Field } from "./design-system/Field";
export function AccountSettings({ user, onChanged, onClose }: { user: PublicUser; onChanged: () => void; onClose: () => void }) {
@@ -21,28 +22,29 @@ export function AccountSettings({ user, onChanged, onClose }: { user: PublicUser
catch (error) { setError(error instanceof Error ? error.message : "Could not save. Try again."); }
finally { saving.current = false; setBusy(false); }
}
- return
}>
+
-
-
- Export my data
-
-
-
- Export includes your profile, habits, history, and sharing records. Sessions and credentials are excluded.
- {deleting && }
- {error && {error}
}
-
- ;
+
+
+ Take your profile, habits, history, and sharing records with you. Sessions and credentials are excluded.}>
+ Export my dataJSON file
+
+ Permanently remove your account and habit history, and sign out every device.}>
+ {!deleting ? :
+ }
+
+ {error}
+ ;
}
diff --git a/src/components/ArchivedHabits.tsx b/src/components/ArchivedHabits.tsx
index e835e16..e4b75eb 100644
--- a/src/components/ArchivedHabits.tsx
+++ b/src/components/ArchivedHabits.tsx
@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from "react";
-import { habitRequest, type TodayHabit } from "../lib/dashboard";
-import { Button, SectionHeading } from "./design-system/primitives";
-import { Card } from "./design-system/Card";
+import { habitRequest, scheduleLabel, type TodayHabit } from "../lib/dashboard";
+import { Button } from "./design-system/primitives";
+import { Archive, ChevronDown, RotateCcw } from "lucide-react";
+import { FormMessage, WorkspacePanel } from "./design-system/WorkspacePanel";
import { HabitHistory } from "./HabitHistory";
-export function ArchivedHabits({ date, revision, onChanged, onExpired }: {
- date: string; revision: number; onChanged: () => void; onExpired: () => void;
+export function ArchivedHabits({ date, revision, onChanged, onExpired, onClose }: {
+ date: string; revision: number; onChanged: () => void; onExpired: () => void; onClose?: () => void;
}) {
const [habits, setHabits] = useState([]);
const [selected, setSelected] = useState();
@@ -36,18 +37,22 @@ export function ArchivedHabits({ date, revision, onChanged, onExpired }: {
setError(message); if (message.includes("session has expired")) expired.current();
} finally { saving.current = false; setBusy(false); }
}
- return
-
- Restore a habit to resume its schedule today. Earlier history stays available.
- {error && {error}
}
- {loading ? Loading archive…
: !habits.length && No archived habits.
}
- {notice}
- {habits.map(habit =>
-
-
-
-
- {selected === habit.habitId && setAttempt(value => value + 1)} />}
- )}
- ;
+ return
+ {error}
+ {error && }
+ {loading ? Loading archive…
: !habits.length && No archived habits.
When you put a habit aside, its history will be waiting here.
}
+ {notice}
+
+ {habits.map(habit =>
+
+ {habit.name ?? "Habit"}
{scheduleLabel(habit.requirements?.schedule)} · Archived
+
+
+
+
+
+ {selected === habit.habitId && setAttempt(value => value + 1)} />
}
+ )}
+
+ ;
}
diff --git a/src/components/CheckInEditor.tsx b/src/components/CheckInEditor.tsx
index 1740095..62c303e 100644
--- a/src/components/CheckInEditor.tsx
+++ b/src/components/CheckInEditor.tsx
@@ -1,5 +1,6 @@
+import { FormMessage } from "./design-system/WorkspacePanel";
import { useEffect, useRef, useState } from "react";
-import { habitRequest, type TodayHabit } from "../lib/dashboard";
+import { habitRequest, formatTrackingDate, type TodayHabit } from "../lib/dashboard";
import { Button, Checkbox } from "./design-system/primitives";
import { Field } from "./design-system/Field";
@@ -38,23 +39,23 @@ export function CheckInEditor({ habitId, date, disabled, onSaved, onExpired }: {
setError(message); if (message.includes("session has expired")) expired.current?.();
} finally { saving.current = false; setBusy(false); }
}
- return
-
Check-in · {date}
- {error &&
{error}
}
+ return
+
+
{error}
{!day ? error ?
:
Loading check-in…
: day.future ?
Future check-ins cannot be edited.
: !day.due ?
Nothing was scheduled for this date.
: <>
-
{day.name} · {day.timezone}. Changes use the requirements saved for this date.
- {day.method === "count" ?
: day.method === "manual" ?
void save({ done: event.target.checked })} />
: day.tasks.map(task => void save({ done: event.target.checked }, task.taskId)} />)}
>}
- {notice}
+ {notice}
;
}
diff --git a/src/components/HabitHistory.tsx b/src/components/HabitHistory.tsx
index dc60d62..05ab10f 100644
--- a/src/components/HabitHistory.tsx
+++ b/src/components/HabitHistory.tsx
@@ -23,6 +23,7 @@ export function HabitHistory({
editor,
onExpired,
onHistorySaved,
+ calendarOnly = false,
}: {
habit: TodayHabit;
date: string;
@@ -35,6 +36,7 @@ export function HabitHistory({
editor?: ReactNode;
onExpired?: () => void;
onHistorySaved?: () => void;
+ calendarOnly?: boolean;
}) {
const [selectedDate, setSelectedDate] = useState
();
const [editingDay, setEditingDay] = useState(false);
@@ -132,18 +134,19 @@ export function HabitHistory({
/>
}
/>
-
+
- {editingDay && <>
-
{id => { if (event.target.value) setSelectedDate(event.target.value); }} />}
+ {editingDay &&
+
{ setAttempt(value => value + 1); onHistorySaved?.(); }} />
- >}
+ }
>
);
+ if (calendarOnly) return chart;
return (
{ if (!controller.signal.aborted) setError(error.message); });
return () => controller.abort();
}, [attempt]);
- return
-
Daily reminders
-
One private Discord message when you still have habits left today. Times follow {timezone}. No habit names are sent.
- {!settings ?
:
-
-
-
+
+
+
+
+
>
)}
- {archive && today &&
setAttempt(value => value + 1)} />}
+ {archive && today && setArchive(false)} date={today.date} revision={revision} onExpired={onExpired} onChanged={() => setAttempt(value => value + 1)} />}
{settings && setSettings(false)} />}
{sharing && today && { setSharing(false); requestAnimationFrame(() => document.getElementById("open-sharing")?.focus()); }} />}
{error && (
diff --git a/styles/design-system.css b/styles/design-system.css
index 737dfc5..2c47742 100644
--- a/styles/design-system.css
+++ b/styles/design-system.css
@@ -1435,3 +1435,73 @@
.ds-editable-task-row { gap: 4px; }
.ds-inline-item-form { padding: 16px; }
}
+
+/* Secondary workspace flows share the same flat surfaces and editorial hierarchy. */
+.ds-action-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; min-width: 0; }
+.ds-action-row > .ds-button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; max-width: 100%; overflow-wrap: anywhere; }
+.ds-action-row > .ds-button svg { flex: 0 0 auto; }
+.ds-primary-actions { margin-top: 24px; }
+.ds-utility-row { border-top: 1px solid var(--ds-rule); padding-top: 12px; margin-top: 20px; gap: 4px; }
+.ds-utility-row .ds-button { color: var(--ds-secondary); @apply type-small; }
+.ds-utility-row .ds-button[aria-expanded="true"] { color: var(--ds-ink); background: var(--ds-paper); }
+.ds-workspace-panel { border: 1px solid var(--ds-rule); padding: 32px; margin: 32px 0 40px; min-width: 0; }
+.ds-panel-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 32px; }
+.ds-panel-heading > div { min-width: 0; }
+.ds-panel-heading h2 { margin-top: 10px; @apply type-section; overflow-wrap: anywhere; }
+.ds-panel-description { margin-top: 12px !important; color: var(--ds-secondary); max-width: 52ch; }
+.ds-panel-close { flex: 0 0 44px; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 1px solid var(--ds-rule); }
+.ds-setting-row { display: grid; grid-template-columns: minmax(180px, 0.8fr) minmax(0, 1.6fr); gap: 48px; padding: 28px 0; border-top: 1px solid var(--ds-rule); }
+.ds-setting-row:last-of-type { padding-bottom: 0; }
+.ds-setting-heading h3 { @apply type-ui-heading; }
+.ds-setting-heading > div { display: grid; gap: 12px; margin-top: 10px; max-width: 32ch; color: var(--ds-secondary); @apply type-small; }
+.ds-setting-controls { display: grid; gap: 16px; min-width: 0; align-content: start; }
+.ds-form-stack { display: grid; gap: 20px; min-width: 0; align-content: start; }
+.ds-form-stack .ds-field { margin-bottom: 0; }
+.ds-supporting-copy, .ds-meta-label { color: var(--ds-secondary); @apply type-small; overflow-wrap: anywhere; }
+.ds-meta-label { font-variant-numeric: tabular-nums; }
+.ds-clock-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
+.ds-clock-fields input { font-variant-numeric: tabular-nums; }
+.ds-preference-toggle { padding: 16px; background: var(--ds-surface); border: 1px solid var(--ds-rule); }
+.ds-preference-toggle .ds-checkbox-description { margin-top: 4px; }
+.ds-delivery-note { display: grid; gap: 6px; padding: 14px 16px; border-left: 2px solid var(--ds-control-border); background: var(--ds-surface); @apply type-small; }
+.ds-confirmation { padding: 24px; background: var(--ds-surface); border-left: 2px solid var(--ds-ink); }
+.ds-form-message { @apply type-small; color: var(--ds-secondary); overflow-wrap: anywhere; }
+.ds-form-message:empty { display: none; }
+.ds-form-message:not(:empty) { padding: 12px 0; }
+.ds-form-message--error:not(:empty) { padding: 12px 16px; border-left: 2px solid var(--ds-ink); background: var(--ds-surface); color: var(--ds-ink); }
+.ds-record { border-top: 1px solid var(--ds-rule); min-width: 0; }
+.ds-record-heading { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 24px 0; }
+.ds-record-copy { min-width: 0; }
+.ds-record-copy h3 { @apply type-title; overflow-wrap: anywhere; }
+.ds-record-copy p { margin-top: 6px; color: var(--ds-secondary); @apply type-small; }
+.ds-record-heading > .ds-action-row { flex-shrink: 0; }
+.ds-disclosure-icon { transition: transform 160ms ease; }
+[aria-expanded="true"] > .ds-disclosure-icon { transform: rotate(180deg); }
+.ds-record-detail { padding: 8px 0 24px; }
+.ds-empty-panel { display: flex; align-items: flex-start; gap: 18px; padding: 28px; background: var(--ds-surface); }
+.ds-empty-panel > svg { flex-shrink: 0; color: var(--ds-secondary); }
+.ds-empty-panel .ds-supporting-copy { margin-top: 8px; max-width: 48ch; }
+.ds-history-actions { justify-content: flex-end; margin-top: 12px; }
+.ds-history-editor { display: grid; grid-template-columns: minmax(180px, 0.8fr) minmax(0, 1.6fr); gap: 32px; padding: 24px; margin-top: 12px; border: 1px solid var(--ds-rule); background: var(--ds-surface); }
+.ds-check-in-heading { display: grid; gap: 6px; }
+.ds-check-in-controls { padding-left: 28px; border-left: 1px solid var(--ds-rule); }
+@media (max-width: 900px) {
+ .ds-setting-row { gap: 28px; grid-template-columns: minmax(160px, 0.8fr) minmax(0, 1.6fr); }
+ .ds-clock-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ds-clock-fields > .ds-field:first-child { grid-column: 1 / -1; }
+ .ds-record-heading { align-items: flex-start; flex-direction: column; gap: 16px; }
+}
+@media (max-width: 640px) {
+ .ds-workspace-panel { padding: 20px; margin: 24px 0 32px; }
+ .ds-panel-heading { gap: 16px; margin-bottom: 24px; }
+ .ds-panel-heading h2 { @apply type-title; }
+ .ds-panel-description { @apply type-small; }
+ .ds-setting-row { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 24px 0; }
+ .ds-setting-heading > div { max-width: none; }
+ .ds-confirmation, .ds-empty-panel { padding: 18px; }
+ .ds-history-editor { grid-template-columns: minmax(0, 1fr); padding: 20px; gap: 24px; }
+ .ds-check-in-controls { padding: 24px 0 0; border-left: 0; border-top: 1px solid var(--ds-rule); }
+ .ds-history-actions { justify-content: flex-start; }
+ .ds-record-heading > .ds-action-row { flex-shrink: 1; gap: 8px; }
+}
+@media (prefers-reduced-motion: reduce) { .ds-disclosure-icon { transition: none; } }