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/android-app/app/src/main/java/com/sonicflow/app/brand/BrandTokens.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/brand/BrandTokens.kt new file mode 100644 index 0000000..f8f341a --- /dev/null +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/brand/BrandTokens.kt @@ -0,0 +1,70 @@ +// Copied from brand/generated/BrandTokens.kt — regenerate via node scripts/brand/generate-tokens.mjs +// 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/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/ColorHex.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/ColorHex.kt index 8599f58..9ef20e8 100644 --- a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/ColorHex.kt +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/ColorHex.kt @@ -11,3 +11,10 @@ fun Color.Companion.fromHex(hex: String): Color { } return Color(colorLong) } + +fun Color.toHexString(): String { + val r = (red * 255f).toInt().coerceIn(0, 255) + val g = (green * 255f).toInt().coerceIn(0, 255) + val b = (blue * 255f).toInt().coerceIn(0, 255) + return "#%02X%02X%02X".format(r, g, b) +} diff --git a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/FlowModeUi.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/FlowModeUi.kt index 48fd462..dfa5968 100644 --- a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/FlowModeUi.kt +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/FlowModeUi.kt @@ -1,5 +1,7 @@ package com.sonicflow.app.ui +import androidx.compose.ui.graphics.Color +import com.sonicflow.app.brand.BrandTokens import com.sonicflow.beatengine.FlowMode val FlowMode.label: String @@ -10,10 +12,26 @@ val FlowMode.label: String FlowMode.SLEEP -> "Sleep" } -val FlowMode.accentColor: String +val FlowMode.modeColor: Color + get() = when (this) { + FlowMode.FOCUS -> BrandTokens.Mode.focus + FlowMode.FLOW -> BrandTokens.Mode.flow + FlowMode.MEDITATION -> BrandTokens.Mode.meditation + FlowMode.SLEEP -> BrandTokens.Mode.sleep + } + +val FlowMode.chakraColor: Color get() = when (this) { - FlowMode.FOCUS -> "#378ADD" - FlowMode.FLOW -> "#7F77DD" - FlowMode.MEDITATION -> "#1D9E75" - FlowMode.SLEEP -> "#534AB7" + FlowMode.FOCUS -> BrandTokens.Chakra.throat + FlowMode.FLOW -> BrandTokens.Chakra.thirdEye + FlowMode.MEDITATION -> BrandTokens.Chakra.heart + FlowMode.SLEEP -> BrandTokens.Chakra.crown } + +/** + * Legacy string form of the mode accent color. Derived from the brand tokens so the + * canonical hex values live only in [BrandTokens]. Kept for source compatibility with + * callers that expect a `#RRGGBB` string (e.g. test assertions, persistence). + */ +val FlowMode.accentColor: String + get() = this.modeColor.toHexString() diff --git a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/MainScreen.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/MainScreen.kt index b5aa543..a5b9b8b 100644 --- a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/MainScreen.kt +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/MainScreen.kt @@ -1,6 +1,7 @@ package com.sonicflow.app.ui import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,6 +21,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.sonicflow.app.brand.BrandTokens +import com.sonicflow.app.ui.components.LeopardBackground import com.sonicflow.app.ui.components.ModeCard import com.sonicflow.app.ui.components.VisualizerBars import com.sonicflow.beatengine.FlowMode @@ -32,64 +35,71 @@ fun MainScreen(viewModel: FlowTonesViewModel) { val beatVolume by viewModel.beatVolume.collectAsState() val selectedFile by viewModel.selectedFile.collectAsState() - val accent = androidx.compose.ui.graphics.Color.fromHex(currentMode.accentColor) + val accent = currentMode.modeColor - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - TopAppBar( - title = { Text("SonicFlow") }, - actions = { - AssistChip( - onClick = {}, - label = { Text(if (isActive) "Active" else "Off") } - ) - } - ) + Box(modifier = Modifier.fillMaxSize()) { + LeopardBackground() - LazyVerticalGrid( - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + Column( modifier = Modifier - .fillMaxWidth() - .heightIn(max = 220.dp) + .fillMaxSize() + .padding( + horizontal = BrandTokens.Spacing.md.dp, + vertical = BrandTokens.Spacing.sm.dp + ), + verticalArrangement = Arrangement.spacedBy(BrandTokens.Spacing.md.dp) ) { - items(FlowMode.entries) { mode -> - ModeCard( - mode = mode, - selected = currentMode == mode, - onClick = { viewModel.setMode(mode) } - ) + TopAppBar( + title = { Text("SonicFlow") }, + actions = { + AssistChip( + onClick = {}, + label = { Text(if (isActive) "Active" else "Off") } + ) + } + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(BrandTokens.Spacing.sm.dp), + verticalArrangement = Arrangement.spacedBy(BrandTokens.Spacing.sm.dp), + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 220.dp) + ) { + items(FlowMode.entries) { mode -> + ModeCard( + mode = mode, + selected = currentMode == mode, + onClick = { viewModel.setMode(mode) } + ) + } } - } - Text(text = "Neural layer", style = MaterialTheme.typography.labelLarge) - Slider( - value = beatVolume, - onValueChange = viewModel::onBeatVolumeChanged, - valueRange = 0f..1f - ) + Text(text = "Neural layer", style = MaterialTheme.typography.labelLarge) + Slider( + value = beatVolume, + onValueChange = viewModel::onBeatVolumeChanged, + valueRange = 0f..1f + ) - VisualizerBars(isActive = isActive, color = accent) + VisualizerBars(isActive = isActive, color = accent) - Button(onClick = viewModel::pickFile, modifier = Modifier.fillMaxWidth()) { - Text(if (selectedFile.isNullOrBlank()) "Pick file" else "Change file") - } + Button(onClick = viewModel::pickFile, modifier = Modifier.fillMaxWidth()) { + Text(if (selectedFile.isNullOrBlank()) "Pick file" else "Change file") + } - Button( - onClick = { if (isActive) viewModel.stopSession() else viewModel.startSession() }, - modifier = Modifier.fillMaxWidth() - ) { - Text(if (isActive) "Stop" else "Start") - } + Button( + onClick = { if (isActive) viewModel.stopSession() else viewModel.startSession() }, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (isActive) "Stop" else "Start") + } - Text( - text = selectedFile ?: "No file selected", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Text( + text = selectedFile ?: "No file selected", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } diff --git a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/LeopardBackground.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/LeopardBackground.kt new file mode 100644 index 0000000..8018b3e --- /dev/null +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/LeopardBackground.kt @@ -0,0 +1,65 @@ +package com.sonicflow.app.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import com.sonicflow.app.brand.BrandTokens +import kotlin.random.Random + +/** + * Deterministic seed for the procedural leopard pattern. Keeping it constant means + * the spot layout doesn't shift between recompositions or cold starts. + */ +private const val LEOPARD_SEED: Long = 0x1E07A2DA1L + +/** + * Procedural leopard-print background layer. Draws filled and ringed circles at + * deterministic positions on top of [BrandTokens.Leopard.base], blurred for the + * Leopard Look. Sits behind a blurred panel — never directly under body text. + */ +@Composable +fun LeopardBackground( + modifier: Modifier = Modifier, + spotCount: Int = BrandTokens.Leopard.spotCount, + seed: Long = LEOPARD_SEED +) { + Canvas( + modifier = modifier + .fillMaxSize() + .background(BrandTokens.Leopard.base) + .blur(BrandTokens.Leopard.blurPx.dp) + ) { + val random = Random(seed) + val minRadius = BrandTokens.Leopard.spotMinPx.toFloat() / 2f + val maxRadius = BrandTokens.Leopard.spotMaxPx.toFloat() / 2f + val w = size.width + val h = size.height + val spotColor = BrandTokens.Leopard.spot.copy(alpha = BrandTokens.Leopard.opacity) + val ringColor = BrandTokens.Leopard.ring.copy(alpha = BrandTokens.Leopard.opacity) + + repeat(spotCount) { + val radius = minRadius + random.nextFloat() * (maxRadius - minRadius) + val cx = random.nextFloat() * w + val cy = random.nextFloat() * h + val center = Offset(cx, cy) + + drawCircle( + color = ringColor, + radius = radius * 1.35f, + center = center, + style = Stroke(width = radius * 0.22f) + ) + drawCircle( + color = spotColor, + radius = radius, + center = center + ) + } + } +} diff --git a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/ModeCard.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/ModeCard.kt index 7477b80..7376ed3 100644 --- a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/ModeCard.kt +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/components/ModeCard.kt @@ -6,6 +6,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.GraphicEq import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -13,14 +16,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.sonicflow.app.ui.accentColor -import com.sonicflow.app.ui.fromHex +import com.sonicflow.app.brand.BrandTokens import com.sonicflow.app.ui.label +import com.sonicflow.app.ui.modeColor import com.sonicflow.beatengine.FlowMode -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.GraphicEq @Composable fun ModeCard( @@ -29,23 +31,49 @@ fun ModeCard( onClick: () -> Unit, modifier: Modifier = Modifier ) { - val accent = androidx.compose.ui.graphics.Color.fromHex(mode.accentColor) + val accent = mode.modeColor + val cardShape = RoundedCornerShape(BrandTokens.Radius.md.dp) + + // Active cards get a layered glow: a tight inner shadow plus a softer outer + // bloom at 30% alpha, matching the CSS `modeGlow` token from brand/tokens.json. + val glowModifier = if (selected) { + Modifier + .shadow( + elevation = 64.dp, + shape = cardShape, + ambientColor = accent.copy(alpha = 0.30f), + spotColor = accent.copy(alpha = 0.30f) + ) + .shadow( + elevation = 24.dp, + shape = cardShape, + ambientColor = accent, + spotColor = accent + ) + } else { + Modifier + } Card( modifier = modifier .fillMaxWidth() + .then(glowModifier) .clickable(onClick = onClick), + shape = cardShape, colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + containerColor = BrandTokens.Neutral.panel ), - border = BorderStroke(if (selected) 2.dp else 1.dp, if (selected) accent else MaterialTheme.colorScheme.outline) + border = BorderStroke( + width = if (selected) 2.dp else 1.dp, + color = if (selected) accent else BrandTokens.Neutral.border + ) ) { Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) + modifier = Modifier.padding(BrandTokens.Spacing.md.dp), + verticalArrangement = Arrangement.spacedBy(BrandTokens.Spacing.xs.dp + 2.dp) ) { Icon(Icons.Default.GraphicEq, contentDescription = null, tint = accent) - Text(text = mode.label, fontSize = 16.sp) + Text(text = mode.label, fontSize = 16.sp, color = BrandTokens.Neutral.fg) Text( text = "${mode.beatHz} Hz", color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/theme/Theme.kt b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/theme/Theme.kt index 8d0818c..85e0cff 100644 --- a/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/theme/Theme.kt +++ b/sonicflow_app/android-app/app/src/main/java/com/sonicflow/app/ui/theme/Theme.kt @@ -3,13 +3,24 @@ package com.sonicflow.app.ui.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.sonicflow.app.ui.fromHex +import com.sonicflow.app.brand.BrandTokens private val FlowTonesDarkScheme = darkColorScheme( - primary = Color.fromHex("#378ADD"), - secondary = Color.fromHex("#7F77DD"), - tertiary = Color.fromHex("#1D9E75") + primary = BrandTokens.Chakra.throat, + onPrimary = BrandTokens.Neutral.fg, + secondary = BrandTokens.Chakra.thirdEye, + onSecondary = BrandTokens.Neutral.fg, + tertiary = BrandTokens.Chakra.heart, + onTertiary = BrandTokens.Neutral.fg, + background = BrandTokens.Neutral.bg, + onBackground = BrandTokens.Neutral.fg, + surface = BrandTokens.Neutral.ink, + onSurface = BrandTokens.Neutral.fg, + surfaceVariant = BrandTokens.Neutral.border, + onSurfaceVariant = BrandTokens.Neutral.muted, + outline = BrandTokens.Neutral.border, + error = BrandTokens.Accent.danger, + onError = BrandTokens.Neutral.fg ) @Composable