feat: restructure styles and components for improved design system

- Added typography and design system styles to globals.css.
- Removed deprecated home.css and landing.css files.
- Introduced DiscordSignInButton component for Discord authentication.
- Created Card and CardGrid components for structured content display.
- Implemented ContainerShowcase to demonstrate card usage and layout.
- Added DesignSystemTabs for navigation between design system sections.
- Established typography.css for consistent text styling across components.
- Added tests for typography styles to ensure compliance with design standards.
This commit is contained in:
syntaxbullet
2026-09-04 12:37:56 +02:00
parent 084603bb6e
commit bfa54851c2
23 changed files with 1177 additions and 2937 deletions

View File

@@ -32,7 +32,7 @@ export function CalendarHeatmap({
historyLabel?: string;
legend?: ReactNode;
}) {
const [months, setMonths] = useState<MonthView>(6);
const [months, setMonths] = useState<MonthView>(12);
const latestDate =
allDays.findLast((day) => day.state !== "future")?.date ??
allDays.at(-1)?.date;

View File

@@ -0,0 +1,44 @@
import { useId, type HTMLAttributes, type ReactNode } from "react";
/** A flat content group. Actions remain explicit buttons or links inside it. */
export function Card({
eyebrow,
heading,
children,
footer,
variant = "soft",
span = "standard",
className = "",
...props
}: HTMLAttributes<HTMLElement> & {
eyebrow?: string;
heading: ReactNode;
footer?: ReactNode;
variant?: "soft" | "outlined";
span?: "standard" | "wide" | "featured";
}) {
const headingId = useId();
return (
<article
aria-labelledby={headingId}
className={`ds-card ds-card--${variant} ds-card--${span} ${className}`}
{...props}
>
<header className="ds-card-header">
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
<h3 id={headingId}>{heading}</h3>
</header>
{children && <div className="ds-card-body">{children}</div>}
{footer && <footer className="ds-card-footer">{footer}</footer>}
</article>
);
}
/** Equal cards by default; bento spans keep the same reading order on mobile. */
export function CardGrid({
layout = "equal",
className = "",
...props
}: HTMLAttributes<HTMLDivElement> & { layout?: "equal" | "bento" }) {
return <div className={`ds-card-grid ds-card-grid--${layout} ${className}`} {...props} />;
}

View File

@@ -0,0 +1,64 @@
import { useState } from "react";
import { Card, CardGrid } from "./Card";
import { Button, Checkbox, Counter, SectionHeading } from "./primitives";
export function ContainerShowcase() {
const [water, setWater] = useState(3);
const [read, setRead] = useState(false);
return (
<section className="ds-section ds-container-showcase" id="containers" aria-labelledby="containers-title">
<SectionHeading number="CONTAINERS & LAYOUT" id="containers-title" title={<>A little structure. <em>Room to breathe.</em></>}>
Group related content with a quiet surface. Keep open layouts for long lists and calendars.
</SectionHeading>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>Two quiet surfaces</h3>
<span className="ds-code">Card · soft / outlined</span>
</div>
<CardGrid>
<Card eyebrow="SOFT SURFACE" heading="A gentle grouping.">
<p>A neutral fill gathers related content without adding another divider. Use it for summaries, guidance, and a small set of actions.</p>
</Card>
<Card variant="outlined" eyebrow="OUTLINED SURFACE" heading="A clear boundary.">
<p>A fine border defines a standalone group on white. Useful when a form or a focused task needs its own space.</p>
</Card>
</CardGrid>
<div className="ds-spec-heading ds-spec-heading--spaced">
<h3>A day, arranged simply</h3>
<span className="ds-code">CardGrid · bento</span>
</div>
<CardGrid layout="bento" aria-label="Interactive bento layout example">
<Card
span="featured"
eyebrow="TODAY / YOUR OWN PACE"
heading={<>Small things, <em>adding up.</em></>}
footer={<p className="ds-muted type-small">Interactive example · nothing is saved</p>}
>
<div className="ds-card-summary" role="status">
<p className="type-display">{Number(water === 8) + Number(read)}<span className="type-title ds-muted"> / 2</span></p>
<p>habits complete</p>
</div>
<p>Make room for a glass of water and a few pages. A little progress belongs here, too.</p>
</Card>
<Card eyebrow="DAILY / 8 GLASSES" heading="Drink water" variant="outlined">
<Counter label="bento glasses of water" value={water} target={8} onChange={setWater} />
<p className="type-small" role="status">{water === 8 ? "Complete for today" : `${8 - water} glasses to go`}</p>
</Card>
<Card eyebrow="A FEW PAGES" heading="Read a little" variant="outlined">
<Checkbox label="Reading done" checked={read} onChange={(event) => setRead(event.target.checked)} />
<p className="type-small">One page is a place to start.</p>
</Card>
<Card span="wide" eyebrow="A GENTLE REMINDER" heading="There’s no catching up.">
<p>Come back to today. Your next small step is enough.</p>
</Card>
</CardGrid>
<div className="ds-container-guidance">
<p className="ds-footnote">Equal grids suit peer items. Bento gives one summary more room; smaller cards hold short tasks. Both collapse in reading order on small screens. Use one surface per group, with spacing inside and no shadows.</p>
<Button variant="text" onClick={() => { setWater(3); setRead(false); }}>Reset card examples ↺</Button>
</div>
<pre className="ds-code ds-container-code"><code>{'<CardGrid layout="bento">\n <Card heading="Today" span="featured">…</Card>\n <Card heading="Water" variant="outlined">…</Card>\n <Card heading="Reading" variant="outlined">…</Card>\n <Card heading="A reminder" span="wide">…</Card>\n</CardGrid>'}</code></pre>
</section>
);
}

View File

@@ -0,0 +1,63 @@
import { useRef, type ReactNode } from "react";
import { useLocation, useNavigate } from "react-router";
type SystemTab = { id: string; label: string; content: ReactNode };
export function DesignSystemTabs({ tabs }: { tabs: SystemTab[] }) {
const { hash } = useLocation();
const navigate = useNavigate();
const buttons = useRef<Array<HTMLButtonElement | null>>([]);
const selected = tabs.findIndex((tab) => hash === `#${tab.id}`);
const active = selected < 0 ? 0 : selected;
function select(index: number) {
navigate({ hash: `#${tabs[index]!.id}` });
}
return (
<div className="ds-explorer">
<div className="ds-tab-bar" role="tablist" aria-label="Design system sections">
{tabs.map((tab, index) => (
<button
key={tab.id}
ref={(element) => { buttons.current[index] = element; }}
type="button"
role="tab"
id={`ds-tab-${tab.id}`}
aria-controls={`ds-panel-${tab.id}`}
aria-selected={active === index}
tabIndex={active === index ? 0 : -1}
onClick={() => select(index)}
onKeyDown={(event) => {
let next = index;
if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
else if (event.key === "ArrowLeft") next = (index - 1 + tabs.length) % tabs.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = tabs.length - 1;
else return;
event.preventDefault();
select(next);
buttons.current[next]?.focus();
}}
>
{tab.label}
</button>
))}
</div>
{/* Keep panels mounted so editors and examples retain their local state. */}
{tabs.map((tab, index) => (
<div
key={tab.id}
id={`ds-panel-${tab.id}`}
role="tabpanel"
aria-labelledby={`ds-tab-${tab.id}`}
tabIndex={0}
hidden={active !== index}
className="ds-tab-panel"
>
{tab.content}
</div>
))}
</div>
);
}

View File

@@ -361,7 +361,7 @@ export function EditingWorkbench({
>
<div className="ds-section-top">
<div>
<span className="ds-eyebrow">02 / EDITING & HISTORY</span>
<span className="ds-eyebrow">EDITING & HISTORY</span>
<h2 id="editing-title">
Room for <em>real life.</em>
</h2>

View File

@@ -1,5 +1,6 @@
import type {
ButtonHTMLAttributes,
AnchorHTMLAttributes,
InputHTMLAttributes,
ReactNode,
} from "react";
@@ -21,19 +22,31 @@ export function Button({
);
}
export function ButtonLink({
variant = "text",
className = "",
...props
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
variant?: "primary" | "secondary" | "text";
}) {
return <a className={`ds-button ds-button--${variant} ${className}`} {...props} />;
}
export function SectionHeading({
number,
title,
children,
id,
}: {
number: string;
title: string;
title: ReactNode;
children?: ReactNode;
id?: string;
}) {
return (
<header className="ds-section-heading">
<span className="ds-eyebrow">{number}</span>
<h2>{title}</h2>
<h2 id={id}>{title}</h2>
{children && <p>{children}</p>}
</header>
);