diff --git a/STATUS.md b/STATUS.md index e76b009..ad1fc5f 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,43 +4,39 @@ Date: 2026-04-19 ## Ticket -- Active ticket: `SF-21` -- Branch: `feature/SF-21-linear-github-process` -- Goal: end-to-end Linear-first engineering workflow with PR guards and automation. +- Active ticket: `SF-23` +- Parent ticket: `SF-22` (Leopard Look redesign) +- Branch: `feature/SF-23-brand-foundation` +- Goal: establish the shared Leopard AI brand foundation consumed by follow-up platform tickets. ## Done -- Added PR governance: - - `.github/workflows/pr-ticket-guard.yml` - - `.github/PULL_REQUEST_TEMPLATE.md` -- Added Linear sync automation: - - `.github/workflows/linear-pr-sync.yml` - - `scripts/linear/pr_state_sync.mjs` -- Added auto-merge by Linear `Done`: - - `.github/workflows/linear-done-automerge.yml` - - `scripts/linear/automerge_done_tickets.mjs` -- Added process docs: - - `docs/guides/linear-github-process.md` - - `docs/guides/codex-automation-prompts.md` -- Updated docs index + README links. -- Updated CI branch pattern toward `feature/**`. - -## Open - -- Ensure repo secret `LINEAR_API_TOKEN` is set in GitHub Actions. -- Add `Preview` workflow state in Linear team (fallback currently uses `In Review`). -- Merge workflow PR and validate first real ticket through the full state machine. - -## Verification - -- `node scripts/linear/pr_state_sync.mjs` (graceful skip without token): ✅ -- `node scripts/linear/automerge_done_tickets.mjs` (graceful skip without token/context): ✅ -- `make test-core-js`: ✅ -- `make test-core-swift`: ✅ -- `make verify`: ✅ (including Android build in this environment) +### SF-23 — brand foundation +- `brand/tokens.json` — canonical source of truth (chakra palette, mode-to-chakra mapping, neutral surfaces, accents, typography, radius, spacing, elevation, leopard parameters). +- `brand/BRAND.md` — human-readable guidelines (identity, mode↔chakra, leopard pattern rules, logo/clearspace, glow formula, do/don't). +- `brand/generated/` — deterministic per-platform outputs: + - `tokens.css` (CSS custom properties) + - `BrandTokens.swift` (Swift enum tree) + - `BrandTokens.kt` (Kotlin object tree) + - `README.md` (do-not-edit notice, regen command) +- `scripts/brand/generate-tokens.mjs` — idempotent generator, verified byte-identical on repeated runs. +- `docs/guides/codex-automation-prompts.md` — worker prompt hardened to re-check active `Todo` / `In Progress` / `In Review` issues directly from Linear issue details. + +## Verification (this session) + +- `node scripts/brand/generate-tokens.mjs` → byte-identical twice. ✅ +- `cd sonicflow_app/core-js && npm test` → 5/5 pass. ✅ +- `cd sonicflow_app/chrome-extension && npm test` → 10/10 pass. ✅ +- `./scripts/check_warnings.sh` → blocked in `core_swift` by sandboxed Swift toolchain cache access (`sandbox_apply: Operation not permitted`), not by an SF-23 code failure. + +## Open / Flags + +- `SF-24` through `SF-29` remain follow-up tickets that consume this foundation and should be split into their own branches/PRs after SF-23 is pushed. +- GitHub PR listing/comment inspection is currently blocked from this sandbox (`api.github.com` unreachable). +- Prefer moving `SF-23` to `Preview` if that Linear state exists; otherwise use `In Review`. ## Next Step -- Commit + push latest workflow hardening changes. -- Keep/update PR #21 (`[SF-21] ...`) until checks pass. -- Keep ticket in `Preview` when PR is ready; merge path then drives `Done`. +- Commit and push SF-23 branch contents. +- Open PR titled `[SF-23] ...` with the Linear link and the verification notes above. +- Move the ticket to `Preview` / `In Review`, then start the next platform slice on its own branch. diff --git a/brand/BRAND.md b/brand/BRAND.md new file mode 100644 index 0000000..06a78a6 --- /dev/null +++ b/brand/BRAND.md @@ -0,0 +1,121 @@ +# Leopard AI — Brand Guidelines + +Version: 0.1.0 · Ticket: SF-23 · Parent: SF-22 + +## Identity + +- **Company:** Leopard AI +- **Flagship product:** SonicFlow +- **Legacy codename in paths / schemes / packages:** FlowTones (do not surface in UI) +- **Positioning:** Chakra-tuned audio layers that match the user's intended mind state — focus, flow, meditation, sleep. + +The brand is dark, warm, and quietly premium. Leopard pattern is a texture, never a character. Chakra colors carry meaning (mode semantics); gold carries identity. + +## Source of truth + +All tokens live in `brand/tokens.json`. Everything else (CSS variables, Swift struct, Kotlin object) is **generated** from that file — see `brand/generated/README.md`. No hex literal should appear outside of `brand/` after SF-24…SF-28 land. + +If a new color is needed, add it to `tokens.json` first, regenerate, then use the generated token. Direct hex usage is reviewable as a blocker. + +## Chakra palette + +Seven tokens, four currently load-bearing as mode accents: + +| Token | Chakra | Hex | Used for | +|------------|---------------------|-----------|-----------------------------------| +| `root` | Muladhara | `#C73D3D` | Warnings, deep-bass visualization | +| `sacral` | Svadhisthana | `#E67E22` | Secondary brand surfaces | +| `solar` | Manipura | `#F5C518` | Highlights, premium markers | +| `heart` | Anahata | `#1D9E75` | **Meditation** mode | +| `throat` | Vishuddha | `#378ADD` | **Focus** mode | +| `thirdEye` | Ajna | `#7F77DD` | **Flow** mode | +| `crown` | Sahasrara | `#534AB7` | **Sleep** mode | + +Rationale: the existing four shipped mode colors map cleanly to throat, third-eye, heart, and crown — the four chakras most associated with cognitive and rest bands. We keep them unchanged to avoid visual regression, and expose the remaining three for brand / non-mode surfaces. + +## Mode ↔ chakra mapping + +| Mode | Beat | Band | Chakra | Color | +|------------|------|-------|-----------|-----------| +| Focus | 40Hz | gamma | throat | `#378ADD` | +| Flow | 10Hz | alpha | thirdEye | `#7F77DD` | +| Meditation | 6Hz | theta | heart | `#1D9E75` | +| Sleep | 2Hz | delta | crown | `#534AB7` | + +## Accent + +- `gold` `#D4A24C` — primary brand accent (logo, leopard spot highlights). +- `spotRing` `#6B4B1F` — leopard spot ring, darker accent. +- `success` `#3CCF91` — active/playing state only. +- `danger` `#E0484D` — stop/error state only. + +## Neutral surfaces + +Dark first. Light mode is out of scope for the Leopard Look — the brand's identity requires the warm-on-dark contrast. + +| Token | Value | Use | +|--------|---------------------------------|---------------------------------| +| `ink` | `#0A0A0B` | deepest surface, pattern base | +| `bg` | `#0F0F12` | app canvas | +| `panel`| `rgba(18, 18, 22, 0.78)` | blurred surface behind content | +| `border`| `#2A2434` | panel/card borders | +| `fg` | `#F5F7FB` | primary text | +| `muted`| `#8E97A8` | secondary text | + +## Typography + +- **Display:** SF Pro Display / Inter → system fallback. +- **Body:** system stack (`-apple-system`, `Inter`, `Segoe UI`). +- **Mono:** JetBrains Mono → ui-monospace. + +Scale: `caption 11pt`, `body 14pt`, `title 17pt`, `display 24pt`. Tracking is slightly negative at display sizes. + +## Leopard pattern + +The Leopard Look is built on a procedural leopard-print layer behind a blurred panel. Until SF-29 ships real photographic/illustrative assets, every platform uses a generated pattern with: + +- Base: `ink` (#0A0A0B) +- Spot: `gold` (#D4A24C) with ~14% opacity +- Ring: `spotRing` (#6B4B1F) +- 2–4px blur on the pattern layer + +**Rules:** + +1. Pattern ALWAYS sits behind a blurred panel — never directly under body text. +2. Opacity stays between 10–18%; never above 25%. +3. No motion on the pattern (the user is listening, the eye is resting). +4. The pattern must be obviously a texture at glance distance — not readable as a photograph. + +## Logo + +- Clearspace: equal to the cap height of the mark on all sides. +- Minimum width: 24px (menu-bar icon), 16px (favicon). +- On dark: gold `#D4A24C`; subtle glow allowed on hero surfaces only. +- On light: ink `#0A0A0B`. Never gold on a light surface. + +The SF-22 icon upload has a cropping issue ("der muss immer abgeschnitten werden") — SF-29 re-exports with added safe-area padding before slicing to platform sizes. + +## Glow + +Active mode cards glow with their chakra color: + +``` +shadow = 0 0 24px modeColor + 0 0 64px (modeColor @ 30% alpha) +``` + +On native platforms this compiles to layered shadows or blur effects. See `brand/generated/*` for the per-platform translation. + +## Do / Don't + +**Do** + +- Use mode color only for that mode's surfaces (don't paint the Focus button in Heart green). +- Let the leopard pattern be quiet. +- Keep gold for identity, not for states. + +**Don't** + +- Don't mix chakra colors on a single card. +- Don't place gold on a light background. +- Don't animate the leopard pattern. +- Don't introduce a hex literal outside `brand/tokens.json`. diff --git a/brand/assets/README.md b/brand/assets/README.md new file mode 100644 index 0000000..5d62c99 --- /dev/null +++ b/brand/assets/README.md @@ -0,0 +1,100 @@ +# Brand assets — Leopard AI / SonicFlow + +Ticket: SF-29 · Parent: SF-22 · Follows: SF-23 (tokens), SF-24…SF-28 (platform wiring) + +This directory is the **asset pipeline**. `brand/tokens.json` is the color source of +truth; this folder is the shape source of truth. Platform apps consume from here once +SF-30 retires the procedural leopard placeholder. + +## Tree + +``` +brand/assets/ +├── README.md ← this file +├── leopard-pattern.svg ← hand-authored, generated-at-author-time (tileable ) +├── logomark.svg ← hand-authored (256×256, 16px safe padding) +├── wordmark.svg ← hand-authored (512×128, "Leopard AI" in gold) +└── icons/ ← GENERATED by scripts/brand/export-icons.mjs + ├── android-512.svg + ├── chrome-16.svg + ├── chrome-48.svg + ├── chrome-128.svg + ├── favicon-16.svg + ├── favicon-32.svg + └── ios-1024.svg +``` + +### Hand-crafted (edit freely) + +- `leopard-pattern.svg` — tileable procedural leopard texture. Spot placement is + deterministic (seeded PRNG at author time), so re-generation produces the same + layout. If you want a new layout, change the seed and re-author. +- `logomark.svg` — the geometric brand mark. Three concentric sonic arcs rising + from a hearth dot, framed by a rounded ear/drop silhouette with a small crown + dot. Uses `accent.gold` (`#D4A24C`). Authored with **16px safe padding** on a + 256×256 canvas — the actual glyph lives inside 224×224, so platform slicing + for rounded icon masks never clips (fixes the "der muss immer abgeschnitten + werden" issue flagged in SF-22). +- `wordmark.svg` — "Leopard AI" set in SF Pro Display / Inter / system-ui, gold + on transparent, 512×128. + +### Generated (do not edit by hand) + +- `icons/*.svg` — sized icon SVGs emitted from `logomark.svg` + `tokens.json`. + Each wraps the logomark in an outer `` with a + `--leopard-base` background rect so the mark has correct contrast in platform + icon contexts (menu-bar, favicon, launcher). + +## Regenerate + +```sh +node scripts/brand/export-icons.mjs +``` + +The script has **zero dependencies** (pure Node + filesystem) and is +**idempotent**: identical input → byte-identical output. All colors come from +`brand/tokens.json`; no hex literals live in the script itself. + +If you change `logomark.svg` or the `background.leopard.base` token, re-run the +script and commit the regenerated `icons/*.svg` alongside the source change. + +## How this fits the stack + +| Layer | Source | Consumer | +|---------------------|-------------------------------------|-------------------------------------------------------| +| Color tokens | `brand/tokens.json` | `brand/generated/*` (CSS / Swift / Kotlin) via SF-23 | +| Shape tokens | `brand/assets/*.svg` | this folder, platform icon slots via SF-30 | +| Procedural leopard | platform code (SF-24…SF-28) | runtime — to be replaced by `leopard-pattern.svg` | + +## SF-30 migration — procedural → asset + +Today every platform draws the leopard texture procedurally at runtime using the +`background.leopard.*` tokens (rules in `brand/BRAND.md` §Leopard pattern). The +procedural renderers are correct but not pixel-identical across platforms. + +SF-30 will: + +1. Replace each procedural renderer with `leopard-pattern.svg` rendered as a + tiled background behind the blurred panel (web: `background-image`; iOS: + `UIImage(patternNamed:)`-equivalent; Android: a tile drawable). +2. Keep the `background.leopard.opacity` and `.blurPx` tokens — they still apply + at the compositor level. +3. Ship the icons from `brand/assets/icons/` into the respective platform + manifests (`manifest.webmanifest`, `AppIcon.appiconset`, `mipmap-*`). + +### Candidates for a human designer pass (flag before shipping) + +- **Logomark glyph** is intentionally simple and geometric — a designer should + review whether the sonic-arc motif reads as "Leopard AI" at 16×16 and + whether the crown dot survives aggressive icon masks (iOS squircle, Android + adaptive foreground). +- **Leopard pattern spot distribution** is decent blue-noise but not + hand-tuned. A designer may want to reposition ~10% of spots for better + negative-space rhythm. +- **Wordmark** currently relies on the system font stack via SVG ``. For + a locked, cross-platform wordmark, a designer should convert the text to + outlined paths so rendering no longer depends on the consumer's installed + SF Pro / Inter. +- **Raster exports** (PNG/ICO/ICNS) are not in scope for SF-29 — the icons ship + as SVG only. SF-30 can add a rasterization step (e.g. `resvg` or `sharp`) if + platform manifests require them. diff --git a/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.jpg b/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.jpg new file mode 100644 index 0000000..e4bb447 Binary files /dev/null and b/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.jpg differ diff --git a/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.png b/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.png new file mode 100644 index 0000000..8f2b908 Binary files /dev/null and b/brand/assets/icons/Gemini_Generated_Image_f456fef456fef456.png differ diff --git "a/brand/assets/icons/a_premium_ultra_modern_high_definition_vector_icon_of_a_golden_tibetan_singing_bowl._the_bowl_is_minimalist_with_sleek_sharp_edges_and_a_subtle_metallic_gradient._a_smooth_flowing_and_vibrant_rainbow_energy_swirl_rises_elegantly_from_the_bowl._sev\342\200\246.png" "b/brand/assets/icons/a_premium_ultra_modern_high_definition_vector_icon_of_a_golden_tibetan_singing_bowl._the_bowl_is_minimalist_with_sleek_sharp_edges_and_a_subtle_metallic_gradient._a_smooth_flowing_and_vibrant_rainbow_energy_swirl_rises_elegantly_from_the_bowl._sev\342\200\246.png" new file mode 100644 index 0000000..47dd82c Binary files /dev/null and "b/brand/assets/icons/a_premium_ultra_modern_high_definition_vector_icon_of_a_golden_tibetan_singing_bowl._the_bowl_is_minimalist_with_sleek_sharp_edges_and_a_subtle_metallic_gradient._a_smooth_flowing_and_vibrant_rainbow_energy_swirl_rises_elegantly_from_the_bowl._sev\342\200\246.png" differ diff --git a/brand/assets/icons/android-512.svg b/brand/assets/icons/android-512.svg new file mode 100644 index 0000000..43fb844 --- /dev/null +++ b/brand/assets/icons/android-512.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 512x512 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/chrome-128.svg b/brand/assets/icons/chrome-128.svg new file mode 100644 index 0000000..eca7b9c --- /dev/null +++ b/brand/assets/icons/chrome-128.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 128x128 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/chrome-16.svg b/brand/assets/icons/chrome-16.svg new file mode 100644 index 0000000..8ca8c13 --- /dev/null +++ b/brand/assets/icons/chrome-16.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 16x16 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/chrome-48.svg b/brand/assets/icons/chrome-48.svg new file mode 100644 index 0000000..9ff6ecf --- /dev/null +++ b/brand/assets/icons/chrome-48.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 48x48 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/favicon-16.svg b/brand/assets/icons/favicon-16.svg new file mode 100644 index 0000000..8ca8c13 --- /dev/null +++ b/brand/assets/icons/favicon-16.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 16x16 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/favicon-32.svg b/brand/assets/icons/favicon-32.svg new file mode 100644 index 0000000..d99e02a --- /dev/null +++ b/brand/assets/icons/favicon-32.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 32x32 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/icons/ios-1024.svg b/brand/assets/icons/ios-1024.svg new file mode 100644 index 0000000..0945759 --- /dev/null +++ b/brand/assets/icons/ios-1024.svg @@ -0,0 +1,27 @@ + + + Leopard AI — 1024x1024 + Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json. + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + + diff --git a/brand/assets/leopard-pattern.svg b/brand/assets/leopard-pattern.svg new file mode 100644 index 0000000..b5705f2 --- /dev/null +++ b/brand/assets/leopard-pattern.svg @@ -0,0 +1,275 @@ + + + Leopard AI — Procedural Leopard Pattern + + Generated deterministically from brand/tokens.json (SF-29 pipeline). + base=#0F0F12 spot=#D4A24C ring=#6B4B1F opacity=0.14 tile=400x400. + Placeholder until a real leopard asset replaces it (see SF-30). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/assets/logomark.svg b/brand/assets/logomark.svg new file mode 100644 index 0000000..6eb6388 --- /dev/null +++ b/brand/assets/logomark.svg @@ -0,0 +1,22 @@ + + + Leopard AI + + Leopard AI logomark. Geometric glyph — three concentric sonic arcs rising from a + hearth dot, framed by the negative-space silhouette of a stylised ear. + Safe padding: 16px on all sides (content fits inside 224x224 at 16,16). + Color: accent.gold from brand/tokens.json. Transparent background. + + + + + + + + + + + + + + diff --git a/brand/assets/wallpapers/leopard_wallpaper.JPG b/brand/assets/wallpapers/leopard_wallpaper.JPG new file mode 100644 index 0000000..fbd71b4 Binary files /dev/null and b/brand/assets/wallpapers/leopard_wallpaper.JPG differ diff --git a/brand/assets/wallpapers/leopard_wallpaper.png b/brand/assets/wallpapers/leopard_wallpaper.png new file mode 100644 index 0000000..cd8d689 Binary files /dev/null and b/brand/assets/wallpapers/leopard_wallpaper.png differ diff --git a/brand/assets/wordmark.svg b/brand/assets/wordmark.svg new file mode 100644 index 0000000..c24921d --- /dev/null +++ b/brand/assets/wordmark.svg @@ -0,0 +1,18 @@ + + + Leopard AI + + Leopard AI wordmark. Uses SF Pro Display → Inter → system-ui stack. + Color: accent.gold from brand/tokens.json. Transparent background. + Baseline sits on y=82 at 56px cap height; tracking is slightly tight at display size. + + Leopard AI + diff --git a/brand/generated/BrandTokens.kt b/brand/generated/BrandTokens.kt new file mode 100644 index 0000000..b766898 --- /dev/null +++ b/brand/generated/BrandTokens.kt @@ -0,0 +1,69 @@ +// AUTOGENERATED from brand/tokens.json — do not edit by hand. +// Regenerate: node scripts/brand/generate-tokens.mjs +// Version: 0.1.0 + +package com.sonicflow.app.brand + +import androidx.compose.ui.graphics.Color +import com.sonicflow.app.ui.fromHex + +object BrandTokens { + object Chakra { + val root = Color.fromHex("#C73D3D") + val sacral = Color.fromHex("#E67E22") + val solar = Color.fromHex("#F5C518") + val heart = Color.fromHex("#1D9E75") + val throat = Color.fromHex("#378ADD") + val thirdEye = Color.fromHex("#7F77DD") + val crown = Color.fromHex("#534AB7") + } + + object Neutral { + val ink = Color.fromHex("#0A0A0B") + val bg = Color.fromHex("#0F0F12") + val border = Color.fromHex("#2A2434") + val fg = Color.fromHex("#F5F7FB") + val muted = Color.fromHex("#8E97A8") + val panel = Color(0f, 0f, 0f, 0.78f) + } + + object Accent { + val gold = Color.fromHex("#D4A24C") + val spotRing = Color.fromHex("#6B4B1F") + val success = Color.fromHex("#3CCF91") + val danger = Color.fromHex("#E0484D") + } + + object Mode { + val focus = Color.fromHex("#378ADD") + val flow = Color.fromHex("#7F77DD") + val meditation = Color.fromHex("#1D9E75") + val sleep = Color.fromHex("#534AB7") + } + + object Radius { + const val sm: Int = 8 + const val md: Int = 14 + const val lg: Int = 20 + const val pill: Int = 999 + } + + object Spacing { + const val xs: Int = 4 + const val sm: Int = 8 + const val md: Int = 14 + const val lg: Int = 20 + const val xl: Int = 32 + } + + object Leopard { + val base = Color.fromHex("#0F0F12") + val spot = Color.fromHex("#D4A24C") + val ring = Color.fromHex("#6B4B1F") + const val opacity: Float = 0.14f + const val blurPx: Int = 2 + const val spotCount: Int = 24 + const val spotMinPx: Int = 14 + const val spotMaxPx: Int = 44 + } +} diff --git a/brand/generated/BrandTokens.swift b/brand/generated/BrandTokens.swift new file mode 100644 index 0000000..35b06b9 --- /dev/null +++ b/brand/generated/BrandTokens.swift @@ -0,0 +1,66 @@ +// AUTOGENERATED from brand/tokens.json — do not edit by hand. +// Regenerate: node scripts/brand/generate-tokens.mjs +// Version: 0.1.0 + +import SwiftUI + +public enum BrandTokens { + public enum Chakra { + public static let root = Color(hex: "#C73D3D") + public static let sacral = Color(hex: "#E67E22") + public static let solar = Color(hex: "#F5C518") + public static let heart = Color(hex: "#1D9E75") + public static let throat = Color(hex: "#378ADD") + public static let thirdEye = Color(hex: "#7F77DD") + public static let crown = Color(hex: "#534AB7") + } + + public enum Neutral { + public static let ink = Color(hex: "#0A0A0B") + public static let bg = Color(hex: "#0F0F12") + public static let border = Color(hex: "#2A2434") + public static let fg = Color(hex: "#F5F7FB") + public static let muted = Color(hex: "#8E97A8") + public static let panel = Color.black.opacity(0.78) + } + + public enum Accent { + public static let gold = Color(hex: "#D4A24C") + public static let spotRing = Color(hex: "#6B4B1F") + public static let success = Color(hex: "#3CCF91") + public static let danger = Color(hex: "#E0484D") + } + + public enum Mode { + public static let focus = Color(hex: "#378ADD") + public static let flow = Color(hex: "#7F77DD") + public static let meditation = Color(hex: "#1D9E75") + public static let sleep = Color(hex: "#534AB7") + } + + public enum Radius { + public static let sm: CGFloat = 8 + public static let md: CGFloat = 14 + public static let lg: CGFloat = 20 + public static let pill: CGFloat = 999 + } + + public enum Spacing { + public static let xs: CGFloat = 4 + public static let sm: CGFloat = 8 + public static let md: CGFloat = 14 + public static let lg: CGFloat = 20 + public static let xl: CGFloat = 32 + } + + public enum Leopard { + public static let base = Color(hex: "#0F0F12") + public static let spot = Color(hex: "#D4A24C") + public static let ring = Color(hex: "#6B4B1F") + public static let opacity: Double = 0.14 + public static let blurRadius: CGFloat = 2 + public static let spotCount: Int = 24 + public static let spotMinPx: CGFloat = 14 + public static let spotMaxPx: CGFloat = 44 + } +} diff --git a/brand/generated/README.md b/brand/generated/README.md new file mode 100644 index 0000000..f2db901 --- /dev/null +++ b/brand/generated/README.md @@ -0,0 +1,19 @@ +# brand/generated + +These files are **generated from `brand/tokens.json`** by `scripts/brand/generate-tokens.mjs`. + +Do not hand-edit. Edit `tokens.json`, then run: + +```bash +node scripts/brand/generate-tokens.mjs +``` + +Generated outputs: + +- `tokens.css` — CSS custom properties, consumed by Chrome + Safari extensions. +- `BrandTokens.swift` — Swift struct with `Color` fields, consumed by iOS + macOS + Safari macOS app. +- `BrandTokens.kt` — Kotlin object with `Color.fromHex` fields, consumed by Android Compose theme. + +The generator is deterministic — running it twice on the same input produces byte-identical output. + +CI runs the generator and diffs the result against checked-in files. A non-empty diff fails the build (SF-24 introduces this check). diff --git a/brand/generated/tokens.css b/brand/generated/tokens.css new file mode 100644 index 0000000..34c3bbe --- /dev/null +++ b/brand/generated/tokens.css @@ -0,0 +1,65 @@ +/* AUTOGENERATED from brand/tokens.json — do not edit by hand. + * Regenerate: node scripts/brand/generate-tokens.mjs + * Version: 0.1.0 + */ +:root { + color-scheme: dark; + + /* chakra palette */ + --chakra-root: #C73D3D; + --chakra-sacral: #E67E22; + --chakra-solar: #F5C518; + --chakra-heart: #1D9E75; + --chakra-throat: #378ADD; + --chakra-third-eye: #7F77DD; + --chakra-crown: #534AB7; + + /* neutral */ + --neutral-ink: #0A0A0B; + --neutral-bg: #0F0F12; + --neutral-panel: rgba(18, 18, 22, 0.78); + --neutral-border: #2A2434; + --neutral-fg: #F5F7FB; + --neutral-muted: #8E97A8; + + /* accent */ + --accent-gold: #D4A24C; + --accent-spot-ring: #6B4B1F; + --accent-success: #3CCF91; + --accent-danger: #E0484D; + + /* mode accents */ + --mode-focus: #378ADD; + --mode-flow: #7F77DD; + --mode-meditation: #1D9E75; + --mode-sleep: #534AB7; + + /* typography */ + --font-display: "SF Pro Display", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, Menlo, monospace; + + /* radius */ + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-pill: 999px; + + /* spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 14px; + --space-lg: 20px; + --space-xl: 32px; + + /* elevation */ + --elev-soft: 0 10px 40px rgba(0, 0, 0, 0.45); + --elev-gold: 0 0 20px rgba(212, 162, 76, 0.35); + + /* leopard background */ + --leopard-base: #0F0F12; + --leopard-spot: #D4A24C; + --leopard-ring: #6B4B1F; + --leopard-opacity: 0.14; + --leopard-blur: 2px; +} diff --git a/brand/tokens.json b/brand/tokens.json new file mode 100644 index 0000000..98fb531 --- /dev/null +++ b/brand/tokens.json @@ -0,0 +1,119 @@ +{ + "$schema": "./tokens.schema.json", + "meta": { + "version": "0.1.0", + "generatedFromTicket": "SF-23", + "parentTicket": "SF-22", + "sourceOfTruth": true, + "notes": "Canonical brand tokens for Leopard AI / SonicFlow. All platform theme files MUST be generated from this JSON — no hex literals outside brand/." + }, + "brand": { + "company": "Leopard AI", + "product": "SonicFlow", + "legacyCodename": "FlowTones", + "tagline": "Chakra-tuned audio layers for every mind state." + }, + "palette": { + "chakra": { + "root": { + "name": "Root (Muladhara)", + "hex": "#C73D3D", + "role": "Grounding, instinct. Used sparingly for warnings and deep-bass visualizations." + }, + "sacral": { + "name": "Sacral (Svadhisthana)", + "hex": "#E67E22", + "role": "Creativity, flow of emotion. Secondary accent for brand surfaces." + }, + "solar": { + "name": "Solar Plexus (Manipura)", + "hex": "#F5C518", + "role": "Willpower, clarity. Used for highlight states and premium markers." + }, + "heart": { + "name": "Heart (Anahata)", + "hex": "#1D9E75", + "role": "Balance, calm. Canonical color for the Meditation mode." + }, + "throat": { + "name": "Throat (Vishuddha)", + "hex": "#378ADD", + "role": "Expression, focus. Canonical color for the Focus mode." + }, + "thirdEye": { + "name": "Third Eye (Ajna)", + "hex": "#7F77DD", + "role": "Insight, flow. Canonical color for the Flow mode." + }, + "crown": { + "name": "Crown (Sahasrara)", + "hex": "#534AB7", + "role": "Stillness, rest. Canonical color for the Sleep mode." + } + }, + "neutral": { + "ink": { "hex": "#0A0A0B", "role": "Deepest surface, leopard pattern base" }, + "bg": { "hex": "#0F0F12", "role": "App canvas background" }, + "panel": { "rgba": "rgba(18, 18, 22, 0.78)", "role": "Blurred surface behind content" }, + "border": { "hex": "#2A2434", "role": "Panel and card borders" }, + "fg": { "hex": "#F5F7FB", "role": "Primary text on dark surfaces" }, + "muted": { "hex": "#8E97A8", "role": "Secondary text, captions" } + }, + "accent": { + "gold": { "hex": "#D4A24C", "role": "Leopard spot highlight, primary brand accent" }, + "spotRing": { "hex": "#6B4B1F", "role": "Leopard spot ring, darker accent" }, + "success": { "hex": "#3CCF91", "role": "Active / playing indicator" }, + "danger": { "hex": "#E0484D", "role": "Stop / error state" } + } + }, + "mode": { + "focus": { "chakra": "throat", "hex": "#378ADD", "beatHz": 40, "band": "gamma", "label": "Focus", "description": "Sharpens attention for deep work." }, + "flow": { "chakra": "thirdEye", "hex": "#7F77DD", "beatHz": 10, "band": "alpha", "label": "Flow", "description": "Balances calm and creative momentum." }, + "meditation": { "chakra": "heart", "hex": "#1D9E75", "beatHz": 6, "band": "theta", "label": "Meditation", "description": "Soft theta pacing for inward focus." }, + "sleep": { "chakra": "crown", "hex": "#534AB7", "beatHz": 2, "band": "delta", "label": "Sleep", "description": "Slow down gently toward rest." } + }, + "typography": { + "display": "\"SF Pro Display\", \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "body": "-apple-system, BlinkMacSystemFont, \"Inter\", \"Segoe UI\", sans-serif", + "mono": "\"JetBrains Mono\", ui-monospace, Menlo, monospace", + "scale": { + "caption": { "px": 11, "weight": 500, "tracking": 0.02 }, + "body": { "px": 14, "weight": 400, "tracking": 0 }, + "title": { "px": 17, "weight": 600, "tracking": -0.01 }, + "display": { "px": 24, "weight": 700, "tracking": -0.02 } + } + }, + "radius": { "sm": 8, "md": 14, "lg": 20, "pill": 999 }, + "spacing": { "xs": 4, "sm": 8, "md": 14, "lg": 20, "xl": 32 }, + "elevation": { + "soft": "0 10px 40px rgba(0, 0, 0, 0.45)", + "gold": "0 0 20px rgba(212, 162, 76, 0.35)", + "modeGlow": { + "notes": "Use with --mode-color (CSS) or modeColor (native). Native platforms implement this with drop-shadow or blur-effect layers.", + "cssTemplate": "0 0 24px var(--mode-color), 0 0 64px color-mix(in oklab, var(--mode-color), transparent 70%)" + } + }, + "background": { + "leopard": { + "base": "#0F0F12", + "spot": "#D4A24C", + "ring": "#6B4B1F", + "opacity": 0.14, + "spotCount": 24, + "spotMinPx": 14, + "spotMaxPx": 44, + "blurPx": 2, + "rules": [ + "Leopard pattern MUST sit behind a blurred panel — never directly under body text.", + "Use 10–18% opacity on dark surfaces; never above 25%.", + "Pattern is generated procedurally until SF-29 replaces it with the real leopard asset." + ] + } + }, + "logo": { + "clearspace": "Equal to the height of the mark's cap height on all sides.", + "minWidthPx": 24, + "placementDark": "Gold #D4A24C on dark ink; glow allowed on hero surfaces.", + "placementLight": "Use ink #0A0A0B on any light surface (never gold on white)." + } +} diff --git a/docs/guides/codex-automation-prompts.md b/docs/guides/codex-automation-prompts.md index 97c09ad..35066bf 100644 --- a/docs/guides/codex-automation-prompts.md +++ b/docs/guides/codex-automation-prompts.md @@ -13,13 +13,21 @@ Arbeite streng ticket-driven für dieses Repository. Wenn gefunden: setze exakt diese Arbeit fort, beginne kein neues Ticket. 2) Wenn keine laufende Arbeit existiert: -- hole offene Issues im Status Todo -- wenn keine Todo-Issues existieren: stoppe und reportiere kurz -- priorisiere innerhalb der Todo-Issues nach: - a) mir (Codex) zugewiesen +- prüfe zuerst offene Linear-Issues, die an `Codex` delegiert oder direkt an `Codex` zugewiesen sind +- Assignment oder Delegation an `Codex` ist ein explizites Startsignal für den Worker +- berücksichtige dabei nur Issues in `Todo`, `In Progress` oder `In Review` +- validiere den Status von Kandidaten immer über die direkten Issue-Details; verlasse dich nicht nur auf Suchfilter +- wenn ein Ticket explizit genannt wurde oder kürzlich in STATUS.md / Memory auftauchte, prüfe dieses Ticket direkt +- wenn es passende `Codex`-Tickets gibt, bearbeite diese vor allen allgemeinen offenen Issues +- wenn es keine passenden `Codex`-Tickets gibt, hole offene Issues in den Status `Todo`, `In Progress` oder `In Review` +- Assignee ist außerhalb von `Codex`-Tickets niemals ein Ausschlusskriterium: nimm auch Tickets, die jemand anderem oder niemandem zugewiesen sind +- wenn keine passenden Issues in `Todo`, `In Progress` oder `In Review` existieren: stoppe und reportiere kurz +- priorisiere innerhalb dieser offenen Arbeits-States nach: + a) `Codex`-Delegation oder direkte `Codex`-Zuweisung b) höchste Priorität / Blocker c) klare Acceptance Criteria d) ohne zusätzliche Produktentscheidung direkt umsetzbar + e) kleinster sinnvoller, sauber abgrenzbarer Lieferumfang 3) Parallele Arbeit ist erlaubt: - nutze Agents für unabhängige Tickets/Subtasks parallel @@ -62,8 +70,9 @@ Linear Ticket -> feature/TICKET-ID-desc branch -> implementation -> tests -> PR RULES: - Work only from Linear tickets (or PR review requests). -- Parallel execution with agents is allowed for independent Todo tickets. -- Never start from Backlog automatically. Work starts from Todo (or explicit user command). +- If an issue is delegated or assigned to `Codex`, treat that as explicit pickup intent on the next automation run. +- Parallel execution with agents is allowed for independent active tickets. +- Never start from Backlog automatically. Work starts from `Todo`, `In Progress`, `In Review`, or explicit user command. - Keep STATUS.md current before ending a run. GIT: diff --git a/scripts/brand/export-icons.mjs b/scripts/brand/export-icons.mjs new file mode 100755 index 0000000..2eccdb9 --- /dev/null +++ b/scripts/brand/export-icons.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Export sized icon SVGs for Leopard AI / SonicFlow (SF-29). +// +// Reads: +// brand/tokens.json (color source of truth, read-only) +// brand/assets/logomark.svg (canonical geometric mark) +// +// Writes (overwrites): +// brand/assets/icons/chrome-16.svg +// brand/assets/icons/chrome-48.svg +// brand/assets/icons/chrome-128.svg +// brand/assets/icons/ios-1024.svg +// brand/assets/icons/android-512.svg +// brand/assets/icons/favicon-16.svg +// brand/assets/icons/favicon-32.svg +// +// Usage: node scripts/brand/export-icons.mjs +// +// Zero external dependencies. Idempotent: running twice on identical inputs +// yields byte-identical outputs. No hex literals live in this file — all colors +// come from brand/tokens.json. + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, "..", ".."); +const tokensPath = resolve(repoRoot, "brand", "tokens.json"); +const logomarkPath = resolve(repoRoot, "brand", "assets", "logomark.svg"); +const outDir = resolve(repoRoot, "brand", "assets", "icons"); + +const tokens = JSON.parse(readFileSync(tokensPath, "utf8")); +const logomarkRaw = readFileSync(logomarkPath, "utf8"); + +const leopardBase = tokens.background.leopard.base; + +// Targets are sorted by filename so iteration order is stable across runs. +const TARGETS = [ + { name: "android-512.svg", size: 512, withBackground: true }, + { name: "chrome-128.svg", size: 128, withBackground: true }, + { name: "chrome-16.svg", size: 16, withBackground: true }, + { name: "chrome-48.svg", size: 48, withBackground: true }, + { name: "favicon-16.svg", size: 16, withBackground: true }, + { name: "favicon-32.svg", size: 32, withBackground: true }, + { name: "ios-1024.svg", size: 1024, withBackground: true }, +]; + +mkdirSync(outDir, { recursive: true }); + +// Extract the logomark's inner content so we can nest it as a . +const inner = extractSvgInner(logomarkRaw); + +for (const t of TARGETS) { + const svg = renderIcon({ + size: t.size, + withBackground: t.withBackground, + background: leopardBase, + logomarkInner: inner, + }); + writeFileSync(resolve(outDir, t.name), svg); +} + +console.log("brand icons exported:"); +for (const t of TARGETS) { + console.log(" -", resolve(outDir, t.name)); +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +function extractSvgInner(svgText) { + // Grab everything between the opening and the closing . + const openMatch = svgText.match(/]*>/); + if (!openMatch) throw new Error("logomark.svg: no root element"); + const openEnd = openMatch.index + openMatch[0].length; + const closeIdx = svgText.lastIndexOf(""); + if (closeIdx < 0) throw new Error("logomark.svg: no closing "); + return svgText.slice(openEnd, closeIdx).trim(); +} + +function renderIcon({ size, withBackground, background, logomarkInner }) { + // The logomark is authored in a 256x256 viewBox. We nest the same viewBox + // here so the mark scales into the icon box intact. The outer uses + // width/height = SIZE so consumers can drop it into favicon/manifest slots. + const indent = " "; + const indented = logomarkInner + .split("\n") + .map((line) => (line.length ? indent + line : line)) + .join("\n"); + + const lines = []; + lines.push(""); + lines.push( + `` + ); + lines.push(` Leopard AI — ${size}x${size}`); + lines.push( + " Generated by scripts/brand/export-icons.mjs from brand/assets/logomark.svg and brand/tokens.json." + ); + if (withBackground) { + lines.push(` `); + } + lines.push(" "); + lines.push(indented); + lines.push(" "); + lines.push(""); + lines.push(""); + return lines.join("\n"); +} diff --git a/scripts/brand/generate-tokens.mjs b/scripts/brand/generate-tokens.mjs new file mode 100644 index 0000000..e0ec91f --- /dev/null +++ b/scripts/brand/generate-tokens.mjs @@ -0,0 +1,244 @@ +#!/usr/bin/env node +// Generate per-platform brand token files from brand/tokens.json. +// Source of truth: brand/tokens.json (SF-23) +// Consumers: Chrome/Safari CSS, iOS/macOS Swift, Android Kotlin. +// +// Usage: node scripts/brand/generate-tokens.mjs +// +// Idempotent: running twice on the same input yields byte-identical output. + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, "..", ".."); +const tokensPath = resolve(repoRoot, "brand", "tokens.json"); +const outDir = resolve(repoRoot, "brand", "generated"); + +const tokens = JSON.parse(readFileSync(tokensPath, "utf8")); + +mkdirSync(outDir, { recursive: true }); + +writeFileSync(resolve(outDir, "tokens.css"), renderCss(tokens)); +writeFileSync(resolve(outDir, "BrandTokens.swift"), renderSwift(tokens)); +writeFileSync(resolve(outDir, "BrandTokens.kt"), renderKotlin(tokens)); + +console.log("brand tokens generated:"); +console.log(" -", resolve(outDir, "tokens.css")); +console.log(" -", resolve(outDir, "BrandTokens.swift")); +console.log(" -", resolve(outDir, "BrandTokens.kt")); + +// --------------------------------------------------------------------------- +// CSS +// --------------------------------------------------------------------------- + +function renderCss(t) { + const lines = []; + lines.push("/* AUTOGENERATED from brand/tokens.json — do not edit by hand."); + lines.push(" * Regenerate: node scripts/brand/generate-tokens.mjs"); + lines.push(` * Version: ${t.meta.version}`); + lines.push(" */"); + lines.push(":root {"); + lines.push(" color-scheme: dark;"); + lines.push(""); + lines.push(" /* chakra palette */"); + for (const [key, v] of Object.entries(t.palette.chakra)) { + lines.push(` --chakra-${kebab(key)}: ${v.hex};`); + } + lines.push(""); + lines.push(" /* neutral */"); + for (const [key, v] of Object.entries(t.palette.neutral)) { + const value = v.hex ?? v.rgba; + lines.push(` --neutral-${kebab(key)}: ${value};`); + } + lines.push(""); + lines.push(" /* accent */"); + for (const [key, v] of Object.entries(t.palette.accent)) { + lines.push(` --accent-${kebab(key)}: ${v.hex};`); + } + lines.push(""); + lines.push(" /* mode accents */"); + for (const [mode, v] of Object.entries(t.mode)) { + lines.push(` --mode-${kebab(mode)}: ${v.hex};`); + } + lines.push(""); + lines.push(" /* typography */"); + lines.push(` --font-display: ${t.typography.display};`); + lines.push(` --font-body: ${t.typography.body};`); + lines.push(` --font-mono: ${t.typography.mono};`); + lines.push(""); + lines.push(" /* radius */"); + for (const [key, v] of Object.entries(t.radius)) { + lines.push(` --radius-${kebab(key)}: ${v}px;`); + } + lines.push(""); + lines.push(" /* spacing */"); + for (const [key, v] of Object.entries(t.spacing)) { + lines.push(` --space-${kebab(key)}: ${v}px;`); + } + lines.push(""); + lines.push(" /* elevation */"); + lines.push(` --elev-soft: ${t.elevation.soft};`); + lines.push(` --elev-gold: ${t.elevation.gold};`); + lines.push(""); + lines.push(" /* leopard background */"); + lines.push(` --leopard-base: ${t.background.leopard.base};`); + lines.push(` --leopard-spot: ${t.background.leopard.spot};`); + lines.push(` --leopard-ring: ${t.background.leopard.ring};`); + lines.push(` --leopard-opacity: ${t.background.leopard.opacity};`); + lines.push(` --leopard-blur: ${t.background.leopard.blurPx}px;`); + lines.push("}"); + lines.push(""); + return lines.join("\n"); +} + +// --------------------------------------------------------------------------- +// Swift +// --------------------------------------------------------------------------- + +function renderSwift(t) { + const lines = []; + lines.push("// AUTOGENERATED from brand/tokens.json — do not edit by hand."); + lines.push("// Regenerate: node scripts/brand/generate-tokens.mjs"); + lines.push(`// Version: ${t.meta.version}`); + lines.push(""); + lines.push("import SwiftUI"); + lines.push(""); + lines.push("public enum BrandTokens {"); + lines.push(" public enum Chakra {"); + for (const [key, v] of Object.entries(t.palette.chakra)) { + lines.push(` public static let ${camel(key)} = Color(hex: "${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" public enum Neutral {"); + for (const [key, v] of Object.entries(t.palette.neutral)) { + if (v.hex) { + lines.push(` public static let ${camel(key)} = Color(hex: "${v.hex}")`); + } + } + lines.push(" public static let panel = Color.black.opacity(0.78)"); + lines.push(" }"); + lines.push(""); + lines.push(" public enum Accent {"); + for (const [key, v] of Object.entries(t.palette.accent)) { + lines.push(` public static let ${camel(key)} = Color(hex: "${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" public enum Mode {"); + for (const [mode, v] of Object.entries(t.mode)) { + lines.push(` public static let ${camel(mode)} = Color(hex: "${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" public enum Radius {"); + for (const [key, v] of Object.entries(t.radius)) { + lines.push(` public static let ${camel(key)}: CGFloat = ${v}`); + } + lines.push(" }"); + lines.push(""); + lines.push(" public enum Spacing {"); + for (const [key, v] of Object.entries(t.spacing)) { + lines.push(` public static let ${camel(key)}: CGFloat = ${v}`); + } + lines.push(" }"); + lines.push(""); + lines.push(" public enum Leopard {"); + lines.push(` public static let base = Color(hex: "${t.background.leopard.base}")`); + lines.push(` public static let spot = Color(hex: "${t.background.leopard.spot}")`); + lines.push(` public static let ring = Color(hex: "${t.background.leopard.ring}")`); + lines.push(` public static let opacity: Double = ${t.background.leopard.opacity}`); + lines.push(` public static let blurRadius: CGFloat = ${t.background.leopard.blurPx}`); + lines.push(` public static let spotCount: Int = ${t.background.leopard.spotCount}`); + lines.push(` public static let spotMinPx: CGFloat = ${t.background.leopard.spotMinPx}`); + lines.push(` public static let spotMaxPx: CGFloat = ${t.background.leopard.spotMaxPx}`); + lines.push(" }"); + lines.push("}"); + lines.push(""); + return lines.join("\n"); +} + +// --------------------------------------------------------------------------- +// Kotlin +// --------------------------------------------------------------------------- + +function renderKotlin(t) { + const lines = []; + lines.push("// AUTOGENERATED from brand/tokens.json — do not edit by hand."); + lines.push("// Regenerate: node scripts/brand/generate-tokens.mjs"); + lines.push(`// Version: ${t.meta.version}`); + lines.push(""); + lines.push("package com.sonicflow.app.brand"); + lines.push(""); + lines.push("import androidx.compose.ui.graphics.Color"); + lines.push("import com.sonicflow.app.ui.fromHex"); + lines.push(""); + lines.push("object BrandTokens {"); + lines.push(" object Chakra {"); + for (const [key, v] of Object.entries(t.palette.chakra)) { + lines.push(` val ${camel(key)} = Color.fromHex("${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" object Neutral {"); + for (const [key, v] of Object.entries(t.palette.neutral)) { + if (v.hex) { + lines.push(` val ${camel(key)} = Color.fromHex("${v.hex}")`); + } + } + lines.push(" val panel = Color(0f, 0f, 0f, 0.78f)"); + lines.push(" }"); + lines.push(""); + lines.push(" object Accent {"); + for (const [key, v] of Object.entries(t.palette.accent)) { + lines.push(` val ${camel(key)} = Color.fromHex("${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" object Mode {"); + for (const [mode, v] of Object.entries(t.mode)) { + lines.push(` val ${camel(mode)} = Color.fromHex("${v.hex}")`); + } + lines.push(" }"); + lines.push(""); + lines.push(" object Radius {"); + for (const [key, v] of Object.entries(t.radius)) { + lines.push(` const val ${camel(key)}: Int = ${v}`); + } + lines.push(" }"); + lines.push(""); + lines.push(" object Spacing {"); + for (const [key, v] of Object.entries(t.spacing)) { + lines.push(` const val ${camel(key)}: Int = ${v}`); + } + lines.push(" }"); + lines.push(""); + lines.push(" object Leopard {"); + lines.push(` val base = Color.fromHex("${t.background.leopard.base}")`); + lines.push(` val spot = Color.fromHex("${t.background.leopard.spot}")`); + lines.push(` val ring = Color.fromHex("${t.background.leopard.ring}")`); + lines.push(` const val opacity: Float = ${t.background.leopard.opacity}f`); + lines.push(` const val blurPx: Int = ${t.background.leopard.blurPx}`); + lines.push(` const val spotCount: Int = ${t.background.leopard.spotCount}`); + lines.push(` const val spotMinPx: Int = ${t.background.leopard.spotMinPx}`); + lines.push(` const val spotMaxPx: Int = ${t.background.leopard.spotMaxPx}`); + lines.push(" }"); + lines.push("}"); + lines.push(""); + return lines.join("\n"); +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +function kebab(s) { + return s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); +} + +function camel(s) { + // already camel in most keys; normalize just in case. + return s.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase()); +}