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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user