From 58ec1affed7df23366794bd948595058a6eafeb2 Mon Sep 17 00:00:00 2001 From: alexanderbrunker-star Date: Sun, 19 Apr 2026 09:08:50 +0200 Subject: [PATCH 1/3] SF-21: update STATUS after merge --- STATUS.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/STATUS.md b/STATUS.md index e76b009..c869327 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,8 +4,8 @@ Date: 2026-04-19 ## Ticket -- Active ticket: `SF-21` -- Branch: `feature/SF-21-linear-github-process` +- Active ticket: (none) +- Last ticket: `SF-21` (merged via PR #21 into `main`, commit `1c979ce`) - Goal: end-to-end Linear-first engineering workflow with PR guards and automation. ## Done @@ -29,7 +29,7 @@ Date: 2026-04-19 - 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. +- Validate first real ticket through the full state machine. ## Verification @@ -41,6 +41,5 @@ Date: 2026-04-19 ## 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`. +- Confirm GitHub Actions is able to talk to Linear (`LINEAR_API_TOKEN`). +- Optionally add Linear status `Preview`, then run a full ticket cycle: `Todo` → `In Progress` → `In Review`/`Preview` → `Done`. From 225172db1ec89e372b820982b2b669b6475d0fef Mon Sep 17 00:00:00 2001 From: alexanderbrunker-star Date: Sun, 19 Apr 2026 15:10:42 +0200 Subject: [PATCH 2/3] SF-23: establish brand foundation --- STATUS.md | 63 +++--- brand/BRAND.md | 121 ++++++++++++ brand/generated/BrandTokens.kt | 69 +++++++ brand/generated/BrandTokens.swift | 66 +++++++ brand/generated/README.md | 19 ++ brand/generated/tokens.css | 65 +++++++ brand/tokens.json | 119 ++++++++++++ docs/guides/codex-automation-prompts.md | 21 +- scripts/brand/generate-tokens.mjs | 244 ++++++++++++++++++++++++ 9 files changed, 748 insertions(+), 39 deletions(-) create mode 100644 brand/BRAND.md create mode 100644 brand/generated/BrandTokens.kt create mode 100644 brand/generated/BrandTokens.swift create mode 100644 brand/generated/README.md create mode 100644 brand/generated/tokens.css create mode 100644 brand/tokens.json create mode 100644 scripts/brand/generate-tokens.mjs diff --git a/STATUS.md b/STATUS.md index c869327..ad1fc5f 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,42 +4,39 @@ Date: 2026-04-19 ## Ticket -- Active ticket: (none) -- Last ticket: `SF-21` (merged via PR #21 into `main`, commit `1c979ce`) -- 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`). -- 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 -- Confirm GitHub Actions is able to talk to Linear (`LINEAR_API_TOKEN`). -- Optionally add Linear status `Preview`, then run a full ticket cycle: `Todo` → `In Progress` → `In Review`/`Preview` → `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()); +} From 5c83894277e62ec28e73504693e28cdf0e6d26fc Mon Sep 17 00:00:00 2001 From: alexanderbrunker-star Date: Sun, 19 Apr 2026 19:07:54 +0200 Subject: [PATCH 3/3] SF-25: adopt Leopard brand in Safari extension (shared web + macOS App) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared web extension: - Add brand.css (copy of brand/generated/tokens.css) - Link brand.css before Style.css in Main.html; wrap content in .leopard-bg + .panel - Rewrite Style.css to use CSS variables only - Procedural leopard radial-gradient; mode glow via box-shadow macOS (App) target (covers SF-27 menu-bar work — shared Xcode project): - Add BrandTokens.swift (copy of brand/generated/BrandTokens.swift) - Add LeopardBackgroundView (deterministic Canvas, SplitMix64 seed) - Rewire FlowMode+UI / ModeCard / FlowTonesPopoverView to BrandTokens.* - Layered mode-glow shadows (24/64) - Register new files in FlowTones.xcodeproj/project.pbxproj --- .../FlowTones.xcodeproj/project.pbxproj | 8 ++ .../Resources/Base.lproj/Main.html | 16 ++- .../Shared (App)/Resources/Style.css | 121 ++++++++++++++++-- .../Shared (App)/Resources/brand.css | 62 +++++++++ .../FlowTones/macOS (App)/BrandTokens.swift | 65 ++++++++++ .../FlowTones/macOS (App)/FlowMode+UI.swift | 14 +- .../macOS (App)/FlowTonesPopoverView.swift | 55 +++++--- .../macOS (App)/LeopardBackgroundView.swift | 84 ++++++++++++ .../FlowTones/macOS (App)/ModeCard.swift | 29 +++-- 9 files changed, 404 insertions(+), 50 deletions(-) create mode 100644 sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/brand.css create mode 100644 sonicflow_app/safari-extension/FlowTones/macOS (App)/BrandTokens.swift create mode 100644 sonicflow_app/safari-extension/FlowTones/macOS (App)/LeopardBackgroundView.swift diff --git a/sonicflow_app/safari-extension/FlowTones/FlowTones.xcodeproj/project.pbxproj b/sonicflow_app/safari-extension/FlowTones/FlowTones.xcodeproj/project.pbxproj index e3fc012..4fac7dc 100644 --- a/sonicflow_app/safari-extension/FlowTones/FlowTones.xcodeproj/project.pbxproj +++ b/sonicflow_app/safari-extension/FlowTones/FlowTones.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ 3294EA5A2F8E5D9400F4BA51 /* FlowMode+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3294EA6A2F8E5D9400F4BA51 /* FlowMode+UI.swift */; }; 3294EA5B2F8E5D9400F4BA51 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3294EA6B2F8E5D9400F4BA51 /* Color+Hex.swift */; }; 3294EA5C2F8E5D9400F4BA51 /* BeatEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3294EA6C2F8E5D9400F4BA51 /* BeatEngine.swift */; }; + 3294EA702F8E5D9400F4BA51 /* BrandTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3294EA722F8E5D9400F4BA51 /* BrandTokens.swift */; }; + 3294EA712F8E5D9400F4BA51 /* LeopardBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3294EA732F8E5D9400F4BA51 /* LeopardBackgroundView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -156,6 +158,8 @@ 3294EA6A2F8E5D9400F4BA51 /* FlowMode+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FlowMode+UI.swift"; sourceTree = ""; }; 3294EA6B2F8E5D9400F4BA51 /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; 3294EA6C2F8E5D9400F4BA51 /* BeatEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeatEngine.swift; sourceTree = ""; }; + 3294EA722F8E5D9400F4BA51 /* BrandTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandTokens.swift; sourceTree = ""; }; + 3294EA732F8E5D9400F4BA51 /* LeopardBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeopardBackgroundView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -268,6 +272,8 @@ 3294EA6A2F8E5D9400F4BA51 /* FlowMode+UI.swift */, 3294EA6B2F8E5D9400F4BA51 /* Color+Hex.swift */, 3294EA6C2F8E5D9400F4BA51 /* BeatEngine.swift */, + 3294EA722F8E5D9400F4BA51 /* BrandTokens.swift */, + 3294EA732F8E5D9400F4BA51 /* LeopardBackgroundView.swift */, 3294E9F12F8E5D9400F4BA51 /* Main.storyboard */, 3294E9F42F8E5D9400F4BA51 /* Info.plist */, ); @@ -541,6 +547,8 @@ 3294EA5A2F8E5D9400F4BA51 /* FlowMode+UI.swift in Sources */, 3294EA5B2F8E5D9400F4BA51 /* Color+Hex.swift in Sources */, 3294EA5C2F8E5D9400F4BA51 /* BeatEngine.swift in Sources */, + 3294EA702F8E5D9400F4BA51 /* BrandTokens.swift in Sources */, + 3294EA712F8E5D9400F4BA51 /* LeopardBackgroundView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Base.lproj/Main.html b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Base.lproj/Main.html index 92d2953..552f103 100644 --- a/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Base.lproj/Main.html +++ b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Base.lproj/Main.html @@ -6,15 +6,19 @@ + - SonicFlow Icon -

