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/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/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()); +} diff --git a/sonicflow_app/ios-app/FlowTones.xcodeproj/project.pbxproj b/sonicflow_app/ios-app/FlowTones.xcodeproj/project.pbxproj index aba0d1f..74b4086 100644 --- a/sonicflow_app/ios-app/FlowTones.xcodeproj/project.pbxproj +++ b/sonicflow_app/ios-app/FlowTones.xcodeproj/project.pbxproj @@ -12,8 +12,10 @@ 36A82B9CFF8B30CD420584F9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC7669FF33C48DCF081937A /* ContentView.swift */; }; 3FFEA8A01131A9DB3E53641B /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3D934AE70BF018F4267AB9C /* AudioManager.swift */; }; 48B66FE39CA63EB3CC265130 /* FlowTonesCore in Frameworks */ = {isa = PBXBuildFile; productRef = B2F1F5A942642AC052287FC9 /* FlowTonesCore */; }; + 6DA50076CA5A138502CC4EA6 /* BrandTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B13AA5EF0B6E625B7A6812 /* BrandTokens.swift */; }; 6E79005DF663AB0765163A8B /* FilePickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBE3023DC3FD6AFBCD6789B7 /* FilePickerButton.swift */; }; 95A046C7C1FB0B1A33D4A8B1 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = A426767C4B6FBD933A517C98 /* Color+Hex.swift */; }; + A5229688C5C0BA7C78FD1612 /* LeopardBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A974ACFCA791728F213C0069 /* LeopardBackgroundView.swift */; }; A9368263684F5005EDF108F2 /* FlowModePresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC2333107C0768B07D49F2C /* FlowModePresentationTests.swift */; }; ACCB5F27E0A112D6B2750981 /* ModeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B31A9687B8413CF704A5C4 /* ModeCard.swift */; }; AEA7607A13B811027BBF21A9 /* FlowTonesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD162B330961300D3CC40AE4 /* FlowTonesApp.swift */; }; @@ -33,6 +35,7 @@ /* Begin PBXFileReference section */ 08CD780998C0435ED0A38B4C /* VisualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualizerView.swift; sourceTree = ""; }; + 17B13AA5EF0B6E625B7A6812 /* BrandTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandTokens.swift; sourceTree = ""; }; 1BC2333107C0768B07D49F2C /* FlowModePresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowModePresentationTests.swift; sourceTree = ""; }; 23386D9E63A3B395D2BF3C6C /* core-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "core-swift"; path = "../core-swift"; sourceTree = SOURCE_ROOT; }; 3488844973DB8E46F8C1562D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -40,6 +43,7 @@ 887C5C269DC105E72DD64832 /* AudioManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManagerTests.swift; sourceTree = ""; }; 9A8C72E5470FAF528C2F24ED /* FlowMode+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FlowMode+UI.swift"; sourceTree = ""; }; A426767C4B6FBD933A517C98 /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; + A974ACFCA791728F213C0069 /* LeopardBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeopardBackgroundView.swift; sourceTree = ""; }; ACC7669FF33C48DCF081937A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; CDDA22EA32E79D74FB29E8D5 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.swift; sourceTree = ""; }; E3D934AE70BF018F4267AB9C /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; @@ -101,12 +105,14 @@ isa = PBXGroup; children = ( E3D934AE70BF018F4267AB9C /* AudioManager.swift */, + 17B13AA5EF0B6E625B7A6812 /* BrandTokens.swift */, A426767C4B6FBD933A517C98 /* Color+Hex.swift */, ACC7669FF33C48DCF081937A /* ContentView.swift */, FBE3023DC3FD6AFBCD6789B7 /* FilePickerButton.swift */, 9A8C72E5470FAF528C2F24ED /* FlowMode+UI.swift */, FD162B330961300D3CC40AE4 /* FlowTonesApp.swift */, 3488844973DB8E46F8C1562D /* Info.plist */, + A974ACFCA791728F213C0069 /* LeopardBackgroundView.swift */, 78B31A9687B8413CF704A5C4 /* ModeCard.swift */, CDDA22EA32E79D74FB29E8D5 /* PlayerManager.swift */, 08CD780998C0435ED0A38B4C /* VisualizerView.swift */, @@ -207,11 +213,13 @@ buildActionMask = 2147483647; files = ( 3FFEA8A01131A9DB3E53641B /* AudioManager.swift in Sources */, + 6DA50076CA5A138502CC4EA6 /* BrandTokens.swift in Sources */, 95A046C7C1FB0B1A33D4A8B1 /* Color+Hex.swift in Sources */, 36A82B9CFF8B30CD420584F9 /* ContentView.swift in Sources */, 6E79005DF663AB0765163A8B /* FilePickerButton.swift in Sources */, E612FFB78E2FD21E605AF239 /* FlowMode+UI.swift in Sources */, AEA7607A13B811027BBF21A9 /* FlowTonesApp.swift in Sources */, + A5229688C5C0BA7C78FD1612 /* LeopardBackgroundView.swift in Sources */, ACCB5F27E0A112D6B2750981 /* ModeCard.swift in Sources */, 33BE95C651A8E82F293F362D /* PlayerManager.swift in Sources */, 2D074129D329F7B251A1D675 /* VisualizerView.swift in Sources */, @@ -242,7 +250,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.sonicflow.ios.tests; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SonicFlow.app/SonicFlow"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlowTones.app/FlowTones"; }; name = Debug; }; @@ -416,7 +424,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.sonicflow.ios.tests; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SonicFlow.app/SonicFlow"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FlowTones.app/FlowTones"; }; name = Release; }; diff --git a/sonicflow_app/ios-app/FlowTones/BrandTokens.swift b/sonicflow_app/ios-app/FlowTones/BrandTokens.swift new file mode 100644 index 0000000..ee579c8 --- /dev/null +++ b/sonicflow_app/ios-app/FlowTones/BrandTokens.swift @@ -0,0 +1,65 @@ +// Copied from brand/generated/BrandTokens.swift — regenerate via 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/sonicflow_app/ios-app/FlowTones/ContentView.swift b/sonicflow_app/ios-app/FlowTones/ContentView.swift index 0ff4f63..65432f3 100644 --- a/sonicflow_app/ios-app/FlowTones/ContentView.swift +++ b/sonicflow_app/ios-app/FlowTones/ContentView.swift @@ -4,18 +4,24 @@ import FlowTonesCore struct ContentView: View { @ObservedObject var audioManager: AudioManager @ObservedObject var playerManager: PlayerManager - private let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: 2) + private let columns = Array( + repeating: GridItem(.flexible(), spacing: BrandTokens.Spacing.md), + count: 2 + ) var body: some View { ZStack { - Color(hex: "#0f0f0f") + BrandTokens.Neutral.bg + .ignoresSafeArea() + + LeopardBackgroundView() .ignoresSafeArea() ScrollView { - VStack(alignment: .leading, spacing: 20) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.lg) { header - LazyVGrid(columns: columns, spacing: 12) { + LazyVGrid(columns: columns, spacing: BrandTokens.Spacing.md) { ForEach(FlowMode.allCases, id: \.self) { mode in ModeCard( mode: mode, @@ -26,10 +32,10 @@ struct ContentView: View { } } - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.sm) { Text("Neural layer") .font(.headline) - .foregroundStyle(.white) + .foregroundStyle(BrandTokens.Neutral.fg) Slider(value: $audioManager.beatVolume, in: 0...1) .tint(audioManager.currentMode.accentColor) } @@ -39,7 +45,7 @@ struct ContentView: View { mode: audioManager.currentMode ) - HStack(spacing: 12) { + HStack(spacing: BrandTokens.Spacing.md) { Button(audioManager.isPlaying ? "Pause" : "Play") { audioManager.togglePlayback() } @@ -52,12 +58,12 @@ struct ContentView: View { if let selectedFileURL = playerManager.selectedFileURL { Text(selectedFileURL.lastPathComponent) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) .lineLimit(1) .truncationMode(.middle) } } - .padding(20) + .padding(BrandTokens.Spacing.lg) } } } @@ -66,19 +72,19 @@ struct ContentView: View { HStack { Text("SonicFlow") .font(.largeTitle.weight(.bold)) - .foregroundStyle(.white) + .foregroundStyle(BrandTokens.Neutral.fg) Spacer() - HStack(spacing: 8) { + HStack(spacing: BrandTokens.Spacing.sm) { Circle() - .fill(audioManager.isPlaying ? .green : .gray) + .fill(audioManager.isPlaying ? BrandTokens.Accent.success : BrandTokens.Neutral.muted) .frame(width: 10, height: 10) Text(audioManager.isPlaying ? "Active" : "Off") .font(.footnote.weight(.medium)) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .background(Color.white.opacity(0.08), in: Capsule()) + .padding(.horizontal, BrandTokens.Spacing.md) + .padding(.vertical, BrandTokens.Spacing.sm) + .background(BrandTokens.Neutral.panel, in: Capsule()) } } } diff --git a/sonicflow_app/ios-app/FlowTones/FlowMode+UI.swift b/sonicflow_app/ios-app/FlowTones/FlowMode+UI.swift index e7d1740..194f975 100644 --- a/sonicflow_app/ios-app/FlowTones/FlowMode+UI.swift +++ b/sonicflow_app/ios-app/FlowTones/FlowMode+UI.swift @@ -28,6 +28,12 @@ extension FlowMode { } } + /// Canonical hex identifier for the mode's chakra color. + /// + /// Kept as a public string for the existing test contract (see + /// `FlowModePresentationTests`). All rendered color consumption routes + /// through ``accentColor`` which resolves from `BrandTokens.Mode.*` — + /// never parse this string for a real color at runtime. var accentColorHex: String { switch self { case .focus: @@ -42,6 +48,29 @@ extension FlowMode { } var accentColor: Color { - Color(hex: accentColorHex) + switch self { + case .focus: + return BrandTokens.Mode.focus + case .flow: + return BrandTokens.Mode.flow + case .meditation: + return BrandTokens.Mode.meditation + case .sleep: + return BrandTokens.Mode.sleep + } + } + + /// Chakra token backing this mode (see brand/BRAND.md mode↔chakra mapping). + var chakraColor: Color { + switch self { + case .focus: + return BrandTokens.Chakra.throat + case .flow: + return BrandTokens.Chakra.thirdEye + case .meditation: + return BrandTokens.Chakra.heart + case .sleep: + return BrandTokens.Chakra.crown + } } } diff --git a/sonicflow_app/ios-app/FlowTones/LeopardBackgroundView.swift b/sonicflow_app/ios-app/FlowTones/LeopardBackgroundView.swift new file mode 100644 index 0000000..d8a3191 --- /dev/null +++ b/sonicflow_app/ios-app/FlowTones/LeopardBackgroundView.swift @@ -0,0 +1,84 @@ +import SwiftUI + +/// Procedural leopard-print background layer. +/// +/// Generated from `BrandTokens.Leopard.*`. Deterministic spot placement — same +/// layout every render, no motion. Always intended to sit BEHIND a blurred panel +/// per brand rules (see `brand/BRAND.md`). +struct LeopardBackgroundView: View { + var body: some View { + Canvas { context, size in + let spots = LeopardBackgroundView.spots(in: size) + for spot in spots { + let rect = CGRect( + x: spot.center.x - spot.radius, + y: spot.center.y - spot.radius, + width: spot.radius * 2, + height: spot.radius * 2 + ) + context.fill( + Path(ellipseIn: rect), + with: .color(BrandTokens.Leopard.spot) + ) + } + } + .background(BrandTokens.Leopard.base) + .opacity(BrandTokens.Leopard.opacity) + .blur(radius: BrandTokens.Leopard.blurRadius) + } + + // MARK: - Deterministic spot layout + + private struct Spot { + let center: CGPoint + let radius: CGFloat + } + + /// Deterministic pseudo-random sequence seeded on spot index. Avoids + /// `Double.random` so the pattern is stable across launches. + private static func spots(in size: CGSize) -> [Spot] { + guard size.width > 0, size.height > 0 else { return [] } + let count = BrandTokens.Leopard.spotCount + let minR = BrandTokens.Leopard.spotMinPx + let maxR = BrandTokens.Leopard.spotMaxPx + var result: [Spot] = [] + result.reserveCapacity(count) + for index in 0.. Double { + var x = UInt64(bitPattern: Int64(index &* 2_654_435_761 &+ salt &* 40_503)) + x ^= x >> 30 + x = x &* 0xBF58_476D_1CE4_E5B9 + x ^= x >> 27 + x = x &* 0x94D0_49BB_1331_11EB + x ^= x >> 31 + // Keep 53 bits of mantissa precision. + let scaled = Double(x & 0x1F_FFFF_FFFF_FFFF) / Double(1 << 53) + return scaled + } +} + +#if DEBUG +struct LeopardBackgroundView_Previews: PreviewProvider { + static var previews: some View { + LeopardBackgroundView() + .frame(width: 320, height: 480) + .background(BrandTokens.Neutral.ink) + } +} +#endif diff --git a/sonicflow_app/ios-app/FlowTones/ModeCard.swift b/sonicflow_app/ios-app/FlowTones/ModeCard.swift index 4e07dbe..52194f3 100644 --- a/sonicflow_app/ios-app/FlowTones/ModeCard.swift +++ b/sonicflow_app/ios-app/FlowTones/ModeCard.swift @@ -8,29 +8,37 @@ struct ModeCard: View { var body: some View { Button(action: onTap) { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.xs + 2) { Text(mode.displayName) .font(.title3.weight(.semibold)) - .foregroundStyle(.white) + .foregroundStyle(BrandTokens.Neutral.fg) Text("\(Int(mode.beatHz)) Hz") .font(.caption.weight(.medium)) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) Text(mode.shortDescription) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) .lineLimit(1) } .frame(maxWidth: .infinity, minHeight: 94, alignment: .leading) - .padding(12) - .background(Color.white.opacity(0.06)) + .padding(BrandTokens.Spacing.md) + .background(BrandTokens.Neutral.panel) .overlay( - RoundedRectangle(cornerRadius: 14) + RoundedRectangle(cornerRadius: BrandTokens.Radius.md) .stroke( - isSelected ? mode.accentColor : Color.white.opacity(0.12), + isSelected ? mode.accentColor : BrandTokens.Neutral.border, lineWidth: isSelected ? 2 : 1 ) ) - .clipShape(RoundedRectangle(cornerRadius: 14)) + .clipShape(RoundedRectangle(cornerRadius: BrandTokens.Radius.md)) + .shadow( + color: isSelected ? mode.accentColor : .clear, + radius: 24 + ) + .shadow( + color: isSelected ? mode.accentColor.opacity(0.3) : .clear, + radius: 64 + ) } .buttonStyle(.plain) }