Toilville design system — UDG tokens, WP themes, brand components
  • Python 23.5%
  • Cython 23.1%
  • PHP 15.2%
  • JavaScript 13.6%
  • HTML 9.6%
  • Other 15%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Peter Swimm a319fc1d46
Some checks failed
Security Scans / Gitleaks secret scan (push) Successful in 36s
Security Scans / Trivy filesystem scan (push) Successful in 46s
Copy coverage / Design copy reaching rendered HTML (push) Failing after 40s
UDG dist drift gate / dist/ matches dtcg + udg sources (push) Successful in 33s
Merge pull request 'feat(site): design pass, with no section numbers, library hero art, a footer text floor and a current-page nav (#407)' (#135) from feat/design-pass into main
Reviewed-on: #135
2026-09-12 19:32:16 +00:00
.claude feat(brand-guide): streamlined two-tab showcase (design system + icons) 2026-07-27 16:51:44 -07:00
.design-sync fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
.forgejo fix(ci): check Toilville_Rituals out beside the repo in Test Generator 2026-09-11 13:53:10 -07:00
.spel chore(spel): add workspace block to forge-manifest stub 2026-09-08 11:52:34 -07:00
.vale/styles/Rituals Add ForgeBlock grammar tests + Vale prose linting configuration 2026-07-01 12:17:19 -07:00
Brand fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
components fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
dist fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
docs docs(review): before and after renders for the design pass 2026-09-12 11:15:55 -07:00
generator fix(tests): the grammar tests' fallback looked for Toilville_Rituals inside the repo 2026-09-11 13:55:28 -07:00
Media feat(site): design pass, with no section numbers, library hero art, a footer text floor and a current-page nav (#407) 2026-09-12 11:15:11 -07:00
out fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
templates fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
tools feat(site): design pass, with no section numbers, library hero art, a footer text floor and a current-page nav (#407) 2026-09-12 11:15:11 -07:00
udg feat(site): design pass, with no section numbers, library hero art, a footer text floor and a current-page nav (#407) 2026-09-12 11:15:11 -07:00
.gitattributes Initial Commits 2025-08-31 14:04:55 -07:00
.gitignore feat(assets): themeable dual-var chain + wire SVG pipeline into gate/MCP 2026-07-09 10:11:49 -07:00
.mcp.json feat(udg): MCP server integration via forge gateway + ritual intents 2026-06-30 12:52:30 -07:00
.pre-commit-config.yaml feat(artifact): vendor wireframe-kit, make artifact HTML reproducible + drift-gated 2026-07-13 17:10:30 -07:00
.vale.ini Add ForgeBlock grammar tests + Vale prose linting configuration 2026-07-01 12:17:19 -07:00
.yamllint feat(udg): single-source color from Brand/UDG/dtcg → dist; retire parallel udg/ SVG layer 2026-06-27 17:48:09 -07:00
CLAUDE.md fix(brand): the mono face is the system monospace — DM Mono was never decided 2026-09-11 13:10:42 -07:00
CONTRIBUTING.md docs: adopt org-wide contributor-review policy (v1) 2026-08-22 11:11:23 -07:00
justfile feat(docs): an HTML target, so one source feeds the ODT and the artifact pages 2026-09-11 11:03:36 -07:00
pyproject.toml themes 2026-07-27 07:25:12 -07:00
README.md docs(brand): canonical terracotta #C05734 + scope the dark-first rule 2026-09-09 13:21:06 -07:00
toilville_design.code-workspace forge clean 2026-06-28 17:38:46 -07:00

Toilville Design System

This repo is the canonical source of truth for Toilville's design tokens, brand assets, and generated design artifacts. Every Toilville frontend — Rituals Web, ToilMail, SPELWork sites — consumes outputs from here. It does not contain application code; it is a design governance repo.

All tokens are authored as DTCG JSON under Brand/UDG/ — color in color.json, and the six non-color tiers (type, space, radius, border, shadow, motion) each in their own *.json file (Brand/UDG/dtcg/ baseline + Brand/UDG/products/<p>/dtcg/ overrides). udg/generators/extract.py fuses them and writes dist/<product>/. Downstream apps consume only the generated artifacts — never re-author values in consumer repos.

The flat-YAML tier files under udg/baseline/tokens/*.yml are not the CSS/native source of truthextract.py reads the DTCG JSON above. Today only the WordPress-theme generator (udg/generators/wp_theme.py) reads that yml, and only space.yml + type.yml; border/radius/shadow/motion.yml are unread legacy pending a delete decision.

See Brand/UDG/README.md for color governance policy and DTCG format detail.


Pipeline at a glance

Brand/UDG/dtcg/color.json                     ← AUTHORED (baseline color, all themes, ARGB)
Brand/UDG/products/<p>/dtcg/color.json        ← AUTHORED (product color, multi-theme)
Brand/UDG/dtcg/{typography,space,radius,border,shadow,motion}.json  ← AUTHORED (6 non-color tiers, DTCG)
Brand/UDG/products/<p>/dtcg/<tier>.json       ← AUTHORED (per-product non-color overrides, optional)
udg/products/<p>/meta.yml                     ← AUTHORED (product config)
udg/products/<p>/ux.yml                       ← AUTHORED (adaptive shell + feature placements, optional)
udg/config/surface_profiles.yml               ← AUTHORED (shared adaptive shells / regions)
         ↓  udg/generators/extract.py   (just build / just check)
dist/<p>/skin.css                             ← GENERATED — never hand-edit
dist/<p>/tokens.dtcg.json                     ← GENERATED — never hand-edit
dist/<p>/artifact.html                        ← GENERATED — interactive design spec
dist/<p>/artifact.bbs                         ← GENERATED — 80-col ANSI terminal spec
dist/<p>/artifact.sms                         ← GENERATED — terse text spec
         ↓
App consumers (Toilville_Rituals, rituals-web, etc.)

just check is the drift gate — it fails if dist/ is stale. Run it before any commit that touches token sources.


Part 1: Creating a new UDG product

1.1 Scaffold the product

just new <product>

Creates udg/products/<product>/meta.yml with draft defaults. Edit the four key fields:

Field Values Guidance
css_var_style short / canonical canonical for any app consumer (--udg-color-* names). short only for internal brand components (--primary, --background).
color_inherits_baseline true / false false for standalone apps with their own palette (Rituals Web). true only for brand extensions of the Toilville baseline.
status draft / candidate / confirmed draft stays local. confirmed means published in sync-manifest.yml.
version semver Bump in the same PR as any token change — the registry rejects duplicate versions.

If the product has adaptive UX rules, add:

  • ux_manifest: ux.yml
  • default_surface_profile: <shared-profile-id>
  • nominal_shell: <shell-kind>

and create udg/products/<product>/ux.yml to bind product features into the shared regions declared in udg/config/surface_profiles.yml.

1.2 Author color tokens

If the product needs colors distinct from the baseline, create:

Brand/UDG/products/<product>/dtcg/color.json

DTCG format — every token carries:

{
  "color": {
    "brand": {
      "primary": {
        "$type": "color",
        "$value": {
          "light": "#FFCC5C37",
          "dark":  "#FFCC5C37",
          "oldschool": "#FFB84B2A"
        },
        "udg.css-var":  "--udg-color-brand-primary",
        "udg.dart-key": "brandPrimary"
      }
    }
  }
}

Key rules:

  • Alpha is the first two hex digits in #AARRGGBB format. FF = fully opaque. Use < FF only for overlay tokens (e.g. scrim).
  • light is always required. Add dark, oldschool, or any named themes as needed.
  • Set color_inherits_baseline: false in meta.yml when the product is self-contained (no merge with the baseline grammar).
  • The baseline taxonomy is brand.*, bg.*, text.*, border.*, accent.*, safety.*, status.*. Mirror this hierarchy for new products that extend the baseline.

If color_inherits_baseline: true, the generator merges baseline ⊕ product dtcg. Product tokens win on conflict. Only add what diverges from baseline.

1.3 Override non-color tiers (optional)

For tier overrides, create Brand/UDG/products/<product>/dtcg/<tier>.json (DTCG format) with only the keys that differ from Brand/UDG/dtcg/<tier>.json. extract.py merges baseline → product. (The udg/baseline/tokens/*.yml files are not read here — see the note at the top of this README.)

Example — a product that uses rounder corners:

// Brand/UDG/products/rituals/dtcg/radius.json
{
  "radius": {
    "lg":   { "$type": "dimension", "$value": "24px",   "udg.css-var": "--radius-lg" },
    "full": { "$type": "dimension", "$value": "9999px", "udg.css-var": "--radius-full" }
  }
}

All other radius keys (sm, md) inherit from baseline.

1.4 Build and verify

just build-one <product>   # writes dist/<product>/skin.css + tokens.dtcg.json
just check                 # drift gate — exit 1 if dist/ is stale
just artifact <product>    # writes dist/<product>/artifact.html (interactive spec)
just bbs <product>         # writes dist/<product>/artifact.bbs (80-col ANSI)
just sms <product>         # writes dist/<product>/artifact.sms (terse text)
just serve                 # http://localhost:8080 to browse dist/

Open dist/<product>/artifact.html in a browser to verify the token values, theme switcher, and design board before declaring the product confirmed.

1.5 Register in sync-manifest

Add to udg/config/sync-manifest.yml:

products:
  <product>:
    version: 0.1.0
    skin: dist/<product>/skin.css
    consumers:
      - repo: spel/apps/<repo>
        mode: package-bump          # bumps the udg Dart package version
        package_name: udg
        pubspec_path: pubspec.yaml
      - repo: Toilville_Rituals
        mode: source-inject         # writes a generated file into the consumer repo
        kind: json-tokens-macos
        output: apps/rituals-macos/Rituals/Resources/udg_tokens.json

Delivery modes:

  • package-bump — consumer uses a hosted udg Dart package. This bumps the version constraint in pubspec.yaml. Used by rituals-web.
  • source-inject — this repo writes a generated JSON file directly into the consumer repo. Used by macOS (Swift) targets that cannot import a Dart package.

Version gate: bump version in the same PR as any token change. The registry rejects duplicate versions — a forgotten bump fails publish and is the intended gate.

1.6 Document theme divergence

Every named theme beyond light and dark needs a rationale. Until RATIONALE.yml is formalized, document divergence in two places:

  1. The $description field in dtcg/color.json
  2. The PR description — why the default themes don't work, who approved, what the contrast target is for the new theme

Part 2: Forge pipeline — actor & tool schema

This section covers how to register a new system actor so its tools are discoverable via ritual query "...". Every registered actor contributes ritual blocks to the forge registry and appears in the design artifact for its UDG product.

2.1 The four required tables

core.mcp_servers   the process: command, transport (stdio), consent zone
core.mcp_tools     the tools: name, params schema, consent zone
core.actors        the system actor: name, type, mcp_server reference
core.intents       natural language → tool invocation mapping

Optional: ux.frames + ux.tool_modalities for modality-aware UX presentation (Part 3.4).

2.2 Step 1: MCP server

.mcp.json in the project root (Claude Code auto-discovers this):

{
  "mcpServers": {
    "<server_name>": {
      "command": "python3",
      "args": ["path/to/mcp_server.py"],
      "cwd": "/absolute/path/to/project"
    }
  }
}

mcp_server.py using FastMCP:

from mcp.server.fastmcp import FastMCP
from typing import Any, Optional

mcp = FastMCP("<server_name>", "<one-line description>")

@mcp.tool()
def my_tool(param: str, optional: Optional[str] = None) -> dict[str, Any]:
    """Tool description — this becomes the forge registry description."""
    return {"result": ..., "param": param}

if __name__ == "__main__":
    mcp.run()

Conventions:

  • Docstring → forge description. Type hints → params_schema. Return dict[str, Any].
  • Tier the tools by mutation risk (UDG convention; apply to any server):
    • Tier 0 Discovery: list, meta, manifest — no side effects
    • Tier 1 Inspection: read tokens, themes, CSS vars
    • Tier 2 Validation: check drift, validate names
    • Tier 3 Build: writes to dist/
    • Tier 4 UX output: generates artifact HTML from forge DB + tokens

2.3 Step 2: Four migrations

Create numbered migration files under ~/.spel/migrations/. Each must insert a record into public.schema_migrations and optionally core.schema_migrations.

Migration 1 — core.mcp_servers:

INSERT INTO core.mcp_servers (server_name, display_name, transport, endpoint, consent_zone, active)
VALUES ('<name>', '<display>', 'stdio', 'python3 /abs/path/to/mcp_server.py', 'Z1', true)
ON CONFLICT (server_name) DO NOTHING;

Migration 2 — core.mcp_tools (one row per @mcp.tool()):

INSERT INTO core.mcp_tools (tool_name, server_name, display_name, description, params_schema, consent_zone)
VALUES (
    'my_tool', '<server_name>', 'My Tool', 'What it does',
    '{"type":"object","properties":{"param":{"type":"string"}},"required":["param"]}'::jsonb,
    'Z1'
)
ON CONFLICT (tool_name, server_name) DO NOTHING;

Migration 3 — core.actors:

INSERT INTO core.actors (actor_name, actor_type, attributes, active)
VALUES (
    '<actor-name>', 'system',
    '{"description":"...","zone":"Z1","category":"tool","mcp_server":"<server_name>"}'::jsonb,
    true
)
ON CONFLICT (actor_name) DO NOTHING;

Migration 4 — core.intents (one row per tool, plus aggregate aliases):

INSERT INTO core.intents (
    intent_name, intent_type, executor, execution_config,
    zone, description, aliases, priority, status
)
VALUES (
    '<prefix>_my_tool',
    'query',                 -- 'query' for reads, 'mutation' for writes, 'action' for external effects
    'ritual_executor',
    '{"invocation":"invoke_mcp_tool","server_ref":"<server_name>","tool_name":"my_tool","params_template":{"param":"","optional":null}}'::jsonb,
    'Z1',
    'What this intent does in one sentence',
    '[{"alias":"natural language phrase","weight":1.0},{"alias":"shorthand","weight":0.85}]'::jsonb,
    50,
    'candidate'
)
ON CONFLICT (intent_name, scope_type) WHERE scope_id IS NULL DO NOTHING;

execution_config shape — the only shape the ritual executor understands:

{
  "invocation":     "invoke_mcp_tool",
  "server_ref":     "<server_name>",
  "tool_name":      "<tool_name>",
  "params_template": {
    "required_param": "",
    "optional_param": null
  }
}

Empty string = required parameter, filled by the ritual matcher. null = optional, passed only if provided.

ON CONFLICT DO NOTHING pitfall: if a prior migration created the row empty, the config won't be applied. Use ON CONFLICT DO UPDATE SET execution_config = EXCLUDED.execution_config instead, or write a follow-up UPDATE migration. This burned us in migration 0101 — see 0105 for the backfill pattern.

Alias weight guidelines:

  • 1.0 — exact canonical phrase (udg list products)
  • 0.9 — close synonym (list udg products, show products)
  • 0.85 — shorthand or informal (udg products)

2.4 Step 3: Verify

ritual query "list <server_name> tools"
ritual query "<natural language alias>"

If routing fails, check in order:

  1. Intent status = candidate or active (not draft)
  2. execution_config->>'server_ref' matches core.mcp_servers.server_name exactly
  3. tool_name matches core.mcp_tools.tool_name exactly (case-sensitive)
  4. aliases JSON is valid (parse it with psql -c "SELECT aliases::jsonb FROM core.intents WHERE intent_name='...'")

2.5 Optional: UX presentation layer

Register modality frames so the artifact generator can scope tools per platform:

-- Declare the canvas type (run once per modality)
INSERT INTO ux.modalities (modality_id, display_name, wf_atom, description)
VALUES ('flutter', 'Flutter', 'phone', 'Flutter/iOS phone canvas')
ON CONFLICT (modality_id) DO NOTHING;

-- Link actor + modality + udg_product
-- WHERE EXISTS guard is required: actor must be registered first
INSERT INTO ux.frames (frame_id, actor_name, modality_id, display_name, udg_product, active)
SELECT '<actor>-flutter', '<actor>', 'flutter', '<Product> Flutter', '<udg_product>', true
WHERE EXISTS (SELECT 1 FROM core.actors WHERE actor_name = '<actor>');

The artifact generator walks ux.frames to resolve the udg_product for token loading, then queries ux.tools for tools in that frame.


Part 3: Ritual blocks

3.1 What a ritual block is

A ritual block is a core.intents row with execution_config->>'server_ref' pointing at an MCP server. In the design artifact, each ritual block becomes a card on the Design tab — showing intent name, type badge, description, execution config shape, and natural language aliases.

Ritual blocks are the forge's UI primitive: they represent what a system can do expressed in natural language. The artifact surfaces them as design reference — not interactive runtime buttons.

3.2 Intent types → visual representation

intent_type Badge color Meaning
query blue Read-only, no side effects — safe to invoke any time
mutation amber Writes to dist/, DB, or filesystem — requires confirmation
action red External side effects (file a wish, push a notification)
form purple Collects structured input before invoking
ritual teal Multi-step SPELWork ceremony (Wish → Ruse → Ritual → Rubric)

Rule: every intent that writes must have intent_type = 'mutation'. Never mark a write as query — the artifact uses the type to render the appropriate confirmation affordance.

3.3 Authoring a new ritual block

To add a new capability to the forge:

  1. Add @mcp.tool() to mcp_server.py — this is the implementation.
  2. Add a core.mcp_tools row in a migration.
  3. Add a core.intents row with the correct intent_type, readable aliases, and execution_config pointing to the tool.
  4. Run just artifact <product> to regenerate the design artifact with the new ritual block.

3.4 Surfacing in artifact.html

The Design tab is built at artifact generation time. The generator queries:

SELECT intent_name, intent_type, description, execution_config, aliases
FROM core.intents
WHERE execution_config->>'server_ref' = '<server_ref>'
ORDER BY intent_type, intent_name

Results are embedded statically — the artifact does not query the DB at runtime. To pick up new or updated intents, re-run just artifact <product> or ritual query "udg build artifact <product>".

The bbs and sms channel artifacts are similarly re-generated on each just bbs/just sms run.


Part 4: Accessibility & ARIA

No formal accessibility documentation existed in this repo before this document. These guidelines apply to every artifact in the Toilville pipeline: HTML/web, Flutter, Swift/SwiftUI, WordPress, CLI.

4.1 Color and contrast

WCAG AA minimums (required for all products with status: confirmed):

Element Minimum ratio
Normal text (< 18px / < 14px bold) 4.5:1 against background
Large text (≥ 18px / ≥ 14px bold) 3:1
UI components (buttons, inputs) 3:1
Focus ring (--focus-ring) 3:1 against any surface it appears on

Token policy:

  • --focus-ring: #005FC8 — do not suppress or override for aesthetic reasons. It must achieve 3:1 contrast against every surface (dark cocoa, warm peach, white).
  • Safety tokens (--safety-allow, --safety-review, --safety-block) are supplementary. Always pair with text or an icon — never communicate tier status by color alone.
  • Consent tier accent colors (Observatory/Workshop/Pilgrimage/Sanctum/Federation) follow the same rule: color + text label, never color alone.

Per-theme contrast audit: Every named theme in color.json must be independently contrast-checked. The accessiblealt theme in Rituals is the WCAG AA reference baseline. When adding a new theme, verify these four pairings at minimum:

  • text.primary on bg.surface
  • text.on-accent on brand.primary
  • text.on-accent on brand.secondary
  • Focus ring (--focus-ring) on bg.surface

Use tokens.dtcg.json ($value.<theme> per token) for the hex values, then verify with browser DevTools Accessibility panel or any WCAG contrast checker.

4.2 Typography

  • Minimum body: 16px (--font-size-body). Never go below 14px (--font-size-small) for non-decorative text.
  • Line height: --leading-normal: 1.55 for body — do not tighten below 1.5.
  • Weight as the only differentiator is forbidden. Weight extremes (200 vs 800) are brand style, not accessibility tools — always pair with size or color changes for visual hierarchy.
  • Negative tracking on body text is forbidden. --tracking-display and --tracking-heading are for headings only.
  • Touch targets: minimum 44px (size.touch.target.min) for all interactive elements. Text links inside paragraphs are exempt. Buttons, tabs, chips, and form controls are not.

4.3 Motion

The motion tier defines durations and easings. Under prefers-reduced-motion: reduce, all durations must collapse to near-zero:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

This block must appear in every production stylesheet that consumes skin.css. The artifact generator does not yet emit it — add it in your app's global CSS reset.

Staggered entrance animations are the brand's primary motion pattern and the highest-risk affordance for users with vestibular disorders. Apply them only to non-critical decorative elements; always test with reduced-motion enabled in OS settings.

4.4 ARIA patterns by component

Component Required ARIA
Icon-only button aria-label="<action>"
Toggle / switch role="switch" · `aria-checked="true
Tab list role="tablist" on container · role="tab" + aria-selected per tab · role="tabpanel" + aria-labelledby per panel
Segmented control Same as tab list
Theme switcher button `aria-pressed="true
Modal / dialog role="dialog" · aria-modal="true" · aria-labelledby → dialog title · focus trap on open, restore on close
Navigation role="navigation" · aria-label="<section>" · aria-current="page" on active item
Form field <label for="id"> (not aria-label as substitute) · aria-describedby → error/hint text
Inline error role="alert" or aria-live="polite" — never color-only
Loading state role="status" · aria-live="polite" · aria-busy="true" during load
Ritual block card aria-label="<intent_name> — <intent_type>" on the card container

Consent tier UI (Rituals-specific):

  • Tier gate screen: aria-describedby pointing to the explanation text for why the tier is required before the user sees the gated content.
  • Mode badges (Observatory / Workshop / etc.): the mode name must be in text content, not only expressed as an accent color swatch.

4.5 Per-platform guidelines

Flutter / Dart

  • Wrap non-semantic widgets in Semantics(label: '...', child: ...).
  • excludeFromSemantics: true for purely decorative elements (icon beside a labeled button, background art, shading dividers).
  • Never override TextScaleFactor — respect the system setting. Verify layouts don't break at 200% text scale before any release cut.
  • Test with TalkBack (Android) and VoiceOver (iOS/macOS) before Rituals release.
  • Token bindings in packages/udg map udg.dart-key keys to token values. Use the key, not Color(0x...) — hardcoded values bypass the accessibility theme system.
  • The accessiblealt theme is the WCAG AA palette for Rituals. Surface it in Settings under "High contrast" or equivalent.

Swift / SwiftUI

  • .accessibilityLabel("...") for all custom drawing and icon-only controls.
  • .accessibilityHint("...") for non-obvious affordances (swipe gestures, long press).
  • .accessibilityElement(children: .ignore) to flatten compound decorative views.
  • .accessibilityAddTraits(.isButton) for tappable non-Button views.
  • Token values arrive via source-inject as udg_tokens.json — use the udg.dart-key field as the Swift dictionary key (matches the JSON structure).

WordPress / web

  • Semantic HTML first: <nav>, <main>, <section aria-label>, <header>, <footer>. ARIA supplements; it does not replace semantic elements.
  • Every <img>: alt="" for decorative, descriptive alt for informative.
  • Provide a skip-navigation link as the first focusable element on pages with navigation before main content.
  • skin.css CSS custom properties work in all modern browsers without a polyfill.

CLI (SPELWork / forge tools)

  • Error output → stderr. stdout must be clean for piping and screen reader pipelines.
  • Never use ANSI color as the only differentiator — pair with prefixes: [ERROR], [OK], [WARN]. Screen readers announce text, not color.
  • Support NO_COLOR=1 (the standard env var) to suppress all ANSI. The BBS artifact generator respects this via --no-color.
  • ritual query supports --json for structured output — enables programmatic consumption by assistive technology pipelines.

4.6 Pre-release checklist

Before moving a product from status: candidatestatus: confirmed:

  • Contrast ratio verified for default (light) theme — text, UI components, focus ring
  • Contrast ratio verified for every additional named theme
  • accessiblealt (or equivalent high-contrast) theme available
  • All interactive components keyboard-navigable (Tab, Enter/Space, Escape, Arrow keys)
  • No interactive element communicates state by color alone
  • Safety / consent tier UI pairs color with text label
  • prefers-reduced-motion: reduce tested — no layout shift, content immediately visible
  • Focus ring visible at 3:1 against all surfaces in the product's themes
  • Touch targets ≥ 44px for Flutter / mobile targets
  • Screen reader tested (TalkBack + VoiceOver for Flutter; NVDA or VoiceOver for web)

Part 5: Text-channel modalities — BBS and SMS

The design artifact has two plain-text siblings for contexts where HTML isn't the right channel. Both render the same ritual blocks as artifact.html but in different fidelity levels.

Channel Context Width Color Fidelity
bbs ritual-cli and ANSI-capable terminals 80 cols fixed ANSI 256-color Full: box-drawing, grouped menus, product header
sms SMS, webhook payloads, notification APIs ≤ 320 chars None Minimal: numbered key:label list

5.1 BBS format

Classic BBS aesthetic: 80-column fixed width, Unicode heavy box-drawing, ANSI 256-color mapped from UDG tokens. Intents are grouped by intent_type into labeled menu sections. Queries get numeric keys (19), mutations get letter keys (AZ).

Reference output for Rituals:

╔══════════════════════════════════════════════════════════════════════════════╗
║  RITUALS v1.1.0  ·  Universal Design Grammar  ·  toilville forge            ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  QUERY                                                                       ║
╟──────────────────────────────────────────────────────────────────────────────╢
║  [1] list products        List all products with metadata                    ║
║  [2] get product meta     Full metadata for a product                        ║
║  [3] list themes          Available themes for a product                     ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  MUTATION                                                                    ║
╟──────────────────────────────────────────────────────────────────────────────╢
║  [B] build product        Build CSS + DTCG tokens for a product              ║
║  [A] build artifact       Generate interactive UX artifact (HTML)            ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  [Q] Quit  ·  forge 15.835.0  ·  zone Z1                                    ║
╚══════════════════════════════════════════════════════════════════════════════╝

Canonical ANSI 256 color mapping from UDG tokens:

UDG token Hex ANSI 256
brand.primary (terracotta) #C05734 166
brand.secondary (mustard) #E1B659 178
brand.background (cocoa) #471F12 52
text.primary #FFFFFF (dark mode) 255
safety.allow #155724 28
safety.review #78350F 130
safety.block #7F1D1D 88

NO_COLOR=1 or --no-color disables all ANSI escapes — the output is plain UTF-8 box-drawing with no color, safe for logging and accessibility pipelines.

5.2 SMS format

≤ 320-char plain ASCII (2 SMS messages). No box-drawing, no ANSI:

RITUALS v1.1.0 [14 tools]
1:list 2:meta 3:themes 4:tokens 5:css-skin
6:drift-check 7:build 8:preview 9:build-all
B:artifact
Reply # to invoke. Z1 zone req.

Format rules:

  • Line 1: <PRODUCT_ABBREV> v<version> [<tool_count> tools]
  • Body lines: <key>:<shortest-alias> pairs, ≤ 8 per line, alias truncated at 10 chars
  • Queries → numeric keys (19 cycling), mutations → letter keys (AZ cycling)
  • Final line: reply instruction + consent zone abbreviation

5.3 Per-product house style

Each product can define udg/products/<product>/bbs-style.yml:

# bbs-style.yml — text-channel house style
bbs:
  header: |
    ╔══════════════════════════════════════════════════════════════════════════════╗
    ║  RITUALS v{version}  ·  Universal Design Grammar  ·  toilville forge        ║
    ╚══════════════════════════════════════════════════════════════════════════════╝
  color_primary:   166    # ANSI 256 — brand.primary
  color_secondary: 178    # ANSI 256 — brand.secondary
  color_surface:   52     # ANSI 256 — brand.background
  border_style: heavy     # 'heavy' (╔╗║╚╝═) or 'light' (┌┐│└┘─)
  shade_char: "░"         # decorative fill character: ░ ▒ ▓

sms:
  product_abbrev: RITUALS  # ≤ 10 chars, all-caps
  key_style: numeric       # 'numeric' (default) or 'alpha'

{version} in the header interpolates from meta.yml. If no bbs-style.yml exists, the generator uses a neutral default: light borders, no ANSI color.

Toilville house convention:

  • Product headers → heavy box-drawing (╔═╗)
  • Section separators within a product → light box-drawing (╟──╢)
  • Decorative fill in headers → for Rituals (warm grain), for dark/terminal products

5.4 Generating text artifacts

just bbs <product>              # dist/<product>/artifact.bbs
just sms <product>              # dist/<product>/artifact.sms
ritual query "udg bbs artifact <product>"
ritual query "udg sms artifact <product>"

Verification:

just bbs rituals | cat          # should render ≤ 80-col, readable box-drawing
just sms rituals | wc -c        # should be ≤ 320 characters
NO_COLOR=1 just bbs rituals     # should emit clean UTF-8 with no escape sequences

Quick reference: commands

just build                         # regenerate all products
just build-one <product>           # regenerate one product
just check                         # drift gate — CI equivalent
just artifact <product> [tab]      # interactive HTML spec (flutter/swift/cli/wordpress/design)
just bbs <product>                 # 80-col ANSI terminal spec
just sms <product>                 # terse SMS/notification spec
just serve                         # http://localhost:8080

ritual query "udg list products"
ritual query "udg get tokens <product>"
ritual query "udg build artifact <product>"
ritual query "udg bbs artifact <product>"
ritual query "udg check drift"
ritual query "udg validate token <path>"

Quick reference: token tiers

Tier Authored in CSS var pattern Example
color Brand/UDG/dtcg/color.json + Brand/UDG/products/<p>/dtcg/ --udg-color-* (canonical) / --primary (short) --udg-color-brand-primary
type Brand/UDG/dtcg/typography.json --font-*, --font-size-*, --weight-*, --leading-*, --tracking-* --font-size-body: 16px
space Brand/UDG/dtcg/space.json --space-1--space-8 --space-3: 16px
radius Brand/UDG/dtcg/radius.json --radius-sm/md/lg/full --radius-md: 9px
border Brand/UDG/dtcg/border.json --border-width, --border-width-emphasis --border-width: 1px
shadow Brand/UDG/dtcg/shadow.json --shadow-sm/md/lg --shadow-md: 0 4px 12px ...
motion Brand/UDG/dtcg/motion.json --duration-*, --ease-* --ease-standard: cubic-bezier(...)

Space scale (px): 1→4 · 2→8 · 3→16 · 4→24 · 5→32 · 6→40 · 7→56 · 8→64


Further reading

  • Brand/UDG/README.md — color token format, DTCG spec, divergence policy
  • udg/config/sync-manifest.yml — product versions and consumer routing
  • .forgejo/workflows/udg-dist-check.yml — CI drift gate
  • .pre-commit-config.yaml — local pre-commit hooks
  • justfile — all available tasks