@consuma/design 0.1.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/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @consuma/design
2
+
3
+ The Consuma design system — **just the parts that need to be shared**.
4
+
5
+ - Color & semantic tokens (CSS variables + JS exports)
6
+ - Tailwind preset wired to those variables
7
+ - Data-viz palettes (sentiment / sequential / categorical)
8
+ - Brand components: `Logo`, `SentimentPill`
9
+
10
+ Everything else (buttons, inputs, dialogs, etc.) belongs in your app via the
11
+ [shadcn/ui CLI](https://ui.shadcn.com/docs/cli) — Consuma's CSS variables drive
12
+ the look automatically, and your app owns the components.
13
+
14
+ > Zero build step. The package ships its TypeScript source and is consumed
15
+ > directly by Vite/Next/etc — change something and it shows up immediately.
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @consuma/design
23
+ ```
24
+
25
+ ## Wire it up (3 steps, ~30 sec)
26
+
27
+ **1. Import the styles once, in your entry file**
28
+
29
+ ```ts
30
+ // main.tsx, layout.tsx, _app.tsx, ...
31
+ import "@consuma/design/styles.css";
32
+ // (optional) extra dataviz CSS variables for chart libs
33
+ import "@consuma/design/dataviz.css";
34
+ ```
35
+
36
+ **2. Use the Tailwind preset**
37
+
38
+ ```ts
39
+ // tailwind.config.ts
40
+ import preset from "@consuma/design/tailwind-preset";
41
+
42
+ export default {
43
+ presets: [preset],
44
+ content: [
45
+ "./src/**/*.{ts,tsx}",
46
+ "./node_modules/@consuma/design/src/**/*.{ts,tsx}",
47
+ ],
48
+ };
49
+ ```
50
+
51
+ **3. Add shadcn components into your app**
52
+
53
+ ```bash
54
+ npx shadcn@latest init # accept defaults
55
+ npx shadcn@latest add button card input dialog
56
+ ```
57
+
58
+ They'll automatically pick up Consuma colors because shadcn reads the same
59
+ CSS variables (`--background`, `--primary`, `--muted`, etc.) that
60
+ `@consuma/design/styles.css` sets.
61
+
62
+ ## What's exported
63
+
64
+ ```ts
65
+ import {
66
+ cn, // clsx + tailwind-merge helper
67
+ tokens, // typed JS access to all colors/space/radius/shadow
68
+ dataviz, // chart palettes (sentiment, plum sequential, categorical)
69
+ pickEvenly, // util for trimming sequential scales
70
+ Logo,
71
+ SentimentPill,
72
+ } from "@consuma/design";
73
+ ```
74
+
75
+ CSS variables you can use in custom CSS / inline styles:
76
+
77
+ ```
78
+ --plum-50 … --plum-950
79
+ --neutral-0 … --neutral-900
80
+ --background --foreground --card --primary --secondary
81
+ --muted --accent --border --ring
82
+ --success --warning --info --destructive
83
+ --sentiment-positive --sentiment-negative --sentiment-neutral --sentiment-mixed
84
+ --cv-div-1 … --cv-div-7 (sentiment diverging)
85
+ --cv-seq-1 … --cv-seq-7 (plum sequential)
86
+ --cv-cat-1 … --cv-cat-12 (categorical)
87
+ ```
88
+
89
+ ## Tailwind classes you get for free
90
+
91
+ `bg-plum-600`, `text-primary`, `bg-card`, `border-border`, `text-success`, …
92
+ plus all of shadcn's semantic names.
93
+
94
+ ## Updating tokens (the workflow)
95
+
96
+ `src/tokens.ts` is the single source of truth.
97
+
98
+ ```bash
99
+ # 1. Edit the value
100
+ # e.g. src/tokens.ts: plum: { 600: "#5B3AAE" → "#6A45BE" }
101
+
102
+ # 2. Regenerate styles.css
103
+ npm run gen
104
+ # ✓ Wrote src/styles.css
105
+
106
+ # 3. Done. Every consuming app picks it up on next reload.
107
+ ```
108
+
109
+ What lives where:
110
+
111
+ | File | Edit when… |
112
+ |---|---|
113
+ | `src/tokens.ts` | You want to change a **scale** value (e.g. plum-600 hue) |
114
+ | `scripts/gen-styles.ts` (SEMANTIC block) | You want to **remap** semantic slots (e.g. `--primary: var(--plum-700)`) or change dark mode |
115
+ | `src/tailwind-preset.js` | You want to expose a **new Tailwind utility** (e.g. `bg-citrus-500`) |
116
+ | `src/dataviz.ts` | You want to add/edit a **chart palette** |
117
+
118
+ Never edit `src/styles.css` directly — it's regenerated.
119
+
120
+ ## How the inheritance works (in 4 layers)
121
+
122
+ 1. **`tokens.ts`** — raw scale values (HEX) in TypeScript
123
+ 2. **`styles.css`** — CSS variables (`--plum-600: 91 58 174`) + semantic mapping (`--primary: var(--plum-600)`)
124
+ 3. **`tailwind-preset.js`** — exposes Tailwind utilities (`bg-primary`, `bg-plum-600`) that read those CSS vars
125
+ 4. **shadcn components in your app** — already use `bg-primary text-primary-foreground` etc.
126
+
127
+ Drop in `styles.css` and the whole chain lights up. No component patches.
128
+
129
+ ## Adding a brand component to the package
130
+
131
+ Only promote a component here when it's used in **2+ apps** with the same
132
+ Consuma styling. Examples that earn their keep: `Logo`, `SentimentPill`,
133
+ `MetricCard`, `ConfidenceCard`. App-specific layout doesn't.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@consuma/design",
3
+ "version": "0.1.0",
4
+ "description": "Consuma design tokens — CSS variables, Tailwind preset, dataviz palettes, brand components.",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "**/*.css"
8
+ ],
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./styles.css": "./src/styles.css",
12
+ "./dataviz.css": "./src/dataviz.css",
13
+ "./tailwind-preset": "./src/tailwind-preset.js",
14
+ "./tokens": "./src/tokens.ts",
15
+ "./dataviz": "./src/dataviz.ts"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "gen": "tsx scripts/gen-styles.ts"
23
+ },
24
+ "peerDependencies": {
25
+ "react": ">=18",
26
+ "tailwindcss": ">=3.4"
27
+ },
28
+ "dependencies": {
29
+ "clsx": "^2.1.1",
30
+ "lucide-react": "^0.460.0",
31
+ "tailwind-merge": "^2.5.4"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.3.12",
35
+ "tsx": "^4.20.6",
36
+ "typescript": "^5.6.3"
37
+ }
38
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ declare module "*.png" {
2
+ const src: string;
3
+ export default src;
4
+ }
5
+
@@ -0,0 +1,34 @@
1
+ // Consuma logo as a React component.
2
+ // The bitmap source is shipped with the package; SVG version coming when the
3
+ // brand team hands off the vector master.
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn";
6
+ import logoUrl from "../assets/consuma-logo.png";
7
+
8
+ export interface LogoProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ variant?: "lockup" | "mark";
10
+ /** Pixel height; width auto. Default 32. */
11
+ height?: number;
12
+ }
13
+
14
+ export function Logo({ variant = "lockup", height = 32, className, ...rest }: LogoProps) {
15
+ return (
16
+ <div className={cn("inline-flex items-center", className)} {...rest}>
17
+ <img
18
+ src={logoUrl}
19
+ alt="Consuma"
20
+ height={height}
21
+ style={{
22
+ height,
23
+ width: "auto",
24
+ objectFit: variant === "mark" ? "none" : "contain",
25
+ objectPosition: variant === "mark" ? "left center" : undefined,
26
+ // For 'mark' we crop to roughly the icon's left ~28% of the lockup.
27
+ clipPath:
28
+ variant === "mark" ? `inset(0 72% 0 0)` : undefined,
29
+ }}
30
+ draggable={false}
31
+ />
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,53 @@
1
+ import * as React from "react";
2
+ import { ArrowDown, ArrowUp, Minus } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+
5
+ export type Sentiment = "positive" | "negative" | "neutral" | "mixed";
6
+
7
+ export interface SentimentPillProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ sentiment: Sentiment;
9
+ /** Score to display alongside the pill (e.g. -0.42, +0.68). */
10
+ score?: number;
11
+ showIcon?: boolean;
12
+ }
13
+
14
+ const TONE: Record<Sentiment, string> = {
15
+ positive: "bg-success-soft text-success-strong",
16
+ negative: "bg-danger-soft text-danger-strong",
17
+ neutral: "bg-neutral-200 text-neutral-700",
18
+ mixed: "bg-warning-soft text-warning-strong",
19
+ };
20
+
21
+ const Icon: React.FC<{ s: Sentiment }> = ({ s }) => {
22
+ if (s === "positive") return <ArrowUp className="h-3 w-3" />;
23
+ if (s === "negative") return <ArrowDown className="h-3 w-3" />;
24
+ return <Minus className="h-3 w-3" />;
25
+ };
26
+
27
+ export const SentimentPill = ({
28
+ sentiment,
29
+ score,
30
+ showIcon = true,
31
+ className,
32
+ ...rest
33
+ }: SentimentPillProps) => {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold",
38
+ TONE[sentiment],
39
+ className
40
+ )}
41
+ {...rest}
42
+ >
43
+ {showIcon && <Icon s={sentiment} />}
44
+ <span className="capitalize">{sentiment}</span>
45
+ {score !== undefined && (
46
+ <span className="font-mono tabular-nums opacity-75">
47
+ {score > 0 ? "+" : ""}
48
+ {score.toFixed(2)}
49
+ </span>
50
+ )}
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Consuma data-viz tokens.
3
+ *
4
+ * Five scales, all tuned for charts (perceptually-uniform-ish, brand-aware):
5
+ * 1. Sentiment diverging — 7 steps, very-negative → neutral → very-positive
6
+ * 2. Plum sequential — 7 steps, light → dark, single-hue
7
+ * 3. Categorical-12 — distinct hues, AA-contrast on neutral-50
8
+ * 4. Categorical CB-safe — 8 colors, Okabe-Ito (colorblind-safe alternate)
9
+ * 5. Sentiment bins — 5 discrete bins for survey/Likert rendering
10
+ *
11
+ * Use as CSS vars (--cv-…) OR import from `consuma-ui/tokens`.
12
+ */
13
+
14
+ :root {
15
+ /* 1 ── Sentiment diverging (7-step) ───────────────────
16
+ Use for heatmaps of sentiment over time/topic.
17
+ Generated from ColorBrewer RdYlGn, brand-tuned. */
18
+ --cv-div-1: #B91C1C; /* very negative */
19
+ --cv-div-2: #EF4444;
20
+ --cv-div-3: #FCA5A5;
21
+ --cv-div-4: #E5E7EB; /* neutral midpoint */
22
+ --cv-div-5: #86EFAC;
23
+ --cv-div-6: #16A34A;
24
+ --cv-div-7: #14532D; /* very positive */
25
+
26
+ /* 2 ── Plum sequential (7-step) ───────────────────────
27
+ Use for magnitude/density (mention volume, frequency).
28
+ Perceptually graded, not just brand tints. */
29
+ --cv-seq-1: #F5F1FB;
30
+ --cv-seq-2: #DDD0F0;
31
+ --cv-seq-3: #B49AE0;
32
+ --cv-seq-4: #8C6AD1;
33
+ --cv-seq-5: #6A45BE;
34
+ --cv-seq-6: #4A2D90;
35
+ --cv-seq-7: #2E1A4D;
36
+
37
+ /* 3 ── Categorical 12 ─────────────────────────────────
38
+ For topic clusters, source breakdowns, etc.
39
+ All AA-contrast (≥4.5:1) on --neutral-50 background. */
40
+ --cv-cat-1: #5B3AAE; /* plum-600 — brand */
41
+ --cv-cat-2: #0F766E; /* teal-700 */
42
+ --cv-cat-3: #C2410C; /* orange-700 */
43
+ --cv-cat-4: #1E40AF; /* blue-800 */
44
+ --cv-cat-5: #9D174D; /* pink-800 */
45
+ --cv-cat-6: #047857; /* emerald-700 */
46
+ --cv-cat-7: #B45309; /* amber-700 */
47
+ --cv-cat-8: #6D28D9; /* violet-700 */
48
+ --cv-cat-9: #155E75; /* cyan-800 */
49
+ --cv-cat-10: #BE123C; /* rose-700 */
50
+ --cv-cat-11: #4D7C0F; /* lime-700 */
51
+ --cv-cat-12: #5B5B5B; /* gray fallback */
52
+
53
+ /* 4 ── Categorical colorblind-safe (Okabe-Ito) ────────
54
+ Use when audience inclusion matters (presentations, public dashboards). */
55
+ --cv-cb-1: #E69F00;
56
+ --cv-cb-2: #56B4E9;
57
+ --cv-cb-3: #009E73;
58
+ --cv-cb-4: #F0E442;
59
+ --cv-cb-5: #0072B2;
60
+ --cv-cb-6: #D55E00;
61
+ --cv-cb-7: #CC79A7;
62
+ --cv-cb-8: #000000;
63
+
64
+ /* 5 ── Sentiment bins (5-step discrete) ───────────────
65
+ For Likert / NPS-style discrete renders. */
66
+ --cv-bin-very-neg: #DC2626;
67
+ --cv-bin-neg: #FCA5A5;
68
+ --cv-bin-neutral: #D1D5DB;
69
+ --cv-bin-pos: #86EFAC;
70
+ --cv-bin-very-pos: #16A34A;
71
+ }
72
+
73
+ /* Optional Tailwind utilities (if you want bg-cv-cat-1 etc.):
74
+ Add to tailwind config:
75
+ theme.extend.colors['cv-cat-1'] = 'var(--cv-cat-1)'
76
+ …or just use them in inline styles / chart libraries. */
package/src/dataviz.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Data-viz tokens — JS-accessible (for Recharts, d3, canvas, anywhere
3
+ * you need an array of strings instead of CSS vars).
4
+ *
5
+ * Mirror of `dataviz.css`. If you change one, change the other.
6
+ */
7
+
8
+ export const dataviz = {
9
+ /** 7-step sentiment diverging scale: very-neg → neutral → very-pos. */
10
+ sentimentDiverging: [
11
+ "#B91C1C", "#EF4444", "#FCA5A5",
12
+ "#E5E7EB",
13
+ "#86EFAC", "#16A34A", "#14532D",
14
+ ],
15
+
16
+ /** 7-step plum sequential — for magnitude/density. */
17
+ plumSequential: [
18
+ "#F5F1FB", "#DDD0F0", "#B49AE0",
19
+ "#8C6AD1", "#6A45BE", "#4A2D90", "#2E1A4D",
20
+ ],
21
+
22
+ /** 12 distinct hues — AA-contrast on neutral-50.
23
+ * First color is plum-600 (brand), so single-series charts stay on-brand. */
24
+ categorical: [
25
+ "#5B3AAE", "#0F766E", "#C2410C", "#1E40AF",
26
+ "#9D174D", "#047857", "#B45309", "#6D28D9",
27
+ "#155E75", "#BE123C", "#4D7C0F", "#5B5B5B",
28
+ ],
29
+
30
+ /** Okabe-Ito — colorblind-safe alternate categorical. */
31
+ categoricalCBSafe: [
32
+ "#E69F00", "#56B4E9", "#009E73", "#F0E442",
33
+ "#0072B2", "#D55E00", "#CC79A7", "#000000",
34
+ ],
35
+
36
+ /** 5 discrete sentiment bins for Likert/NPS renders. */
37
+ sentimentBins: {
38
+ veryNegative: "#DC2626",
39
+ negative: "#FCA5A5",
40
+ neutral: "#D1D5DB",
41
+ positive: "#86EFAC",
42
+ veryPositive: "#16A34A",
43
+ },
44
+ } as const;
45
+
46
+ /** Pick N evenly-spaced colors from an array (for trimmed sequential scales). */
47
+ export function pickEvenly<T>(arr: readonly T[], n: number): T[] {
48
+ if (n >= arr.length) return [...arr];
49
+ if (n <= 1) return [arr[Math.floor(arr.length / 2)]];
50
+ const step = (arr.length - 1) / (n - 1);
51
+ return Array.from({ length: n }, (_, i) => arr[Math.round(i * step)]);
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./lib/cn";
2
+ export * from "./tokens";
3
+ export * from "./dataviz";
4
+
5
+ export * from "./components/logo";
6
+ export * from "./components/sentiment-pill";
package/src/lib/cn.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /** Merge Tailwind class strings, resolving conflicts. */
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
package/src/styles.css ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Consuma UI — global stylesheet.
3
+ * Import once at app root: `import "@consuma/design/styles.css";`
4
+ *
5
+ * AUTO-GENERATED from src/tokens.ts by scripts/gen-styles.ts.
6
+ * Do not edit by hand — run `npm run gen` after changing tokens.
7
+ */
8
+
9
+ @layer base {
10
+ :root {
11
+ /* Typography */
12
+ --font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
13
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
14
+
15
+ /* Brand (plum) */
16
+ --plum-50: 245 241 251;
17
+ --plum-100: 233 224 245;
18
+ --plum-200: 210 192 235;
19
+ --plum-300: 180 154 224;
20
+ --plum-400: 149 118 204;
21
+ --plum-500: 122 87 187;
22
+ --plum-600: 91 58 174;
23
+ --plum-700: 74 45 144;
24
+ --plum-800: 61 36 112;
25
+ --plum-900: 46 26 77;
26
+ --plum-950: 27 15 48;
27
+
28
+ /* Neutrals (warm slate) */
29
+ --neutral-0: 255 255 255;
30
+ --neutral-50: 250 250 249;
31
+ --neutral-100: 244 243 241;
32
+ --neutral-200: 232 230 226;
33
+ --neutral-300: 212 209 203;
34
+ --neutral-400: 168 163 154;
35
+ --neutral-500: 120 115 106;
36
+ --neutral-600: 90 86 80;
37
+ --neutral-700: 63 60 55;
38
+ --neutral-800: 39 37 31;
39
+ --neutral-900: 22 20 15;
40
+
41
+ /* Sentiment (data viz) */
42
+ --sentiment-positive: 46 158 110;
43
+ --sentiment-neutral: 168 163 154;
44
+ --sentiment-negative: 217 83 79;
45
+ --sentiment-mixed: 199 122 10;
46
+
47
+ /* Status atoms */
48
+ --success: 31 138 91;
49
+ --warning: 199 122 10;
50
+ --info: 42 111 219;
51
+ --destructive: 192 57 43;
52
+ --destructive-foreground: 255 255 255;
53
+
54
+ /* Status tints */
55
+ --brand-soft: 245 241 251;
56
+ --brand-strong: 74 45 144;
57
+ --brand-accent: 149 118 204;
58
+ --brand-rail: 180 154 224;
59
+ --success-soft: 231 247 238;
60
+ --success-strong: 31 122 82;
61
+ --success-accent: 46 158 110;
62
+ --warning-soft: 254 243 199;
63
+ --warning-strong: 180 83 9;
64
+ --warning-accent: 245 158 11;
65
+ --danger-soft: 251 233 232;
66
+ --danger-strong: 166 51 47;
67
+ --danger-accent: 217 83 79;
68
+ --info-soft: 224 242 254;
69
+ --info-strong: 3 105 161;
70
+ --info-accent: 14 165 233;
71
+
72
+ /* SEMANTIC LAYER — edit this section in scripts/gen-styles.ts to remap.
73
+ Uses space-separated RGB so Tailwind `bg-foo/50` opacity works. */
74
+ --background: var(--neutral-50);
75
+ --foreground: var(--neutral-900);
76
+
77
+ --card: var(--neutral-0);
78
+ --card-foreground: var(--neutral-900);
79
+
80
+ --popover: var(--neutral-0);
81
+ --popover-foreground: var(--neutral-900);
82
+
83
+ --primary: var(--plum-600);
84
+ --primary-foreground: var(--neutral-0);
85
+
86
+ --secondary: var(--neutral-100);
87
+ --secondary-foreground: var(--neutral-900);
88
+
89
+ --muted: var(--neutral-100);
90
+ --muted-foreground: var(--neutral-600);
91
+
92
+ --accent: var(--plum-50);
93
+ --accent-foreground: var(--plum-700);
94
+
95
+ --border: var(--neutral-200);
96
+ --input: var(--neutral-200);
97
+ --ring: var(--plum-500);
98
+
99
+ --radius: 0.625rem;
100
+ }
101
+
102
+ .dark {
103
+
104
+ --background: var(--neutral-900);
105
+ --foreground: var(--neutral-50);
106
+
107
+ --card: var(--neutral-800);
108
+ --card-foreground: var(--neutral-50);
109
+
110
+ --popover: var(--neutral-800);
111
+ --popover-foreground: var(--neutral-50);
112
+
113
+ --primary: var(--plum-400);
114
+ --primary-foreground: var(--plum-950);
115
+
116
+ --secondary: var(--neutral-800);
117
+ --secondary-foreground: var(--neutral-50);
118
+
119
+ --muted: var(--neutral-800);
120
+ --muted-foreground: var(--neutral-400);
121
+
122
+ --accent: var(--plum-900);
123
+ --accent-foreground: var(--plum-200);
124
+
125
+ --destructive: 220 100 95;
126
+ --destructive-foreground: 255 255 255;
127
+
128
+ --border: var(--neutral-700);
129
+ --input: var(--neutral-700);
130
+ --ring: var(--plum-400);
131
+ }
132
+
133
+ * {
134
+ border-color: rgb(var(--border));
135
+ }
136
+
137
+ body {
138
+ background-color: rgb(var(--background));
139
+ color: rgb(var(--foreground));
140
+ font-family: var(--font-sans);
141
+ font-feature-settings: "ss01", "cv11";
142
+ -webkit-font-smoothing: antialiased;
143
+ -moz-osx-font-smoothing: grayscale;
144
+ }
145
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Consuma Tailwind preset.
3
+ * Use in your app:
4
+ *
5
+ * // tailwind.config.{js,ts}
6
+ * import consumaPreset from "@consuma/design/tailwind-preset";
7
+ * export default {
8
+ * presets: [consumaPreset],
9
+ * content: [
10
+ * "./app/**\/*.{ts,tsx}",
11
+ * "./node_modules/@consuma/design/src/**\/*.{ts,tsx}",
12
+ * ],
13
+ * };
14
+ */
15
+
16
+ const rgb = (v) => `rgb(var(${v}) / <alpha-value>)`;
17
+
18
+ /** @type {import('tailwindcss').Config} */
19
+ export default {
20
+ darkMode: ["class"],
21
+ theme: {
22
+ extend: {
23
+ colors: {
24
+ // shadcn semantic
25
+ border: rgb("--border"),
26
+ input: rgb("--input"),
27
+ ring: rgb("--ring"),
28
+ background: rgb("--background"),
29
+ foreground: rgb("--foreground"),
30
+ primary: {
31
+ DEFAULT: rgb("--primary"),
32
+ foreground: rgb("--primary-foreground"),
33
+ },
34
+ secondary: {
35
+ DEFAULT: rgb("--secondary"),
36
+ foreground: rgb("--secondary-foreground"),
37
+ },
38
+ destructive: {
39
+ DEFAULT: rgb("--destructive"),
40
+ foreground: rgb("--destructive-foreground"),
41
+ },
42
+ muted: {
43
+ DEFAULT: rgb("--muted"),
44
+ foreground: rgb("--muted-foreground"),
45
+ },
46
+ accent: {
47
+ DEFAULT: rgb("--accent"),
48
+ foreground: rgb("--accent-foreground"),
49
+ },
50
+ popover: {
51
+ DEFAULT: rgb("--popover"),
52
+ foreground: rgb("--popover-foreground"),
53
+ },
54
+ card: {
55
+ DEFAULT: rgb("--card"),
56
+ foreground: rgb("--card-foreground"),
57
+ },
58
+ // Status atoms + tints. Use:
59
+ // bg-success — bold mid-tone
60
+ // bg-success-soft — light tinted background (badges)
61
+ // text-success-strong — dark text on tint (badges)
62
+ // bg-success-accent — saturated mid for filled pills
63
+ success: {
64
+ DEFAULT: rgb("--success"),
65
+ soft: rgb("--success-soft"),
66
+ strong: rgb("--success-strong"),
67
+ accent: rgb("--success-accent"),
68
+ },
69
+ warning: {
70
+ DEFAULT: rgb("--warning"),
71
+ soft: rgb("--warning-soft"),
72
+ strong: rgb("--warning-strong"),
73
+ accent: rgb("--warning-accent"),
74
+ },
75
+ info: {
76
+ DEFAULT: rgb("--info"),
77
+ soft: rgb("--info-soft"),
78
+ strong: rgb("--info-strong"),
79
+ accent: rgb("--info-accent"),
80
+ },
81
+ danger: {
82
+ DEFAULT: rgb("--destructive"),
83
+ soft: rgb("--danger-soft"),
84
+ strong: rgb("--danger-strong"),
85
+ accent: rgb("--danger-accent"),
86
+ },
87
+ // Sentiment scale (data viz)
88
+ sentiment: {
89
+ positive: rgb("--sentiment-positive"),
90
+ neutral: rgb("--sentiment-neutral"),
91
+ negative: rgb("--sentiment-negative"),
92
+ mixed: rgb("--sentiment-mixed"),
93
+ },
94
+ // Override Tailwind defaults with Consuma's warm neutral scale
95
+ neutral: {
96
+ 0: rgb("--neutral-0"),
97
+ 50: rgb("--neutral-50"),
98
+ 100: rgb("--neutral-100"),
99
+ 200: rgb("--neutral-200"),
100
+ 300: rgb("--neutral-300"),
101
+ 400: rgb("--neutral-400"),
102
+ 500: rgb("--neutral-500"),
103
+ 600: rgb("--neutral-600"),
104
+ 700: rgb("--neutral-700"),
105
+ 800: rgb("--neutral-800"),
106
+ 900: rgb("--neutral-900"),
107
+ },
108
+ // Brand scales (use directly: bg-plum-600, text-plum-300, etc.)
109
+ plum: {
110
+ 50: rgb("--plum-50"),
111
+ 100: rgb("--plum-100"),
112
+ 200: rgb("--plum-200"),
113
+ 300: rgb("--plum-300"),
114
+ 400: rgb("--plum-400"),
115
+ 500: rgb("--plum-500"),
116
+ 600: rgb("--plum-600"),
117
+ 700: rgb("--plum-700"),
118
+ 800: rgb("--plum-800"),
119
+ 900: rgb("--plum-900"),
120
+ 950: rgb("--plum-950"),
121
+ },
122
+ },
123
+ fontFamily: {
124
+ sans: ["var(--font-sans)"],
125
+ mono: ["var(--font-mono)"],
126
+ },
127
+ borderRadius: {
128
+ lg: "var(--radius)",
129
+ md: "calc(var(--radius) - 2px)",
130
+ sm: "calc(var(--radius) - 4px)",
131
+ },
132
+ keyframes: {
133
+ "accordion-down": {
134
+ from: { height: "0" },
135
+ to: { height: "var(--radix-accordion-content-height)" },
136
+ },
137
+ "accordion-up": {
138
+ from: { height: "var(--radix-accordion-content-height)" },
139
+ to: { height: "0" },
140
+ },
141
+ },
142
+ animation: {
143
+ "accordion-down": "accordion-down 0.2s ease-out",
144
+ "accordion-up": "accordion-up 0.2s ease-out",
145
+ },
146
+ },
147
+ },
148
+ };
package/src/tokens.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Consuma design tokens — the single source of truth.
3
+ * JS/TS access (charts, canvas, emails, anywhere not Tailwind).
4
+ * CSS variables in styles.css are auto-generated from these values.
5
+ */
6
+
7
+ export const tokens = {
8
+ color: {
9
+ // Brand — plum scale
10
+ plum: {
11
+ 50: "#F5F1FB",
12
+ 100: "#E9E0F5",
13
+ 200: "#D2C0EB",
14
+ 300: "#B49AE0",
15
+ 400: "#9576CC",
16
+ 500: "#7A57BB",
17
+ 600: "#5B3AAE", // primary
18
+ 700: "#4A2D90",
19
+ 800: "#3D2470",
20
+ 900: "#2E1A4D",
21
+ 950: "#1B0F30",
22
+ },
23
+ // Neutrals — warm slate
24
+ neutral: {
25
+ 0: "#FFFFFF",
26
+ 50: "#FAFAF9",
27
+ 100: "#F4F3F1",
28
+ 200: "#E8E6E2",
29
+ 300: "#D4D1CB",
30
+ 400: "#A8A39A",
31
+ 500: "#78736A",
32
+ 600: "#5A5650",
33
+ 700: "#3F3C37",
34
+ 800: "#27251F",
35
+ 900: "#16140F",
36
+ },
37
+ // Status atoms (single hex)
38
+ success: "#1F8A5B",
39
+ warning: "#C77A0A",
40
+ danger: "#C0392B",
41
+ info: "#2A6FDB",
42
+ // Sentiment (data viz)
43
+ sentiment: {
44
+ positive: "#2E9E6E",
45
+ neutral: "#A8A39A",
46
+ negative: "#D9534F",
47
+ mixed: "#C77A0A",
48
+ },
49
+ // Categorical (data viz, 8-step)
50
+ chart: [
51
+ "#5B3AAE",
52
+ "#2A6FDB",
53
+ "#1F8A5B",
54
+ "#C77A0A",
55
+ "#C0392B",
56
+ "#7A57BB",
57
+ "#0E7490",
58
+ "#9D174D",
59
+ ],
60
+ // Tinted bg/fg pairs — used in transactional emails, badges, rails.
61
+ // Designed to render inline (Gmail-safe). Hand-tuned to match
62
+ // the dashboard's iterated palette so every surface stays consistent.
63
+ tint: {
64
+ brand: { bg: "#F5F1FB", fg: "#4A2D90", accent: "#9576CC", rail: "#B49AE0" },
65
+ success: { bg: "#E7F7EE", fg: "#1F7A52", accent: "#2E9E6E" },
66
+ warning: { bg: "#FEF3C7", fg: "#B45309", accent: "#F59E0B" },
67
+ danger: { bg: "#FBE9E8", fg: "#A6332F", accent: "#D9534F" },
68
+ info: { bg: "#E0F2FE", fg: "#0369A1", accent: "#0EA5E9" },
69
+ },
70
+ },
71
+
72
+ font: {
73
+ family: {
74
+ sans: '"Manrope", ui-sans-serif, system-ui, sans-serif',
75
+ mono: '"JetBrains Mono", ui-monospace, "SF Mono", monospace',
76
+ display: '"Geist", ui-sans-serif, system-ui, sans-serif',
77
+ },
78
+ // Sizes (numbers = px). Used inline in JS-rendered surfaces (emails).
79
+ size: {
80
+ xs: 10,
81
+ sm: 11,
82
+ md: 12,
83
+ base: 13,
84
+ lg: 14,
85
+ xl: 15,
86
+ "2xl": 17,
87
+ "3xl": 18,
88
+ "4xl": 20,
89
+ "5xl": 22,
90
+ "6xl": 26,
91
+ "7xl": 28,
92
+ "8xl": 32,
93
+ },
94
+ weight: {
95
+ regular: 400,
96
+ medium: 500,
97
+ semibold: 600,
98
+ bold: 700,
99
+ extrabold: 800,
100
+ },
101
+ tracking: {
102
+ tighter: "-0.025em",
103
+ tight: "-0.02em",
104
+ snug: "-0.01em",
105
+ normal: "0",
106
+ wide: "0.08em",
107
+ wider: "0.1em",
108
+ widest: "0.14em",
109
+ },
110
+ leading: {
111
+ tight: 1.2,
112
+ snug: 1.25,
113
+ base: 1.35,
114
+ relaxed: 1.5,
115
+ loose: 1.6,
116
+ },
117
+ },
118
+
119
+ // Radius
120
+ radius: {
121
+ none: "0",
122
+ sm: "0.375rem",
123
+ md: "0.625rem",
124
+ lg: "0.875rem",
125
+ xl: "1.125rem",
126
+ "2xl": "1.5rem",
127
+ full: "9999px",
128
+ },
129
+
130
+ // 4-pt spacing scale
131
+ space: {
132
+ 0: "0",
133
+ 1: "0.25rem",
134
+ 2: "0.5rem",
135
+ 3: "0.75rem",
136
+ 4: "1rem",
137
+ 5: "1.25rem",
138
+ 6: "1.5rem",
139
+ 8: "2rem",
140
+ 10: "2.5rem",
141
+ 12: "3rem",
142
+ 16: "4rem",
143
+ 20: "5rem",
144
+ 24: "6rem",
145
+ },
146
+
147
+ shadow: {
148
+ xs: "0 1px 2px 0 rgb(46 26 77 / 0.04)",
149
+ sm: "0 1px 3px 0 rgb(46 26 77 / 0.06), 0 1px 2px -1px rgb(46 26 77 / 0.06)",
150
+ md: "0 4px 6px -1px rgb(46 26 77 / 0.08), 0 2px 4px -2px rgb(46 26 77 / 0.06)",
151
+ lg: "0 10px 15px -3px rgb(46 26 77 / 0.10), 0 4px 6px -4px rgb(46 26 77 / 0.08)",
152
+ xl: "0 20px 25px -5px rgb(46 26 77 / 0.12), 0 8px 10px -6px rgb(46 26 77 / 0.08)",
153
+ focus: "0 0 0 3px rgb(91 58 174 / 0.35)",
154
+ cardEmail: "0 1px 2px rgba(15,13,30,0.04)",
155
+ },
156
+
157
+ motion: {
158
+ duration: { fast: "120ms", base: "200ms", slow: "320ms" },
159
+ ease: {
160
+ out: "cubic-bezier(0.22, 1, 0.36, 1)",
161
+ inOut: "cubic-bezier(0.65, 0, 0.35, 1)",
162
+ spring: "cubic-bezier(0.34, 1.56, 0.64, 1)",
163
+ },
164
+ },
165
+
166
+ // Brand gradients — referenced by emails / hero surfaces.
167
+ gradient: {
168
+ plumBar:
169
+ "linear-gradient(90deg,#4A2D90 0%,#9576CC 50%,#B49AE0 100%)",
170
+ plumDuo: "linear-gradient(135deg,#9576CC,#4A2D90)",
171
+ plumHeroSoft:
172
+ "radial-gradient(ellipse at 50% -20%,#E9DFF6 0%,#FFFFFF 55%)",
173
+ },
174
+ } as const;
175
+
176
+ export type Tokens = typeof tokens;