next
This commit is contained in:
1 parent
b244287f6a
commit
5a2d49c17f
77 files changed
+2289
-31
No files matched your search
@@ -0,0 +1,111 @@
|
||||
# design-sync notes — ciphercell-homepage
|
||||
|
||||
## What this repo is, for sync purposes
|
||||
|
||||
- **The site itself is not a component library.** It is Astro: `.astro` files compile to HTML
|
||||
at build time and have no runtime form, so they cannot be bundled or shipped to a design
|
||||
agent. The first sync therefore went out tokens-only, and the Claude Design component pane
|
||||
was empty — correct output, but useless to design with.
|
||||
- **What ships instead is `.design-sync/ds-src/`**: fourteen React primitives that emit
|
||||
exactly the classes `src/styles/base.css` already defines, and no CSS of their own. That is
|
||||
the point — the visual truth stays in the site's stylesheet, so the design system cannot
|
||||
drift from the site even as `base.css` changes, and each component's markup ports back to
|
||||
`.astro` one to one (every `<Name>.md` states the Astro equivalent).
|
||||
- **Do not port the page-level `.astro` components** (Hero, Stages, HowItWorks, GridObject).
|
||||
Their styles are scoped inside the `.astro` files, so a React copy would duplicate CSS and
|
||||
drift. The line is: global primitive classes are fair game, component-scoped styles are not.
|
||||
- `Stack` is the one component with no class behind it. Nothing in this system carries its own
|
||||
margins, so without it the design agent hand-rolls pixel gaps, which the brief forbids. It
|
||||
emits `display: grid; gap: var(--space-N)` and nothing else.
|
||||
- Ownership: `ds-src/*.tsx` + `ds-src/*.md` are the source; `ds-types/` is generated by
|
||||
`tsc -p .design-sync/ds-src/tsconfig.json`; `previews/*.tsx` are the authored preview
|
||||
cards. Regenerate the types after ANY edit to a component's props, or the design agent gets
|
||||
a contract that does not match the code.
|
||||
|
||||
## Repo-specific gotchas
|
||||
|
||||
- **React and its types come from `.ds-sync/node_modules` by symlink**, so the site's
|
||||
`package.json` keeps no React dependency: `ln -sfn ../.ds-sync/node_modules/react
|
||||
node_modules/react` (same for `react-dom`), plus `ln -sfn ../../.ds-sync/node_modules/@types/react
|
||||
node_modules/@types/react` — that last one is what lets `tsc` resolve `ReactNode` when
|
||||
emitting `ds-types/`, and what keeps the converter from collapsing inherited props to `any`.
|
||||
**Recreate all three links after any `npm install`** in the repo root — installs prune them.
|
||||
- **`package.json` carries `"types": ".design-sync/ds-types/index.d.ts"`.** That is how the
|
||||
converter discovers the components and their props (`findTypesRoot` → `exportedNames`).
|
||||
It is inert for Astro: the package is private and never published. Remove it and the sync
|
||||
silently falls back to tokens-only with an empty component pane.
|
||||
- **`--node-modules` must be `./node_modules`**, not `.ds-sync/node_modules`. The font
|
||||
containment bound is the git repo enclosing `dirname(--node-modules)`, and **this repo is not
|
||||
a git repo**, so the bound collapses to that directory itself. Pointing at `.ds-sync` puts
|
||||
`public/fonts/` outside the bound and the fonts are silently refused.
|
||||
- **`cssEntry` is derived, not the site's own file.** Run `node .design-sync/make-ds-styles.mjs`
|
||||
before the converter. It rewrites exactly two things in `src/styles/base.css`:
|
||||
- `@import './tokens.css'` → the token file's **contents, inlined**. The converter copies the
|
||||
CSS entry verbatim, so an `@import` pointing outside the bundle dangles and every
|
||||
`var(--token)` resolves to nothing. This is silent: the build and validate both pass, the
|
||||
fonts look right, and the whole palette is simply missing. Check
|
||||
`grep -- '--paper:' ds-bundle/_ds_bundle.css` after any build.
|
||||
- `url('/fonts/…')` → `url('../public/fonts/…')`. The site serves fonts from the web root, so
|
||||
the URLs are absolute; `lib/css.mjs` resolves them with `resolve(srcDir, url)`, an absolute
|
||||
path resolves to `/fonts/…` on disk, fails, and the rule is emitted **verbatim** — leaving
|
||||
`fonts/fonts.css` pointing at a path that does not exist. Every design then renders in a
|
||||
fallback font with nothing flagged.
|
||||
- **The brief cannot ship under its own name.** `claude.ai/design` refuses any uploaded path
|
||||
whose basename is `CLAUDE.md` (and anything under `.claude/`) with
|
||||
`permission_denied: … 1 path(s) are reserved`, and the converter names each guideline file
|
||||
after its source (`lib/docs.mjs:emitGuidelines` uses the source's package-relative path).
|
||||
So `guidelinesGlob` points at **`docs/design-brief.md`**, a verbatim copy of `CLAUDE.md` that
|
||||
`make-ds-styles.mjs` regenerates, and it uploads as `guidelines/docs/design-brief.md`. Edit
|
||||
`CLAUDE.md`; never edit the copy. The refusal is a hard API rule, not a warning, and it
|
||||
rejects the whole `write_files` call without writing any of its other paths.
|
||||
- **`cfg.tokensGlob` does not work here.** `lib/css.mjs:copyTokens` resolves it inside
|
||||
`node_modules` for a tokens *package*; these tokens live in the repo. `tokens/` is therefore
|
||||
empty by design and the definitions ship inside `_ds_bundle.css`, which is in the `styles.css`
|
||||
import closure — which is what designs actually receive. Do not chase the empty directory.
|
||||
- **`.design-sync/overrides/css.mjs` fork**: `cpSync` of the font files fails with `EACCES` on
|
||||
this machine's virtiofs mount (`/home/ciphercell/projects`), creating a 0-byte, write-only
|
||||
file and aborting the build. The fork copies with `readFileSync`/`writeFileSync` instead,
|
||||
which works. Reproducible, not transient. Declared in `cfg.libOverrides`.
|
||||
- **This repo is not under git**, so none of the durable files can be committed. If it is ever
|
||||
initialised, commit `.design-sync/{config.json,NOTES.md,conventions.md,make-ds-styles.mjs,
|
||||
ds-src/,ds-types/,previews/,overrides/}` plus the generated `docs/design-brief.md` and the
|
||||
`types` field in `package.json` (the sync needs both present), and keep `.ds-sync/`,
|
||||
`ds-bundle/` and `.design-sync/.cache/` ignored (already in `.gitignore`).
|
||||
|
||||
## Known render warns
|
||||
|
||||
None outstanding. Two `[GRID_OVERFLOW]` warns fired once on `Grid` and `Display`, whose
|
||||
stories are wider than a grid cell; both are resolved by `cfg.overrides.*.cardMode: "column"`
|
||||
and cannot re-fire by construction. All 14 cards render clean, 30 cells graded `good`.
|
||||
|
||||
The card scaffold sets `body{background:#fff}` AFTER the stylesheets, so a preview that does
|
||||
not wrap in `Surface` renders on white instead of paper. Every authored preview wraps in
|
||||
`Surface` for that reason. Keep it that way.
|
||||
|
||||
## Re-sync risks — what can go stale
|
||||
|
||||
- **`.design-sync/ds-styles.css` is generated and gitignored-adjacent.** It is a copy of
|
||||
`base.css` at sync time. If `src/styles/base.css` or `tokens.css` change and nobody re-runs
|
||||
`make-ds-styles.mjs`, the sync ships the old look with no warning. Always regenerate first.
|
||||
The script throws if `base.css` stops `@import`ing `./tokens.css`, which is the one structural
|
||||
change that would break it silently otherwise.
|
||||
- **`conventions.md` enumerates real token and class names.** It was validated against the build
|
||||
at sync time (30 tokens, 25 classes, all present). Renaming or removing a token or primitive
|
||||
class in `src/styles/` makes the header lie to the design agent, which will then write
|
||||
vocabulary that does not resolve. Re-validate on every sync.
|
||||
- **The brief in `guidelines/docs/design-brief.md`** is a generated copy of the repo's own
|
||||
`CLAUDE.md`, which mixes the design brief with project instructions. It is shipped whole on
|
||||
purpose — it is the system's rulebook — but if it ever grows non-design content, narrow what
|
||||
`make-ds-styles.mjs` copies. The copy is generated, so a `CLAUDE.md` edit that nobody
|
||||
regenerates ships the old brief silently: always run the script before the converter.
|
||||
- **The React symlinks are machine state**, not committed. A fresh clone needs them recreated
|
||||
before the converter will run.
|
||||
- **`conventions.md` was rewritten** when the components landed; the tokens-only version that
|
||||
said `window.CipherCell` is empty is gone. If a future sync ever drops back to tokens-only,
|
||||
that file has to change with it — a header that promises components which do not exist is
|
||||
worse than no header.
|
||||
- **Preview cards are verified by rendering**: `package-validate.mjs` screenshots all 14, and
|
||||
`package-capture.mjs` captures each of the 30 cells for grading. Grades live in the
|
||||
gitignored `.cache/review/`; what makes them durable is the uploaded `_ds_sync.json`.
|
||||
- **`_screenshots/` must never be uploaded.** It is local review state and is not in the
|
||||
approved write paths; including it fails the whole `write_files` call.
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"projectId": "a8a613fc-8b82-4ce8-a202-0f1016f94e79",
|
||||
"pkg": "ciphercell-homepage",
|
||||
"globalName": "CipherCell",
|
||||
"shape": "package",
|
||||
"cssEntry": ".design-sync/ds-styles.css",
|
||||
"guidelinesGlob": [
|
||||
"docs/design-brief.md"
|
||||
],
|
||||
"readmeHeader": ".design-sync/conventions.md",
|
||||
"libOverrides": {
|
||||
"css.mjs": "cpSync of font files fails EACCES on this virtiofs mount; fork copies via read+write"
|
||||
},
|
||||
"entry": ".design-sync/ds-src/index.tsx",
|
||||
"srcDir": ".design-sync/ds-src",
|
||||
"tsconfig": ".design-sync/ds-src/tsconfig.json",
|
||||
"overrides": {
|
||||
"Grid": {
|
||||
"cardMode": "column"
|
||||
},
|
||||
"Display": {
|
||||
"cardMode": "column"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# CipherCell — build with the primitives, never with raw CSS
|
||||
|
||||
CipherCell's site is Astro. What ships here is the layer underneath it: fourteen React
|
||||
primitives that emit exactly the classes the site's own stylesheet defines, plus the tokens
|
||||
and the typeface. Styling lives in the stylesheet, not in the components, so a design built
|
||||
from these parts cannot drift from the site, and its markup ports to `.astro` one to one.
|
||||
Each component's `.prompt.md` gives that Astro equivalent.
|
||||
|
||||
## Wrapping
|
||||
|
||||
Every screen starts with `Surface`. It sets the paper ground, the typeface and the base
|
||||
size; without it a design inherits the canvas defaults and none of CipherCell's typography.
|
||||
`Surface` takes `tone="paper"` (the default) or `tone="night"`.
|
||||
|
||||
Inside it the order is `Section` → `Container` → `Grid` → `Col`. A `Section` carries the
|
||||
vertical air. A `Container` holds the page margins and the maximum width. Nothing else may
|
||||
set page margins.
|
||||
|
||||
There is no provider, no theme object and no load order to get right.
|
||||
|
||||
## The idiom
|
||||
|
||||
Compose from the components. Reach for your own CSS only for layout glue the primitives do
|
||||
not cover, and then use tokens for every value: a raw hex or pixel value is a defect in this
|
||||
system. Do not invent class names; the stylesheet has no utility classes and yours will not
|
||||
resolve.
|
||||
|
||||
**Components.** `Surface`, `Section`, `Container`, `Grid`, `Col`, `Stack` for structure.
|
||||
`Display`, `Heading`, `Text`, `Eyebrow` for type. `Button`, `TextLink` for actions.
|
||||
`Prose`, `Rule` for content.
|
||||
|
||||
**Spacing.** Nothing carries its own margins, so two blocks placed together sit flush.
|
||||
`Stack` is what separates them, and its `gap` is a step on the space scale, 1 to 9. Never
|
||||
hand-roll a pixel gap.
|
||||
|
||||
**Tokens** for your own CSS: `--paper`, `--paper-deep`, `--ink`, `--ink-muted`, `--night`,
|
||||
`--line`, `--overlay`; `--font-sans`; the sizes `--step-display`, `--step-h2`, `--step-title`,
|
||||
`--step-h3`, `--step-body`, `--step-small`, each with a matching `--track-*` and `--leading-*`;
|
||||
`--weight-regular` and `--weight-medium`, and no bold; `--space-1` … `--space-9`, `--section`,
|
||||
`--gutter`, `--gap`, `--container`, `--measure`; `--radius` (0) and `--radius-sm` (2px);
|
||||
`--hairline`; `--dur-fast`, `--dur`, `--dur-slow` and `--ease`.
|
||||
|
||||
**There is no accent colour, and there are no shadows.** Links and buttons work through
|
||||
underline, weight and surface. Separation comes from a `Rule` or from a change of surface.
|
||||
Inside a night surface the ink tokens flip themselves, so muted text, hairlines and buttons
|
||||
all correct themselves with nothing to set.
|
||||
|
||||
## Where the truth lives
|
||||
|
||||
Read each component's `.prompt.md` before using it: it carries the usage judgment and the
|
||||
Astro equivalent. `.d.ts` files carry the props. `styles.css` and its imports hold every token
|
||||
and class definition. The written brief is in `guidelines/`, and it governs attitude, colour,
|
||||
typography, layout, imagery, motion and language, including the patterns that are banned.
|
||||
|
||||
## Building with it
|
||||
|
||||
```jsx
|
||||
<Surface tone="night">
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Grid>
|
||||
<Col span={7}>
|
||||
<Heading>Security is built in,<br />not bolted on.</Heading>
|
||||
</Col>
|
||||
<Col span={4} start={9}>
|
||||
<Stack gap={5} align="start">
|
||||
<Text measure>Every interface between components is an attack surface.</Text>
|
||||
<Button href="/contact">Talk to us</Button>
|
||||
</Stack>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
```
|
||||
|
||||
Note what this does: the blocks sit asymmetrically rather than centred or evenly split, the
|
||||
line break in the headline is placed by hand, the button names the action, and every value
|
||||
that is not a grid position comes from a token.
|
||||
@@ -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">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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" />;
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">`.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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(' ');
|
||||
@@ -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';
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/* tokens.css inlined by .design-sync/make-ds-styles.mjs */
|
||||
/* Design tokens. The only place where colours, sizes and durations are defined.
|
||||
Components consume these; a hard-coded hex or px value in a component is a defect. */
|
||||
|
||||
:root {
|
||||
/* Colour: paper, night, image. Nothing else. */
|
||||
--paper: #E9E5DF;
|
||||
--paper-deep: #DFDAD1;
|
||||
--ink: #1A1A18;
|
||||
--ink-muted: #57564F;
|
||||
--night: #0E100F;
|
||||
--line: rgb(26 26 24 / 0.14);
|
||||
|
||||
/* Immutable copies for resetting inside a .night or overlay context */
|
||||
--ink-base: #1A1A18;
|
||||
--ink-muted-base: #57564F;
|
||||
--line-base: rgb(26 26 24 / 0.14);
|
||||
|
||||
/* Counterparts on night surfaces */
|
||||
--ink-on-night: var(--paper);
|
||||
--ink-muted-on-night: rgb(233 229 223 / 0.62);
|
||||
--line-on-night: rgb(233 229 223 / 0.18);
|
||||
|
||||
/* Image overlay */
|
||||
--overlay: linear-gradient(to top, rgb(14 16 15 / 0.75), rgb(14 16 15 / 0.2));
|
||||
|
||||
/* Type */
|
||||
--font-sans: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
|
||||
--step-display: clamp(2.75rem, 1.2rem + 6.2vw, 7rem);
|
||||
--step-h2: clamp(1.9rem, 1.1rem + 2.6vw, 3.25rem);
|
||||
--step-h3: clamp(1.25rem, 1.05rem + 0.7vw, 1.6rem);
|
||||
/* Card and footer titles: between h2 and h3, for headings that name rather than claim. */
|
||||
--step-title: clamp(1.6rem, 1.2rem + 1.5vw, 2.5rem);
|
||||
--step-body: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
|
||||
--step-small: 0.875rem;
|
||||
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
|
||||
--track-display: -0.03em;
|
||||
--track-h2: -0.02em;
|
||||
--track-h3: -0.01em;
|
||||
|
||||
--leading-display: 1.02;
|
||||
--leading-h2: 1.08;
|
||||
--leading-h3: 1.2;
|
||||
--leading-title: 1.1;
|
||||
--leading-body: 1.55;
|
||||
--leading-small: 1.4;
|
||||
|
||||
--measure: 34em;
|
||||
|
||||
/* Space */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
--space-7: 3rem;
|
||||
--space-8: 4rem;
|
||||
--space-9: 6rem;
|
||||
|
||||
--section: clamp(6rem, 12vw, 11rem);
|
||||
--gutter: clamp(1.25rem, 5vw, 5rem);
|
||||
--gap: clamp(1rem, 2vw, 2rem);
|
||||
--container: 90rem; /* 1440px */
|
||||
|
||||
/* Shape */
|
||||
--hairline: 1px;
|
||||
--radius: 0;
|
||||
--radius-sm: 2px;
|
||||
|
||||
/* Motion */
|
||||
--ease: cubic-bezier(0.2, 0, 0, 1);
|
||||
--dur-fast: 180ms;
|
||||
--dur: 240ms;
|
||||
--dur-slow: 320ms;
|
||||
--dur-hero: 1400ms;
|
||||
|
||||
/* Chrome */
|
||||
--header-height: 4.5rem;
|
||||
|
||||
/* Focus */
|
||||
--focus-width: 2px;
|
||||
--focus-offset: 3px;
|
||||
}
|
||||
|
||||
/* Self-hosted Inter Tight, variable, latin + latin-ext. */
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/inter-tight-latin-wght-normal.woff2') format('woff2-variations');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/inter-tight-latin-ext-wght-normal.woff2') format('woff2-variations');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { margin: 0; }
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
[id] { scroll-margin-top: var(--header-height); }
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--step-body);
|
||||
line-height: var(--leading-body);
|
||||
font-weight: var(--weight-regular);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: 'ss01' off, 'cv11' on;
|
||||
}
|
||||
img, picture, svg, video { display: block; max-width: 100%; }
|
||||
img { height: auto; }
|
||||
input, button, textarea, select { font: inherit; color: inherit; }
|
||||
button { background: none; border: 0; padding: 0; cursor: pointer; }
|
||||
ul[role='list'], ol[role='list'] { list-style: none; padding: 0; }
|
||||
a { color: inherit; text-decoration-color: currentColor; text-underline-offset: 0.14em; text-decoration-thickness: from-font; }
|
||||
h1, h2, h3, h4 { font-weight: var(--weight-medium); overflow-wrap: break-word; }
|
||||
p { overflow-wrap: break-word; }
|
||||
|
||||
/* Focus: one ring, everywhere. */
|
||||
:focus-visible {
|
||||
outline: var(--focus-width) solid currentColor;
|
||||
outline-offset: var(--focus-offset);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
:focus:not(:focus-visible) { outline: none; }
|
||||
|
||||
/* Type scale */
|
||||
.display {
|
||||
font-size: var(--step-display);
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: var(--track-display);
|
||||
line-height: var(--leading-display);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.h2 {
|
||||
font-size: var(--step-h2);
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: var(--track-h2);
|
||||
line-height: var(--leading-h2);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.h3 {
|
||||
font-size: var(--step-h3);
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: var(--track-h3);
|
||||
line-height: var(--leading-h3);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.body { font-size: var(--step-body); line-height: var(--leading-body); }
|
||||
.small {
|
||||
font-size: var(--step-small);
|
||||
line-height: var(--leading-small);
|
||||
}
|
||||
.muted { color: var(--ink-muted); }
|
||||
|
||||
/* An eyebrow is sentence case, muted, small. Never all caps. */
|
||||
.eyebrow {
|
||||
font-size: var(--step-small);
|
||||
line-height: var(--leading-small);
|
||||
color: var(--ink-muted);
|
||||
font-weight: var(--weight-regular);
|
||||
}
|
||||
|
||||
/* Primitives */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--gutter);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
column-gap: var(--gap);
|
||||
row-gap: var(--space-7);
|
||||
}
|
||||
.section {
|
||||
padding-block: var(--section);
|
||||
}
|
||||
.section--tight { padding-block: calc(var(--section) * 0.6); }
|
||||
/* A page head: full air above, less below, so the content follows the title. */
|
||||
.section--intro { padding-block: var(--section) calc(var(--section) * 0.4); }
|
||||
.measure { max-width: var(--measure); }
|
||||
|
||||
/* Night surface */
|
||||
.night {
|
||||
background: var(--night);
|
||||
color: var(--ink-on-night);
|
||||
--ink: var(--ink-on-night);
|
||||
--ink-muted: var(--ink-muted-on-night);
|
||||
--line: var(--line-on-night);
|
||||
}
|
||||
|
||||
/* Hairline rules carry information: they separate, they don't decorate. */
|
||||
.rule { border: 0; border-top: var(--hairline) solid var(--line); }
|
||||
|
||||
/* Links and buttons work through underline, weight and surface. */
|
||||
.link {
|
||||
display: inline-block;
|
||||
font-weight: var(--weight-medium);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: var(--hairline);
|
||||
text-underline-offset: 0.2em;
|
||||
transition: text-underline-offset var(--dur-fast) var(--ease);
|
||||
}
|
||||
.link:hover { text-underline-offset: 0.3em; }
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: var(--space-3) var(--space-5);
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
font-weight: var(--weight-medium);
|
||||
text-decoration: none;
|
||||
line-height: var(--leading-small);
|
||||
border-radius: var(--radius);
|
||||
transition: background var(--dur-fast) var(--ease);
|
||||
}
|
||||
.button:hover { background: var(--ink-muted); }
|
||||
.button--small { padding: var(--space-2) var(--space-4); font-size: var(--step-small); }
|
||||
.night .button { background: var(--paper); color: var(--night); }
|
||||
.night .button:hover { background: var(--paper-deep); }
|
||||
|
||||
/* Prose: long-form text */
|
||||
.prose { max-width: var(--measure); }
|
||||
.prose > * + * { margin-top: var(--space-5); }
|
||||
.prose h2 {
|
||||
font-size: var(--step-h3);
|
||||
letter-spacing: var(--track-h3);
|
||||
line-height: var(--leading-h3);
|
||||
margin-top: var(--space-8);
|
||||
}
|
||||
.prose h3 { font-size: var(--step-body); margin-top: var(--space-7); }
|
||||
.prose ul, .prose ol { padding-left: 1.2em; }
|
||||
.prose li + li { margin-top: var(--space-3); }
|
||||
.prose a { text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.2em; }
|
||||
.prose strong { font-weight: var(--weight-medium); }
|
||||
.prose blockquote {
|
||||
padding-left: var(--space-5);
|
||||
border-left: var(--hairline) solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.prose img { width: 100%; }
|
||||
.prose figcaption { margin-top: var(--space-3); font-size: var(--step-small); color: var(--ink-muted); }
|
||||
|
||||
/* Images */
|
||||
.img-cover { width: 100%; height: 100%; object-fit: cover; }
|
||||
.img-doc { filter: saturate(0.55) contrast(1.02); }
|
||||
.img-mono { filter: grayscale(1) contrast(1.04); }
|
||||
/* Portraits were shot on light grey; multiply lets the paper carry them like a print. */
|
||||
.img-print { mix-blend-mode: multiply; }
|
||||
|
||||
/* Skip link */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: var(--gutter);
|
||||
top: var(--space-3);
|
||||
transform: translateY(-200%);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
z-index: 100;
|
||||
}
|
||||
.skip-link:focus { transform: none; }
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute !important;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden; clip: rect(0 0 0 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
|
||||
/* Motion off. No exceptions. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
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 declare function Button({ href, size, children }: ButtonProps): import("react").JSX.Element;
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
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 declare function Col({ span, start, children }: ColProps): import("react").JSX.Element;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
export interface ContainerProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
/** Centres content to the page width and holds the page margins. */
|
||||
export declare function Container({ children }: ContainerProps): import("react").JSX.Element;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
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 declare function Display({ as: Tag, children }: DisplayProps): import("react").JSX.Element;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
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 declare function Eyebrow({ children }: EyebrowProps): import("react").JSX.Element;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
export interface GridProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
/** The twelve-column grid. Place children with Col, asymmetrically. */
|
||||
export declare function Grid({ children }: GridProps): import("react").JSX.Element;
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
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 declare function Heading({ level, id, children }: HeadingProps): import("react").JSX.Element;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
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 declare function Prose({ children }: ProseProps): import("react").JSX.Element;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export interface RuleProps {
|
||||
/** Nothing. A rule separates; it never decorates. */
|
||||
children?: never;
|
||||
}
|
||||
/** A hairline. Use it only where it carries information. */
|
||||
export declare function Rule(_props: RuleProps): import("react").JSX.Element;
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
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 declare function Section({ tone, spacing, id, children }: SectionProps): import("react").JSX.Element;
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
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 declare function Stack({ gap, align, children }: StackProps): import("react").JSX.Element;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
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 declare function Surface({ tone, children }: SurfaceProps): import("react").JSX.Element;
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import type { ReactNode } from 'react';
|
||||
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 declare function Text({ size, muted, measure, as: Tag, children }: TextProps): import("react").JSX.Element;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
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 declare function TextLink({ href, children }: TextLinkProps): import("react").JSX.Element;
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const cx: (...parts: Array<string | false | null | undefined>) => string;
|
||||
Vendored
+28
@@ -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';
|
||||
@@ -0,0 +1,39 @@
|
||||
// Derives the two files the converter reads that cannot be the site's own:
|
||||
// the design-system stylesheet, and the guideline copy of the brief.
|
||||
//
|
||||
// The stylesheet comes from src/styles/base.css. Two things have to change, and nothing else:
|
||||
// 1. `@import './tokens.css'` is replaced by the token file's contents. The bundle is
|
||||
// copied verbatim, so an @import pointing outside it would dangle and every
|
||||
// var(--token) in the sheet would resolve to nothing.
|
||||
// 2. `url('/fonts/...')` is absolute because the site serves fonts from the web root;
|
||||
// rewritten relative to this directory so the converter can resolve and copy them.
|
||||
//
|
||||
// The guideline copy is docs/design-brief.md, a verbatim copy of CLAUDE.md. The converter
|
||||
// names each uploaded guideline after its source file, and claude.ai/design refuses any
|
||||
// path called CLAUDE.md, so the brief cannot be shipped under its own name.
|
||||
//
|
||||
// Run it before the converter: node .design-sync/make-ds-styles.mjs
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const tokens = readFileSync(join(root, 'src/styles/tokens.css'), 'utf8');
|
||||
const base = readFileSync(join(root, 'src/styles/base.css'), 'utf8');
|
||||
|
||||
const importRx = /@import\s+['"]\.\/tokens\.css['"]\s*;/;
|
||||
if (!importRx.test(base)) throw new Error("src/styles/base.css no longer @imports './tokens.css' - update this script");
|
||||
|
||||
const out = base
|
||||
.replace(importRx, `/* tokens.css inlined by .design-sync/make-ds-styles.mjs */\n${tokens.trim()}`)
|
||||
.replace(/url\('\/fonts\//g, "url('../public/fonts/");
|
||||
|
||||
writeFileSync(join(root, '.design-sync/ds-styles.css'), out);
|
||||
console.log(`wrote .design-sync/ds-styles.css (${out.length} bytes, tokens inlined)`);
|
||||
|
||||
const brief = readFileSync(join(root, 'CLAUDE.md'), 'utf8');
|
||||
const banner =
|
||||
'<!-- Generated from CLAUDE.md by .design-sync/make-ds-styles.mjs. Edit CLAUDE.md, not this file. -->\n\n';
|
||||
mkdirSync(join(root, 'docs'), { recursive: true });
|
||||
writeFileSync(join(root, 'docs/design-brief.md'), banner + brief);
|
||||
console.log(`wrote docs/design-brief.md (${brief.length} bytes, copied from CLAUDE.md)`);
|
||||
@@ -0,0 +1,165 @@
|
||||
// forked from design-sync lib/css.mjs - cpSync of font files fails with EACCES on this
|
||||
// repo's virtiofs mount; read+write copies the same bytes and works.
|
||||
// CSS handling: token-file copy, @font-face extraction, and the final
|
||||
// styles.css writer (the styles entry point - an @import list, never inlined CSS).
|
||||
// Storybook-only fallbacks live in css-fallback.mjs.
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { ls } from '../../.ds-sync/lib/common.mjs';
|
||||
|
||||
// Parse @font-face blocks from `cssPath` -> resolve url() paths relative to
|
||||
// `srcDir` -> copy .woff2/.woff/.ttf/.otf to fonts/ -> return rewritten rules.
|
||||
// `roots` bounds the resolved path so a `url(../../etc/passwd)` can't escape -
|
||||
// one or more directories the font file may legitimately be under.
|
||||
export function extractFonts(cssPath, srcDir, { fontsOut, roots }) {
|
||||
// Bounds and targets are REALPATHED (a font-named symlink inside the
|
||||
// workspace pointing outside must not smuggle an arbitrary file into the
|
||||
// uploadable fonts/ - same containment rule as cfg.extraFonts), and the
|
||||
// check is relative()-based, not a startsWith prefix: case-insensitive on
|
||||
// win32 where a pnpm junction can realpath to canonical D:\ while a
|
||||
// symlink-free root keeps user-typed d:\ (failure direction of the prefix
|
||||
// form is false-rejection - legit brand fonts silently skipped).
|
||||
const realOf = (p) => { try { return realpathSync(p); } catch { return null; } };
|
||||
const rootsReal = (Array.isArray(roots) ? roots : [roots]).map((r) => realOf(resolve(r)) ?? resolve(r));
|
||||
const insideRoots = (p) => rootsReal.some((root) => {
|
||||
const rel = relative(root, p);
|
||||
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
||||
});
|
||||
if (!existsSync(cssPath)) return [];
|
||||
const css = readFileSync(cssPath, 'utf8');
|
||||
const rules = [];
|
||||
for (const m of css.matchAll(/@font-face\s*\{([^}]+)\}/g)) {
|
||||
const body = m[1];
|
||||
const fam = body.match(/font-family\s*:\s*['"]?([^;'"\n]+)['"]?/)?.[1]?.trim();
|
||||
const urls = [...body.matchAll(/url\(\s*['"]?([^'")]+?\.(?:woff2?|ttf|otf))['"]?\s*\)/gi)].map((u) => u[1]);
|
||||
if (!fam || !urls.length) continue;
|
||||
let rewritten = body;
|
||||
for (const u of urls) {
|
||||
if (/^(https?:|data:)/.test(u)) continue; // CDN / inline - leave as-is
|
||||
const src = resolve(srcDir, u.replace(/^\.\//, ''));
|
||||
const real = realOf(src);
|
||||
if (!real || !insideRoots(real)) continue;
|
||||
const name = basename(src);
|
||||
mkdirSync(fontsOut, { recursive: true });
|
||||
// cpSync trips EACCES on this mount; a plain read+write copies the same bytes.
|
||||
writeFileSync(join(fontsOut, name), readFileSync(real));
|
||||
rewritten = rewritten.split(u).join(`./${name}`);
|
||||
}
|
||||
rules.push(`@font-face{${rewritten}}`);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
// Copy a tokens package's shipped CSS verbatim into OUT/tokens/. tokensGlob
|
||||
// supports a single trailing `**` segment for deep recursion.
|
||||
export function copyTokens({ tokensPkg, tokensGlob, nodeModules, out }) {
|
||||
const tokenFiles = [];
|
||||
if (!tokensPkg) return tokenFiles;
|
||||
const tdir = join(nodeModules, tokensPkg);
|
||||
const tjson = JSON.parse(readFileSync(join(tdir, 'package.json'), 'utf8'));
|
||||
if (tokensGlob) {
|
||||
const parts = tokensGlob.split('/');
|
||||
const pat = parts.pop();
|
||||
const rx = new RegExp('^' + pat.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
||||
const deep = parts.includes('**');
|
||||
const base = join(tdir, ...parts.filter((p) => p !== '**'));
|
||||
(function collect(d, rel = '') {
|
||||
if (!existsSync(d)) return;
|
||||
for (const e of ls(d, { withFileTypes: true })) {
|
||||
const r = rel ? `${rel}/${e.name}` : e.name;
|
||||
if (e.isDirectory() && deep) collect(join(d, e.name), r);
|
||||
else if (e.isFile() && rx.test(e.name)) {
|
||||
// Preserve subdir structure so @import './sub/x.css' inside a
|
||||
// copied file keeps resolving.
|
||||
mkdirSync(dirname(join(out, 'tokens', r)), { recursive: true });
|
||||
cpSync(join(d, e.name), join(out, 'tokens', r));
|
||||
tokenFiles.push(r);
|
||||
}
|
||||
}
|
||||
})(base);
|
||||
} else {
|
||||
for (const sub of ['dist/css', 'css', 'dist', '.']) {
|
||||
const d = join(tdir, sub);
|
||||
if (!existsSync(d)) continue;
|
||||
for (const f of ls(d)) {
|
||||
if (f.endsWith('.css')) {
|
||||
cpSync(join(d, f), join(out, 'tokens', f));
|
||||
tokenFiles.push(f);
|
||||
}
|
||||
}
|
||||
if (tokenFiles.length) break;
|
||||
}
|
||||
}
|
||||
console.error(` tokens: ${tokenFiles.length} files from ${tokensPkg}@${tjson.version}`);
|
||||
return tokenFiles;
|
||||
}
|
||||
|
||||
// _ds_bundle.css enters the styles.css closure (rendered designs load it),
|
||||
// so its @font-face blocks must not carry package-relative url()s: the font
|
||||
// binaries aren't uploaded at those paths, and a dead-src face declared
|
||||
// AFTER fonts/fonts.css shadows the working copy of the same family
|
||||
// (browsers don't fall back to an earlier duplicate face) - brand fonts
|
||||
// silently degrade to system fonts. Rewrite urls to the fonts/ copies
|
||||
// extractFonts made (matched by basename, same flattening); drop any face
|
||||
// still referencing an unresolvable relative url (a dead src is worse than
|
||||
// no face - and it recurs as an app-side error on every compile).
|
||||
export function rewriteBundleFontFaces({ out, bundleCss }) {
|
||||
const p = bundleCss ?? join(out, '_ds_bundle.css');
|
||||
let css;
|
||||
try { css = readFileSync(p, 'utf8'); } catch { return; }
|
||||
if (!/@font-face/i.test(css)) return;
|
||||
let dropped = 0, rewrote = 0;
|
||||
const next = css.replace(/@font-face\s*\{[^}]*\}/gi, (block) => {
|
||||
let b = block;
|
||||
for (const m of block.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)) {
|
||||
const u = m[1];
|
||||
if (/^(?:https?:|data:|\.\/fonts\/)/.test(u)) continue;
|
||||
const name = basename(u.split(/[?#]/)[0]);
|
||||
if (existsSync(join(out, 'fonts', name))) { b = b.split(u).join(`./fonts/${name}`); rewrote++; }
|
||||
}
|
||||
if (/url\(\s*['"]?(?!https?:|data:|\.\/fonts\/)/i.test(b)) { dropped++; return '/* @ds-font-face-dropped: unresolvable src */'; }
|
||||
return b;
|
||||
});
|
||||
if (rewrote || dropped) {
|
||||
writeFileSync(p, next);
|
||||
console.error(` _ds_bundle.css fonts: ${rewrote} url(s) rewritten to fonts/${dropped ? `, ${dropped} dead @font-face block(s) dropped` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
// styles.css - the styles entry point. The claude.ai/design app's
|
||||
// contract: rendered designs consume ONLY this file's transitive @import
|
||||
// closure (plus the JS bundle) - `_ds_bundle.css` is not loaded by anything
|
||||
// app-side, so component CSS left out of this closure never reaches a design
|
||||
// built with the DS (the DS-pane cards link it directly, which masks the
|
||||
// gap). Import it LAST, after tokens/fonts. Token pollution: the app's
|
||||
// scope filter is a permissive heuristic - :root/theme containers, but also
|
||||
// single lowercase class selectors (`.btn { --btn-pad: ... }`) and data-attr
|
||||
// selectors register as token scopes - so public component vars from the
|
||||
// bundle DO enter the token list. That's tolerable (they're real, usable
|
||||
// vars) and the price of designs actually receiving component CSS.
|
||||
export function writeStylesCss({ out, tokenFiles, bundleCss, fontRules, remoteImports }) {
|
||||
let hasBundleCss = false;
|
||||
try {
|
||||
// The CSS-in-JS placeholder (@ds-css-runtime) isn't real CSS - importing
|
||||
// it would also suppress the [CSS_RUNTIME] message below.
|
||||
const css = readFileSync(bundleCss ?? join(out, '_ds_bundle.css'), 'utf8');
|
||||
hasBundleCss = css.trim().length > 0 && !css.startsWith('/* @ds-css-runtime');
|
||||
} catch { /* absent */ }
|
||||
const styleImports = [
|
||||
...tokenFiles.map((f) => `@import "./tokens/${f}";`),
|
||||
...(fontRules.length ? ['@import "./fonts/fonts.css";'] : []),
|
||||
...remoteImports.map((u) => `@import url("${u}");`),
|
||||
...(hasBundleCss ? ['@import "./_ds_bundle.css";'] : []),
|
||||
];
|
||||
if (styleImports.length) {
|
||||
writeFileSync(join(out, 'styles.css'), styleImports.join('\n') + '\n');
|
||||
console.error(` styles.css: ${styleImports.length} @import(s)${hasBundleCss ? ' (incl. _ds_bundle.css \u2014 component styles ship to designs via this closure)' : ''}`);
|
||||
return;
|
||||
}
|
||||
writeFileSync(
|
||||
join(out, 'styles.css'),
|
||||
'/* @ds-styles: runtime \u2014 this design system injects its styles at runtime (CSS-in-JS); no static stylesheet to import. */\n',
|
||||
);
|
||||
console.error('[CSS_RUNTIME] no static CSS found (tokens/component/fonts/remote all empty) \u2014 wrote a self-styling styles.css. Expected for CSS-in-JS DSes; if this DS does ship a stylesheet, set cfg.cssEntry to it. If cfg.cssEntry is ALREADY set and renders verify, this line refers only to the scrape \u2014 do not chase it.');
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Surface, Section, Container, Stack, Button, TextLink, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Primary = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5} align="start">
|
||||
<Button href="/contact">Talk to us</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithSecondary = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={4} align="start">
|
||||
<Button href="/cipherverifier">Explore CipherVerifier</Button>
|
||||
<TextLink href="/method">Explore the method</TextLink>
|
||||
<Text size="small" muted>A second action is a link, never a second button.</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Small = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5} align="start">
|
||||
<Button size="small">Copy the report</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const OnNight = () => (
|
||||
<Surface tone="night">
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={4} align="start">
|
||||
<Button href="/contact">Talk to us</Button>
|
||||
<Text size="small" muted>On a night surface the button inverts itself.</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Surface, Section, Container, Grid, Col, Stack, Heading, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Offset = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Grid>
|
||||
<Col span={8}>
|
||||
<Heading>Four stages. One method.</Heading>
|
||||
</Col>
|
||||
<Col span={5} start={8}>
|
||||
<Text measure>
|
||||
Threat analysis, engineering, verification and monitoring run alongside your development.
|
||||
</Text>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const FullWidth = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Grid>
|
||||
<Col>
|
||||
<Stack gap={4}>
|
||||
<Heading level={3}>A block across all twelve columns.</Heading>
|
||||
<Text muted>Twelve is the default, so span may be left out.</Text>
|
||||
</Stack>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Surface, Section, Container, Stack, Heading, Text, Rule } from 'ciphercell-homepage';
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading level={3}>Talk to us.</Heading>
|
||||
<Text measure muted>
|
||||
The page margins and the maximum page width both come from the Container.
|
||||
</Text>
|
||||
<Rule />
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Surface, Section, Container, Stack, Display, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Hero = () => (
|
||||
<Surface tone="night">
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Display>
|
||||
Security for the software<br />everything else runs on.
|
||||
</Display>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const OnPaper = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Display as="p">CipherVerifier.<br />Every finding, verified.</Display>
|
||||
<Text muted>Set as a paragraph, because the page heading is elsewhere.</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Surface, Section, Container, Stack, Eyebrow, Heading, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const AboveHeadline = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={3}>
|
||||
<Eyebrow>Insights</Eyebrow>
|
||||
<Heading>What a verification report owes you.</Heading>
|
||||
<Text measure muted>
|
||||
The eyebrow names the section. It is sentence case, and never set in all caps.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Surface, Section, Container, Grid, Col, Stack, Heading, Text, TextLink } from 'ciphercell-homepage';
|
||||
|
||||
export const Asymmetric = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Grid>
|
||||
<Col span={7}>
|
||||
<Heading>Security is built in,<br />not bolted on.</Heading>
|
||||
</Col>
|
||||
<Col span={4} start={9}>
|
||||
<Stack gap={5} align="start">
|
||||
<Text measure>
|
||||
We model real adversaries and build zero trust into the architecture rather than
|
||||
around it.
|
||||
</Text>
|
||||
<TextLink href="/about">About CipherCell</TextLink>
|
||||
</Stack>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Thirds = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Grid>
|
||||
<Col span={4}>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Private 5G</Heading>
|
||||
<Text size="small" muted>Open interfaces, many vendors.</Text>
|
||||
</Stack>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Critical networks</Heading>
|
||||
<Text size="small" muted>Where a breach stops production.</Text>
|
||||
</Stack>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Cloud-native</Heading>
|
||||
<Text size="small" muted>Security that runs as code.</Text>
|
||||
</Stack>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Surface, Section, Container, Stack, Heading, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const SectionHeadline = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Heading>Verifiable, not asserted.</Heading>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Levels = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={6}>
|
||||
<Heading>Four stages. One method.</Heading>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Threat analysis</Heading>
|
||||
<Text measure muted>
|
||||
Automated analysis of real attack paths, running inside your pipeline.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Verification</Heading>
|
||||
<Text measure muted>
|
||||
Independent review of code and security measures, hardening and testing.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Surface, Section, Container, Prose } from 'ciphercell-homepage';
|
||||
|
||||
export const Insight = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Prose>
|
||||
<h2>What a verification report owes you</h2>
|
||||
<p>
|
||||
A finding without its reasoning is an opinion. Every verdict in a CipherVerifier report
|
||||
carries the file, the line and the argument that produced it.
|
||||
</p>
|
||||
<ul>
|
||||
<li>The analyser that raised it</li>
|
||||
<li>The verdict, and how certain it is</li>
|
||||
<li>The fix we would ship</li>
|
||||
</ul>
|
||||
<blockquote>Verifiable, not asserted.</blockquote>
|
||||
</Prose>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Surface, Section, Container, Stack, Rule, Heading, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const BetweenItems = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Threat analysis</Heading>
|
||||
<Text size="small" muted>Threat model, attack paths, CI/CD integration.</Text>
|
||||
</Stack>
|
||||
<Rule />
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Engineering</Heading>
|
||||
<Text size="small" muted>Zero trust, key management, secure APIs.</Text>
|
||||
</Stack>
|
||||
<Rule />
|
||||
<Stack gap={3}>
|
||||
<Heading level={3}>Verification</Heading>
|
||||
<Text size="small" muted>Independent review, hardening, testing.</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Surface, Section, Container, Stack, Heading, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Section>
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading>Who we work with</Heading>
|
||||
<Text measure muted>Operators, integrators and the people who run critical networks.</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Tight = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={4}>
|
||||
<Heading level={3}>In numbers.</Heading>
|
||||
<Text measure muted>A subordinate band carries less air than a full section.</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Intro = () => (
|
||||
<Surface>
|
||||
<Section spacing="intro">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading>Insights</Heading>
|
||||
<Text measure muted>
|
||||
Notes on software security in private 5G, critical networks and the systems that run them.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Night = () => (
|
||||
<Surface>
|
||||
<Section tone="night">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading>Where we come from.</Heading>
|
||||
<Text measure muted>
|
||||
CipherCell grew out of Open RAN security research at the University of Passau.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Surface, Section, Container, Stack, Heading, Text, Button } from 'ciphercell-homepage';
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading>Four stages. One method.</Heading>
|
||||
<Text measure>
|
||||
Threat analysis, engineering, verification and monitoring, applied to your software as
|
||||
one continuous method rather than four separate projects.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Tight = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={2}>
|
||||
<Text size="small" muted>Threat analysis</Text>
|
||||
<Text size="small">Threat model</Text>
|
||||
<Text size="small">Attack paths</Text>
|
||||
<Text size="small">CI/CD integration</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const AlignStart = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5} align="start">
|
||||
<Text measure muted>A button should keep its natural width.</Text>
|
||||
<Button href="/contact">Talk to us</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Surface, Section, Container, Stack, Heading, Text, Button } from 'ciphercell-homepage';
|
||||
|
||||
export const Paper = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5}>
|
||||
<Heading>Security is built in,<br />not bolted on.</Heading>
|
||||
<Text measure>
|
||||
Infrastructure is software now. Every interface between components is an attack surface.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Night = () => (
|
||||
<Surface tone="night">
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5} align="start">
|
||||
<Heading>Four stages.<br />One method.</Heading>
|
||||
<Text measure muted>
|
||||
Threat analysis, engineering, verification and monitoring, applied as one method.
|
||||
</Text>
|
||||
<Button href="/cipherverifier">Explore CipherVerifier</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Surface, Section, Container, Stack, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Measured = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Text measure>
|
||||
Infrastructure is software now. Private 5G networks, industrial control systems and cloud
|
||||
platforms are assembled from the code and components of many vendors. Every interface
|
||||
between them is an attack surface, and off-the-shelf tools were not built for that.
|
||||
</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Muted = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Text measure muted>
|
||||
CipherCell is a security engineering company for software. From the first threat model to
|
||||
the running system.
|
||||
</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Small = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={3}>
|
||||
<Text size="small">Threat model, attack paths, CI/CD integration.</Text>
|
||||
<Text size="small" muted>Passau, Innstr. 43</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Surface, Section, Container, Stack, TextLink, Text } from 'ciphercell-homepage';
|
||||
|
||||
export const Standalone = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Stack gap={5} align="start">
|
||||
<TextLink href="/method">Explore the method</TextLink>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const InSentence = () => (
|
||||
<Surface>
|
||||
<Section spacing="tight">
|
||||
<Container>
|
||||
<Text measure>
|
||||
Every finding is verified twice. The pipeline is described in{' '}
|
||||
<TextLink href="/cipherverifier">the CipherVerifier notes</TextLink>.
|
||||
</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Surface>
|
||||
);
|
||||
Reference in new issue
Block a user