@seedgrid/fe-theme 0.3.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,184 @@
1
+ # @seedgrid/fe-theme
2
+
3
+ Sistema de temas do SeedGrid baseado em **seed color** com geração automática de paletas harmoniosas.
4
+
5
+ ## Características
6
+
7
+ - 🎨 **Geração automática de paletas** a partir de uma cor seed
8
+ - 🌓 **Dark/Light mode** com suporte a `auto` (detecta preferência do sistema)
9
+ - 📦 **Paletas completas** 50-900 para todas as cores (primary, secondary, tertiary, warning, error, info, success)
10
+ - 🎯 **Tokens de componentes** pré-configurados (botões, inputs, cards, etc.)
11
+ - 💾 **Persistência** de preferências no localStorage
12
+ - ⚡ **CSS Variables** para integração com Tailwind
13
+ - 🔄 **Troca dinâmica** de tema em runtime
14
+
15
+ ## Instalação
16
+
17
+ ```bash
18
+ pnpm add @seedgrid/fe-theme
19
+ ```
20
+
21
+ ## Uso Básico
22
+
23
+ ### 1. Configurar o Provider
24
+
25
+ ```tsx
26
+ import { SeedThemeProvider } from "@seedgrid/fe-theme";
27
+
28
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
29
+ return (
30
+ <html lang="pt-br">
31
+ <body className="bg-[rgb(var(--sg-bg))] text-[rgb(var(--sg-text))]">
32
+ <SeedThemeProvider
33
+ initialTheme={{
34
+ seed: "#16803D", // Verde SeedGrid
35
+ mode: "auto", // "light" | "dark" | "auto"
36
+ radius: 12,
37
+ persistMode: true,
38
+ }}
39
+ >
40
+ {children}
41
+ </SeedThemeProvider>
42
+ </body>
43
+ </html>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ### 2. Usar o Hook
49
+
50
+ ```tsx
51
+ "use client";
52
+
53
+ import { useSgTheme } from "@seedgrid/fe-theme";
54
+
55
+ export function ThemeToggle() {
56
+ const { setMode, currentMode, setTheme } = useSgTheme();
57
+
58
+ return (
59
+ <div className="flex gap-2">
60
+ <button onClick={() => setMode(currentMode === "light" ? "dark" : "light")}>
61
+ Toggle Mode ({currentMode})
62
+ </button>
63
+
64
+ <button onClick={() => setTheme({ seed: "#0EA5E9" })}>
65
+ Mudar para Azul
66
+ </button>
67
+ </div>
68
+ );
69
+ }
70
+ ```
71
+
72
+ ## CSS Variables Disponíveis
73
+
74
+ ### Cores Base
75
+ - `--sg-bg` - Background principal
76
+ - `--sg-surface` - Superfícies (cards, modais)
77
+ - `--sg-text` - Texto principal
78
+ - `--sg-muted` - Texto secundário
79
+ - `--sg-border` - Bordas
80
+ - `--sg-ring` - Focus ring
81
+
82
+ ### Paletas (50-900)
83
+ - `--sg-primary-{50-900}` - Cor primária
84
+ - `--sg-secondary-{50-900}` - Cor secundária
85
+ - `--sg-tertiary-{50-900}` - Cor terciária
86
+ - `--sg-warning-{50-900}` - Avisos
87
+ - `--sg-error-{50-900}` - Erros
88
+ - `--sg-info-{50-900}` - Informações
89
+ - `--sg-success-{50-900}` - Sucesso
90
+
91
+ ### Tokens de Componentes
92
+ - `--sg-btn-{variant}-bg` - Background de botões
93
+ - `--sg-input-border` - Borda de inputs
94
+ - `--sg-card-bg` - Background de cards
95
+ - E muitos outros...
96
+
97
+ ## Exemplos de Uso com Tailwind
98
+
99
+ ### Botão
100
+ ```tsx
101
+ <button className="
102
+ rounded-[var(--sg-radius)]
103
+ bg-[rgb(var(--sg-primary-600))]
104
+ text-[rgb(var(--sg-on-primary))]
105
+ hover:bg-[rgb(var(--sg-primary-700))]
106
+ active:bg-[rgb(var(--sg-primary-800))]
107
+ border border-[rgb(var(--sg-border))]
108
+ focus:outline-none focus:ring-2 focus:ring-[rgb(var(--sg-ring))]
109
+ px-4 py-2
110
+ ">
111
+ Salvar
112
+ </button>
113
+ ```
114
+
115
+ ### Card
116
+ ```tsx
117
+ <div className="
118
+ rounded-[var(--sg-radius)]
119
+ bg-[rgb(var(--sg-surface))]
120
+ border border-[rgb(var(--sg-border))]
121
+ p-4
122
+ ">
123
+ <h3 className="text-[rgb(var(--sg-text))] font-semibold">Título</h3>
124
+ <p className="text-[rgb(var(--sg-muted))]">Descrição</p>
125
+ </div>
126
+ ```
127
+
128
+ ### Alert
129
+ ```tsx
130
+ <div className="
131
+ rounded-[var(--sg-radius)]
132
+ bg-[rgb(var(--sg-warning-100))]
133
+ text-[rgb(var(--sg-warning-700))]
134
+ border border-[rgb(var(--sg-warning-300))]
135
+ p-4
136
+ ">
137
+ <strong>Atenção:</strong> Algo importante aconteceu.
138
+ </div>
139
+ ```
140
+
141
+ ## Customização Avançada
142
+
143
+ ### Sobrescrever Cores Semânticas
144
+ ```tsx
145
+ <SeedThemeProvider
146
+ initialTheme={{
147
+ seed: "#16803D",
148
+ warning: "#FF9800", // Laranja customizado
149
+ error: "#F44336", // Vermelho customizado
150
+ info: "#2196F3", // Azul customizado
151
+ success: "#4CAF50", // Verde customizado
152
+ }}
153
+ />
154
+ ```
155
+
156
+ ### Custom CSS Variables
157
+ ```tsx
158
+ <SeedThemeProvider
159
+ initialTheme={{
160
+ seed: "#16803D",
161
+ customVars: {
162
+ "--sg-radius": "8px",
163
+ "--sg-primary-600": "255 0 0", // RGB format
164
+ },
165
+ }}
166
+ />
167
+ ```
168
+
169
+ ## Migração do ThemeProvider Antigo
170
+
171
+ O provider antigo ainda está disponível para compatibilidade, mas está marcado como deprecated:
172
+
173
+ ```tsx
174
+ // ❌ Antigo (deprecated)
175
+ import { ThemeProvider, useTheme } from "@seedgrid/fe-theme";
176
+
177
+ // ✅ Novo (recomendado)
178
+ import { SeedThemeProvider, useSgTheme } from "@seedgrid/fe-theme";
179
+ ```
180
+
181
+ ## Licença
182
+
183
+ MIT
184
+
@@ -0,0 +1,5 @@
1
+ {
2
+ "theme.brand": "Marca",
3
+ "theme.colors": "Cores",
4
+ "theme.layout": "Layout"
5
+ }
@@ -0,0 +1,8 @@
1
+ export { themeManifest } from "./manifest";
2
+ export * from "./theme/ThemeConfig";
3
+ export * from "./theme/ThemeProvider";
4
+ export * from "./theme/colorUtils";
5
+ export * from "./theme/themeGenerator";
6
+ export * from "./theme/componentTokens";
7
+ export * from "./ui/AppShell";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { themeManifest } from "./manifest";
2
+ export * from "./theme/ThemeConfig";
3
+ export * from "./theme/ThemeProvider";
4
+ export * from "./theme/colorUtils";
5
+ export * from "./theme/themeGenerator";
6
+ export * from "./theme/componentTokens";
7
+ export * from "./ui/AppShell";
@@ -0,0 +1,3 @@
1
+ import type { SeedGridModuleManifest } from "@seedgrid/fe-core";
2
+ export declare const themeManifest: SeedGridModuleManifest;
3
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,eAAO,MAAM,aAAa,EAAE,sBAe3B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import ptBr from "./i18n/pt-BR.json";
2
+ export const themeManifest = {
3
+ id: "seedgrid-fe-theme",
4
+ name: "SeedGrid FE Theme",
5
+ version: "0.2.0",
6
+ i18n: {
7
+ defaultLocale: "pt-BR",
8
+ bundles: [
9
+ {
10
+ namespace: "theme",
11
+ resources: ptBr,
12
+ distPath: "dist/i18n/pt-BR.json"
13
+ }
14
+ ]
15
+ },
16
+ register() { }
17
+ };
@@ -0,0 +1,48 @@
1
+ import type { PersistenceStrategy } from "@seedgrid/fe-core";
2
+ export type Mode = "light" | "dark" | "auto";
3
+ export type SeedThemeInput = {
4
+ seed: string;
5
+ mode?: Mode;
6
+ radius?: number;
7
+ warning?: string;
8
+ error?: string;
9
+ info?: string;
10
+ success?: string;
11
+ customVars?: Record<string, string>;
12
+ persistMode?: boolean;
13
+ persistenceStrategy?: PersistenceStrategy;
14
+ };
15
+ export type ThemeVars = Record<string, string>;
16
+ export type ThemeContextValue = {
17
+ vars: ThemeVars;
18
+ setTheme: (next: Partial<SeedThemeInput>) => void;
19
+ setMode: (m: Exclude<Mode, "auto">) => void;
20
+ currentMode: "light" | "dark";
21
+ currentTheme: Pick<SeedThemeInput, "seed" | "mode" | "radius">;
22
+ };
23
+ export type SeedGridThemeConfig = {
24
+ brand: {
25
+ name: string;
26
+ logoUrl?: string;
27
+ };
28
+ colors: {
29
+ primary: string;
30
+ onPrimary: string;
31
+ secondary: string;
32
+ onSecondary: string;
33
+ tertiary: string;
34
+ onTertiary: string;
35
+ error: string;
36
+ onError: string;
37
+ accent: string;
38
+ background: string;
39
+ foreground: string;
40
+ };
41
+ layout: {
42
+ sidebarWidth: number;
43
+ radius: number;
44
+ };
45
+ };
46
+ export declare const defaultTheme: SeedGridThemeConfig;
47
+ export declare const defaultSeedTheme: SeedThemeInput;
48
+ //# sourceMappingURL=ThemeConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeConfig.d.ts","sourceRoot":"","sources":["../../src/theme/ThemeConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEpC,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAC5C,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,YAAY,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;CAChE,CAAC;AAGF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,MAAM,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,mBAmB1B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,cAK9B,CAAC"}
@@ -0,0 +1,26 @@
1
+ export const defaultTheme = {
2
+ brand: { name: "SeedGrid" },
3
+ colors: {
4
+ primary: "142 76% 36%",
5
+ onPrimary: "0 0% 100%",
6
+ secondary: "262 83% 58%",
7
+ onSecondary: "0 0% 100%",
8
+ tertiary: "173 80% 40%",
9
+ onTertiary: "0 0% 100%",
10
+ error: "0 65% 51%",
11
+ onError: "0 0% 100%",
12
+ accent: "152 57% 40%",
13
+ background: "0 0% 100%",
14
+ foreground: "222.2 84% 4.9%"
15
+ },
16
+ layout: {
17
+ sidebarWidth: 260,
18
+ radius: 12
19
+ }
20
+ };
21
+ export const defaultSeedTheme = {
22
+ seed: "#16803D", // Verde SeedGrid
23
+ mode: "light",
24
+ radius: 12,
25
+ persistMode: true
26
+ };
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import type { SeedThemeInput, ThemeContextValue } from "./ThemeConfig";
3
+ export declare function useSgTheme(): ThemeContextValue;
4
+ export declare function SeedThemeProvider({ initialTheme, children, applyTo, }: {
5
+ initialTheme?: SeedThemeInput;
6
+ children: React.ReactNode;
7
+ applyTo?: "html" | "wrapper";
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ import type { SeedGridThemeConfig } from "./ThemeConfig";
10
+ /**
11
+ * @deprecated Use SeedThemeProvider instead
12
+ */
13
+ export declare function ThemeProvider(props: {
14
+ children: React.ReactNode;
15
+ theme?: Partial<SeedGridThemeConfig>;
16
+ }): import("react/jsx-runtime").JSX.Element;
17
+ /**
18
+ * @deprecated Use useSgTheme instead
19
+ */
20
+ export declare function useTheme(): SeedGridThemeConfig;
21
+ //# sourceMappingURL=ThemeProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/ThemeProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAQ,MAAM,eAAe,CAAC;AAW7E,wBAAgB,UAAU,sBAIzB;AAaD,wBAAgB,iBAAiB,CAAC,EAChC,YAAY,EACZ,QAAQ,EACR,OAAgB,GACjB,EAAE;IACD,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B,2CA6KA;AAID,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAsDzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAA;CAAE,2CA2DvG;AAED;;GAEG;AACH,wBAAgB,QAAQ,wBAIvB"}
@@ -0,0 +1,278 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { defaultSeedTheme } from "./ThemeConfig";
5
+ import { generateThemeVars, getSystemMode } from "./themeGenerator";
6
+ import { generateComponentTokens } from "./componentTokens";
7
+ import { createLocalStorageStrategy } from "@seedgrid/fe-core";
8
+ /* ------------- React Provider ------------- */
9
+ const ThemeContext = React.createContext(null);
10
+ export function useSgTheme() {
11
+ const ctx = React.useContext(ThemeContext);
12
+ if (!ctx)
13
+ throw new Error("useSgTheme must be used within SeedThemeProvider");
14
+ return ctx;
15
+ }
16
+ /** Try sync load from strategy; returns null for async strategies */
17
+ function trySyncLoad(strategy, key) {
18
+ try {
19
+ const result = strategy.load(key);
20
+ if (result instanceof Promise)
21
+ return null;
22
+ return result;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export function SeedThemeProvider({ initialTheme, children, applyTo = "html", }) {
29
+ const persistedModeKey = "sg:theme:mode";
30
+ const persistedThemeKey = "sg:theme:config";
31
+ // Strategy is determined once from initialTheme prop
32
+ const strategy = React.useMemo(() => initialTheme?.persistenceStrategy ?? createLocalStorageStrategy(), [initialTheme?.persistenceStrategy]);
33
+ // Try to load persisted theme (sync for localStorage, null for async)
34
+ const getPersistedTheme = React.useCallback(() => {
35
+ if (typeof window === "undefined")
36
+ return null;
37
+ const stored = trySyncLoad(strategy, persistedThemeKey);
38
+ if (stored && typeof stored.seed === "string") {
39
+ return stored;
40
+ }
41
+ return null;
42
+ }, [strategy]);
43
+ // Merge persisted theme with initial theme (persisted takes precedence)
44
+ const mergedInitialTheme = React.useMemo(() => {
45
+ const persisted = getPersistedTheme();
46
+ const base = initialTheme ?? defaultSeedTheme;
47
+ if (persisted) {
48
+ // Merge: persisted overrides initial, but keep persistMode from initial
49
+ return {
50
+ ...base,
51
+ ...persisted,
52
+ persistMode: base.persistMode, // Keep persistMode from initial config
53
+ persistenceStrategy: base.persistenceStrategy, // Keep strategy from initial config
54
+ };
55
+ }
56
+ return base;
57
+ }, [initialTheme, getPersistedTheme]);
58
+ // Resolve initial mode: if auto, detect system (or persisted mode)
59
+ const initialResolvedMode = React.useMemo(() => {
60
+ const theme = mergedInitialTheme;
61
+ const m = theme.mode ?? "light";
62
+ if (m === "auto") {
63
+ // If persisted, prefer it
64
+ const persisted = typeof window !== "undefined" ? trySyncLoad(strategy, persistedModeKey) : null;
65
+ if (persisted === "light" || persisted === "dark")
66
+ return persisted;
67
+ return getSystemMode();
68
+ }
69
+ return m;
70
+ }, [mergedInitialTheme, strategy]);
71
+ const [mode, setModeState] = React.useState(initialResolvedMode);
72
+ const [themeInput, setThemeInput] = React.useState(mergedInitialTheme);
73
+ // Async hydration for async strategies (e.g. API)
74
+ React.useEffect(() => {
75
+ let alive = true;
76
+ const themeResult = strategy.load(persistedThemeKey);
77
+ const modeResult = strategy.load(persistedModeKey);
78
+ // Only run async hydration if either load returns a Promise
79
+ if (!(themeResult instanceof Promise) && !(modeResult instanceof Promise))
80
+ return;
81
+ (async () => {
82
+ try {
83
+ const [loadedTheme, loadedMode] = await Promise.all([
84
+ Promise.resolve(themeResult),
85
+ Promise.resolve(modeResult),
86
+ ]);
87
+ if (!alive)
88
+ return;
89
+ if (loadedTheme && typeof loadedTheme.seed === "string") {
90
+ setThemeInput((prev) => ({
91
+ ...prev,
92
+ ...loadedTheme,
93
+ persistMode: prev.persistMode,
94
+ persistenceStrategy: prev.persistenceStrategy,
95
+ }));
96
+ }
97
+ if (loadedMode === "light" || loadedMode === "dark") {
98
+ setModeState(loadedMode);
99
+ }
100
+ }
101
+ catch { }
102
+ })();
103
+ return () => { alive = false; };
104
+ }, [strategy]);
105
+ const vars = React.useMemo(() => {
106
+ const baseVars = generateThemeVars(themeInput, mode);
107
+ const componentVars = generateComponentTokens(baseVars);
108
+ return { ...baseVars, ...componentVars };
109
+ }, [themeInput, mode]);
110
+ // Apply CSS vars
111
+ React.useEffect(() => {
112
+ const el = applyTo === "html" ? document.documentElement : null;
113
+ if (!el)
114
+ return;
115
+ for (const [k, v] of Object.entries(vars))
116
+ el.style.setProperty(k, v);
117
+ el.classList.toggle("dark", mode === "dark");
118
+ el.setAttribute("data-theme", mode);
119
+ el.style.colorScheme = mode;
120
+ }, [vars, applyTo, mode]);
121
+ // Optionally persist mode and theme
122
+ React.useEffect(() => {
123
+ if (themeInput.persistMode) {
124
+ const themeToStore = {
125
+ seed: themeInput.seed,
126
+ mode: themeInput.mode,
127
+ radius: themeInput.radius,
128
+ };
129
+ void Promise.resolve(strategy.save(persistedModeKey, mode)).catch(() => { });
130
+ void Promise.resolve(strategy.save(persistedThemeKey, themeToStore)).catch(() => { });
131
+ }
132
+ }, [mode, themeInput, strategy, persistedModeKey, persistedThemeKey]);
133
+ // Listen for system changes when initial mode = auto
134
+ React.useEffect(() => {
135
+ if (themeInput.mode !== "auto")
136
+ return;
137
+ if (typeof window === "undefined")
138
+ return;
139
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
140
+ const handler = (ev) => setModeState(ev.matches ? "dark" : "light");
141
+ mq.addEventListener ? mq.addEventListener("change", handler) : mq.addListener(handler);
142
+ return () => {
143
+ mq.removeEventListener ? mq.removeEventListener("change", handler) : mq.removeListener(handler);
144
+ };
145
+ }, [themeInput.mode]);
146
+ const setTheme = React.useCallback((next) => {
147
+ setThemeInput((prev) => ({ ...prev, ...next }));
148
+ // If next.mode is explicit light/dark set it too
149
+ if (next.mode && next.mode !== "auto")
150
+ setModeState(next.mode);
151
+ else if (next.mode === "auto")
152
+ setModeState(getSystemMode());
153
+ }, []);
154
+ const setMode = React.useCallback((m) => {
155
+ setModeState(m);
156
+ if (themeInput.persistMode) {
157
+ void Promise.resolve(strategy.save(persistedModeKey, m)).catch(() => { });
158
+ }
159
+ }, [themeInput.persistMode, strategy, persistedModeKey]);
160
+ // Export currentMode normalized (no "auto")
161
+ const currentMode = mode;
162
+ const currentTheme = React.useMemo(() => ({
163
+ seed: themeInput.seed,
164
+ mode: themeInput.mode,
165
+ radius: themeInput.radius,
166
+ }), [themeInput.seed, themeInput.mode, themeInput.radius]);
167
+ if (applyTo === "wrapper") {
168
+ return (_jsx(ThemeContext.Provider, { value: { vars, setTheme, setMode, currentMode, currentTheme }, children: _jsx("div", { className: mode === "dark" ? "dark" : undefined, "data-theme": mode, style: {
169
+ ...Object.fromEntries(Object.entries(vars).map(([k, v]) => [k, v])),
170
+ colorScheme: mode
171
+ }, children: children }) }));
172
+ }
173
+ return (_jsx(ThemeContext.Provider, { value: { vars, setTheme, setMode, currentMode, currentTheme }, children: children }));
174
+ }
175
+ import { defaultTheme } from "./ThemeConfig";
176
+ const LegacyThemeContext = React.createContext(null);
177
+ function hexToHsl(hex) {
178
+ const cleaned = hex.replace("#", "").trim();
179
+ const full = cleaned.length === 3
180
+ ? cleaned.split("").map((c) => c + c).join("")
181
+ : cleaned.padEnd(6, "0");
182
+ const r = parseInt(full.slice(0, 2), 16) / 255;
183
+ const g = parseInt(full.slice(2, 4), 16) / 255;
184
+ const b = parseInt(full.slice(4, 6), 16) / 255;
185
+ const max = Math.max(r, g, b);
186
+ const min = Math.min(r, g, b);
187
+ let h = 0;
188
+ let s = 0;
189
+ const l = (max + min) / 2;
190
+ if (max !== min) {
191
+ const d = max - min;
192
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
193
+ switch (max) {
194
+ case r:
195
+ h = (g - b) / d + (g < b ? 6 : 0);
196
+ break;
197
+ case g:
198
+ h = (b - r) / d + 2;
199
+ break;
200
+ default:
201
+ h = (r - g) / d + 4;
202
+ }
203
+ h /= 6;
204
+ }
205
+ const hDeg = Math.round(h * 360);
206
+ const sPct = Math.round(s * 100);
207
+ const lPct = Math.round(l * 100);
208
+ return `${hDeg} ${sPct}% ${lPct}%`;
209
+ }
210
+ function normalizeHsl(value) {
211
+ const trimmed = value.trim();
212
+ if (trimmed.startsWith("#"))
213
+ return hexToHsl(trimmed);
214
+ if (trimmed.startsWith("hsl")) {
215
+ return trimmed.replace(/hsl\(|\)/g, "").replace(/,/g, " ").replace(/\s+/g, " ").trim();
216
+ }
217
+ return trimmed;
218
+ }
219
+ /**
220
+ * @deprecated Use SeedThemeProvider instead
221
+ */
222
+ export function ThemeProvider(props) {
223
+ const theme = React.useMemo(() => {
224
+ const merged = {
225
+ ...defaultTheme,
226
+ ...props.theme,
227
+ brand: { ...defaultTheme.brand, ...(props.theme?.brand ?? {}) },
228
+ colors: { ...defaultTheme.colors, ...(props.theme?.colors ?? {}) },
229
+ layout: { ...defaultTheme.layout, ...(props.theme?.layout ?? {}) }
230
+ };
231
+ return {
232
+ ...merged,
233
+ colors: {
234
+ primary: normalizeHsl(merged.colors.primary),
235
+ onPrimary: normalizeHsl(merged.colors.onPrimary),
236
+ secondary: normalizeHsl(merged.colors.secondary),
237
+ onSecondary: normalizeHsl(merged.colors.onSecondary),
238
+ tertiary: normalizeHsl(merged.colors.tertiary),
239
+ onTertiary: normalizeHsl(merged.colors.onTertiary),
240
+ error: normalizeHsl(merged.colors.error),
241
+ onError: normalizeHsl(merged.colors.onError),
242
+ accent: normalizeHsl(merged.colors.accent),
243
+ background: normalizeHsl(merged.colors.background),
244
+ foreground: normalizeHsl(merged.colors.foreground)
245
+ }
246
+ };
247
+ }, [props.theme]);
248
+ return (_jsx(LegacyThemeContext.Provider, { value: { theme }, children: _jsx("div", { style: {
249
+ "--background": theme.colors.background,
250
+ "--foreground": theme.colors.foreground,
251
+ "--primary": theme.colors.primary,
252
+ "--primary-foreground": theme.colors.onPrimary,
253
+ "--secondary": theme.colors.secondary,
254
+ "--secondary-foreground": theme.colors.onSecondary,
255
+ "--tertiary": theme.colors.tertiary,
256
+ "--tertiary-foreground": theme.colors.onTertiary,
257
+ "--accent": theme.colors.accent,
258
+ "--destructive": theme.colors.error,
259
+ "--destructive-foreground": theme.colors.onError,
260
+ "--ring": theme.colors.primary,
261
+ "--radius": `${theme.layout.radius}px`,
262
+ "--sg-primary": `hsl(${theme.colors.primary})`,
263
+ "--sg-accent": `hsl(${theme.colors.accent})`,
264
+ "--sg-bg": `hsl(${theme.colors.background})`,
265
+ "--sg-fg": `hsl(${theme.colors.foreground})`,
266
+ "--sg-radius": `${theme.layout.radius}px`,
267
+ "--sg-sidebar": `${theme.layout.sidebarWidth}px`,
268
+ }, children: props.children }) }));
269
+ }
270
+ /**
271
+ * @deprecated Use useSgTheme instead
272
+ */
273
+ export function useTheme() {
274
+ const ctx = React.useContext(LegacyThemeContext);
275
+ if (!ctx)
276
+ throw new Error("useTheme must be used inside ThemeProvider");
277
+ return ctx.theme;
278
+ }
@@ -0,0 +1,27 @@
1
+ export declare function clamp(n: number, a: number, b: number): number;
2
+ export declare function hexToRgb(hex: string): {
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ };
7
+ export declare function rgbToHex(r: number, g: number, b: number): string;
8
+ export declare function rgbToHsl(r: number, g: number, b: number): {
9
+ h: number;
10
+ s: number;
11
+ l: number;
12
+ };
13
+ export declare function hslToRgb(h: number, s: number, l: number): {
14
+ r: number;
15
+ g: number;
16
+ b: number;
17
+ };
18
+ export declare function hslToHex(h: number, s: number, l: number): string;
19
+ export declare function relativeLuminance(hex: string): number;
20
+ export declare function pickOnColor(bgHex: string): "#0B0B0C" | "#FFFFFF";
21
+ export declare function toRgbVarValue(hex: string): string;
22
+ export declare function shiftHue(hex: string, dh: number, targetS?: number, targetL?: number): string;
23
+ export declare function buildScaleFromHex(baseHex: string, resolvedMode: "light" | "dark", opts?: {
24
+ boostS?: number;
25
+ biasL?: number;
26
+ }): Record<number, string>;
27
+ //# sourceMappingURL=colorUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colorUtils.d.ts","sourceRoot":"","sources":["../../src/theme/colorUtils.ts"],"names":[],"mappings":"AAEA,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAEpD;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM;;;;EAOnC;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAGvD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;;;;EAiBvD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;;;;EAiBvD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAGvD;AAGD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,UAM5C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,yBAGxC;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,UAGxC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,UAOnF;AAKD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,GAAG,MAAM,EAC9B,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,0BAqC3C"}
@@ -0,0 +1,147 @@
1
+ /* ------------- Color conversion utilities ------------- */
2
+ export function clamp(n, a, b) {
3
+ return Math.max(a, Math.min(b, n));
4
+ }
5
+ export function hexToRgb(hex) {
6
+ const h = hex.replace("#", "").trim();
7
+ if (!/^[0-9a-fA-F]{6}$/.test(h))
8
+ throw new Error(`Invalid hex: ${hex}`);
9
+ const r = parseInt(h.slice(0, 2), 16);
10
+ const g = parseInt(h.slice(2, 4), 16);
11
+ const b = parseInt(h.slice(4, 6), 16);
12
+ return { r, g, b };
13
+ }
14
+ export function rgbToHex(r, g, b) {
15
+ const toHex = (v) => clamp(Math.round(v), 0, 255).toString(16).padStart(2, "0");
16
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
17
+ }
18
+ export function rgbToHsl(r, g, b) {
19
+ r /= 255;
20
+ g /= 255;
21
+ b /= 255;
22
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
23
+ let h = 0, s = 0;
24
+ const l = (max + min) / 2;
25
+ const d = max - min;
26
+ if (d !== 0) {
27
+ s = d / (1 - Math.abs(2 * l - 1));
28
+ switch (max) {
29
+ case r:
30
+ h = ((g - b) / d) % 6;
31
+ break;
32
+ case g:
33
+ h = (b - r) / d + 2;
34
+ break;
35
+ case b:
36
+ h = (r - g) / d + 4;
37
+ break;
38
+ }
39
+ h *= 60;
40
+ if (h < 0)
41
+ h += 360;
42
+ }
43
+ return { h, s: s * 100, l: l * 100 };
44
+ }
45
+ export function hslToRgb(h, s, l) {
46
+ s /= 100;
47
+ l /= 100;
48
+ const c = (1 - Math.abs(2 * l - 1)) * s;
49
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
50
+ const m = l - c / 2;
51
+ let rp = 0, gp = 0, bp = 0;
52
+ if (0 <= h && h < 60) {
53
+ rp = c;
54
+ gp = x;
55
+ bp = 0;
56
+ }
57
+ else if (60 <= h && h < 120) {
58
+ rp = x;
59
+ gp = c;
60
+ bp = 0;
61
+ }
62
+ else if (120 <= h && h < 180) {
63
+ rp = 0;
64
+ gp = c;
65
+ bp = x;
66
+ }
67
+ else if (180 <= h && h < 240) {
68
+ rp = 0;
69
+ gp = x;
70
+ bp = c;
71
+ }
72
+ else if (240 <= h && h < 300) {
73
+ rp = x;
74
+ gp = 0;
75
+ bp = c;
76
+ }
77
+ else {
78
+ rp = c;
79
+ gp = 0;
80
+ bp = x;
81
+ }
82
+ return {
83
+ r: (rp + m) * 255,
84
+ g: (gp + m) * 255,
85
+ b: (bp + m) * 255,
86
+ };
87
+ }
88
+ export function hslToHex(h, s, l) {
89
+ const { r, g, b } = hslToRgb(h, s, l);
90
+ return rgbToHex(r, g, b);
91
+ }
92
+ // Relative luminance (approx) to decide black/white text
93
+ export function relativeLuminance(hex) {
94
+ const { r, g, b } = hexToRgb(hex);
95
+ const srgb = [r, g, b].map((v) => v / 255).map((c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
96
+ return 0.2126 * (srgb[0] ?? 0) + 0.7152 * (srgb[1] ?? 0) + 0.0722 * (srgb[2] ?? 0);
97
+ }
98
+ export function pickOnColor(bgHex) {
99
+ // Simple and efficient: threshold ~0.5
100
+ return relativeLuminance(bgHex) > 0.5 ? "#0B0B0C" : "#FFFFFF";
101
+ }
102
+ export function toRgbVarValue(hex) {
103
+ const { r, g, b } = hexToRgb(hex);
104
+ return `${r} ${g} ${b}`; // <- ideal format for Tailwind rgb(var(--x)/alpha)
105
+ }
106
+ export function shiftHue(hex, dh, targetS, targetL) {
107
+ const { r, g, b } = hexToRgb(hex);
108
+ const { h, s, l } = rgbToHsl(r, g, b);
109
+ const nh = (h + dh + 360) % 360;
110
+ const ns = targetS ?? s;
111
+ const nl = targetL ?? l;
112
+ return hslToHex(nh, clamp(ns, 0, 100), clamp(nl, 0, 100));
113
+ }
114
+ /* ------------- Palette generation ------------- */
115
+ // Scale 50..900 "beautiful" (harmonizes in HSL, with L varying by stop)
116
+ export function buildScaleFromHex(baseHex, resolvedMode, opts) {
117
+ const { r, g, b } = hexToRgb(baseHex);
118
+ const { h, s, l } = rgbToHsl(r, g, b);
119
+ // stop -> Lightness target (lighter at 50, darker at 900)
120
+ // For dark mode, we still want a "useful" scale (50 is less "white").
121
+ const L_LIGHT = { 50: 96, 100: 92, 200: 86, 300: 78, 400: 68, 500: 56, 600: 48, 700: 40, 800: 32, 900: 24 };
122
+ const L_DARK = { 50: 22, 100: 28, 200: 34, 300: 40, 400: 48, 500: 56, 600: 64, 700: 72, 800: 80, 900: 88 };
123
+ const stops = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
124
+ const Lmap = resolvedMode === "light" ? L_LIGHT : L_DARK;
125
+ // Saturation adjusts slightly: less at extremes, more in the middle
126
+ const Smap = {
127
+ 50: s * 0.25,
128
+ 100: s * 0.35,
129
+ 200: s * 0.5,
130
+ 300: s * 0.7,
131
+ 400: s * 0.85,
132
+ 500: s * 1.0,
133
+ 600: s * 1.05,
134
+ 700: s * 1.05,
135
+ 800: s * 0.95,
136
+ 900: s * 0.85,
137
+ };
138
+ const boostS = opts?.boostS ?? 0;
139
+ const biasL = opts?.biasL ?? 0;
140
+ const out = {};
141
+ for (const stop of stops) {
142
+ const targetL = clamp((Lmap[stop] ?? 50) + biasL, 0, 100);
143
+ const targetS = clamp((Smap[stop] ?? s) + boostS, 0, 100);
144
+ out[stop] = hslToHex(h, targetS, targetL);
145
+ }
146
+ return out;
147
+ }
@@ -0,0 +1,8 @@
1
+ import type { ThemeVars } from "./ThemeConfig";
2
+ /**
3
+ * Generate component-level tokens from base palette tokens.
4
+ * This creates semantic aliases like --sg-btn-primary-bg, --sg-input-border, etc.
5
+ * so components can use consistent tokens without choosing specific stops (600/700) manually.
6
+ */
7
+ export declare function generateComponentTokens(baseVars: ThemeVars): ThemeVars;
8
+ //# sourceMappingURL=componentTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"componentTokens.d.ts","sourceRoot":"","sources":["../../src/theme/componentTokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,CAyItE"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Generate component-level tokens from base palette tokens.
3
+ * This creates semantic aliases like --sg-btn-primary-bg, --sg-input-border, etc.
4
+ * so components can use consistent tokens without choosing specific stops (600/700) manually.
5
+ */
6
+ export function generateComponentTokens(baseVars) {
7
+ const componentVars = {};
8
+ // Helper to wrap RGB values
9
+ const rgb = (varName) => {
10
+ const value = baseVars[varName];
11
+ return value ? `rgb(${value})` : "";
12
+ };
13
+ // Button tokens
14
+ // Primary button
15
+ componentVars["--sg-btn-primary-bg"] = rgb("--sg-primary-600");
16
+ componentVars["--sg-btn-primary-fg"] = rgb("--sg-on-primary");
17
+ componentVars["--sg-btn-primary-border"] = rgb("--sg-primary-600");
18
+ componentVars["--sg-btn-primary-hover-bg"] = rgb("--sg-primary-700");
19
+ componentVars["--sg-btn-primary-active-bg"] = rgb("--sg-primary-800");
20
+ componentVars["--sg-btn-primary-ring"] = rgb("--sg-primary-400");
21
+ // Secondary button
22
+ componentVars["--sg-btn-secondary-bg"] = rgb("--sg-secondary-600");
23
+ componentVars["--sg-btn-secondary-fg"] = rgb("--sg-on-secondary");
24
+ componentVars["--sg-btn-secondary-border"] = rgb("--sg-secondary-600");
25
+ componentVars["--sg-btn-secondary-hover-bg"] = rgb("--sg-secondary-700");
26
+ componentVars["--sg-btn-secondary-active-bg"] = rgb("--sg-secondary-800");
27
+ componentVars["--sg-btn-secondary-ring"] = rgb("--sg-secondary-400");
28
+ // Success button
29
+ componentVars["--sg-btn-success-bg"] = rgb("--sg-success-600");
30
+ componentVars["--sg-btn-success-fg"] = rgb("--sg-on-success");
31
+ componentVars["--sg-btn-success-border"] = rgb("--sg-success-600");
32
+ componentVars["--sg-btn-success-hover-bg"] = rgb("--sg-success-700");
33
+ componentVars["--sg-btn-success-active-bg"] = rgb("--sg-success-800");
34
+ componentVars["--sg-btn-success-ring"] = rgb("--sg-success-400");
35
+ // Info button
36
+ componentVars["--sg-btn-info-bg"] = rgb("--sg-info-600");
37
+ componentVars["--sg-btn-info-fg"] = rgb("--sg-on-info");
38
+ componentVars["--sg-btn-info-border"] = rgb("--sg-info-600");
39
+ componentVars["--sg-btn-info-hover-bg"] = rgb("--sg-info-700");
40
+ componentVars["--sg-btn-info-active-bg"] = rgb("--sg-info-800");
41
+ componentVars["--sg-btn-info-ring"] = rgb("--sg-info-400");
42
+ // Warning button
43
+ componentVars["--sg-btn-warning-bg"] = rgb("--sg-warning-600");
44
+ componentVars["--sg-btn-warning-fg"] = rgb("--sg-on-warning");
45
+ componentVars["--sg-btn-warning-border"] = rgb("--sg-warning-600");
46
+ componentVars["--sg-btn-warning-hover-bg"] = rgb("--sg-warning-700");
47
+ componentVars["--sg-btn-warning-active-bg"] = rgb("--sg-warning-800");
48
+ componentVars["--sg-btn-warning-ring"] = rgb("--sg-warning-400");
49
+ // Danger/Error button
50
+ componentVars["--sg-btn-danger-bg"] = rgb("--sg-error-600");
51
+ componentVars["--sg-btn-danger-fg"] = rgb("--sg-on-error");
52
+ componentVars["--sg-btn-danger-border"] = rgb("--sg-error-600");
53
+ componentVars["--sg-btn-danger-hover-bg"] = rgb("--sg-error-700");
54
+ componentVars["--sg-btn-danger-active-bg"] = rgb("--sg-error-800");
55
+ componentVars["--sg-btn-danger-ring"] = rgb("--sg-error-400");
56
+ // Help button (uses tertiary color for better visibility)
57
+ componentVars["--sg-btn-help-bg"] = rgb("--sg-tertiary-600");
58
+ componentVars["--sg-btn-help-fg"] = rgb("--sg-on-tertiary");
59
+ componentVars["--sg-btn-help-border"] = rgb("--sg-tertiary-600");
60
+ componentVars["--sg-btn-help-hover-bg"] = rgb("--sg-tertiary-700");
61
+ componentVars["--sg-btn-help-active-bg"] = rgb("--sg-tertiary-800");
62
+ componentVars["--sg-btn-help-ring"] = rgb("--sg-tertiary-400");
63
+ // Plain/Ghost button
64
+ componentVars["--sg-btn-plain-bg"] = "transparent";
65
+ componentVars["--sg-btn-plain-fg"] = rgb("--sg-text");
66
+ componentVars["--sg-btn-plain-border"] = rgb("--sg-border");
67
+ componentVars["--sg-btn-plain-hover-bg"] = rgb("--sg-muted-surface");
68
+ componentVars["--sg-btn-plain-active-bg"] = rgb("--sg-border");
69
+ componentVars["--sg-btn-plain-ring"] = rgb("--sg-ring");
70
+ // Input tokens
71
+ componentVars["--sg-input-bg"] = rgb("--sg-surface");
72
+ componentVars["--sg-input-fg"] = rgb("--sg-text");
73
+ componentVars["--sg-input-border"] = rgb("--sg-border");
74
+ componentVars["--sg-input-border-hover"] = rgb("--sg-primary-400");
75
+ componentVars["--sg-input-border-focus"] = rgb("--sg-primary-600");
76
+ componentVars["--sg-input-ring"] = rgb("--sg-ring");
77
+ componentVars["--sg-input-placeholder"] = rgb("--sg-muted");
78
+ componentVars["--sg-input-disabled-bg"] = rgb("--sg-disabled");
79
+ componentVars["--sg-input-disabled-fg"] = rgb("--sg-on-disabled");
80
+ // Card tokens
81
+ componentVars["--sg-card-bg"] = rgb("--sg-surface");
82
+ componentVars["--sg-card-fg"] = rgb("--sg-text");
83
+ componentVars["--sg-card-border"] = rgb("--sg-border");
84
+ componentVars["--sg-card-header-bg"] = rgb("--sg-muted-surface");
85
+ // Alert/Banner tokens
86
+ componentVars["--sg-alert-info-bg"] = rgb("--sg-info-100");
87
+ componentVars["--sg-alert-info-fg"] = rgb("--sg-info-700");
88
+ componentVars["--sg-alert-info-border"] = rgb("--sg-info-300");
89
+ componentVars["--sg-alert-success-bg"] = rgb("--sg-success-100");
90
+ componentVars["--sg-alert-success-fg"] = rgb("--sg-success-700");
91
+ componentVars["--sg-alert-success-border"] = rgb("--sg-success-300");
92
+ componentVars["--sg-alert-warning-bg"] = rgb("--sg-warning-100");
93
+ componentVars["--sg-alert-warning-fg"] = rgb("--sg-warning-700");
94
+ componentVars["--sg-alert-warning-border"] = rgb("--sg-warning-300");
95
+ componentVars["--sg-alert-error-bg"] = rgb("--sg-error-100");
96
+ componentVars["--sg-alert-error-fg"] = rgb("--sg-error-700");
97
+ componentVars["--sg-alert-error-border"] = rgb("--sg-error-300");
98
+ // Badge tokens
99
+ componentVars["--sg-badge-bg"] = rgb("--sg-badge");
100
+ componentVars["--sg-badge-fg"] = rgb("--sg-on-badge");
101
+ // Tooltip tokens
102
+ componentVars["--sg-tooltip-bg"] = rgb("--sg-tooltip");
103
+ componentVars["--sg-tooltip-fg"] = rgb("--sg-on-tooltip");
104
+ // Modal/Dialog tokens
105
+ componentVars["--sg-modal-bg"] = rgb("--sg-surface");
106
+ componentVars["--sg-modal-fg"] = rgb("--sg-text");
107
+ componentVars["--sg-modal-overlay"] = "rgb(0 0 0)";
108
+ // Dropdown/Menu tokens
109
+ componentVars["--sg-menu-bg"] = rgb("--sg-surface");
110
+ componentVars["--sg-menu-fg"] = rgb("--sg-text");
111
+ componentVars["--sg-menu-border"] = rgb("--sg-border");
112
+ componentVars["--sg-menu-item-hover-bg"] = rgb("--sg-muted-surface");
113
+ componentVars["--sg-menu-item-active-bg"] = rgb("--sg-primary-100");
114
+ // Table tokens
115
+ componentVars["--sg-table-bg"] = rgb("--sg-surface");
116
+ componentVars["--sg-table-fg"] = rgb("--sg-text");
117
+ componentVars["--sg-table-border"] = rgb("--sg-border");
118
+ componentVars["--sg-table-header-bg"] = rgb("--sg-muted-surface");
119
+ componentVars["--sg-table-row-hover-bg"] = rgb("--sg-muted-surface");
120
+ componentVars["--sg-table-row-selected-bg"] = rgb("--sg-primary-100");
121
+ return componentVars;
122
+ }
@@ -0,0 +1,4 @@
1
+ import type { SeedThemeInput, ThemeVars } from "./ThemeConfig";
2
+ export declare function getSystemMode(): "light" | "dark";
3
+ export declare function generateThemeVars(input: SeedThemeInput, resolvedMode: "light" | "dark"): ThemeVars;
4
+ //# sourceMappingURL=themeGenerator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themeGenerator.d.ts","sourceRoot":"","sources":["../../src/theme/themeGenerator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAwB/D,wBAAgB,aAAa,IAAI,OAAO,GAAG,MAAM,CAKhD;AAUD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAiJlG"}
@@ -0,0 +1,159 @@
1
+ import { buildScaleFromHex, hexToRgb, pickOnColor, rgbToHsl, shiftHue, toRgbVarValue, hslToHex, } from "./colorUtils";
2
+ /* ------------- Semantic color generation ------------- */
3
+ // Semantic colors use FIXED hues to maintain universal UI/UX conventions:
4
+ // - Danger/Error = Red (recognizable as danger)
5
+ // - Warning = Yellow/Orange (recognizable as warning)
6
+ // - Success = Green (recognizable as success)
7
+ // - Info = Blue (recognizable as information)
8
+ function buildSemanticBaseFromHue(mode, hue) {
9
+ const saturation = mode === "light" ? 85 : 80;
10
+ const lightness = mode === "light" ? 48 : 56;
11
+ return hslToHex(hue, saturation, lightness);
12
+ }
13
+ export function getSystemMode() {
14
+ if (typeof window === "undefined")
15
+ return "light";
16
+ return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
17
+ ? "dark"
18
+ : "light";
19
+ }
20
+ function toHslVarValue(hex) {
21
+ const { r, g, b } = hexToRgb(hex);
22
+ const { h, s, l } = rgbToHsl(r, g, b);
23
+ return `${Math.round(h)} ${Math.round(s)}% ${Math.round(l)}%`;
24
+ }
25
+ /* ------------- Main theme generator ------------- */
26
+ export function generateThemeVars(input, resolvedMode) {
27
+ const seed = input.seed;
28
+ // Base palette sources
29
+ const primaryBase = seed;
30
+ // Derive secondary/tertiary via hue shift (harmonious)
31
+ const secondaryBase = shiftHue(seed, +35, 72, resolvedMode === "light" ? 48 : 54);
32
+ const tertiaryBase = shiftHue(seed, +210, 62, resolvedMode === "light" ? 52 : 60);
33
+ // Semantic bases (allow override) - FIXED hues for universal recognition
34
+ const warningBase = input.warning ?? buildSemanticBaseFromHue(resolvedMode, 45); // yellow/orange
35
+ const errorBase = input.error ?? buildSemanticBaseFromHue(resolvedMode, 5); // red
36
+ const infoBase = input.info ?? buildSemanticBaseFromHue(resolvedMode, 210); // blue
37
+ const successBase = input.success ?? buildSemanticBaseFromHue(resolvedMode, 145); // green
38
+ // Build scales
39
+ const primary = buildScaleFromHex(primaryBase, resolvedMode, { boostS: 4 });
40
+ const secondary = buildScaleFromHex(secondaryBase, resolvedMode, { boostS: 2 });
41
+ const tertiary = buildScaleFromHex(tertiaryBase, resolvedMode, { boostS: 0 });
42
+ const warning = buildScaleFromHex(warningBase, resolvedMode, { boostS: 4 });
43
+ const error = buildScaleFromHex(errorBase, resolvedMode, { boostS: 4 });
44
+ const info = buildScaleFromHex(infoBase, resolvedMode, { boostS: 2 });
45
+ const success = buildScaleFromHex(successBase, resolvedMode, { boostS: 2 });
46
+ // Neutrals harmonized (pulled towards seed)
47
+ const neutralBgHex = resolvedMode === "light"
48
+ ? shiftHue(seed, 0, 10, 98)
49
+ : shiftHue(seed, 0, 10, 10);
50
+ const neutralSurfaceHex = resolvedMode === "light"
51
+ ? shiftHue(seed, 0, 12, 95)
52
+ : shiftHue(seed, 0, 12, 14);
53
+ const neutralMutedSurfaceHex = resolvedMode === "light"
54
+ ? shiftHue(seed, 0, 14, 90)
55
+ : shiftHue(seed, 0, 14, 18);
56
+ const borderHex = resolvedMode === "light"
57
+ ? shiftHue(seed, 0, 18, 84)
58
+ : shiftHue(seed, 0, 18, 26);
59
+ const ringHex = primary[400] ?? primaryBase; // "beautiful" ring derived from primary
60
+ const textHex = resolvedMode === "light" ? "#0B0B0C" : "#F6F6F7";
61
+ const mutedTextHex = resolvedMode === "light" ? "#4B4B51" : "#B6B6BD";
62
+ const disabledHex = resolvedMode === "light" ? "#E7E7EA" : "#2A2A2F";
63
+ const onDisabledHex = resolvedMode === "light" ? "#8A8A93" : "#9B9BA5";
64
+ const linkHex = info[600] ?? infoBase;
65
+ const linkHoverHex = info[700] ?? infoBase;
66
+ const badgeBgHex = resolvedMode === "light" ? (primary[100] ?? primaryBase) : (primary[800] ?? primaryBase);
67
+ const badgeOnHex = pickOnColor(badgeBgHex);
68
+ const tooltipBgHex = resolvedMode === "light" ? "#111317" : "#EDEEF0";
69
+ const tooltipOnHex = pickOnColor(tooltipBgHex);
70
+ const primaryHex500 = primary[500] ?? primaryBase;
71
+ const errorHex500 = error[500] ?? errorBase;
72
+ const radius = input.radius ?? 12;
73
+ const onPrimaryHex = pickOnColor(primaryHex500);
74
+ const onSecondaryHex = pickOnColor(secondary[500] ?? secondaryBase);
75
+ const onTertiaryHex = pickOnColor(tertiary[500] ?? tertiaryBase);
76
+ const onErrorHex = pickOnColor(errorHex500);
77
+ const vars = {
78
+ "--sg-mode": resolvedMode,
79
+ // Core singles as rgb
80
+ "--sg-bg": toRgbVarValue(neutralBgHex),
81
+ "--sg-surface": toRgbVarValue(neutralSurfaceHex),
82
+ "--sg-muted-surface": toRgbVarValue(neutralMutedSurfaceHex),
83
+ "--sg-border": toRgbVarValue(borderHex),
84
+ "--sg-ring": toRgbVarValue(ringHex),
85
+ "--sg-text": toRgbVarValue(textHex),
86
+ "--sg-muted": toRgbVarValue(mutedTextHex),
87
+ "--sg-disabled": toRgbVarValue(disabledHex),
88
+ "--sg-on-disabled": toRgbVarValue(onDisabledHex),
89
+ "--sg-link": toRgbVarValue(linkHex),
90
+ "--sg-link-hover": toRgbVarValue(linkHoverHex),
91
+ "--sg-badge": toRgbVarValue(badgeBgHex),
92
+ "--sg-on-badge": toRgbVarValue(badgeOnHex),
93
+ "--sg-tooltip": toRgbVarValue(tooltipBgHex),
94
+ "--sg-on-tooltip": toRgbVarValue(tooltipOnHex),
95
+ // Legacy aliases used by Tailwind semantic tokens in existing consumers.
96
+ "--background": toHslVarValue(neutralBgHex),
97
+ "--foreground": toHslVarValue(textHex),
98
+ "--card": toHslVarValue(neutralSurfaceHex),
99
+ "--card-foreground": toHslVarValue(textHex),
100
+ "--popover": toHslVarValue(neutralSurfaceHex),
101
+ "--popover-foreground": toHslVarValue(textHex),
102
+ "--primary": toHslVarValue(primaryHex500),
103
+ "--primary-foreground": toHslVarValue(onPrimaryHex),
104
+ "--secondary": toHslVarValue(neutralMutedSurfaceHex),
105
+ "--secondary-foreground": toHslVarValue(textHex),
106
+ "--muted": toHslVarValue(neutralMutedSurfaceHex),
107
+ "--muted-foreground": toHslVarValue(mutedTextHex),
108
+ "--accent": toHslVarValue(neutralMutedSurfaceHex),
109
+ "--accent-foreground": toHslVarValue(textHex),
110
+ "--destructive": toHslVarValue(errorHex500),
111
+ "--destructive-foreground": toHslVarValue(onErrorHex),
112
+ "--border": toHslVarValue(borderHex),
113
+ "--input": toHslVarValue(borderHex),
114
+ "--ring": toHslVarValue(ringHex),
115
+ "--radius": `${radius}px`,
116
+ "--sg-radius": `${radius}px`,
117
+ };
118
+ // On colors (for 500 generally)
119
+ vars["--sg-on-primary"] = toRgbVarValue(onPrimaryHex);
120
+ vars["--sg-on-secondary"] = toRgbVarValue(onSecondaryHex);
121
+ vars["--sg-on-tertiary"] = toRgbVarValue(onTertiaryHex);
122
+ // Semantic on colors (base at 500)
123
+ vars["--sg-on-warning"] = toRgbVarValue(pickOnColor(warning[500] ?? warningBase));
124
+ vars["--sg-on-error"] = toRgbVarValue(onErrorHex);
125
+ vars["--sg-on-info"] = toRgbVarValue(pickOnColor(info[500] ?? infoBase));
126
+ vars["--sg-on-success"] = toRgbVarValue(pickOnColor(success[500] ?? successBase));
127
+ // Palette vars
128
+ const stops = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
129
+ for (const s of stops) {
130
+ vars[`--sg-primary-${s}`] = toRgbVarValue(primary[s] ?? primaryBase);
131
+ vars[`--sg-secondary-${s}`] = toRgbVarValue(secondary[s] ?? secondaryBase);
132
+ vars[`--sg-tertiary-${s}`] = toRgbVarValue(tertiary[s] ?? tertiaryBase);
133
+ vars[`--sg-warning-${s}`] = toRgbVarValue(warning[s] ?? warningBase);
134
+ vars[`--sg-error-${s}`] = toRgbVarValue(error[s] ?? errorBase);
135
+ vars[`--sg-info-${s}`] = toRgbVarValue(info[s] ?? infoBase);
136
+ vars[`--sg-success-${s}`] = toRgbVarValue(success[s] ?? successBase);
137
+ }
138
+ // Handy hover/active (normally 600/700)
139
+ vars["--sg-primary-hover"] = vars["--sg-primary-600"] ?? toRgbVarValue(primaryBase);
140
+ vars["--sg-primary-active"] = vars["--sg-primary-700"] ?? toRgbVarValue(primaryBase);
141
+ vars["--sg-secondary-hover"] = vars["--sg-secondary-600"] ?? toRgbVarValue(secondaryBase);
142
+ vars["--sg-secondary-active"] = vars["--sg-secondary-700"] ?? toRgbVarValue(secondaryBase);
143
+ vars["--sg-tertiary-hover"] = vars["--sg-tertiary-600"] ?? toRgbVarValue(tertiaryBase);
144
+ vars["--sg-tertiary-active"] = vars["--sg-tertiary-700"] ?? toRgbVarValue(tertiaryBase);
145
+ vars["--sg-warning-hover"] = vars["--sg-warning-600"] ?? toRgbVarValue(warningBase);
146
+ vars["--sg-error-hover"] = vars["--sg-error-600"] ?? toRgbVarValue(errorBase);
147
+ vars["--sg-info-hover"] = vars["--sg-info-600"] ?? toRgbVarValue(infoBase);
148
+ vars["--sg-success-hover"] = vars["--sg-success-600"] ?? toRgbVarValue(successBase);
149
+ // Custom overrides
150
+ if (input.customVars) {
151
+ for (const k of Object.keys(input.customVars)) {
152
+ const value = input.customVars[k];
153
+ if (value !== undefined) {
154
+ vars[k] = value;
155
+ }
156
+ }
157
+ }
158
+ return vars;
159
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ export declare function AppShell(props: {
3
+ children: React.ReactNode;
4
+ nav?: React.ReactNode;
5
+ header?: React.ReactNode;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=AppShell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,wBAAgB,QAAQ,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAAC,GAAG,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAkC7G"}
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTheme } from "../theme/ThemeProvider";
4
+ export function AppShell(props) {
5
+ const theme = useTheme();
6
+ return (_jsx("div", { className: "min-h-screen", style: { background: `hsl(${theme.colors.background})`, color: `hsl(${theme.colors.foreground})` }, children: _jsxs("div", { className: "flex min-h-screen", children: [_jsxs("aside", { className: "border-r p-4 hidden md:block", style: { width: "var(--sg-sidebar)", borderColor: "rgba(0,0,0,0.08)" }, children: [_jsxs("div", { className: "font-semibold mb-4 flex items-center gap-2", children: [theme.brand.logoUrl ? (_jsx("img", { src: theme.brand.logoUrl, alt: theme.brand.name, className: "h-6" })) : null, _jsx("span", { children: theme.brand.name })] }), props.nav] }), _jsxs("main", { className: "flex-1 p-4", children: [props.header ? _jsx("div", { className: "mb-4", children: props.header }) : null, _jsx("div", { className: "rounded-xl border p-4", style: { borderColor: "rgba(0,0,0,0.08)", borderRadius: "var(--sg-radius)" }, children: props.children })] })] }) }));
7
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@seedgrid/fe-theme",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist"],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json && node ./scripts/copy-i18n.mjs",
16
+ "typecheck": "tsc -p tsconfig.json --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@seedgrid/fe-core": "workspace:*"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18.2.0 || ^19.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^19.0.0",
26
+ "fs-extra": "^11.2.0"
27
+ }
28
+ }