// Disposable visual fixtures: no user data and no outbound Discord calls. import { ThemeProvider, ThemeControl, useTheme } from "../src/components/ThemeProvider"; import { createRoot } from "react-dom/client"; import { useEffect, useState } from "react"; import { addDays } from "../src/habits/calendar"; import { renderProgressCard } from "../src/lib/progress-card"; import type { ShareData } from "../src/sharing/contracts"; import "../styles/globals.css"; const names = ["A few pages, every day", "An afternoon walk", "A very long habit name that should truncate gracefully without hitting the count", "Drink enough water", "练习中文 · 日本語", "Make time to unwind"]; const colors = ["#90647e", "#397f76", "#ba793c", "#4b70b4", "#834f9e", "#bd5863"]; function Preview({ count, empty = false, privateCard = false }: { count: number; empty?: boolean; privateCard?: boolean }) { const { theme } = useTheme(); const [url, setUrl] = useState(""); useEffect(() => { let disposed = false, objectUrl = ""; const total = count === 6 ? 365 : 30; const from = addDays("2026-09-04", 1 - total); const data: ShareData = { from, to: "2026-09-04", timezone: "America/Argentina/Buenos_Aires", habits: names.slice(0, count).map((name, i) => { const days = Array.from({length: total}, (_, n) => { const due = !empty && (n + i) % 7 !== 0; const ratio = due ? (n % 4 === 0 ? 0 : n % 3 === 0 ? .5 : 1) : null; return { date: addDays(from, n), due, complete: ratio === 1, ratio, color: ratio === 1 ? colors[i]! : ratio ? "#c7b0bf" : "#e4e7e0" }; }); return { name, color: colors[i]!, completed: days.filter(d => d.complete).length, scheduled: days.filter(d => d.due).length, days, legend: { empty: "#e4e7e0", partial: ["#c7b0bf"], complete: colors[i]! } }; }) }; void renderProgressCard(data, { id: "qa", discordId: "qa", username: "alice", displayName: "Alexandra with a very long display name", avatarUrl: null, timezone: data.timezone }, { name: !privateCard, avatar: !privateCard, habitNames: !privateCard, timezone: privateCard }, theme).then(result => { if (disposed) return; objectUrl = URL.createObjectURL(result.blob); setUrl(objectUrl); }); return () => { disposed = true; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [theme]); return

{count} {count === 1 ? "habit" : "habits"}{empty ? " · no schedule" : ""}{privateCard ? " · private" : ""}

{url && QA progress card at 320 pixels}
; } createRoot(document.getElementById("root")!).render(

Card readability at 320px

390 × 844 mobile composer