@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 +184 -0
- package/dist/i18n/pt-BR.json +5 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +17 -0
- package/dist/theme/ThemeConfig.d.ts +48 -0
- package/dist/theme/ThemeConfig.d.ts.map +1 -0
- package/dist/theme/ThemeConfig.js +26 -0
- package/dist/theme/ThemeProvider.d.ts +21 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +278 -0
- package/dist/theme/colorUtils.d.ts +27 -0
- package/dist/theme/colorUtils.d.ts.map +1 -0
- package/dist/theme/colorUtils.js +147 -0
- package/dist/theme/componentTokens.d.ts +8 -0
- package/dist/theme/componentTokens.d.ts.map +1 -0
- package/dist/theme/componentTokens.js +122 -0
- package/dist/theme/themeGenerator.d.ts +4 -0
- package/dist/theme/themeGenerator.d.ts.map +1 -0
- package/dist/theme/themeGenerator.js +159 -0
- package/dist/ui/AppShell.d.ts +7 -0
- package/dist/ui/AppShell.d.ts.map +1 -0
- package/dist/ui/AppShell.js +7 -0
- package/package.json +28 -0
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
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/manifest.js
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|