feat: enhance typography and accessibility in design system

- Added new UI heading utility with specific styles in typography.css.
- Updated typography tests to include new UI heading role and ensure proper size and styling.
- Documented design system audit findings, focusing on accessibility improvements and component consistency.
- Introduced CalendarExamples component to demonstrate calendar functionality with habit tracking.
- Created a shared Field component for consistent labeling and hinting of form controls.
- Implemented comprehensive accessibility tests for calendar and editor components, ensuring proper focus management and keyboard navigation.
- Added structure tests for Field and Card components to verify correct ID generation and heading levels.
This commit is contained in:
syntaxbullet
2026-09-04 13:44:31 +02:00
parent bfa54851c2
commit dde5e77317
19 changed files with 1077 additions and 681 deletions

View File

@@ -6,65 +6,22 @@ import {
Counter,
SectionHeading,
} from "../components/design-system/primitives";
import { CalendarHeatmap } from "../components/design-system/CalendarHeatmap";
import { HabitChart } from "../components/design-system/HabitChart";
import { EditingWorkbench } from "../components/design-system/EditingWorkbench";
import { CalendarExamples } from "../components/design-system/CalendarExamples";
import { ContainerShowcase } from "../components/design-system/ContainerShowcase";
import type { HabitColors } from "../components/design-system/HabitColorPicker";
import { Field } from "../components/design-system/Field";
import {
combinedProgress,
demoCalendar,
PROGRESS_SHADES,
HABIT_COLORS,
} from "../components/design-system/calendar-model";
import todayImage from "../assets/design/today-v2.png";
import habitImage from "../assets/design/habit-detail.png";
import { DesignSystemTabs } from "../components/design-system/DesignSystemTabs";
type View = "Today" | "Habit detail" | "Combined chart";
export function DesignSystem() {
const navigate = useNavigate();
const [habitColors, setHabitColors] = useState<HabitColors>({ ...HABIT_COLORS });
const [view, setView] = useState<View>("Today");
const [water, setWater] = useState(7);
const [read, setRead] = useState(true);
const [tasks, setTasks] = useState([true, true, false]);
const [movement, setMovement] = useState(10);
const [included, setIncluded] = useState([true, true, true, true]);
const [exampleChecked, setExampleChecked] = useState(false);
const [exampleCount, setExampleCount] = useState(3);
const [habitName, setHabitName] = useState("");
const [savedName, setSavedName] = useState("");
const taskCount = tasks.filter(Boolean).length;
const habits = [
{ value: water, target: 8, due: true },
{ value: Number(read), target: 1, due: true },
{ value: taskCount, target: 3, due: true },
{ value: movement, target: 20, due: true },
];
const total = combinedProgress(habits);
const combined = combinedProgress(
habits.filter((_, index) => included[index]),
);
const isIndividual = view === "Habit detail";
const chartDays = demoCalendar(
isIndividual ? water : combined.completed,
isIndividual ? 8 : combined.due,
).map((day) =>
!isIndividual && combined.due === 0 && day.state !== "future"
? { ...day, state: "not-due" as const }
: day,
);
function resetDemo() {
setWater(7);
setRead(true);
setTasks([true, true, false]);
setMovement(10);
setIncluded([true, true, true, true]);
}
return (
<div className="ds-root" id="top">
<a className="ds-skip-link" href="#ds-main" onClick={(event) => {
@@ -82,18 +39,13 @@ export function DesignSystem() {
<div>
<p className="ds-eyebrow">MINABOT INTERFACE LANGUAGE</p>
<h1 id="ds-title">Design system<span aria-hidden="true">.</span></h1>
<p>Foundations, reusable components, and working examples for what comes next.</p>
</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.">
A small vocabulary.
<br />
Used with intention.
</SectionHeading>
<SectionHeading number="FOUNDATIONS" title="The essentials." />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Typography</h3>
@@ -117,8 +69,10 @@ export function DesignSystem() {
{ 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>
@@ -132,23 +86,27 @@ export function DesignSystem() {
</div>
<div className="ds-palette">
{[
{ name: "Ink", value: "#111111" },
{ name: "Secondary", value: "#666666" },
{ name: "Rule", value: "#DEDEDE" },
{ name: "Paper", value: "#FFFFFF" },
{ 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: color.value }}
style={{ background: `var(--ds-${color.token})` }}
/>
<p>
{color.name}
<span className="ds-code">{color.value}</span>
<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>
@@ -182,11 +140,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.">
Purpose-built primitives.
<br />
Try them for yourself.
</SectionHeading>
<SectionHeading number="COMPONENTS" title="Only what matters." />
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>Actions</h3>
@@ -214,9 +168,8 @@ export function DesignSystem() {
<Button
variant="text"
onClick={() => {
setView("Habit detail");
navigate({ hash: "#playground" });
document.getElementById("ds-tab-playground")?.focus();
navigate({ hash: "#calendar-states" });
document.getElementById("ds-tab-calendar-states")?.focus();
}}
>
View habit <span aria-hidden="true"></span>
@@ -254,34 +207,40 @@ export function DesignSystem() {
}}
>
<div className="ds-spec-heading">
<label htmlFor="habit-name">Give it a name</label>
<span className="ds-code">Text input</span>
<h3>Give it a name</h3>
<span className="ds-code">Field · text input</span>
</div>
<div className="ds-input-row">
<input
id="habit-name"
required
maxLength={80}
placeholder="e.g. Read ten pages"
value={habitName}
onChange={(event) => {
setHabitName(event.target.value);
setSavedName("");
}}
aria-describedby="name-hint"
/>
<Button
type="submit"
variant="secondary"
disabled={!habitName.trim()}
>
Try it <span aria-hidden="true"></span>
</Button>
</div>
<p id="name-hint" className="ds-footnote" role="status">
{savedName
? `${savedName}” — preview accepted. No habit was created.`
: "Keep it short and personal. This is a form preview, not a new habit."}
<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>
@@ -291,15 +250,12 @@ export function DesignSystem() {
<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."
>
Progress is a spectrum.
<br />
Completion is a clear rule.
</SectionHeading>
/>
<div className="ds-section-content">
<div className="ds-spec-heading">
<h3>One glass at a time</h3>
@@ -340,258 +296,8 @@ export function DesignSystem() {
</p>
</div>
</section>
) },
{ id: "playground", label: "Playground", content: (
<section
className="ds-section"
id="playground"
aria-labelledby="playground-title"
>
<div className="ds-section-top">
<div>
<span className="ds-eyebrow">IN PRACTICE</span>
<h2 id="playground-title">
A little, <em>every day.</em>
</h2>
</div>
<span className="ds-demo-note">
<span aria-hidden="true"></span> Interactive demo · not saved
</span>
</div>
<div className="ds-preview-toolbar">
<div
className="ds-view-switch"
role="group"
aria-label="Preview view"
>
{(["Today", "Habit detail", "Combined chart"] as const).map(
(item) => (
<button
type="button"
key={item}
aria-pressed={view === item}
onClick={() => setView(item)}
>
{item}
</button>
),
)}
</div>
<Button variant="text" onClick={resetDemo}>
Reset demo <span aria-hidden="true"></span>
</Button>
</div>
<div className="ds-preview-heading">
<div>
<p className="ds-eyebrow">FRIDAY, SEPTEMBER 4, 2026</p>
<h3>
{view === "Today"
? "Your habits, at a glance."
: isIndividual
? "Drink water"
: "Your rhythm, together."}
</h3>
</div>
<p aria-live="polite">
{view === "Today"
? `${total.completed} of ${total.due} complete`
: isIndividual
? "Daily · 8 glasses"
: combined.due
? `${combined.completed} of ${combined.due} habits complete`
: "No habits selected"}
</p>
</div>
{view === "Today" ? (
<div className="ds-habit-chart-grid">
<HabitChart
name="Drink water"
method="Count target"
value={water}
target={8}
unit="glasses"
color={habitColors.water}
>
<Counter
label="glasses of water"
value={water}
target={8}
onChange={setWater}
/>
</HabitChart>
<HabitChart
name="Read a little"
method="Manual checkbox"
value={Number(read)}
target={1}
unit="reading session"
color={habitColors.reading}
>
<Checkbox
label="Mark reading done"
className="ds-checkbox--icon"
checked={read}
onChange={(event) => setRead(event.target.checked)}
/>
</HabitChart>
<HabitChart
name="Evening reset"
method="Task-based"
value={taskCount}
target={3}
unit="tasks"
color={habitColors.tasks}
tasks={["Clear desk", "Plan tomorrow", "Stretch"].map(
(label, index) => (
<Checkbox
key={label}
label={label}
checked={tasks[index]}
onChange={(event) =>
setTasks((current) =>
current.map((done, i) =>
i === index ? event.target.checked : done,
),
)
}
/>
),
)}
/>
<HabitChart
name="Move a little"
method="Count target"
value={movement}
target={20}
unit="minutes"
color={habitColors.movement}
>
<Counter
label="minutes of movement"
value={movement}
target={20}
onChange={setMovement}
/>
</HabitChart>
<p className="ds-footnote ds-habit-chart-grid-note">
Each habit has its own rhythm. Expand tasks to log your day;
only fully completed habits count in a combined chart.
</p>
</div>
) : (
<div className="ds-chart-preview">
{isIndividual ? (
<div className="ds-detail-count">
<span className="ds-big-value">
{water}
<span className="ds-muted"> / 8</span>
<small>glasses</small>
</span>
<Counter
label="detail glasses"
value={water}
target={8}
onChange={setWater}
/>
</div>
) : (
<div
className="ds-chart-selection"
role="group"
aria-label="Included habits"
>
{[
"Drink water",
"Read a little",
"Evening reset",
"Move a little",
].map((label, index) => (
<Checkbox
key={label}
label={label}
checked={included[index]}
onChange={(event) =>
setIncluded((current) =>
current.map((checked, i) =>
i === index ? event.target.checked : checked,
),
)
}
/>
))}
</div>
)}
<CalendarHeatmap
key={view}
days={chartDays}
unit={isIndividual ? "glasses" : "habits complete"}
label={
isIndividual
? "Drink water progress calendar"
: "Combined habit calendar"
}
color={isIndividual ? habitColors.water : "#111111"}
/>
<p className="ds-footnote">
{isIndividual
? "Partial progress is visible here. Only 8 of 8 glasses counts as complete in a combined chart."
: "Equal weight. Only fully completed, due habits count. Nothing scheduled means no score, not a missed day."}
</p>
</div>
)}
</section>
) },
{ id: "editing", label: "Editing", content: (
<EditingWorkbench onColorsChange={setHabitColors} />
) },
{ id: "views", label: "References", content: (
<section className="ds-section" id="views">
<div className="ds-section-top">
<div>
<span className="ds-eyebrow">VISUAL DIRECTION</span>
<h2>From idea to interface.</h2>
</div>
<p className="ds-muted">
Two imagegen studies, grounded in the PRD.
</p>
</div>
<div className="ds-reference-grid">
<figure>
<a href={todayImage} target="_blank" rel="noreferrer">
<img
src={todayImage}
alt="Generated Today view: four colored habit charts in a two-column grid with an expanded task accordion, serif headings, and no cards"
loading="lazy"
/>
</a>
<figcaption>
<span>01 Today</span>
<span className="ds-muted">
Daily progress <span aria-hidden="true"></span>
</span>
</figcaption>
</figure>
<figure>
<a href={habitImage} target="_blank" rel="noreferrer">
<img
src={habitImage}
alt="Generated Drink water detail view: large seven-of-eight count and monochromatic calendar"
loading="lazy"
/>
</a>
<figcaption>
<span>02 Habit detail</span>
<span className="ds-muted">
A closer look <span aria-hidden="true"></span>
</span>
</figcaption>
</figure>
</div>
<p className="ds-footnote">
Visual references, not authoritative calendar data. The interactive
components above implement exact counts and distinct date states.
</p>
</section>
<CalendarExamples />
</>
) },
]} />
<footer className="ds-footer">