diff --git a/README.md b/README.md index 49cf0a8..af2d628 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# Symphony State +

+ Symphony State — Every state source, in concert. +

-[![npm](https://img.shields.io/npm/v/@shiftbloom-studio/symphony-state)](https://www.npmjs.com/package/@shiftbloom-studio/symphony-state) -[![CI](https://img.shields.io/github/actions/workflow/status/shiftbloom-studio/symphony-state/ci.yml)](https://github.com/shiftbloom-studio/symphony-state/actions/workflows/ci.yml) -[![license](https://img.shields.io/npm/l/@shiftbloom-studio/symphony-state)](LICENSE) +

Symphony State

-**Orchestrate multiple state sources without a monolithic global store.** +

+ npm + CI + Apache-2.0 license +

-Symphony State is a lightweight orchestration layer that keeps server caches, UI state, and browser persistence in tempo. It does not replace your existing stores. Instead, it composes them into predictable, dependency-driven flows with atomic transactions, derived sections, and observable reconciliation. +

Every state source, in concert.

+ +Symphony State is a lightweight orchestration layer for server caches, UI state, URL params, and browser persistence. It keeps your existing stores independent, then composes their updates into predictable flows with atomic transactions, derived sections, and observable reconciliation. --- diff --git a/demo/public/fonts/clash-display-600.woff2 b/demo/public/fonts/clash-display-600.woff2 new file mode 100644 index 0000000..217e88e Binary files /dev/null and b/demo/public/fonts/clash-display-600.woff2 differ diff --git a/demo/public/fonts/general-sans-400.woff2 b/demo/public/fonts/general-sans-400.woff2 new file mode 100644 index 0000000..2bc4672 Binary files /dev/null and b/demo/public/fonts/general-sans-400.woff2 differ diff --git a/demo/public/fonts/general-sans-500.woff2 b/demo/public/fonts/general-sans-500.woff2 new file mode 100644 index 0000000..c90195f Binary files /dev/null and b/demo/public/fonts/general-sans-500.woff2 differ diff --git a/demo/public/fonts/general-sans-600.woff2 b/demo/public/fonts/general-sans-600.woff2 new file mode 100644 index 0000000..b17a037 Binary files /dev/null and b/demo/public/fonts/general-sans-600.woff2 differ diff --git a/demo/public/fonts/jetbrains-mono-400.woff2 b/demo/public/fonts/jetbrains-mono-400.woff2 new file mode 100644 index 0000000..5858873 Binary files /dev/null and b/demo/public/fonts/jetbrains-mono-400.woff2 differ diff --git a/demo/public/fonts/jetbrains-mono-500.woff2 b/demo/public/fonts/jetbrains-mono-500.woff2 new file mode 100644 index 0000000..be878e6 Binary files /dev/null and b/demo/public/fonts/jetbrains-mono-500.woff2 differ diff --git a/demo/public/fonts/jetbrains-mono-600.woff2 b/demo/public/fonts/jetbrains-mono-600.woff2 new file mode 100644 index 0000000..59c24e7 Binary files /dev/null and b/demo/public/fonts/jetbrains-mono-600.woff2 differ diff --git a/demo/src/app/globals.css b/demo/src/app/globals.css index 6c2cb4a..7995443 100644 --- a/demo/src/app/globals.css +++ b/demo/src/app/globals.css @@ -1,67 +1,234 @@ +/* Self-hosted fonts. Clash Display & General Sans: Fontshare (free for commercial + use, fontshare.com). JetBrains Mono: SIL OFL 1.1, via Fontsource 5.3.0. + WOFF2 files live in /public/fonts. */ +@font-face { + font-family: "Clash Display"; + src: url("/fonts/clash-display-600.woff2") format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "General Sans"; + src: url("/fonts/general-sans-400.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "General Sans"; + src: url("/fonts/general-sans-500.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "General Sans"; + src: url("/fonts/general-sans-600.woff2") format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/jetbrains-mono-400.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/jetbrains-mono-500.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "JetBrains Mono"; + src: url("/fonts/jetbrains-mono-600.woff2") format("woff2"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + @import "tailwindcss"; @theme { - /* Light, clean surfaces */ - --color-surface-0: #fafbfc; + /* shiftbloom studio — Brand Book v2.0 */ + --color-bloom-red: #ff2e52; + --color-root-red: #c81040; + --color-petal-pink: #ff8fab; + --color-blush: #ffe9ef; + --color-ink: #1a1216; + --color-paper: #ffffff; + --color-paper-warm: #faf6f4; + + /* Existing semantic aliases, now constrained to the brand palette. */ + --color-surface-0: #ffffff; --color-surface-1: #ffffff; - --color-surface-2: #f4f5f7; - --color-surface-3: #ebedf0; - --color-border: #e0e3e8; - --color-border-subtle: #ebedf0; - --color-text-primary: #1a1d23; - --color-text-secondary: #5a6070; - --color-text-muted: #8b90a0; + --color-surface-2: #faf6f4; + --color-surface-3: #ffe9ef; + --color-border: color-mix(in srgb, #1a1216 16%, #ffffff); + --color-border-subtle: color-mix(in srgb, #1a1216 9%, #ffffff); + --color-text-primary: #1a1216; + --color-text-secondary: color-mix(in srgb, #1a1216 72%, #ffffff); + --color-text-muted: color-mix(in srgb, #1a1216 65%, #ffffff); - /* Softer, muted source colors */ - --color-source-server: #4a7ccc; - --color-source-ui: #7c6cb5; - --color-source-url: #c4880d; - --color-source-persisted: #2f9e7a; - --color-source-optimistic: #cd5a8e; - --color-source-derived: #2d9bb5; + --color-source-server: #c81040; + --color-source-ui: #1a1216; + --color-source-url: #c81040; + --color-source-persisted: #1a1216; + --color-source-optimistic: #c81040; + --color-source-derived: #1a1216; - --color-accent: #5b63d3; - --color-accent-hover: #6e76e0; + --color-accent: #ff2e52; + --color-accent-hover: #c81040; + + --font-display: "Clash Display", "Arial Black", ui-sans-serif, system-ui, sans-serif; + --font-sans: "General Sans", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, ui-monospace, monospace; +} - --font-mono: "SF Mono", "Fira Code", "JetBrains Mono", ui-monospace, monospace; - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; +:root { + --bloom-ease: cubic-bezier(0.22, 1.1, 0.36, 1); + --settle-ease: cubic-bezier(0.4, 0, 0.2, 1); } * { box-sizing: border-box; } +html { + scroll-behavior: smooth; +} + body { - background: var(--color-surface-0); - color: var(--color-text-primary); + margin: 0; + background: var(--color-paper); + color: var(--color-ink); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +button, +input, +select, +textarea { + font: inherit; } ::selection { - background: color-mix(in srgb, var(--color-accent) 25%, transparent); - color: var(--color-text-primary); + background: var(--color-root-red); + color: var(--color-paper); +} + +:focus-visible { + outline: 2px solid var(--color-bloom-red); + outline-offset: 3px; } -/* Source badge colors — soft tinted backgrounds */ -.badge-server { background: color-mix(in srgb, var(--color-source-server) 10%, white); color: var(--color-source-server); border-color: color-mix(in srgb, var(--color-source-server) 20%, transparent); } -.badge-ui { background: color-mix(in srgb, var(--color-source-ui) 10%, white); color: var(--color-source-ui); border-color: color-mix(in srgb, var(--color-source-ui) 20%, transparent); } -.badge-url { background: color-mix(in srgb, var(--color-source-url) 10%, white); color: var(--color-source-url); border-color: color-mix(in srgb, var(--color-source-url) 20%, transparent); } -.badge-persisted { background: color-mix(in srgb, var(--color-source-persisted) 10%, white); color: var(--color-source-persisted); border-color: color-mix(in srgb, var(--color-source-persisted) 20%, transparent); } -.badge-optimistic { background: color-mix(in srgb, var(--color-source-optimistic) 10%, white); color: var(--color-source-optimistic); border-color: color-mix(in srgb, var(--color-source-optimistic) 20%, transparent); } -.badge-derived { background: color-mix(in srgb, var(--color-source-derived) 10%, white); color: var(--color-source-derived); border-color: color-mix(in srgb, var(--color-source-derived) 20%, transparent); } +.brand-display { + font-family: var(--font-display); + letter-spacing: -0.035em; +} + +.brand-label { + font-family: var(--font-mono); + font-size: 0.675rem; + font-weight: 500; + letter-spacing: 0.16em; + line-height: 1.4; + text-transform: uppercase; +} + +.brand-button { + align-items: center; + background: var(--color-root-red); + border: 1px solid var(--color-root-red); + border-radius: 999px; + color: var(--color-paper); + display: inline-flex; + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 600; + justify-content: center; + letter-spacing: 0.15em; + min-height: 44px; + padding: 0.85rem 1.5rem; + text-transform: uppercase; + transition: background-color 180ms var(--settle-ease), border-color 180ms var(--settle-ease), transform 180ms var(--settle-ease); +} + +.brand-button:hover { + background: var(--color-ink); + border-color: var(--color-ink); + transform: translateY(-1px); +} + +.brand-link { + color: var(--color-root-red); + text-decoration-color: color-mix(in srgb, var(--color-root-red) 38%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 0.28em; +} + +.brand-link:hover { + text-decoration-color: var(--color-root-red); +} + +/* Source badges use labels and shape, not an off-brand rainbow. */ +.badge-server, +.badge-url, +.badge-optimistic { + background: var(--color-blush); + border-color: color-mix(in srgb, var(--color-root-red) 24%, transparent); + color: var(--color-root-red); +} + +.badge-ui, +.badge-persisted, +.badge-derived { + background: var(--color-paper-warm); + border-color: color-mix(in srgb, var(--color-ink) 18%, transparent); + color: var(--color-ink); +} -/* Score panel animations */ @keyframes fade-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } + from { opacity: 0; transform: scale(0.98); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes bloom-unfurl { + from { opacity: 0; transform: scale(0.72) rotate(-2deg); } + to { opacity: 1; transform: scale(1) rotate(0deg); } +} + +@keyframes bloom-breathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.02); } } + .animate-fade-in { - animation: fade-in 0.3s ease-out; + animation: fade-in 300ms var(--bloom-ease); +} + +.bloom-unfurl { + animation: bloom-unfurl 720ms var(--bloom-ease) both, bloom-breathe 6000ms var(--settle-ease) 720ms infinite; + transform-origin: center; } -/* Scrollbar styling */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); } +::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--color-ink) 22%, transparent); border-radius: 999px; } +::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-ink) 42%, transparent); } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/demo/src/app/layout.tsx b/demo/src/app/layout.tsx index cd5829f..592f296 100644 --- a/demo/src/app/layout.tsx +++ b/demo/src/app/layout.tsx @@ -3,8 +3,8 @@ import type { ReactNode } from "react"; import "./globals.css"; export const metadata: Metadata = { - title: "Symphony State — Demo", - description: "Orchestrate multiple state sources without a monolithic global store." + title: "Symphony State — Every source in concert", + description: "Compose server data, UI state, URL state, and persistence through one predictable React conductor.", }; export default function RootLayout({ diff --git a/demo/src/app/page.tsx b/demo/src/app/page.tsx index dc80479..7888f1d 100644 --- a/demo/src/app/page.tsx +++ b/demo/src/app/page.tsx @@ -1,9 +1,10 @@ -import { Navigation } from "@/components/shared/Navigation"; +import Link from "next/link"; + +import { Features } from "@/components/landing/Features"; import { Hero } from "@/components/landing/Hero"; -import { OrchestraVisual } from "@/components/landing/OrchestraVisual"; import { LiveWidget } from "@/components/landing/LiveWidget"; -import { Features } from "@/components/landing/Features"; -import Link from "next/link"; +import { OrchestraVisual } from "@/components/landing/OrchestraVisual"; +import { Navigation } from "@/components/shared/Navigation"; export default function Home() { return ( @@ -12,43 +13,56 @@ export default function Home() {
- +
+ +
- {/* CTA */} -
-
-

- See it in action -

-

- An Inventory Dashboard that uses all four state sources, with a live Score panel showing every decision. -

-
- - Open Inventory Demo - - +
+

Open by default / 05

+
+

- Open Playground - + We grow in public. We ship the work. We leave the door open. +

+
+ + Open the inventory demo + + + Compose your own score → + +
- {/* Footer */} -