@saasflare/ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fonts/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Public type surface for typography presets.
4
+ * @module packages/ui/fonts
5
+ * @package ui
6
+ *
7
+ * Presets are loaded via subpath imports so each consumer only bundles
8
+ * the fonts of the preset they actually use:
9
+ *
10
+ * import { fontVariables } from "@saasflare/ui/fonts/editorial"
11
+ * <SaasflareShell className={fontVariables}>
12
+ *
13
+ * Available subpaths (declared in package.json `exports`):
14
+ * @saasflare/ui/fonts/default Inter + Inter + JetBrains Mono
15
+ * @saasflare/ui/fonts/editorial Inter + Fraunces + JetBrains Mono
16
+ * @saasflare/ui/fonts/geometric Geist + Geist + Geist Mono
17
+ * @saasflare/ui/fonts/rounded Nunito + Nunito + JetBrains Mono
18
+ * @saasflare/ui/fonts/distinctive Inter + Bricolage Grotesque + JetBrains Mono
19
+ * @saasflare/ui/fonts/neutral Roboto + Roboto + Roboto Mono
20
+ *
21
+ * Internal file structure (`presets/*.ts`) is not part of the public API
22
+ * and may change between versions; only the subpaths above are stable.
23
+ *
24
+ * Required app setup: add `transpilePackages: ["@saasflare/ui"]` to
25
+ * `next.config.{js,ts}` so the Next.js compiler can process the
26
+ * `next/font/google` calls in the package source.
27
+ */
28
+
29
+ /** All available font preset ids. Use as `import "@saasflare/ui/fonts/<id>"`. */
30
+ export type FontPreset =
31
+ | "default"
32
+ | "editorial"
33
+ | "geometric"
34
+ | "rounded"
35
+ | "distinctive"
36
+ | "neutral"
37
+
38
+ /**
39
+ * Shape of every preset module's default export contract.
40
+ * Each `@saasflare/ui/fonts/<preset>` module exports these names:
41
+ *
42
+ * - `fontBody` → next/font loader instance (carries `.variable` className)
43
+ * - `fontHeading` → next/font loader instance
44
+ * - `fontMono` → next/font loader instance
45
+ * - `fontVariables` → joined className string for `<SaasflareShell className={…}>`
46
+ *
47
+ * The loader instance type is intentionally `unknown` here — `next/font`
48
+ * generates a unique anonymous type per call, and we don't want this
49
+ * package to take a hard dependency on `next/font`'s internal types.
50
+ */
51
+ export interface FontPresetModule {
52
+ fontBody: { variable: string }
53
+ fontHeading: { variable: string }
54
+ fontMono: { variable: string }
55
+ fontVariables: string
56
+ }
@@ -0,0 +1,37 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Default typography preset — Inter + JetBrains Mono.
4
+ * @module packages/ui/fonts/presets/default
5
+ * @package ui
6
+ *
7
+ * SaaS-neutral, the safest pick. Inter is system-ui-adjacent, dense,
8
+ * excellent across sizes. JetBrains Mono pairs cleanly without competing.
9
+ *
10
+ * Use for: dashboards, admin panels, generic SaaS UI.
11
+ *
12
+ * Public import path (do not rely on this internal location):
13
+ * import { fontVariables } from "@saasflare/ui/fonts/default"
14
+ */
15
+ import { Inter, JetBrains_Mono } from "next/font/google"
16
+
17
+ export const fontBody = Inter({
18
+ subsets: ["latin"],
19
+ variable: "--font-body",
20
+ })
21
+
22
+ export const fontHeading = Inter({
23
+ subsets: ["latin"],
24
+ variable: "--font-heading",
25
+ })
26
+
27
+ export const fontMono = JetBrains_Mono({
28
+ subsets: ["latin"],
29
+ variable: "--font-mono",
30
+ })
31
+
32
+ /** Joined className string — pass to <SaasflareShell className={...}>. */
33
+ export const fontVariables = [
34
+ fontBody.variable,
35
+ fontHeading.variable,
36
+ fontMono.variable,
37
+ ].join(" ")
@@ -0,0 +1,43 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Distinctive typography preset — Inter body + Bricolage Grotesque heading + JetBrains Mono.
4
+ * @module packages/ui/fonts/presets/distinctive
5
+ * @package ui
6
+ *
7
+ * Modern, with attitude. Bricolage Grotesque is a contemporary display
8
+ * sans with strong character — confident headings that still pair with
9
+ * Inter's quiet body text. Less serious than Editorial, more brand-forward
10
+ * than Default.
11
+ *
12
+ * `display: "swap"` on the heading: Bricolage's silhouette is distinct
13
+ * enough that the optional fallback would feel like a different brand.
14
+ *
15
+ * Use for: products that want a recognizable type voice without going
16
+ * full editorial — startups, design tools, brand-led marketing sites.
17
+ *
18
+ * Public import path:
19
+ * import { fontVariables } from "@saasflare/ui/fonts/distinctive"
20
+ */
21
+ import { Inter, Bricolage_Grotesque, JetBrains_Mono } from "next/font/google"
22
+
23
+ export const fontBody = Inter({
24
+ subsets: ["latin"],
25
+ variable: "--font-body",
26
+ })
27
+
28
+ export const fontHeading = Bricolage_Grotesque({
29
+ subsets: ["latin"],
30
+ variable: "--font-heading",
31
+ display: "swap",
32
+ })
33
+
34
+ export const fontMono = JetBrains_Mono({
35
+ subsets: ["latin"],
36
+ variable: "--font-mono",
37
+ })
38
+
39
+ export const fontVariables = [
40
+ fontBody.variable,
41
+ fontHeading.variable,
42
+ fontMono.variable,
43
+ ].join(" ")
@@ -0,0 +1,44 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Editorial typography preset — Inter body + Fraunces heading + JetBrains Mono.
4
+ * @module packages/ui/fonts/presets/editorial
5
+ * @package ui
6
+ *
7
+ * Content-heavy products with strong editorial voice. Fraunces is a
8
+ * high-contrast contemporary serif with personality; Inter keeps body
9
+ * text quiet and readable. The serif/sans split signals "this is content,
10
+ * not chrome."
11
+ *
12
+ * `display: "swap"` on the heading: Fraunces diverges enough from system-ui
13
+ * that the optional fallback would feel bare during load.
14
+ *
15
+ * Use for: blogs, documentation sites, knowledge bases, magazine-style
16
+ * marketing pages.
17
+ *
18
+ * Public import path:
19
+ * import { fontVariables } from "@saasflare/ui/fonts/editorial"
20
+ */
21
+ import { Inter, Fraunces, JetBrains_Mono } from "next/font/google"
22
+
23
+ export const fontBody = Inter({
24
+ subsets: ["latin"],
25
+ variable: "--font-body",
26
+ })
27
+
28
+ export const fontHeading = Fraunces({
29
+ subsets: ["latin"],
30
+ variable: "--font-heading",
31
+ display: "swap",
32
+ axes: ["opsz"],
33
+ })
34
+
35
+ export const fontMono = JetBrains_Mono({
36
+ subsets: ["latin"],
37
+ variable: "--font-mono",
38
+ })
39
+
40
+ export const fontVariables = [
41
+ fontBody.variable,
42
+ fontHeading.variable,
43
+ fontMono.variable,
44
+ ].join(" ")
@@ -0,0 +1,37 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Geometric typography preset — Geist + Geist Mono.
4
+ * @module packages/ui/fonts/presets/geometric
5
+ * @package ui
6
+ *
7
+ * Technical-minimal aesthetic. Geist is tighter and more architectural
8
+ * than Inter; the matched mono keeps visual language uniform across copy
9
+ * and code blocks. Vercel-style minimalism.
10
+ *
11
+ * Use for: developer tools, API products, infrastructure dashboards.
12
+ *
13
+ * Public import path:
14
+ * import { fontVariables } from "@saasflare/ui/fonts/geometric"
15
+ */
16
+ import { Geist, Geist_Mono } from "next/font/google"
17
+
18
+ export const fontBody = Geist({
19
+ subsets: ["latin"],
20
+ variable: "--font-body",
21
+ })
22
+
23
+ export const fontHeading = Geist({
24
+ subsets: ["latin"],
25
+ variable: "--font-heading",
26
+ })
27
+
28
+ export const fontMono = Geist_Mono({
29
+ subsets: ["latin"],
30
+ variable: "--font-mono",
31
+ })
32
+
33
+ export const fontVariables = [
34
+ fontBody.variable,
35
+ fontHeading.variable,
36
+ fontMono.variable,
37
+ ].join(" ")
@@ -0,0 +1,41 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Neutral typography preset — Roboto + Roboto Mono.
4
+ * @module packages/ui/fonts/presets/neutral
5
+ * @package ui
6
+ *
7
+ * Material-Design-aligned, maximally familiar. Roboto is the most-seen
8
+ * web font on earth — users won't notice it, which is exactly the point.
9
+ * Use when typography should disappear so other brand elements (color,
10
+ * imagery, layout) carry the identity.
11
+ *
12
+ * Use for: enterprise tools, internal dashboards, products with strong
13
+ * non-typographic brand systems, Android-adjacent companion web apps.
14
+ *
15
+ * Public import path:
16
+ * import { fontVariables } from "@saasflare/ui/fonts/neutral"
17
+ */
18
+ import { Roboto, Roboto_Mono } from "next/font/google"
19
+
20
+ export const fontBody = Roboto({
21
+ subsets: ["latin"],
22
+ variable: "--font-body",
23
+ weight: ["400", "500", "700"],
24
+ })
25
+
26
+ export const fontHeading = Roboto({
27
+ subsets: ["latin"],
28
+ variable: "--font-heading",
29
+ weight: ["500", "700"],
30
+ })
31
+
32
+ export const fontMono = Roboto_Mono({
33
+ subsets: ["latin"],
34
+ variable: "--font-mono",
35
+ })
36
+
37
+ export const fontVariables = [
38
+ fontBody.variable,
39
+ fontHeading.variable,
40
+ fontMono.variable,
41
+ ].join(" ")
@@ -0,0 +1,38 @@
1
+ // @reviewed 2026-04-19
2
+ /**
3
+ * @fileoverview Rounded typography preset — Nunito + JetBrains Mono.
4
+ * @module packages/ui/fonts/presets/rounded
5
+ * @package ui
6
+ *
7
+ * Consumer-friendly, warm. Nunito's rounded terminals soften the entire
8
+ * UI without sacrificing legibility. JetBrains Mono keeps code blocks
9
+ * feeling like code so the warmth stays scoped to prose.
10
+ *
11
+ * Use for: education, kids/family apps, wellness, any product that wants
12
+ * warmth over precision.
13
+ *
14
+ * Public import path:
15
+ * import { fontVariables } from "@saasflare/ui/fonts/rounded"
16
+ */
17
+ import { Nunito, JetBrains_Mono } from "next/font/google"
18
+
19
+ export const fontBody = Nunito({
20
+ subsets: ["latin"],
21
+ variable: "--font-body",
22
+ })
23
+
24
+ export const fontHeading = Nunito({
25
+ subsets: ["latin"],
26
+ variable: "--font-heading",
27
+ })
28
+
29
+ export const fontMono = JetBrains_Mono({
30
+ subsets: ["latin"],
31
+ variable: "--font-mono",
32
+ })
33
+
34
+ export const fontVariables = [
35
+ fontBody.variable,
36
+ fontHeading.variable,
37
+ fontMono.variable,
38
+ ].join(" ")
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@saasflare/ui",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "description": "Saasflare UI Components Library – Standalone components built on Radix UI + Tailwind CSS",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/saasflare/saasflare-ui.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org",
15
+ "provenance": true
16
+ },
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.mjs",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.mjs",
24
+ "require": "./dist/index.js"
25
+ },
26
+ "./styles": "./styles/globals.css",
27
+ "./globals.css": "./styles/globals.css",
28
+ "./theme.css": "./styles/theme.css",
29
+ "./fonts": "./fonts/index.ts",
30
+ "./fonts/default": "./fonts/presets/default.ts",
31
+ "./fonts/editorial": "./fonts/presets/editorial.ts",
32
+ "./fonts/geometric": "./fonts/presets/geometric.ts",
33
+ "./fonts/rounded": "./fonts/presets/rounded.ts",
34
+ "./fonts/distinctive": "./fonts/presets/distinctive.ts",
35
+ "./fonts/neutral": "./fonts/presets/neutral.ts",
36
+ "./fonts/*": null
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "styles",
41
+ "fonts"
42
+ ],
43
+ "dependencies": {
44
+ "radix-ui": "^1.4.3",
45
+ "@base-ui/react": "^1.2.0",
46
+ "class-variance-authority": "^0.7.1",
47
+ "clsx": "^2.1.1",
48
+ "cmdk": "^1.1.1",
49
+ "input-otp": "^1.4.2",
50
+ "lucide-react": "^0.577.0",
51
+ "nprogress": "^0.2.0",
52
+ "tailwind-merge": "^3.5.0",
53
+ "tw-animate-css": "^1.4.0"
54
+ },
55
+ "peerDependencies": {
56
+ "next": "16.2.3",
57
+ "next-themes": "^0.4.0",
58
+ "react": "^19.0.0",
59
+ "react-dom": "^19.0.0",
60
+ "tailwindcss": "^4.0.0",
61
+ "framer-motion": "^12.0.0",
62
+ "recharts": "^3.0.0",
63
+ "react-hook-form": "^7.0.0",
64
+ "@hookform/resolvers": "^5.0.0",
65
+ "zod": "^4.0.0",
66
+ "sonner": "^2.0.0",
67
+ "vaul": "^1.0.0",
68
+ "react-day-picker": "^9.0.0",
69
+ "date-fns": "^4.0.0",
70
+ "embla-carousel-react": "^8.0.0",
71
+ "react-resizable-panels": "^4.0.0"
72
+ },
73
+ "devDependencies": {
74
+ "@types/nprogress": "^0.2.3",
75
+ "@types/react": "^19.2.14",
76
+ "@types/react-dom": "^19.2.3",
77
+ "next": "16.2.3",
78
+ "next-themes": "^0.4.6",
79
+ "react": "^19.2.4",
80
+ "react-dom": "^19.2.4",
81
+ "tsup": "^8.5.1",
82
+ "typescript": "^5.9.3"
83
+ },
84
+ "scripts": {
85
+ "build": "tsup",
86
+ "dev": "tsup --watch",
87
+ "lint": "eslint .",
88
+ "typecheck": "tsc --noEmit"
89
+ }
90
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Saasflare UI — single-file entry point.
3
+ * @module packages/ui/styles/globals
4
+ * @package ui
5
+ * @reviewed 2026-04-19
6
+ *
7
+ * Apps should import exactly this file:
8
+ * @import "@saasflare/ui/globals.css";
9
+ *
10
+ * theme.css transitively pulls in motion.css, surfaces.css, and themes.css.
11
+ */
12
+ @import "./theme.css";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview Motion tokens + animation kill-switch.
3
+ * @module packages/ui/styles/motion
4
+ * @package ui
5
+ * @reviewed 2026-04-19
6
+ *
7
+ * Duration and easing tokens consumed by components and utility classes.
8
+ * The [data-animated="false"] attribute — set by SaasflareProvider on <html> —
9
+ * acts as a global kill-switch that collapses every animation and transition,
10
+ * including those in third-party libraries that don't read our tokens.
11
+ *
12
+ * Reduced-motion media query is an additional safety net for users with an
13
+ * OS-level preference, regardless of the provider's `animated` prop.
14
+ */
15
+
16
+ :root {
17
+ --duration-fast: 150ms;
18
+ --duration-normal: 220ms;
19
+ --duration-slow: 320ms;
20
+ --duration-overlay: 400ms;
21
+
22
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
23
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
24
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
25
+ --ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
26
+ --ease-exit: cubic-bezier(0.3, 0, 1, 1);
27
+ }
28
+
29
+ [data-animated="false"],
30
+ [data-animated="false"] *,
31
+ [data-animated="false"] *::before,
32
+ [data-animated="false"] *::after {
33
+ animation-duration: 0ms !important;
34
+ animation-iteration-count: 1 !important;
35
+ transition-duration: 0ms !important;
36
+ scroll-behavior: auto !important;
37
+ }
38
+
39
+ @media (prefers-reduced-motion: reduce) {
40
+ :root {
41
+ --duration-fast: 0ms;
42
+ --duration-normal: 0ms;
43
+ --duration-slow: 0ms;
44
+ --duration-overlay: 0ms;
45
+ }
46
+
47
+ *, *::before, *::after {
48
+ animation-duration: 0ms !important;
49
+ animation-iteration-count: 1 !important;
50
+ transition-duration: 0ms !important;
51
+ scroll-behavior: auto !important;
52
+ }
53
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @fileoverview 16 preset brand palettes — activate via [data-palette="id"] on <html>.
3
+ * @module packages/ui/styles/palettes
4
+ * @package ui
5
+ * @reviewed 2026-04-19
6
+ *
7
+ * Each preset overrides only the rebrand surface:
8
+ * --primary-h / --primary-c / --primary-l (brand axis)
9
+ * --neutral-h / --neutral-c (neutral axis, only when palette wants neutral greys)
10
+ *
11
+ * All downstream semantic tokens (secondary, muted, border, card, chart-*, …)
12
+ * are derived in theme.css from these axes. Adding a preset costs one line.
13
+ *
14
+ * Selectors are scoped with `:root` prefix (e.g. `:root[data-palette="ocean"]`)
15
+ * to guarantee specificity (0,2,0) — this beats the `:root` baseline (0,1,0)
16
+ * and the `.dark` class (0,1,0) regardless of @import source order.
17
+ *
18
+ * Ink & Stone are achromatic palettes (primary-c: 0). Chart palettes are
19
+ * overridden so dashboards stay legible with 5 distinct hues.
20
+ */
21
+
22
+ /* ─── Saasflare (house brand) ───────────────────── */
23
+ :root[data-palette="saasflare"] { --primary-h: 259.1; --primary-c: 0.214; --primary-l: 0.623; }
24
+ :root[data-palette="saasflare"].dark { --primary-l: 0.72; }
25
+
26
+ /* ─── Ocean ─────────────────────────────────────── */
27
+ :root[data-palette="ocean"] { --primary-h: 230; --primary-c: 0.18; --primary-l: 0.60; }
28
+ :root[data-palette="ocean"].dark { --primary-l: 0.70; }
29
+
30
+ /* ─── Ink (neutral) ─────────────────────────────── */
31
+ :root[data-palette="ink"] {
32
+ --primary-h: 0;
33
+ --primary-c: 0;
34
+ --primary-l: 0.45;
35
+ --neutral-h: 0;
36
+ --neutral-c: 0;
37
+ }
38
+ :root[data-palette="ink"].dark { --primary-l: 0.75; }
39
+
40
+ /* ─── Achromatic (near-black / near-white) ──────── */
41
+ /* Achromatic palettes invert primary lightness across modes, so the default
42
+ * white --primary-foreground would collide with a near-white --primary in
43
+ * dark mode. Pin the foreground per-mode to keep button contrast readable. */
44
+ :root[data-palette="achromatic"] {
45
+ --primary-h: 0;
46
+ --primary-c: 0;
47
+ --primary-l: 0.15; /* ≈ #171717, near-black */
48
+ --neutral-h: 0;
49
+ --neutral-c: 0;
50
+ --primary-foreground: oklch(1 0 0); /* white on near-black */
51
+ }
52
+ :root[data-palette="achromatic"].dark {
53
+ --primary-l: 0.95; /* ≈ #f2f2f2, near-white */
54
+ --primary-foreground: oklch(0.15 0 0); /* near-black on near-white */
55
+ }
56
+
57
+ /* ─── Black (pure) ──────────────────────────────── */
58
+ :root[data-palette="black"] {
59
+ --primary-h: 0;
60
+ --primary-c: 0;
61
+ --primary-l: 0; /* #000 */
62
+ --neutral-h: 0;
63
+ --neutral-c: 0;
64
+ --primary-foreground: oklch(1 0 0); /* white on black */
65
+ }
66
+ :root[data-palette="black"].dark {
67
+ --primary-l: 1; /* #fff */
68
+ --primary-foreground: oklch(0 0 0); /* black on white */
69
+ }
70
+
71
+ /* ─── Aurora ────────────────────────────────────── */
72
+ :root[data-palette="aurora"] { --primary-h: 195; --primary-c: 0.19; --primary-l: 0.65; }
73
+ :root[data-palette="aurora"].dark { --primary-l: 0.75; }
74
+
75
+ /* ─── Indigo ────────────────────────────────────── */
76
+ :root[data-palette="indigo"] { --primary-h: 265; --primary-c: 0.20; --primary-l: 0.60; }
77
+ :root[data-palette="indigo"].dark { --primary-l: 0.70; }
78
+
79
+ /* ─── Emerald ───────────────────────────────────── */
80
+ :root[data-palette="emerald"] { --primary-h: 155; --primary-c: 0.17; --primary-l: 0.55; }
81
+ :root[data-palette="emerald"].dark { --primary-l: 0.68; }
82
+
83
+ /* ─── Violet ────────────────────────────────────── */
84
+ :root[data-palette="violet"] { --primary-h: 290; --primary-c: 0.20; --primary-l: 0.60; }
85
+ :root[data-palette="violet"].dark { --primary-l: 0.70; }
86
+
87
+ /* ─── Coral ─────────────────────────────────────── */
88
+ :root[data-palette="coral"] { --primary-h: 20; --primary-c: 0.19; --primary-l: 0.65; }
89
+ :root[data-palette="coral"].dark { --primary-l: 0.72; }
90
+
91
+ /* ─── Stone (neutral) ───────────────────────────── */
92
+ :root[data-palette="stone"] {
93
+ --primary-h: 0;
94
+ --primary-c: 0;
95
+ --primary-l: 0.55;
96
+ --neutral-h: 0;
97
+ --neutral-c: 0;
98
+ }
99
+ :root[data-palette="stone"].dark { --primary-l: 0.78; }
100
+
101
+ /* ─── Jade ──────────────────────────────────────── */
102
+ :root[data-palette="jade"] { --primary-h: 165; --primary-c: 0.16; --primary-l: 0.58; }
103
+ :root[data-palette="jade"].dark { --primary-l: 0.70; }
104
+
105
+ /* ─── Cobalt ────────────────────────────────────── */
106
+ :root[data-palette="cobalt"] { --primary-h: 240; --primary-c: 0.20; --primary-l: 0.60; }
107
+ :root[data-palette="cobalt"].dark { --primary-l: 0.70; }
108
+
109
+ /* ─── Amber ─────────────────────────────────────── */
110
+ :root[data-palette="amber"] { --primary-h: 50; --primary-c: 0.17; --primary-l: 0.65; }
111
+ :root[data-palette="amber"].dark { --primary-l: 0.72; }
112
+
113
+ /* ─── Fuchsia ───────────────────────────────────── */
114
+ :root[data-palette="fuchsia"] { --primary-h: 340; --primary-c: 0.21; --primary-l: 0.62; }
115
+ :root[data-palette="fuchsia"].dark { --primary-l: 0.72; }
116
+
117
+ /* ─── Honey ─────────────────────────────────────── */
118
+ :root[data-palette="honey"] { --primary-h: 70; --primary-c: 0.16; --primary-l: 0.68; }
119
+ :root[data-palette="honey"].dark { --primary-l: 0.76; }
120
+
121
+ /* ─── Teal ──────────────────────────────────────── */
122
+ :root[data-palette="teal"] { --primary-h: 185; --primary-c: 0.15; --primary-l: 0.60; }
123
+ :root[data-palette="teal"].dark { --primary-l: 0.70; }
124
+
125
+ /* ─── Iris ──────────────────────────────────────── */
126
+ :root[data-palette="iris"] { --primary-h: 255; --primary-c: 0.16; --primary-l: 0.60; }
127
+ :root[data-palette="iris"].dark { --primary-l: 0.70; }
128
+
129
+ /* ─── Ruby ──────────────────────────────────────── */
130
+ :root[data-palette="ruby"] { --primary-h: 10; --primary-c: 0.21; --primary-l: 0.58; }
131
+ :root[data-palette="ruby"].dark { --primary-l: 0.68; }
132
+
133
+ /* ============================================
134
+ * Chart palette overrides for achromatic palettes
135
+ *
136
+ * With --primary-c: 0, the derived chart colors collapse to grayscale.
137
+ * Give Ink and Stone a fixed distinguishable 5-hue palette.
138
+ * ============================================ */
139
+ :root[data-palette="ink"],
140
+ :root[data-palette="stone"],
141
+ :root[data-palette="black"] {
142
+ --chart-1: oklch(0.60 0.18 230); /* blue */
143
+ --chart-2: oklch(0.65 0.17 55); /* orange */
144
+ --chart-3: oklch(0.60 0.15 155); /* green */
145
+ --chart-4: oklch(0.58 0.20 25); /* red */
146
+ --chart-5: oklch(0.60 0.18 290); /* violet */
147
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview Surface system — flat / glass as semantic overlay.
3
+ * @module packages/ui/styles/surfaces
4
+ * @package ui
5
+ * @reviewed 2026-04-19
6
+ *
7
+ * Components bind to four surface tokens:
8
+ * --surface-bg background color or gradient
9
+ * --surface-border border color
10
+ * --surface-backdrop backdrop-filter value (blur/saturate) or "none"
11
+ * --surface-shadow box-shadow value
12
+ *
13
+ * Switching [data-style="…"] on <html> swaps the tokens without touching
14
+ * any component. Extend by adding a new selector block:
15
+ *
16
+ * [data-style="neumorphic"] { --surface-bg: …; … }
17
+ *
18
+ * and registering the id in the StyleVariant union (types.ts).
19
+ */
20
+
21
+ :root {
22
+ --surface-bg: var(--card);
23
+ --surface-border: var(--border);
24
+ --surface-backdrop: none;
25
+ --surface-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
26
+ }
27
+
28
+ [data-style="flat"] {
29
+ --surface-bg: var(--card);
30
+ --surface-border: var(--border);
31
+ --surface-backdrop: none;
32
+ --surface-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
33
+ }
34
+
35
+ [data-style="glass"] {
36
+ --surface-bg: oklch(0.99 0.005 var(--neutral-h) / 0.6);
37
+ --surface-border: oklch(1 0 0 / 0.2);
38
+ --surface-backdrop: blur(12px) saturate(140%);
39
+ --surface-shadow:
40
+ 0 8px 32px oklch(0 0 0 / 0.08),
41
+ inset 0 1px 0 oklch(1 0 0 / 0.4);
42
+ }
43
+
44
+ [data-style="glass"].dark,
45
+ .dark [data-style="glass"] {
46
+ --surface-bg: oklch(0.2 0.015 var(--neutral-h) / 0.5);
47
+ --surface-border: oklch(1 0 0 / 0.1);
48
+ --surface-shadow:
49
+ 0 8px 32px oklch(0 0 0 / 0.4),
50
+ inset 0 1px 0 oklch(1 0 0 / 0.08);
51
+ }