@amplemarket/design-system-web
v0.1.2
Published
Website-scoped design system for Amplemarket. Inherits primitives from @amplemarket/design-system and adds a semantic layer plus website-only primitive extensions.
Readme
@amplemarket/design-system-web
The website-scoped design system for Amplemarket. Inherits the primitives from @amplemarket/design-system and adds website-specific layers on top:
- Primitive extensions — extra stops on existing core scales that only marketing pages need (e.g.
color.neutral.25,color.blue.25). - Semantics — usage roles that are unique to the website surface (e.g.
surface.primary,text.primary,typography.label-m). These reference primitives directly; there are intentionally no per-component semantic tokens. - Astro components — atoms (
<Button variant="primary" />,<Header />) shipped as.astrosource and compiled by the consuming Astro project. Each component's CSS reads fromvar(--…)tokens, so the token surface stands on its own and components are an optional convenience.
Anything brand-universal (the indigo ramp, the spacing scale, Labil Grotesk) lives in the core and is consumed unchanged. This package never re-declares core primitives — it only adds what the website surface needs and assigns roles. Other surfaces (product UI, email, decks) maintain their own semantic and component layers in their own repos.
The two halves are independent: the token system (Style Dictionary → CSS/SCSS/JS/JSON in dist/) builds and publishes on its own; the Astro component layer carries no build step and consumes the emitted token CSS vars.
Install
This package is published publicly to the npmjs registry. The core package is a transitive dependency and is pulled in automatically — consumers never need to install or import it directly.
# npm
npm install @amplemarket/design-system-web
# pnpm
pnpm add @amplemarket/design-system-web
# yarn
yarn add @amplemarket/design-system-webPin to an exact package version:
npm install @amplemarket/[email protected]astro is a peerDependency (^4 || ^5 || ^6 || ^7) — consumers compile the shipped .astro files with their own Astro version.
How publishing works
dist/ is not committed. The publish workflow builds it in CI before npm publish, and the generated files are included in the public npmjs tarball. The build resolves against the core's source tokens (a transitive dependency) and emits a single self-contained bundle: core primitives + website extensions + semantics, all in one file per platform. The .astro components need no build — they ship as source.
The prepare lifecycle script remains for local npm pack / npm publish and git-URL fallback installs, but registry consumers should receive the already-built dist/ output from the package.
Usage
The token bundle is self-contained: every primitive a website token references is also declared in the bundle's output. Consumers import this package only — the core is an implementation detail.
CSS custom properties
@import "@amplemarket/design-system-web/css";
.hero {
background: var(--surface-primary);
padding: var(--spacing-64) var(--spacing-24);
color: var(--color-indigo-900);
}
.faq-row:hover {
background: var(--color-blue-25);
}Core primitives (--color-indigo-900, --spacing-64, --shadow-300, …) are declared right alongside the website's own tokens (--color-neutral-25, --surface-primary, …) — one import, everything resolves.
SCSS variables
@use "@amplemarket/design-system-web/scss" as ds;
.hero { background: ds.$surface-primary; }
.cta { color: ds.$color-indigo-900; }JavaScript / TypeScript
The build also emits a nested JS/TS object (dist/index.mjs, index.cjs, index.d.ts) so tokens can be read programmatically — every core primitive plus every website extension and semantic, in one tree.
import ds from "@amplemarket/design-system-web";
const pageBg = ds.surface.primary; // resolved value
const indigo = ds.color.indigo["900"]; // "#10054d"
const gap = ds.spacing["24"]; // "24px"Raw JSON (tool-to-tool, AI prompts, scripts)
import webTokens from "@amplemarket/design-system-web/json";Or read any source file directly, e.g. @amplemarket/design-system-web/tokens/semantic/color.json.
Astro components
Components are exported per-file under ./astro/* and imported by path (there is no barrel — .astro files can't be named-exported from an index.ts):
---
import Button from "@amplemarket/design-system-web/astro/Button.astro";
import "@amplemarket/design-system-web/css"; // token CSS vars, once at the app root
import "@amplemarket/design-system-web/astro/styles.css"; // component CSS, once
---
<Button variant="primary" size="large">Click me</Button>.astro files are compiled by the consuming Astro project — this package ships them as source and runs no component build. Components own only structural markup and CSS modifier classes (variant, size); every visual property comes from a var(--…) token, so opting out and rebuilding the markup against the raw tokens is a no-cost path.
What's inside
tokens/
├── primitives/ Website-only extensions to core scales.
│ └── color.json color.neutral.25, color.blue.25
└── semantic/ Role assignments unique to the website surface.
├── color.json surface.* (primary, secondary) + text.* (primary, muted, inverse)
└── typography.json typography.label-m, … (each: fontFamily/fontSize/fontWeight/lineHeight)
src/
└── astro/ Astro component layer — shipped as source, no build step.
├── Button/
│ ├── Button.astro Markup + variant/size modifier classes, no inline values.
│ ├── Button.css All values via var(--…) — no hex, no px.
│ └── Button.stories.ts Storybook stories (.astro renderer).
└── Header/
├── Header.astro Vendored Amplemarket nav.
├── header.client.ts Client behavior (transpiled by Vite; never inline TS in <script>).
├── Header.stories.ts
└── webflow/ Vendored Webflow markup + CSS layers (see sync:header).Semantic files are organized by primitive category, not usage namespace: color.json holds both surface.* and text.* at the top level; a new color role (e.g. border.subtle) goes under a new top-level namespace in color.json, not in a new border.json. Token paths stay role-first (surface > primary), so emitted CSS vars are --surface-primary, not --color-surface-primary.
Token files use the W3C DTCG format ($value, $type, $description) — same as the core.
What goes in primitives/ vs semantic/
primitives/— extra stops on a core scale that the website needs but other surfaces don't. The keys still encode values (color.neutral.25,color.blue.25); they are just additions to scales the core already owns. If a value would also be useful in product/email/decks, it belongs in the core instead — not here.semantic/— usage roles that are website-specific, referencing primitives directly (surface.primary→{color.neutral.25},typography.label-m→ composite offontFamily/fontSize/…). Values are always references ({color.neutral.25}), never raw hex / px. Roles describe intent (what this color is in the design) and can be repointed without touching consumers.
There are intentionally no per-component semantic tokens (no button.json). A component reads primitives and abstract semantic roles straight from its CSS and owns its own variation logic via modifier classes. Only introduce a new semantic role when the abstraction is shared across multiple components — not for one-off component values.
Anything cross-surface — "this is our brand primary", "this is the workhorse weight" — does not belong here. Each surface (product, email, decks, …) makes its own semantic calls; only primitives are shared.
Theming
Themes are declared per-token via an optional $extensions.themes.<name> map; tokens that don't change across themes omit it. The custom css/themed-variables formatter emits one :root block of defaults plus one [data-theme="<name>"] block per theme. An app opts in by setting data-theme="dark" on <html>. There is intentionally no prefers-color-scheme query — OS-preference mapping is an application-shell concern.
How the merge works in the build
style-dictionary.config.js reads the core's raw DTCG tokens via include and this package's tokens via source. Both are emitted into this package's dist/, producing a single self-contained bundle per platform. Consumers see the merged tree in the JS object, and a single CSS file declares the full set of custom properties.
We deliberately consume the core's source tokens rather than its compiled dist/. That keeps reference paths ({color.indigo.900}) identical to how the core itself uses them, so semantic refs always resolve cleanly. outputReferences: true keeps semantic vars compiled to var(--color-neutral-50) rather than the literal hex, so when a core primitive's value changes (after a core bump), the semantic role picks it up without an edit here.
Build
npm install # installs deps; `prepare` hook auto-builds dist/ (tokens)
npm run build # alias for build:tokens — components need no build
npm run build:tokens # Style Dictionary only — re-run after tokens/**/*.json edits
npm run clean # rimraf distbuild runs a single Style Dictionary v4 pass over tokens/**/*.json (with the core's tokens resolvable via include). The Astro components have no build step — they are published as source and compiled by consumers.
Outputs under dist/:
| Output | Path | Use |
|--------------|---------------------|--------------------------------------|
| Token CSS | dist/tokens.css | Browsers, any CSS pipeline |
| Token SCSS | dist/_tokens.scss | Sass-based projects |
| Token ESM | dist/index.mjs | Modern JS/TS (nested default export) |
| Token CJS | dist/index.cjs | Node / legacy bundlers |
| Token types | dist/index.d.ts | TypeScript |
| Token JSON | dist/tokens.json | Arbitrary consumers, AI tools |
dist/ is generated — it is not committed (it holds only token output). The prepare script regenerates it on every npm install of this package (including git-URL installs by the website repo) and before npm publish / npm pack.
See style-dictionary.config.js to add a new token output target.
Storybook
npm run storybook # dev server at http://localhost:6006 (needs a built dist/)
npm run build-storybook # static site → storybook-static/Storybook is the visual verification path, via the community @storybook-astro/framework, which renders .astro components server-side. Stories live next to their component as src/astro/<Name>/<Name>.stories.ts. The framework renders each component to an HTML fragment and does not run Astro's page pipeline that hoists frontmatter CSS imports, so .storybook/preview.ts imports all CSS globally: dist/tokens.css (run npm run build first), an import.meta.glob over every co-located src/astro/**/*.css, and the core font side-effect import. storybook-static/ is gitignored.
Adding or changing a token
- Decide the layer: a new stop on a core scale →
tokens/primitives/; a new website role →tokens/semantic/. If the value is meaningful to other surfaces, raise it into the core repo instead. - Edit (or create) the relevant file under
tokens/. Add new color roles tocolor.jsonunder a new top-level namespace — don't create a per-namespace file. Use DTCG format. Semantic values must be references ({color.neutral.25}), not literals. - If you bump the core dependency to pick up a primitive you need, update the
@amplemarket/design-systemversion inpackage.jsonand call out the bump in the changelog entry. - Run
npm run buildlocally to verifydist/regenerates cleanly. Do not commit the generateddist/output — it is gitignored and rebuilt on install. - Add an entry to CHANGELOG.md describing the change (see Versioning below).
- Bump the version in
package.jsonaccording to the versioning policy before merge. - Open a PR. When it lands on
main/master, the publish workflow tags the version and publishes the public npmjs package.
Adding a component
- Component folder. Create
src/astro/<Name>/<Name>.astroplus a sibling<Name>.css. The CSS uses onlyvar(--…)references — no hex, no px, no fallbacks. The.astrofile owns structural markup and modifier classes (variant,size); size/density variants are CSS modifier classes that rebind a private--_*local at the base class. Do not scaffold atokens/semantic/<name>.json— components own their variation logic. - Export. Add an
./astro/<Name>.astroentry toexportsin package.json (and fold the component CSS into the shared./astro/styles.cssentry). There is no barrel re-export. - Story. Add
src/astro/<Name>/<Name>.stories.ts(typedsatisfies Meta<AstroRenderer>). Slot content maps to the reservedslotsarg; all other args map to props. - Conventions. Keep CSS in a sibling
.cssfile (a bare<style>block is scoped and would mangle.ds-*). Any client behavior goes in a sibling.tsmodule imported by a plain-JS<script>— never inline TypeScript in<script>. - Verify.
npm run storybookfor visual verification.
See CLAUDE.md for the full set of .astro authoring conventions.
Versioning
This package follows Semantic Versioning. "Breaking" is defined from the point of view of a consumer who has built against a specific version of dist/ (tokens) or a pinned version of the .astro source:
| Change | Bump |
|-------------------------------------------------------------------------|-------|
| Renaming or removing any existing token key (primitive extension or semantic role) | major |
| Repointing a semantic role to a different primitive such that the resolved value shifts visibly | major |
| Changing the value of a website-owned primitive extension | major |
| Removing or renaming a component, prop, or variant | major |
| Changing a component's default rendered DOM in a way that's visible to users | major |
| Bumping the astro peerDependency range so a previously-supported version is dropped | major |
| Removing a build output (CSS/SCSS/ESM/CJS/JSON/d.ts) or an exports path (token or ./astro/*) | major |
| Bumping the core @amplemarket/design-system version to one that is itself breaking — the core's break leaks through | major |
| Adding a new primitive extension, semantic role, output target, component, prop, or variant | minor |
| Bumping the core to a non-breaking minor / patch | minor |
| Cosmetic re-ordering of keys, comment edits, $description additions | patch |
| Internal-only refactors of component markup that don't change the rendered output | patch |
| Fixing an obvious typo in a value (with negligible visual impact) | patch |
If in doubt, assume major. Consumers snapshot the resolved tokens — we would rather over-signal a breaking change than silently ship one.
Pre-1.0 caveat: while the package is on 0.x, treat a minor bump as potentially breaking (per SemVer's 0.x convention) and pin exactly in the website repo until 1.0.
Automated publishing
Merges to main or master publish a public package to the npmjs registry through .github/workflows/publish-package.yml.
- If the merged PR did not change
package.json's version, CI applies an automatic patch bump, commits it back to the branch, tagsvX.Y.Z, and publishes that version. - If the merged PR does not change
package.json's version, CI fails the release. - If the merged PR already changed the version, CI uses that version as-is, tags the merge commit, and publishes it.
- The workflow publishes with npm trusted publishing / OIDC, not a long-lived npm token. Configure the package's trusted publisher on npmjs.com for GitHub Actions, repository
amplemarket/design-system-web, workflow filenamepublish-package.yml, and allownpm publish. - Do not set
"private": trueinpackage.json— that flag means "never publish" to npm. Public package access is controlled bypublishConfig.access. - If
@amplemarket/design-systemis private and the defaultGITHUB_TOKENcannot read it duringnpm ci, add a repository secret namedDEPENDENCY_GITHUB_TOKENwith read access to that dependency repo.
Changelog
All notable changes are recorded in CHANGELOG.md. The repo follows the Keep a Changelog convention. Every version bump must be accompanied by a changelog entry, with a clear note when a change is considered breaking.
Rules for contributors
- Don't re-declare core primitives. If
color.indigo.900already exists in the core, don't redefine it here. Only add what the core doesn't ship. - Semantic values are references. A semantic token's
$valueis{color.neutral.25}, never#fdfcfb. If you can't write the reference, the underlying primitive is missing — add it (here or in the core) first. - Roles describe intent.
surface.primarysays "this is the default website surface", not "this is#fdfcfb". The whole point of the semantic layer is that we can repoint it without touching consumers. - No per-component tokens. Components own their variation logic in CSS modifier classes. Add a semantic role only when an abstraction is shared across multiple components (a new typography role, a new surface color) — never a
button.*/card.*slot. - No surface-crossing semantics. This package is for the website only. Roles like
surface.primaryortext.primarybelong here as long as the values they encode are website-specific opinions — every other surface (product UI, email, decks) defines its own. If you find yourself wanting the same role to apply identically across surfaces, the value belongs in the core primitives. - Keys still encode values where possible. Primitive extensions follow the core's convention:
color.neutral.25is a stop on the neutral ramp;color.blue.25is a stop on the blue ramp. Use ordinal integers only when the value isn't a single scalar. - Brand-universal facts go in the core. If you find yourself writing a
$descriptionthat any surface would benefit from, the token probably belongs in the core repo, not here.
