Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/design-tokens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Design Tokens

on:
push:
branches: [main, develop]
paths:
- 'packages/tokens/**'
- 'docs/BRAND.md'
- '.github/workflows/design-tokens.yml'
pull_request:
branches: [main, develop]
paths:
- 'packages/tokens/**'
- 'docs/BRAND.md'
- '.github/workflows/design-tokens.yml'

concurrency:
group: design-tokens-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
tokens:
name: Build and verify tokens
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20.x

# docs/BRAND.md is the source of truth; this fails when tokens.json has
# drifted from it, which is the drift a generated-and-ignored dist/ cannot
# catch on its own.
- name: Verify tokens match docs/BRAND.md
working-directory: ./packages/tokens
run: node build.js --check

- name: Build
working-directory: ./packages/tokens
run: node build.js

- name: Confirm the built outputs load
working-directory: ./packages/tokens
run: |
test -s dist/tokens.css
node --input-type=module -e "
import { light, dark, tokens, theme, version } from './dist/tokens.js';
const names = Object.keys(light);
if (names.length === 0) throw new Error('no tokens exported');
for (const mode of ['light', 'dark']) {
const missing = names.filter((n) => tokens[mode][n] === undefined);
if (missing.length) throw new Error(\`\${mode} is missing: \${missing.join(', ')}\`);
}
if (theme('dark') !== dark) throw new Error('theme(\"dark\") did not resolve');
console.log(\`ok: v\${version}, \${names.length} tokens in both modes\`);
"

- name: Confirm dist/ is not tracked
run: |
if git ls-files --error-unmatch packages/tokens/dist >/dev/null 2>&1; then
echo "packages/tokens/dist is generated and must not be committed."
exit 1
fi
echo "dist/ is untracked, as expected."
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,5 @@ stellar contract build
- [docs/PRIVACY_MODEL.md](docs/PRIVACY_MODEL.md) — ZK proof tiers
- [docs/ACCOUNT_KERNEL.md](docs/ACCOUNT_KERNEL.md) — session keys and recovery
- [docs/PATTERNS.md](docs/PATTERNS.md) — recommended gameplay patterns
- [docs/ONCHAIN_OFFCHAIN_BOUNDARY.md](docs/ONCHAIN_OFFCHAIN_BOUNDARY.md) — deciding what belongs on-chain
- [examples/README.md](examples/README.md) — example catalog and usage guide
4 changes: 3 additions & 1 deletion docs/BRAND.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ if a future pass wants a further-simplified glyph specifically for 16px contexts
written vocabulary list) to be their own follow-up rather than folded into this color/type/logo
pass.
- Consuming these tokens as an actual code package (CSS variables / Tailwind config / etc.) —
explicitly out of scope per #259, tracked as a sibling sub-issue.
explicitly out of scope per #259, and now shipped separately as
[`packages/tokens`](../packages/tokens). This document stays the source of truth; that package
encodes it. Change a value here first, then mirror it there and rebuild.
- Any change to `README.md`'s current logo usage — a follow-up application of this system, not
part of defining it.
307 changes: 307 additions & 0 deletions docs/ONCHAIN_OFFCHAIN_BOUNDARY.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Start here if you know what you're trying to build but not which Cougr module an
| **To serialize mutations / guard against reentrancy-like issues** | `ExecutionGuard` | — | [STANDARDS_LAYER.md § ExecutionGuard](./STANDARDS_LAYER.md#executionguard) |
| **Delayed or timelocked execution** | `DelayedExecutionPolicy` | — | [STANDARDS_LAYER.md § DelayedExecutionPolicy](./STANDARDS_LAYER.md#delayedexecutionpolicy) |
| **To batch several operations safely** | `BatchExecutor` | — | [STANDARDS_LAYER.md § BatchExecutor](./STANDARDS_LAYER.md#batchexecutor) |
| **To decide what belongs on-chain at all** (which state and rules justify their cost, and which should stay client-side) | The five-question boundary framework, applied per piece of state | [`battleship`](../examples/battleship), [`snake`](../examples/snake), [`blind_auction`](../examples/blind_auction) | [ONCHAIN_OFFCHAIN_BOUNDARY.md](./ONCHAIN_OFFCHAIN_BOUNDARY.md) |
| **To know whether I even need ECS** | Direct contract model for small/config-driven contracts | — | [When Not To Use ECS](#when-not-to-use-ecs) below |
| **To pick table vs. sparse storage** | Table for hot-loop state, sparse for infrequent markers | — | [Storage Guidance](#storage-guidance) below |
| **A thin, explicit contract entrypoint / gameplay loop** | `GameApp` + explicit stage placement | [`spawn_and_move`](../examples/spawn_and_move), [`snake`](../examples/snake) | [Default Entry Point](#default-entry-point) and [Stage Layout](#stage-layout) below |
Expand Down
6 changes: 6 additions & 0 deletions docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,9 @@ For real contracts, evaluate:
Performance guidance should always be tied back to those conditions.

If benchmark results and your data shape disagree, trust the data shape first.

## Related

This guide answers where a component should live once you have decided it belongs on-chain. For
the prior decision, whether a piece of state or logic justifies being on-chain in the first place,
see [ONCHAIN_OFFCHAIN_BOUNDARY.md](./ONCHAIN_OFFCHAIN_BOUNDARY.md).
3 changes: 3 additions & 0 deletions packages/tokens/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generated by build.js. Built on demand (npm run build) and on install or
# publish via the prepare script, so it is never committed.
dist/
21 changes: 21 additions & 0 deletions packages/tokens/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Changelog

All notable changes to `cougr-tokens`. This package versions independently of `cougr-core`, per
the policy in [README.md](./README.md#versioning-policy).

## 1.0.0

### Added

- **`tokens.json`**: the token source of truth, encoding every value defined in
[docs/BRAND.md](../../docs/BRAND.md): four neutrals, primary and accent, three maturity-tier
colors, two font stacks, an eight-step spacing scale, four radii, and the four fixed logo tones
- **`dist/tokens.css`**: built CSS custom properties with light and dark sets, switched by
`prefers-color-scheme` and overridable with a `data-theme` attribute on the root element
- **`dist/tokens.js`**: built ESM module exporting `light`, `dark`, `tokens`, `theme(mode)`,
and `version`, for consumers that need literal values at build time
- **`build.js`**: zero-dependency transform. `dist/` is generated rather than committed, produced
by `npm run build` and by the `prepare` script on install and publish. `--check` writes nothing
and fails when `tokens.json` has drifted from `docs/BRAND.md`, the source of truth
- **CI**: a `Design Tokens` workflow that verifies the source against `docs/BRAND.md`, builds,
loads the built module, and asserts `dist/` is not tracked
139 changes: 139 additions & 0 deletions packages/tokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# cougr-tokens

The single, versioned source for Cougr's design tokens. Every surface that renders Cougr's visual
identity imports these values instead of copying them, so the documentation site and the showcase
cannot drift apart.

The values themselves are specified and justified in [docs/BRAND.md](../../docs/BRAND.md),
including the contrast measurements behind each color pair. This package encodes that document;
it does not extend it.

## What is in here

| Path | Role |
|---|---|
| `tokens.json` | Source of truth. The only file edited by hand. |
| `build.js` | Zero-dependency transform, `tokens.json` to `dist/`. |
| `dist/tokens.css` | Built CSS custom properties, for static HTML/CSS consumers. |
| `dist/tokens.js` | Built ESM module of literal values, for build-time consumers. |

`dist/` is generated and is not committed:

```bash
npm run build # or: node build.js
```

The `prepare` script runs the same build on `npm install` and before
`npm pack`/`npm publish`, so anyone installing this package gets built output
without running the build themselves, including a consumer in a separate
repository (see
[docs/strategy/10-repository-strategy.md](../../docs/strategy/10-repository-strategy.md)).

`node build.js --check` validates the source and a dry-run build without writing
anything. It fails when `tokens.json` has drifted from `docs/BRAND.md`, which is the drift that
matters once the built output is no longer in version control. CI runs it on any change to this
package or to `docs/BRAND.md`.

## Why two output formats

CSS custom properties are the simpler option and are the right default for anything rendering in a
browser. They are not sufficient on their own, because some consumers need literal values at
generation time rather than at CSS resolution time: anything producing a standalone artifact (an
SVG, a PNG, terminal output) is consumed outside a document, so custom properties declared by a
host page never reach it. The showcase preview generator is the case this package was sized
against. That is the build-time transform need that justifies shipping a package rather than a lone
stylesheet.

Both outputs come from the same source in the same build, so they cannot disagree.

## Using the CSS

```html
<link rel="stylesheet" href="node_modules/cougr-tokens/dist/tokens.css">
```

```css
.card {
background: var(--color-surface);
color: var(--color-text);
border-radius: var(--radius-md);
padding: var(--space-4);
font-family: var(--font-sans);
}
```

Theming works in two layers:

- Light is the default, declared on `:root`.
- Dark applies automatically under `@media (prefers-color-scheme: dark)`, unless the document has
opted out with `data-theme="light"`.
- An explicit `data-theme="light"` or `data-theme="dark"` on the root element always wins, which is
what a theme toggle sets.

```html
<html data-theme="dark">
```

## Using the JavaScript

```js
import { dark, light, theme, version } from 'cougr-tokens';

dark.colorBg; // '#14100D'
light.colorPrimary; // '#8A5A22'
theme('dark').colorTierStable;
```

Token names are the CSS custom property names without the `--` prefix, camel-cased:
`--color-text-secondary` becomes `colorTextSecondary`, `--space-4` becomes `space4`. Values that do
not change between modes (typography, spacing, radius, logo tones) are present in both objects.

## Consuming it

A separate repository depends on the package normally and pins a version, and `prepare` builds
`dist/` during install:

```json
{ "dependencies": { "cougr-tokens": "^1.0.0" } }
```

This repository has no npm workspace, so an in-repo consumer runs this package's build and then
imports the output by relative path. Wire the build into whatever script produces the consumer's
artifacts, so the two cannot be run out of order:

```json
{ "scripts": { "prebuild": "node ../../packages/tokens/build.js" } }
```

## Changing a token

1. Update [docs/BRAND.md](../../docs/BRAND.md) first. It is the source of truth, and it carries the
contrast measurement that justifies the value.
2. Mirror the change in `tokens.json` and bump `version` there and in `package.json`.
3. Run `node build.js --check` to confirm the two agree, then `node build.js`. There is no built
output to commit.
4. Add a `CHANGELOG.md` entry.
5. Regenerate anything downstream that bakes token values into committed artifacts.

`node build.js --check` writes nothing and exits non-zero if `tokens.json` has drifted from
`docs/BRAND.md` or fails to build, which is the check to run before opening a pull request. CI runs
it too.

## Versioning policy

Semantic versioning, against the token surface rather than the code:

| Change | Bump |
|---|---|
| A token is removed, or renamed | Major |
| A token's value changes enough to be a visible redesign | Major |
| A new token is added | Minor |
| A value is corrected without changing the design intent (a contrast fix, a rounding fix) | Patch |
| Documentation, build script internals, output formatting | Patch |

Consumers pin a range and upgrade deliberately. Because both the documentation site and the
showcase resolve their own dependency, one can upgrade ahead of the other; the version each is on
is visible in its lockfile, so a divergence is a fact someone can look up rather than something
that has to be noticed by eye.

Every release is recorded in [CHANGELOG.md](./CHANGELOG.md).
Loading
Loading