Skip to content
Open
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
7 changes: 7 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

# Offline. A tarball whose styles/tokens.css does not match the
# generator is a tarball nobody can reproduce from this commit.
- name: Verify generated token artifacts
run: pnpm tokens:check

# Publish to npm only. Both scopes are hosted on npm: @nyuchi/* and
# @bundu/* (@bundu/ui). NPM_TOKEN must have publish access to both orgs.
# NPM_TOKEN is an ORG secret on mukoko-dev (verified 2026-09-12, after
# the repo moved from nyuchi), so this step keeps working post-transfer.
- name: Publish to npmjs
run: pnpm -r publish --access public --no-git-checks
env:
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/tokens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Token gates.
#
# Two jobs, deliberately separate, because they fail for different reasons
# and only one of them is allowed to touch the network.
#
# tokens OFFLINE. Regenerates every token artifact from
# tokens/canon.snapshot.json and fails if the working tree
# disagrees. This is the gate that makes hand-editing a
# generated file a build failure. It needs no credential and
# no third party, so it runs on every PR and every push.
#
# canon-parity NETWORK. Asks whether the committed snapshot still matches
# live canon (api.mzizi.dev/api/v1/brand + mzizi-registry's
# palette.source.ts). It runs HERE ONLY — never in a package
# build, never in prepack, never at runtime. A design-token
# package that phones home at build time renders a blank page
# the afternoon the API is unwell.
#
# Neither job can pass vacuously: `tokens` compares bytes it just generated,
# `canon-parity` compares against an upstream this repo does not control.
# Both are demonstrated failing on a wrong hex in the PR that added them.

name: Tokens

on:
pull_request:
push:
branches: [main]
merge_group:
schedule:
# Weekly. Canon moving is not urgent, but it must not be invisible.
- cron: "17 6 * * 1"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
tokens:
name: generated files match the generator
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm tokens:check

canon-parity:
name: snapshot matches live canon
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm canon:parity
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,36 @@ additional UI packages over time.

## Packages

| Package | Description |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| [`@nyuchi/ui`](./packages/ui) | Svelte 5 / SvelteKit component library on the seven-mineral tokens |
| [`@bundu/ui`](./packages/bundu-ui) | Marketing UI kit — Astro marketing components + shadcn primitives on the seven-mineral tokens |
| Package | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| [`@nyuchi/ui`](./packages/ui) | Svelte 5 / SvelteKit component library on the Mzizi tokens |
| [`@bundu/ui`](./packages/bundu-ui) | Marketing UI kit — Astro marketing components + shadcn primitives on the same tokens |

Both packages ship **byte-identical** `styles/tokens.css`, `styles/theme.css`,
`tokens.json`, `tailwind-palette.mjs` and `styles/brand-*.css`. They are two outputs of
one generator, not two hand-maintained files.

## Tokens

All 21 Mzizi colour families (7 minerals, 7 heritage, 7 experimental) under one
`--color-*` namespace, plus the nine-step surface ladder and the connectivity status
trio. Everything is **generated** from `tokens/canon.snapshot.json`:

```sh
pnpm canon:fetch # refresh the snapshot from canon (network, on demand)
pnpm tokens:build # regenerate every artifact from the snapshot (offline)
pnpm tokens:check # CI gate: fail if any generated file was hand-edited (offline)
pnpm canon:parity # CI gate: fail if the snapshot has drifted from canon (network)
```

The snapshot is machine-written from two sources that are cross-checked against each
other — `https://api.mzizi.dev/api/v1/brand` and `mzizi-dev/mzizi-registry`'s
`lib/tokens/palette.source.ts`. Nobody types a hex. The values do **not** live in a
database; Mzizi holds no brand or primitive token data in one.

`canon:parity` reaches the network and therefore runs in **CI only** — it refuses to run
without `CI` set unless given `--force`. It is never in a build, a `prepack`, a
`postinstall` or a runtime path.

## Development

