Initial commit 🚀

This commit is contained in:
Felix Klement committed 2026-10-09 14:00:40 +02:00
commit fd58a8d32f
235 files changed
+19775

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
---
category: Controls
---
# Button
The filled action. Ink on paper, and paper on ink inside a night surface, which it handles
itself. There is no accent colour and no secondary button variant: a second action on the
screen is a TextLink.
Pass `href` for navigation and it renders an anchor. Omit it for an in-page action.
The label names the action. "Talk to us", "Explore the method", "Request a verification".
Never "Learn more", and never an arrow inside the label.
```jsx
<Button href="/contact">Talk to us</Button>
<Button size="small">Copy the report</Button>
```
Astro equivalent: `<a class="button" href="/contact">`.
+26
View File
@@ -0,0 +1,26 @@
import type { ReactNode } from 'react';
import { cx } from './cx';
export interface ButtonProps {
/** Renders an anchor when set, a button element otherwise. */
href?: string;
size?: 'default' | 'small';
children?: ReactNode;
}
/** A filled action. Its label names the action: "Talk to us", never "Learn more". */
export function Button({ href, size = 'default', children }: ButtonProps) {
const className = cx('button', size === 'small' && 'button--small');
if (href) {
return (
<a className={className} href={href}>
{children}
</a>
);
}
return (
<button className={className} type="button">
{children}
</button>
);
}
+23
View File
@@ -0,0 +1,23 @@
---
category: Layout
---
# Col
One block on the grid. `span` is how many of the twelve columns it occupies; `start` is the
column it begins at, counting from 1.
Use `start` to leave a gap. An offset block is the house style: content that begins at
column 7 or 9 rather than immediately after its neighbour reads as deliberate, and the empty
columns are doing work.
A full-width block is `span={12}`, which is also the default.
```jsx
<Col span={5} start={8}>
<Text measure>Every interface between components is an attack surface.</Text>
</Col>
```
Astro equivalent: `<div style="grid-column: 8 / span 5;">`. Grid positions are the one place
a raw number is allowed; every other value comes from a token.
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
export interface ColProps {
/** How many of the twelve columns this block occupies. */
span?: number;
/** The column it starts at, counting from 1. Offsets are how asymmetry is made. */
start?: number;
children?: ReactNode;
}
/** One block on the grid. A headline takes 7 or 8 columns, its body copy 4 or 5. */
export function Col({ span = 12, start, children }: ColProps) {
return <div style={{ gridColumn: start ? `${start} / span ${span}` : `span ${span}` }}>{children}</div>;
}
+19
View File
@@ -0,0 +1,19 @@
---
category: Layout
---
# Container
Centres content to the page width and holds the page margins. Every band of text sits in
one. The only things that may escape it are full-bleed images and video.
Container and Grid are separate on purpose: a band with a single column of text needs the
Container and no grid at all.
```jsx
<Container>
<Grid>…</Grid>
</Container>
```
Astro equivalent: `<div class="container">`.
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
export interface ContainerProps {
children?: ReactNode;
}
/** Centres content to the page width and holds the page margins. */
export function Container({ children }: ContainerProps) {
return <div className="container">{children}</div>;
}
+22
View File
@@ -0,0 +1,22 @@
---
category: Type
---
# Display
The largest type on the page, and the one place the page raises its voice. One per screen,
in the hero.
Break its lines by hand with `<br />`. A display headline is a composed object, not a
paragraph that happens to be large, and where the line turns changes how it reads.
It renders an `h1` by default. Keep one `h1` per page; if the hero headline is not the
page's heading, set `as="p"`.
```jsx
<Display>
Security engineering<br />for critical software.
</Display>
```
Astro equivalent: `<h1 class="display">`.
+12
View File
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';
export interface DisplayProps {
/** h1 by default. One h1 per page. */
as?: 'h1' | 'h2' | 'p';
children?: ReactNode;
}
/** The largest type on the page. Break its lines by hand; it is a composed object. */
export function Display({ as: Tag = 'h1', children }: DisplayProps) {
return <Tag className="display">{children}</Tag>;
}
+17
View File
@@ -0,0 +1,17 @@
---
category: Type
---
# Eyebrow
A short label above a headline, in sentence case, muted, at small size.
Use it only when the section genuinely needs naming. An eyebrow above every heading is an
anti-pattern here, and it must never be set in all caps.
```jsx
<Eyebrow>Insights</Eyebrow>
<Heading>What a verification report owes you.</Heading>
```
Astro equivalent: `<p class="eyebrow">`.
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
export interface EyebrowProps {
children?: ReactNode;
}
/** A label above a headline, in sentence case. Never all caps, and never decorative. */
export function Eyebrow({ children }: EyebrowProps) {
return <p className="eyebrow">{children}</p>;
}
+21
View File
@@ -0,0 +1,21 @@
---
category: Layout
---
# Grid
The twelve-column grid. It is the tool for asymmetry, which is how this site composes a
band: a headline across 7 or 8 columns, its supporting copy across 4 or 5, offset against
each other rather than centred or evenly split.
Place children with Col. Do not nest a Grid inside a Col unless the inner block genuinely
has its own twelve-column structure.
```jsx
<Grid>
<Col span={7}><Heading>Security is built in, not bolted on.</Heading></Col>
<Col span={4} start={9}><Text measure>…</Text></Col>
</Grid>
```
Astro equivalent: `<div class="grid">`.
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
export interface GridProps {
children?: ReactNode;
}
/** The twelve-column grid. Place children with Col, asymmetrically. */
export function Grid({ children }: GridProps) {
return <div className="grid">{children}</div>;
}
+19
View File
@@ -0,0 +1,19 @@
---
category: Type
---
# Heading
A section headline. `level={2}` is the headline of a band, `level={3}` a block inside it.
The level sets both the element and the size, so the document outline and the visual
hierarchy cannot drift apart. Never skip a level.
A headline may state a claim. It never advertises, and it ends with a full stop.
Contrast pairs carry a lot of weight here: "Verifiable, not asserted."
```jsx
<Heading>Four stages. One method.</Heading>
<Heading level={3}>Threat analysis</Heading>
```
Astro equivalent: `<h2 class="h2">` and `<h3 class="h3">`.
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react';
export interface HeadingProps {
/** 2 for a section headline, 3 for a block within it. Never skip a level. */
level?: 2 | 3;
id?: string;
children?: ReactNode;
}
/** A section headline. It may state a claim, but it never advertises. */
export function Heading({ level = 2, id, children }: HeadingProps) {
const Tag = level === 3 ? 'h3' : 'h2';
return (
<Tag id={id} className={level === 3 ? 'h3' : 'h2'}>
{children}
</Tag>
);
}
+22
View File
@@ -0,0 +1,22 @@
---
category: Content
---
# Prose
Long-form text: an insight, a legal page, anything written as a document rather than
composed as a layout. It styles the headings, lists, quotes, links and figures inside it and
caps the measure, so the markup underneath stays plain.
Use it for a block of authored copy. Do not use it as a general text wrapper; a single
paragraph in a layout is a Text.
```jsx
<Prose>
<h2>What a verification report owes you</h2>
<p>Every finding names the file, the line and the reasoning.</p>
<ul><li>Reproduction steps</li><li>The fix we would ship</li></ul>
</Prose>
```
Astro equivalent: `<div class="prose">`.
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
export interface ProseProps {
children?: ReactNode;
}
/** Long-form text: an insight, a legal page. Styles the headings and lists inside it. */
export function Prose({ children }: ProseProps) {
return <div className="prose">{children}</div>;
}
+17
View File
@@ -0,0 +1,17 @@
---
category: Content
---
# Rule
A hairline. It separates, and it is allowed only where the separation carries information:
between the items of a list that genuinely divide, or under the header.
There are no shadows and no borders around cards in this system. Separation comes from a
hairline or from a change of surface, and nothing else.
```jsx
<Rule />
```
Astro equivalent: `<hr class="rule">`.
+9
View File
@@ -0,0 +1,9 @@
export interface RuleProps {
/** Nothing. A rule separates; it never decorates. */
children?: never;
}
/** A hairline. Use it only where it carries information. */
export function Rule(_props: RuleProps) {
return <hr className="rule" />;
}
+24
View File
@@ -0,0 +1,24 @@
---
category: Layout
---
# Section
One horizontal band of the page. It carries the vertical air above and below its content,
which is the single biggest reason the site reads as composed rather than busy. Put a
Container inside it; never put page margins on the Section itself.
`spacing="tight"` for a subordinate band, `spacing="intro"` for a page head, where the air
above is full and the air below is reduced so the content follows the title.
`tone="night"` makes this band a night surface and flips the ink tokens inside it.
```jsx
<Section tone="night" spacing="tight">
<Container>
<Heading>Verifiable, not asserted.</Heading>
</Container>
</Section>
```
Astro equivalent: `<section class="section section--tight night">`.
+28
View File
@@ -0,0 +1,28 @@
import type { ReactNode } from 'react';
import { cx } from './cx';
export interface SectionProps {
/** Paper is the default. Night is a dramatic device: the hero and at most one more. */
tone?: 'paper' | 'night';
/** `tight` for a subordinate band, `intro` for a page head. */
spacing?: 'default' | 'tight' | 'intro';
id?: string;
children?: ReactNode;
}
/** One horizontal band of the page, carrying the vertical air around its content. */
export function Section({ tone = 'paper', spacing = 'default', id, children }: SectionProps) {
return (
<section
id={id}
className={cx(
'section',
spacing === 'tight' && 'section--tight',
spacing === 'intro' && 'section--intro',
tone === 'night' && 'night',
)}
>
{children}
</section>
);
}
+26
View File
@@ -0,0 +1,26 @@
---
category: Layout
---
# Stack
Vertical rhythm between blocks. Nothing in this system carries its own margins, so two
headings placed next to each other sit flush; Stack is what separates them.
`gap` is a step on the space scale, 1 to 9. It is never a raw pixel value: hard-coded
spacing is a defect in this system, and this component is how you avoid one.
`align="start"` keeps children at their natural width, which is what a button or a link
wants. The default stretches them to the column.
```jsx
<Stack gap={6}>
<Heading>Four stages. One method.</Heading>
<Text measure>Threat analysis, engineering, verification and monitoring.</Text>
<Stack gap={4} align="start">
<Button href="/contact">Talk to us</Button>
</Stack>
</Stack>
```
Astro equivalent: `<div style="display: grid; gap: var(--space-6);">`.
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react';
export interface StackProps {
/** A step on the space scale, 1 (tightest) to 9. Defaults to 5. */
gap?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
/** `start` keeps children at their natural width, which is what buttons want. */
align?: 'start' | 'stretch';
children?: ReactNode;
}
/** Vertical rhythm between blocks, always from the space scale. Never a raw pixel gap. */
export function Stack({ gap = 5, align = 'stretch', children }: StackProps) {
return (
<div style={{ display: 'grid', gap: `var(--space-${gap})`, justifyItems: align === 'start' ? 'start' : undefined }}>
{children}
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
---
category: Foundations
---
# Surface
The root of every screen. It sets the ground and the base type, so put it outermost and
put everything else inside it. Without it a design inherits the canvas defaults and none
of CipherCell's typography.
`tone="paper"` is the base surface for the whole site, a warm paper tone, never pure white.
`tone="night"` flips the ink tokens as well as the background, so muted text, hairlines and
buttons inside it correct themselves with nothing to set.
Night is a dramatic device. A page gets the hero and at most one other night band; more than
that and the move stops working.
```jsx
<Surface tone="paper">
<Section>…</Section>
</Surface>
```
In the Astro site this is the `body` element, styled in `src/styles/base.css`.
+26
View File
@@ -0,0 +1,26 @@
import type { ReactNode } from 'react';
export interface SurfaceProps {
/** Warm paper, the base surface for the whole site, or the rare night surface. */
tone?: 'paper' | 'night';
children?: ReactNode;
}
/** The root of every screen: sets the paper or night ground and the base type. */
export function Surface({ tone = 'paper', children }: SurfaceProps) {
return (
<div
className={tone === 'night' ? 'night' : undefined}
style={{
fontFamily: 'var(--font-sans)',
fontSize: 'var(--step-body)',
lineHeight: 'var(--leading-body)',
fontWeight: 'var(--weight-regular)',
background: tone === 'night' ? undefined : 'var(--paper)',
color: tone === 'night' ? undefined : 'var(--ink)',
}}
>
{children}
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
---
category: Type
---
# Text
Body copy and metadata. Set `measure` on every paragraph: it caps the line at 68 characters,
and long unmeasured lines are the fastest way to make this layout look careless.
`muted` is for secondary text and metadata. `size="small"` is for metadata and captions.
Neither is a way to fit more in.
Write short declarative sentences with a full stop. Concrete nouns over categories.
```jsx
<Text measure>Threat models are written once and then run in the pipeline.</Text>
<Text size="small" muted>Passau, Innstr. 43</Text>
```
Astro equivalent: `<p class="body measure">`, `<p class="small muted">`.
+19
View File
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react';
import { cx } from './cx';
export interface TextProps {
size?: 'body' | 'small';
/** Secondary text and metadata. */
muted?: boolean;
/** Caps the line at a readable measure. Use it on every paragraph. */
measure?: boolean;
as?: 'p' | 'span' | 'div';
children?: ReactNode;
}
/** Body copy and metadata. Short declarative sentences, with a full stop. */
export function Text({ size = 'body', muted, measure, as: Tag = 'p', children }: TextProps) {
return (
<Tag className={cx(size === 'small' ? 'small' : 'body', muted && 'muted', measure && 'measure')}>{children}</Tag>
);
}
+17
View File
@@ -0,0 +1,17 @@
---
category: Controls
---
# TextLink
An inline or standalone link. The underline carries the signal, and it deepens slightly on
hover; there is no colour change, because the system has no accent colour.
This is the right control for a secondary action next to a Button, and for any link inside
a sentence.
```jsx
<TextLink href="/method">Explore the method</TextLink>
```
Astro equivalent: `<a class="link" href="/method">`.
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
export interface TextLinkProps {
href: string;
children?: ReactNode;
}
/** An underlined link. There is no accent colour; the underline carries the signal. */
export function TextLink({ href, children }: TextLinkProps) {
return (
<a className="link" href={href}>
{children}
</a>
);
}
+5
View File
@@ -0,0 +1,5 @@
// Joins class names, dropping anything falsy. Every class this package emits is
// a primitive that already exists in the site's own stylesheet - these
// components add no CSS of their own, so the look can never drift from the site.
export const cx = (...parts: Array<string | false | null | undefined>): string =>
parts.filter(Boolean).join(' ');
+28
View File
@@ -0,0 +1,28 @@
export { Surface } from './Surface';
export type { SurfaceProps } from './Surface';
export { Stack } from './Stack';
export type { StackProps } from './Stack';
export { Section } from './Section';
export type { SectionProps } from './Section';
export { Container } from './Container';
export type { ContainerProps } from './Container';
export { Grid } from './Grid';
export type { GridProps } from './Grid';
export { Col } from './Col';
export type { ColProps } from './Col';
export { Display } from './Display';
export type { DisplayProps } from './Display';
export { Heading } from './Heading';
export type { HeadingProps } from './Heading';
export { Text } from './Text';
export type { TextProps } from './Text';
export { Eyebrow } from './Eyebrow';
export type { EyebrowProps } from './Eyebrow';
export { Button } from './Button';
export type { ButtonProps } from './Button';
export { TextLink } from './TextLink';
export type { TextLinkProps } from './TextLink';
export { Rule } from './Rule';
export type { RuleProps } from './Rule';
export { Prose } from './Prose';
export type { ProseProps } from './Prose';
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "../ds-types"
},
"include": ["*.ts", "*.tsx"]
}