Files
minabot/src/pages/DesignSystem.tsx
syntaxbullet ecc2620a4c feat: add Home page with user dashboard and habit tracking features
- Implemented Home component with user authentication and loading states.
- Created Welcome component for unauthenticated users with a sign-in option.
- Developed Dashboard component to display user's habits and progress.
- Added functionality for habit management, including adding, updating, and deleting habits.
- Integrated HabitChart and HabitHistory components for visual representation of habits.
- Introduced sharing functionality for progress via Discord integration.

feat: establish Discord sharing configuration and routes

- Added DiscordSharingConfig type and readDiscordSharingConfig function for environment variable management.
- Created sharing contracts for input validation and data structure.
- Implemented sharing routes for previewing and sending progress images to Discord.
- Added tests for sharing routes to ensure authentication and proper error handling.
2026-09-04 17:48:54 +02:00

297 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useNavigate } from "react-router";
import {
Button,
Checkbox,
Counter,
SectionHeading,
} from "../components/design-system/primitives";
import { CalendarExamples } from "../components/design-system/CalendarExamples";
import { ContainerShowcase } from "../components/design-system/ContainerShowcase";
import { Field } from "../components/design-system/Field";
import {
PROGRESS_SHADES,
HABIT_COLORS,
} from "../components/design-system/calendar-model";
import { PageLayout } from "../components/design-system/PageLayout";
import { DesignSystemTabs } from "../components/design-system/DesignSystemTabs";
export function DesignSystem() {
const navigate = useNavigate();
const [exampleChecked, setExampleChecked] = useState(false);
const [exampleCount, setExampleCount] = useState(3);
const [habitName, setHabitName] = useState("");
const [savedName, setSavedName] = useState("");
return (
<PageLayout mainId="ds-main" wordmarkTo="/design-system" header={<span className="ds-library-label">Interface library / 01</span>}>
<header className="ds-library-intro">
<div>
<p className="ds-eyebrow">MINABOT INTERFACE LANGUAGE</p>
<h1 id="ds-title">Design system<span aria-hidden="true">.</span></h1>
</div>
<span className="ds-library-note">Interactive examples · nothing is saved</span>
</header>
<DesignSystemTabs tabs={[
{ id: "foundations", label: "Foundations", content: (
<section className="ds-section ds-split-section" id="foundations">
<SectionHeading number="FOUNDATIONS" title="The essentials." />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Typography</h3>
<span className="ds-code">Instrument Serif + system sans</span>
</div>
<div className="ds-type-specimen">
<span className="ds-type-display">
Small steps.
<br />
<em>Lasting rhythm.</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>
<p>System sans · 16/24px body & controls · 14/21px supporting text</p>
</div>
<ul className="ds-type-scale" aria-label="Shared typography scale">
{[
{ name: "Section heading", className: "type-section", spec: "48/54px · small screens 36/45px" },
{ name: "Component title", className: "type-title", spec: "32/40px" },
{ name: "Lead text", className: "type-lead", spec: "20/30px" },
{ name: "Body & actions", className: "type-body", spec: "16/24px" },
{ name: "Interface heading", className: "type-ui-heading", spec: "18/27px · medium weight" },
{ name: "Supporting text", className: "type-small", spec: "14/21px" },
{ name: "SECTION LABEL", className: "type-label", spec: "14/21px · medium weight" },
{ name: "EYEBROW", className: "type-eyebrow", spec: "12/18px · regular weight" },
{ name: " / +", className: "type-control", spec: "24/30px · counter symbols" },
].map((type) => <li key={type.className}>
<span className={type.className}>{type.name}</span>
<code>{type.className} · {type.spec}</code>
</li>)}
</ul>
<p className="ds-footnote">Fixed rem sizes respect browser text preferences. Use the same type utilities on every page; no fluid type or page-specific size overrides. Ink and secondary text on paper exceed 4.5:1 contrast.</p>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>Monochrome</h3>
<span className="ds-code">The interface stays neutral</span>
</div>
<div className="ds-palette">
{[
{ name: "Ink", value: "#111111", token: "ink" },
{ name: "Secondary", value: "#666666", token: "secondary" },
{ name: "Control border", value: "#767676", token: "control-border" },
{ name: "Rule", value: "#DEDEDE", token: "rule" },
{ name: "Surface", value: "#F5F5F5", token: "surface" },
{ name: "Empty calendar", value: "#EEEEEE", token: "empty" },
{ name: "Paper", value: "#FFFFFF", token: "paper" },
].map((color) => (
<div key={color.name}>
<div
className="ds-swatch"
style={{ background: `var(--ds-${color.token})` }}
/>
<p>
{color.name}
<span className="ds-code">{color.value} · --ds-{color.token}</span>
</p>
</div>
))}
</div>
<p className="ds-footnote">Use secondary for supporting text, surface for grouping and neutral hover states, and control border for inputs. Rule is for decorative dividers. Calendar shades and habit colors retain their own progress and identity roles.</p>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>Habit colors</h3>
<span className="ds-code">Identity, not decoration</span>
</div>
<div className="ds-habit-color-key">
{Object.entries(HABIT_COLORS).map(([name, color]) => (
<span key={name}>
<i style={{ backgroundColor: color }} aria-hidden="true" />
{name}
</span>
))}
</div>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>Space & structure</h3>
<span className="ds-code">4px base</span>
</div>
<div className="ds-spacing">
{[4, 8, 16, 24, 32, 48].map((space) => (
<div key={space}>
<span style={{ width: space }} />
<code>{space}</code>
</div>
))}
</div>
<p className="ds-footnote">
Square corners. Space within groups, light rules between sections.
Optional neutral cards give related content a home without shadows.
</p>
</div>
</section>
) },
{ id: "components", label: "Components", content: (
<section className="ds-section ds-split-section" id="components">
<SectionHeading number="COMPONENTS" title="Only what matters." />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Actions</h3>
<span className="ds-code">Button</span>
</div>
<div className="ds-actions">
<Button
onClick={() => {
document.getElementById("habit-name")?.focus();
}}
>
Add a habit <span aria-hidden="true">+</span>
</Button>
<Button
variant="secondary"
onClick={() => {
setExampleCount(3);
setExampleChecked(false);
setHabitName("");
setSavedName("");
}}
>
Reset examples
</Button>
<Button
variant="text"
onClick={() => {
navigate({ hash: "#calendar-states" });
document.getElementById("ds-tab-calendar-states")?.focus();
}}
>
View habit <span aria-hidden="true"></span>
</Button>
<Button disabled>Unavailable</Button>
</div>
<div className="ds-component-row">
<div>
<h3>A simple yes</h3>
<span className="ds-code">Checkbox</span>
</div>
<Checkbox
label={exampleChecked ? "Done for today" : "Mark as done"}
checked={exampleChecked}
onChange={(event) => setExampleChecked(event.target.checked)}
/>
</div>
<div className="ds-component-row">
<div>
<h3>A little more</h3>
<span className="ds-code">Counter · bounded 08</span>
</div>
<Counter
label="example count"
value={exampleCount}
target={8}
onChange={setExampleCount}
/>
</div>
<form
className="ds-example-form"
onSubmit={(event) => {
event.preventDefault();
if (habitName.trim()) setSavedName(habitName.trim());
}}
>
<div className="ds-spec-heading">
<h3>Give it a name</h3>
<span className="ds-code">Field · text input</span>
</div>
<Field
id="habit-name"
label="Habit name"
hint="This previews a name; it does not create a habit."
>
{(id, describedBy) => (
<div className="ds-input-row">
<input
id={id}
required
maxLength={80}
placeholder="e.g. Read ten pages"
value={habitName}
onChange={(event) => {
setHabitName(event.target.value);
setSavedName("");
}}
aria-describedby={describedBy}
/>
<Button
type="submit"
variant="secondary"
disabled={!habitName.trim()}
>
Try it <span aria-hidden="true"></span>
</Button>
</div>
)}
</Field>
<p className="ds-footnote" role="status">
{savedName && `${savedName}” — preview accepted. No habit was created.`}
</p>
</form>
</div>
</section>
) },
{ id: "containers", label: "Layout", content: (
<ContainerShowcase />
) },
{ id: "calendar-states", label: "Calendars", content: (
<>
<section className="ds-section ds-split-section" id="calendar-states">
<SectionHeading
number="PROGRESS LANGUAGE"
title="Every shade counts."
/>
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>One glass at a time</h3>
<span className="ds-code">CalendarHeatmap</span>
</div>
<div className="ds-shade-scale">
{PROGRESS_SHADES.map((color, index) => (
<div key={color}>
<span style={{ backgroundColor: color }} />
<small>{index}/8</small>
</div>
))}
</div>
<div className="ds-scale-caption">
<span>Due · no progress</span>
<span>Partial progress</span>
<span>Complete</span>
</div>
<div className="ds-neutral-states">
<div>
<span className="ds-state-square ds-state-square--not-due" />
<div>
<h4>Nothing scheduled</h4>
<p>A neutral dot, not a missed day. Excluded from the score.</p>
</div>
</div>
<div>
<span className="ds-state-square ds-state-square--future" />
<div>
<h4>Upcoming</h4>
<p>Same fill as zero. Labeled Upcoming on inspection.</p>
</div>
</div>
</div>
<p className="ds-footnote">
Calendar cells expose exact values and status on selection, hover,
and keyboard focus. Shade is never the only signal.
</p>
</div>
</section>
<CalendarExamples />
</>
) },
]} />
</PageLayout>
);
}