Expand All @@ -26,8 +52,13 @@ Packages are published to [npm](https://www.npmjs.com) automatically by the
[`publish` workflow](./.github/workflows/publish.yml) when a GitHub Release is
published — `@nyuchi/*` under the [`@nyuchi`](https://www.npmjs.com/org/nyuchi)
org and `@bundu/*` under the [`@bundu`](https://www.npmjs.com/org/bundu) org.
The workflow requires an `NPM_TOKEN` repository secret with publish access to
both orgs.
The workflow requires an `NPM_TOKEN` with publish access to both orgs.
`NPM_TOKEN` is an **organisation** secret on `mukoko-dev` and is visible to this
repository (verified 2026-09-12, after the transfer from `nyuchi`), so no repository-level
secret is needed.

Publishing is the owner's call. CI does not publish on a branch push — only on a
published GitHub Release or a `v*` tag.

## License

Expand Down
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@
"name": "@nyuchi/packages-ui",
"version": "0.1.0",
"private": true,
"description": "Nyuchi Design System shared, publishable UI packages. Nyuchi's implementation of the Mzizi architecture.",
"description": "Nyuchi Design System \u2014 shared, publishable UI packages. Nyuchi's implementation of the Mzizi architecture.",
"type": "module",
"license": "MIT",
"author": "Nyuchi Africa (Pvt) Ltd",
"repository": {
"type": "git",
"url": "git+https://github.com/nyuchi/packages-ui.git"
"url": "git+https://github.com/mukoko-dev/packages-ui.git"
},
"bugs": {
"url": "https://github.com/nyuchi/packages-ui/issues"
"url": "https://github.com/mukoko-dev/packages-ui/issues"
},
"scripts": {
"canon:fetch": "node scripts/fetch-canon.mjs",
"canon:parity": "node scripts/check-canon-parity.mjs",
"tokens:build": "node scripts/generate-tokens.mjs",
"tokens:check": "node scripts/generate-tokens.mjs --check",
"format": "prettier --write \"**/*.{md,mdx,json,jsonc}\" --config .prettierrc --ignore-path .prettierignore --no-error-on-unmatched-pattern",
"format:check": "prettier --check \"**/*.{md,mdx,json,jsonc}\" --config .prettierrc --ignore-path .prettierignore --no-error-on-unmatched-pattern"
},
"devDependencies": {
"prettier": "3.9.4"
},
"engines": {
"node": ">=20",
Expand Down
8 changes: 7 additions & 1 deletion packages/bundu-ui/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ them with a client directive in Astro (`<Switch client:load />`).
## 2. The design-system tools

`@bundu/ui` is downstream of the **Mzizi** registry. The registry — not this package — is
the source of truth for tokens and net-new components.
the source of truth for tokens and net-new components, and since 0.2.0 that is mechanical
rather than aspirational: `styles/tokens.css`, `styles/theme.css`, `tokens.json`,
`tailwind-palette.mjs` and every `styles/brand-*.css` are generated from
`tokens/canon.snapshot.json`, which `scripts/fetch-canon.mjs` writes from
`api.mzizi.dev/api/v1/brand` and `mzizi-registry`'s `lib/tokens/palette.source.ts`.
Hand-editing any of them fails `pnpm tokens:check`. The token values are **not** stored in
a database — Mzizi holds no brand or primitive token data in one.

### mzizi MCP

Expand Down
76 changes: 66 additions & 10 deletions packages/bundu-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,53 @@ semantic token system.
- **shadcn CVA + `cn()` React primitives** — `Button`, `Card`, `Badge`, `Input`,
`Textarea`, `Select`, `Label`, `Alert`, `Avatar`, `Separator`, `Skeleton`, `Switch`,
`Checkbox`, `Tabs`, `Tooltip`.
- **Option-A styling** — one canonical `styles/globals.css` (all seven minerals, light +
dark, semantic tokens, component/utility classes) plus tiny `brand-*.css` overlays that
swap the brand primary, and a `tailwind-preset.mjs` that exposes the utility classes.
- **The whole Mzizi palette** — `styles/tokens.css` carries all **21 colour families**
under one `--color-*` namespace: 7 minerals, 7 heritage tones, 7 experimental tones.
Plus the nine-step surface ladder (`--pitch --void --base --surface --container
--overlay --raised --scrim --wash`) and the connectivity status trio (`--syncing
--offline --neutral`).
- **Tailwind v3 and v4** — `styles/theme.css` is a native v4 `@theme` entrypoint;
`tailwind-preset.mjs` is the v3-shape preset, still shipped and still working (v4 loads
it through `@config`).
- **Brand overlays** — `brand-bundu`, `brand-nyuchi`, `brand-mukoko`, `brand-shamwari`,
`brand-mzizi`. Each repoints `--primary` and `--ring` and nothing else.
- **`tokens.json`** — the same values machine-readable, including every custom property
resolved to a literal hex per mode, for the surfaces that cannot consume CSS at all:
Expo (`mukoko-weather-mobile`) and Satori-based OG-image / email / PDF generators.

## Generated, not transcribed

`styles/tokens.css`, `styles/theme.css`, `tokens.json`, `tailwind-palette.mjs` and every
`styles/brand-*.css` are **generated** by `scripts/generate-tokens.mjs` from
`tokens/canon.snapshot.json`, which is itself machine-written by `scripts/fetch-canon.mjs`
from canon:

| Source | Supplies |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `https://api.mzizi.dev/api/v1/brand` | the 21 families, semantic tokens, the background ladder, radii, the brand→mineral table |
| `mzizi-dev/mzizi-registry` → `lib/tokens/palette.source.ts` | the minerals' `onContainer` pairs, which `/v1/brand` does not project |

The two are cross-checked against each other on every fetch: if they disagree about a hex,
the fetch fails rather than silently preferring one. That disagreement is how an estate
ends up with four different terracottas.

The hex values do **not** live in a database. Mzizi holds no brand or primitive token data
in Supabase or anywhere else — 0.1.1's `tokens.css` header said otherwise and was wrong.

Three gates, none of which can pass vacuously:

| Command | Network | Runs | Fails when |
| ------------------- | ------- | ----------------------- | -------------------------------------------------- |
| `pnpm tokens:check` | no | every CI job, `prepack` | a generated file disagrees with the generator |
| `pnpm canon:parity` | yes | **CI only** | the committed snapshot has drifted from live canon |
| `pnpm canon:fetch` | yes | on demand | the API and `palette.source.ts` disagree |

`canon:parity` refuses to run outside CI without `--force`, so it cannot creep into a build
or a runtime path. A token package that phones home to render a page is a page that goes
blank the afternoon the API is unwell.

Every value flows through CSS custom properties / Tailwind tokens — **never a raw hex in
source**. All seven minerals are valid tokens: `cobalt`, `tanzanite`, `malachite`, `gold`,
`terracotta`, `sodalite`, `copper`.
source**.

## Install

Expand All @@ -30,16 +70,22 @@ pnpm add react react-dom

## Quick usage

**1. Styles** — import the canonical tokens, then the brand overlay for your site, into
your global stylesheet (after `@import "tailwindcss";`):
**1a. Tailwind v4 (recommended)** — one stylesheet, no `tailwind.config.mjs` at all:

```css
@import "tailwindcss";
@import "@bundu/ui/styles/globals.css";
@import "@bundu/ui/styles/brand-nyuchi.css"; /* or brand-bundu / brand-mukoko */
@import "@bundu/ui/styles/theme.css"; /* @imports tokens.css, adds @theme */
@import "@bundu/ui/styles/brand-nyuchi.css";
```

**2. Tailwind** — add the preset so the mineral + semantic utility classes exist:
**1b. Tailwind v3, or v4 via `@config`** — unchanged from 0.1.x, still supported:

```css
@import "tailwindcss";
@import "@bundu/ui/styles/tokens.css"; /* or globals.css for the @layer rules */
@import "@bundu/ui/styles/brand-nyuchi.css";
@config "../../tailwind.config.mjs";
```

```js
// tailwind.config.mjs
Expand All @@ -51,6 +97,16 @@ export default {
};
```

**2. Outside the browser** — Expo, Satori, PDF:

```js
import tokens from "@bundu/ui/tokens.json" with { type: "json" };

tokens.resolved.dark["--color-sodalite"]; // "#3d5afe"
tokens.color.heritage.savanna.dark; // "#e5c158"
tokens.surface.base.light; // "#f3f3f1"
```

**3. Components:**

```astro
Expand Down
23 changes: 16 additions & 7 deletions packages/bundu-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "@bundu/ui",
"version": "0.1.1",
"description": "The marketing UI kit for the Bundu Ecosystem — Nyuchi's implementation of the Mzizi design system. Astro marketing components (Hero, Section, MineralStrip, Container, Breadcrumb with valid BreadcrumbList JSON-LD, …) plus shadcn CVA + cn() React primitives mapped onto the seven-African-mineral tokens, a framework-agnostic tokens.css, a canonical globals.css, tiny brand overrides, and a Tailwind preset.",
"version": "0.2.0",
"description": "The marketing UI kit for the Bundu Ecosystem — Nyuchi's implementation of the Mzizi design system. All 21 Mzizi colour families (7 minerals, 7 heritage, 7 experimental) under one --color-* namespace, the nine-step surface ladder, the connectivity status trio, a Tailwind v4 @theme entrypoint and the v3 preset, brand overlays for bundu/nyuchi/mukoko/shamwari/mzizi, machine-readable tokens.json for Expo and Satori, plus Astro marketing components and shadcn CVA + cn() React primitives. Generated from canon — nothing in it is hand-copied.",
"type": "module",
"license": "MIT",
"author": "Nyuchi Africa (Pvt) Ltd",
"homepage": "https://github.com/nyuchi/packages-ui#readme",
"homepage": "https://github.com/mukoko-dev/packages-ui#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/nyuchi/packages-ui.git",
"url": "git+https://github.com/mukoko-dev/packages-ui.git",
"directory": "packages/bundu-ui"
},
"bugs": {
"url": "https://github.com/nyuchi/packages-ui/issues"
"url": "https://github.com/mukoko-dev/packages-ui/issues"
},
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down Expand Up @@ -46,11 +46,16 @@
"./Breadcrumb.astro": "./src/Breadcrumb.astro",
"./lib/utils": "./src/lib/utils.ts",
"./styles/tokens.css": "./styles/tokens.css",
"./styles/theme.css": "./styles/theme.css",
"./styles/globals.css": "./styles/globals.css",
"./styles/brand-bundu.css": "./styles/brand-bundu.css",
"./styles/brand-nyuchi.css": "./styles/brand-nyuchi.css",
"./styles/brand-mukoko.css": "./styles/brand-mukoko.css",
"./tailwind-preset": "./tailwind-preset.mjs"
"./tailwind-preset": "./tailwind-preset.mjs",
"./styles/brand-shamwari.css": "./styles/brand-shamwari.css",
"./styles/brand-mzizi.css": "./styles/brand-mzizi.css",
"./tokens.json": "./tokens.json",
"./tailwind-palette": "./tailwind-palette.mjs"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
Expand All @@ -64,7 +69,9 @@
"files": [
"src",
"styles",
"tailwind-preset.mjs"
"tailwind-preset.mjs",
"tailwind-palette.mjs",
"tokens.json"
],
"publishConfig": {
"access": "public"
Expand All @@ -73,6 +80,8 @@
"bundu",
"mzizi",
"design-system",
"design-tokens",
"tailwind",
"marketing",
"astro",
"ui"
Expand Down
8 changes: 4 additions & 4 deletions packages/bundu-ui/styles/brand-bundu.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* brand-bundu — Bundu Foundation primary: copper.
Copper is Bundu's canonical brand→mineral in the seven-mineral system
(stewardship; the ecosystem commons). Import AFTER globals.css (or
tokens.css) to select this brand's primary + ring. */
/* brand-bundu — bundu primary: copper (mineral). Canon (/v1/brand -> ecosystem)
maps bundu to copper — the ecosystem. GENERATED by
scripts/generate-tokens.mjs; edit the canon ecosystem table, not this file.
Import AFTER tokens.css (or globals.css / theme.css) so it wins. */
:root {
--primary: var(--color-copper);
--ring: var(--color-copper);
Expand Down
8 changes: 4 additions & 4 deletions packages/bundu-ui/styles/brand-mukoko.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* brand-mukoko — Mukoko primary: tanzanite.
Tanzanite is Mukoko's canonical brand→mineral (identity; Africa's super
app). Import AFTER globals.css (or tokens.css) to select this brand's
primary + ring. */
/* brand-mukoko — mukoko primary: tanzanite (mineral). Canon (/v1/brand ->
ecosystem) maps mukoko to tanzanite — africa's super app. GENERATED by
scripts/generate-tokens.mjs; edit the canon ecosystem table, not this file.
Import AFTER tokens.css (or globals.css / theme.css) so it wins. */
:root {
--primary: var(--color-tanzanite);
--ring: var(--color-tanzanite);
Expand Down
15 changes: 15 additions & 0 deletions packages/bundu-ui/styles/brand-mzizi.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* brand-mzizi — mzizi primary: hematite (heritage tone). Canon carries no mzizi
-> mineral row; hematite is the nearest canon-grounded fit (symbolism:
foundation, endurance, the substrate). Pending confirmation — see
mukoko-dev/packages-ui PR. GENERATED by scripts/generate-tokens.mjs; edit the
canon ecosystem table, not this file. Import AFTER tokens.css (or globals.css
/ theme.css) so it wins. */
:root {
--primary: var(--color-hematite);
--ring: var(--color-hematite);
}
.dark,
[data-theme="dark"] {
--primary: var(--color-hematite);
--ring: var(--color-hematite);
}
Loading
Loading