You can turn on SonicFlow's Safari extension in Settings.

-

You can turn on SonicFlow's extension in Safari Extensions preferences.

-

SonicFlow's extension is currently on. You can turn it off in Safari Extensions preferences.

-

SonicFlow's extension is currently off. You can turn it on in Safari Extensions preferences.

- + +
+ SonicFlow Icon +

You can turn on SonicFlow's Safari extension in Settings.

+

You can turn on SonicFlow's extension in Safari Extensions preferences.

+

SonicFlow's extension is currently on. You can turn it off in Safari Extensions preferences.

+

SonicFlow's extension is currently off. You can turn it on in Safari Extensions preferences.

+ +
diff --git a/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Style.css b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Style.css index b6d0608..f6a89d2 100644 --- a/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Style.css +++ b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/Style.css @@ -4,30 +4,98 @@ cursor: default; } -:root { - color-scheme: light dark; - - --spacing: 20px; -} - html { height: 100%; } body { + position: relative; display: flex; align-items: center; justify-content: center; flex-direction: column; - gap: var(--spacing); - margin: 0 calc(var(--spacing) * 2); - height: 100%; + gap: var(--space-lg); + margin: 0; + padding: calc(var(--space-xl) + var(--space-md)) var(--space-xl); + min-height: 100%; - font: -apple-system-short-body; + background-color: var(--neutral-bg); + color: var(--neutral-fg); + + font-family: var(--font-body); + font-size: 14px; text-align: center; + overflow: hidden; +} + +/* Procedural leopard background — behind everything. + Spot colour: --accent-gold at --leopard-opacity. + Ring colour: --accent-spot-ring. Rules: no motion, low opacity. */ +.leopard-bg { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background-color: var(--neutral-ink); + background-image: + radial-gradient(circle at 12% 18%, var(--accent-gold) 0 6px, var(--accent-spot-ring) 7px 10px, transparent 11px), + radial-gradient(circle at 32% 72%, var(--accent-gold) 0 9px, var(--accent-spot-ring) 10px 13px, transparent 14px), + radial-gradient(circle at 58% 22%, var(--accent-gold) 0 7px, var(--accent-spot-ring) 8px 11px, transparent 12px), + radial-gradient(circle at 82% 44%, var(--accent-gold) 0 10px, var(--accent-spot-ring) 11px 14px, transparent 15px), + radial-gradient(circle at 24% 52%, var(--accent-gold) 0 8px, var(--accent-spot-ring) 9px 12px, transparent 13px), + radial-gradient(circle at 68% 82%, var(--accent-gold) 0 7px, var(--accent-spot-ring) 8px 11px, transparent 12px), + radial-gradient(circle at 90% 12%, var(--accent-gold) 0 6px, var(--accent-spot-ring) 7px 10px, transparent 11px), + radial-gradient(circle at 6% 88%, var(--accent-gold) 0 9px, var(--accent-spot-ring) 10px 13px, transparent 14px), + radial-gradient(circle at 46% 38%, var(--accent-gold) 0 5px, var(--accent-spot-ring) 6px 9px, transparent 10px), + radial-gradient(circle at 74% 64%, var(--accent-gold) 0 8px, var(--accent-spot-ring) 9px 12px, transparent 13px); + background-size: 360px 360px; + background-repeat: repeat; + opacity: var(--leopard-opacity); + filter: blur(var(--leopard-blur)); +} + +/* Blurred panel — sits above the leopard layer. */ +.panel { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-md); + + max-width: 420px; + padding: var(--space-xl); + + background-color: var(--neutral-panel); + border: 1px solid var(--neutral-border); + border-radius: var(--radius-lg); + box-shadow: var(--elev-soft); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); +} + +.brand-mark { + border-radius: var(--radius-md); + box-shadow: var(--elev-gold); +} + +p { + margin: 0; + color: var(--neutral-muted); + line-height: 1.45; +} + +p.state-on { + color: var(--accent-success); +} + +p.state-off { + color: var(--neutral-muted); } +/* Platform / state gating — preserved behaviour from original Style.css. */ body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) { display: none; } @@ -56,6 +124,39 @@ body.state-off :is(.state-on, .state-unknown) { display: none; } +/* Buttons — default styled like a mode button, tinted by --mode-color. */ button { + --mode-color: var(--mode-focus); + + font: inherit; font-size: 1em; + color: var(--neutral-fg); + + padding: var(--space-sm) var(--space-lg); + border: 1px solid var(--neutral-border); + border-radius: var(--radius-md); + background-color: var(--neutral-panel); + + cursor: default; + transition: box-shadow 160ms ease, border-color 160ms ease, transform 120ms ease; +} + +button:hover { + border-color: var(--mode-color); +} + +button:active, +button.is-active { + border-color: var(--mode-color); + /* Active-mode glow per BRAND.md: + box-shadow: 0 0 24px , 0 0 64px rgba(, 0.3) */ + box-shadow: + 0 0 24px var(--mode-color), + 0 0 64px color-mix(in oklab, var(--mode-color), transparent 70%); } + +/* Mode helper classes — map each mode to its chakra token. */ +.mode-focus { --mode-color: var(--mode-focus); } +.mode-flow { --mode-color: var(--mode-flow); } +.mode-meditation { --mode-color: var(--mode-meditation); } +.mode-sleep { --mode-color: var(--mode-sleep); } diff --git a/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/brand.css b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/brand.css new file mode 100644 index 0000000..49dbe21 --- /dev/null +++ b/sonicflow_app/safari-extension/FlowTones/Shared (App)/Resources/brand.css @@ -0,0 +1,62 @@ +/* Copied from brand/generated/tokens.css — regenerate via node scripts/brand/generate-tokens.mjs */ +: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/sonicflow_app/safari-extension/FlowTones/macOS (App)/BrandTokens.swift b/sonicflow_app/safari-extension/FlowTones/macOS (App)/BrandTokens.swift new file mode 100644 index 0000000..ee579c8 --- /dev/null +++ b/sonicflow_app/safari-extension/FlowTones/macOS (App)/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/safari-extension/FlowTones/macOS (App)/FlowMode+UI.swift b/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowMode+UI.swift index 6c28676..7d9c136 100644 --- a/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowMode+UI.swift +++ b/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowMode+UI.swift @@ -27,20 +27,16 @@ extension FlowMode { } } - var accentColorHex: String { + var accentColor: Color { switch self { case .focus: - return "#378ADD" + return BrandTokens.Mode.focus case .flow: - return "#7F77DD" + return BrandTokens.Mode.flow case .meditation: - return "#1D9E75" + return BrandTokens.Mode.meditation case .sleep: - return "#534AB7" + return BrandTokens.Mode.sleep } } - - var accentColor: Color { - Color(hex: accentColorHex) - } } diff --git a/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowTonesPopoverView.swift b/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowTonesPopoverView.swift index cad7732..59d761e 100644 --- a/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowTonesPopoverView.swift +++ b/sonicflow_app/safari-extension/FlowTones/macOS (App)/FlowTonesPopoverView.swift @@ -4,19 +4,25 @@ struct FlowTonesPopoverView: View { @ObservedObject var audioManager: AudioManager @ObservedObject var playerManager: PlayerManager - private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 2) + private let columns = Array( + repeating: GridItem(.flexible(), spacing: BrandTokens.Spacing.sm), + count: 2 + ) var body: some View { ZStack { - Color(hex: "#121212") + BrandTokens.Neutral.bg .ignoresSafeArea() - VStack(alignment: .leading, spacing: 12) { + LeopardBackgroundView() + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: BrandTokens.Spacing.md) { Text("SonicFlow") .font(.title3.weight(.bold)) - .foregroundStyle(.white) + .foregroundStyle(BrandTokens.Neutral.fg) - LazyVGrid(columns: columns, spacing: 8) { + LazyVGrid(columns: columns, spacing: BrandTokens.Spacing.sm) { ForEach(FlowMode.allCases, id: \.self) { mode in ModeCard(mode: mode, isSelected: mode == audioManager.currentMode) { audioManager.currentMode = mode @@ -32,20 +38,24 @@ struct FlowTonesPopoverView: View { sourceSection - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.xs + 2) { Text("Beat volume") .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) Slider(value: $audioManager.beatVolume, in: 0...1) .tint(audioManager.currentMode.accentColor) } - HStack(spacing: 8) { + HStack(spacing: BrandTokens.Spacing.sm) { Image(systemName: "waveform") - .foregroundStyle(audioManager.isPlaying ? .green : .secondary) + .foregroundStyle( + audioManager.isPlaying + ? BrandTokens.Accent.success + : BrandTokens.Neutral.muted + ) Text(audioManager.statusText) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) } Button(audioManager.isPlaying ? "Pause" : "Start") { @@ -54,7 +64,16 @@ struct FlowTonesPopoverView: View { .buttonStyle(.borderedProminent) .tint(audioManager.currentMode.accentColor) } - .padding(12) + .padding(BrandTokens.Spacing.md) + .background( + RoundedRectangle(cornerRadius: BrandTokens.Radius.lg) + .fill(BrandTokens.Neutral.panel) + .overlay( + RoundedRectangle(cornerRadius: BrandTokens.Radius.lg) + .stroke(BrandTokens.Neutral.border, lineWidth: 1) + ) + ) + .padding(BrandTokens.Spacing.sm) } .frame(width: 300, height: 400) } @@ -63,13 +82,17 @@ struct FlowTonesPopoverView: View { private var sourceSection: some View { switch audioManager.selectedSource { case .system: - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.sm) { Text(audioManager.canCaptureSystemAudio ? "System capture verfügbar" : "System capture erst ab macOS 14.2") .font(.caption2) - .foregroundStyle(audioManager.canCaptureSystemAudio ? Color.secondary : Color.orange) + .foregroundStyle( + audioManager.canCaptureSystemAudio + ? BrandTokens.Neutral.muted + : BrandTokens.Chakra.sacral + ) Text("Permission: \(audioManager.systemAudioPermissionStatus)") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) Button("Start Capture") { audioManager.startSystemAudioCapture() } @@ -77,7 +100,7 @@ struct FlowTonesPopoverView: View { .disabled(!audioManager.canCaptureSystemAudio) } case .file: - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.sm) { Button(playerManager.selectedFileURL == nil ? "Pick File" : "Replace File") { playerManager.pickAudioFile() } @@ -86,7 +109,7 @@ struct FlowTonesPopoverView: View { if let selectedFileURL = playerManager.selectedFileURL { Text(selectedFileURL.lastPathComponent) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) .lineLimit(1) .truncationMode(.middle) } diff --git a/sonicflow_app/safari-extension/FlowTones/macOS (App)/LeopardBackgroundView.swift b/sonicflow_app/safari-extension/FlowTones/macOS (App)/LeopardBackgroundView.swift new file mode 100644 index 0000000..d8a3191 --- /dev/null +++ b/sonicflow_app/safari-extension/FlowTones/macOS (App)/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/safari-extension/FlowTones/macOS (App)/ModeCard.swift b/sonicflow_app/safari-extension/FlowTones/macOS (App)/ModeCard.swift index c3aeb2a..d95f463 100644 --- a/sonicflow_app/safari-extension/FlowTones/macOS (App)/ModeCard.swift +++ b/sonicflow_app/safari-extension/FlowTones/macOS (App)/ModeCard.swift @@ -7,26 +7,37 @@ struct ModeCard: View { var body: some View { Button(action: onTap) { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: BrandTokens.Spacing.xs) { Text(mode.displayName) .font(.headline) - .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(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(BrandTokens.Neutral.muted) .lineLimit(1) } .frame(maxWidth: .infinity, minHeight: 72, alignment: .leading) - .padding(10) - .background(Color.white.opacity(0.06)) + .padding(BrandTokens.Spacing.md) + .background(BrandTokens.Neutral.panel) .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(isSelected ? mode.accentColor : Color.white.opacity(0.12), lineWidth: isSelected ? 2 : 1) + RoundedRectangle(cornerRadius: BrandTokens.Radius.md) + .stroke( + isSelected ? mode.accentColor : BrandTokens.Neutral.border, + lineWidth: isSelected ? 2 : 1 + ) + ) + .clipShape(RoundedRectangle(cornerRadius: BrandTokens.Radius.md)) + .shadow( + color: isSelected ? mode.accentColor : Color.clear, + radius: isSelected ? 24 : 0 + ) + .shadow( + color: isSelected ? mode.accentColor.opacity(0.3) : Color.clear, + radius: isSelected ? 64 : 0 ) - .clipShape(RoundedRectangle(cornerRadius: 12)) } .buttonStyle(.plain) }