@bsuite/theme 0.2.0 → 0.3.1
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/dist/react/BrandingProvider.d.ts +30 -16
- package/dist/react/BrandingProvider.d.ts.map +1 -1
- package/dist/react/BrandingProvider.js +99 -34
- package/package.json +5 -5
- package/src/css/braden.css +209 -0
- package/src/css/index.css +10 -5
- package/src/css/vars.css +218 -67
- package/src/preset-v4.css +149 -57
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BrandingProvider — runtime enterprise white-labelling
|
|
3
|
+
* Version 0.3.0
|
|
3
4
|
*
|
|
4
5
|
* On mount:
|
|
5
6
|
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
7
|
+
* 2. Validates each colour value is a well-formed oklch() string.
|
|
8
|
+
* 3. Applies each key as a CSS custom property on document.documentElement.
|
|
9
|
+
* 4. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
10
|
+
* 5. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
9
11
|
* propagate without a page reload (within ~1 second).
|
|
10
12
|
*
|
|
13
|
+
* Security / brand policies:
|
|
14
|
+
* - Only oklch() values are accepted for colour fields — hex/rgb/hsl are rejected.
|
|
15
|
+
* - --role-error and --role-destructive are NOT overridable by tenants.
|
|
16
|
+
* Colourblind policy (purple error) is a system-level requirement, not a brand pref.
|
|
17
|
+
* - brand="braden" short-circuits all Supabase calls — Braden corporate site uses
|
|
18
|
+
* static CSS tokens, not runtime tenant overrides.
|
|
19
|
+
*
|
|
11
20
|
* Environment flags:
|
|
12
|
-
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set
|
|
13
|
-
* entirely without code changes (emergency kill switch).
|
|
21
|
+
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set 'false' as kill switch.
|
|
14
22
|
*
|
|
15
23
|
* Usage:
|
|
16
|
-
* Wrap BrandingProvider inside ThemeProvider so branding CSS vars override theme defaults:
|
|
17
|
-
*
|
|
18
24
|
* <ThemeProvider>
|
|
19
25
|
* <BrandingProvider supabaseClient={supabase}>
|
|
20
26
|
* {children}
|
|
21
27
|
* </BrandingProvider>
|
|
22
28
|
* </ThemeProvider>
|
|
29
|
+
*
|
|
30
|
+
* For Braden corporate (skips all RPC/Realtime):
|
|
31
|
+
* <BrandingProvider brand="braden" supabaseClient={supabase}>
|
|
32
|
+
* {children}
|
|
33
|
+
* </BrandingProvider>
|
|
23
34
|
*/
|
|
24
35
|
import type { ReactNode } from 'react';
|
|
25
36
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
@@ -27,9 +38,9 @@ export declare const BRANDING_STORAGE_KEY = "bsuite_tenant_branding";
|
|
|
27
38
|
export declare const BRANDING_OVERRIDE_FLAG = "VITE_ENABLE_BRANDING_OVERRIDE";
|
|
28
39
|
/** Shape returned by the branding_json_for_tenant RPC */
|
|
29
40
|
export interface TenantBranding {
|
|
30
|
-
/** Primary action colour
|
|
41
|
+
/** Primary action colour — MUST be an oklch() string */
|
|
31
42
|
primary?: string;
|
|
32
|
-
/** Accent / secondary colour
|
|
43
|
+
/** Accent / secondary colour — MUST be an oklch() string */
|
|
33
44
|
accent?: string;
|
|
34
45
|
/** Full logo URL (SVG or raster) */
|
|
35
46
|
logo_url?: string;
|
|
@@ -39,25 +50,28 @@ export interface TenantBranding {
|
|
|
39
50
|
font_stack?: string | null;
|
|
40
51
|
}
|
|
41
52
|
export interface BrandingContextValue {
|
|
42
|
-
/** Current resolved branding, or null if no tenant override */
|
|
43
53
|
branding: TenantBranding | null;
|
|
44
|
-
/** True while the initial RPC call is in flight */
|
|
45
54
|
isLoading: boolean;
|
|
46
|
-
/** Any error from the RPC call */
|
|
47
55
|
error: Error | null;
|
|
48
|
-
/** Manually trigger a branding refresh */
|
|
49
56
|
refresh: () => Promise<void>;
|
|
50
57
|
}
|
|
51
58
|
export declare const BrandingContext: import("react").Context<BrandingContextValue | undefined>;
|
|
59
|
+
export type BrandContext = 'bsuite' | 'braden';
|
|
52
60
|
export interface BrandingProviderProps {
|
|
53
61
|
children: ReactNode;
|
|
54
62
|
/** Authenticated Supabase client from the consuming app */
|
|
55
63
|
supabaseClient: SupabaseClient;
|
|
56
64
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
65
|
+
* Brand context. 'braden' short-circuits all Supabase RPC/Realtime calls —
|
|
66
|
+
* the Braden corporate site uses static CSS tokens only, not runtime overrides.
|
|
67
|
+
* Default: 'bsuite'
|
|
68
|
+
*/
|
|
69
|
+
brand?: BrandContext;
|
|
70
|
+
/**
|
|
71
|
+
* If true, apply persisted branding immediately on mount (before RPC resolves)
|
|
72
|
+
* to prevent FOUC. Default: true. Ignored when brand='braden'.
|
|
59
73
|
*/
|
|
60
74
|
applyPersistedOnMount?: boolean;
|
|
61
75
|
}
|
|
62
|
-
export declare function BrandingProvider({ children, supabaseClient, applyPersistedOnMount, }: BrandingProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
export declare function BrandingProvider({ children, supabaseClient, brand, applyPersistedOnMount, }: BrandingProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
63
77
|
//# sourceMappingURL=BrandingProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandingProvider.d.ts","sourceRoot":"","sources":["../../src/react/BrandingProvider.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"BrandingProvider.d.ts","sourceRoot":"","sources":["../../src/react/BrandingProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAUH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE3D,eAAO,MAAM,oBAAoB,2BAA2B,CAAA;AAC5D,eAAO,MAAM,sBAAsB,kCAAkC,CAAA;AAErE,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,eAAO,MAAM,eAAe,2DAA6D,CAAA;AAsJzF,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE9C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAA;IACnB,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAA;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,cAAc,EACd,KAAgB,EAChB,qBAA4B,GAC7B,EAAE,qBAAqB,2CA+FvB"}
|
|
@@ -1,31 +1,69 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* BrandingProvider — runtime enterprise white-labelling
|
|
4
|
+
* Version 0.3.0
|
|
4
5
|
*
|
|
5
6
|
* On mount:
|
|
6
7
|
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
8
|
+
* 2. Validates each colour value is a well-formed oklch() string.
|
|
9
|
+
* 3. Applies each key as a CSS custom property on document.documentElement.
|
|
10
|
+
* 4. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
11
|
+
* 5. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
10
12
|
* propagate without a page reload (within ~1 second).
|
|
11
13
|
*
|
|
14
|
+
* Security / brand policies:
|
|
15
|
+
* - Only oklch() values are accepted for colour fields — hex/rgb/hsl are rejected.
|
|
16
|
+
* - --role-error and --role-destructive are NOT overridable by tenants.
|
|
17
|
+
* Colourblind policy (purple error) is a system-level requirement, not a brand pref.
|
|
18
|
+
* - brand="braden" short-circuits all Supabase calls — Braden corporate site uses
|
|
19
|
+
* static CSS tokens, not runtime tenant overrides.
|
|
20
|
+
*
|
|
12
21
|
* Environment flags:
|
|
13
|
-
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set
|
|
14
|
-
* entirely without code changes (emergency kill switch).
|
|
22
|
+
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set 'false' as kill switch.
|
|
15
23
|
*
|
|
16
24
|
* Usage:
|
|
17
|
-
* Wrap BrandingProvider inside ThemeProvider so branding CSS vars override theme defaults:
|
|
18
|
-
*
|
|
19
25
|
* <ThemeProvider>
|
|
20
26
|
* <BrandingProvider supabaseClient={supabase}>
|
|
21
27
|
* {children}
|
|
22
28
|
* </BrandingProvider>
|
|
23
29
|
* </ThemeProvider>
|
|
30
|
+
*
|
|
31
|
+
* For Braden corporate (skips all RPC/Realtime):
|
|
32
|
+
* <BrandingProvider brand="braden" supabaseClient={supabase}>
|
|
33
|
+
* {children}
|
|
34
|
+
* </BrandingProvider>
|
|
24
35
|
*/
|
|
25
36
|
import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
26
37
|
export const BRANDING_STORAGE_KEY = 'bsuite_tenant_branding';
|
|
27
38
|
export const BRANDING_OVERRIDE_FLAG = 'VITE_ENABLE_BRANDING_OVERRIDE';
|
|
28
39
|
export const BrandingContext = createContext(undefined);
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// OKLCH validation
|
|
42
|
+
// Accepts: oklch(L C H) or oklch(L C H / A)
|
|
43
|
+
// where each component is a number or percentage.
|
|
44
|
+
// Rejects: hex, rgb(), hsl(), and any non-oklch value.
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
const OKLCH_RE = /^oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+%?(?:\s*\/\s*[\d.]+%?)?\s*\)$/i;
|
|
47
|
+
function isValidOklch(value) {
|
|
48
|
+
return OKLCH_RE.test(value.trim());
|
|
49
|
+
}
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// OVERRIDABLE KEYS
|
|
52
|
+
// --role-error and --role-destructive are EXCLUDED — colourblind policy.
|
|
53
|
+
// Tenants cannot change the error signifier to red.
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
const OVERRIDABLE_ROLE_KEYS = new Set([
|
|
56
|
+
'--primary',
|
|
57
|
+
'--accent',
|
|
58
|
+
'--accent-primary',
|
|
59
|
+
'--app-primary',
|
|
60
|
+
'--app-accent',
|
|
61
|
+
'--accent-secondary',
|
|
62
|
+
'--ring',
|
|
63
|
+
'--logo-url',
|
|
64
|
+
'--mark-url',
|
|
65
|
+
'--font-stack',
|
|
66
|
+
]);
|
|
29
67
|
const BRANDING_CSS_MAP = {
|
|
30
68
|
primary: '--primary',
|
|
31
69
|
accent: '--accent',
|
|
@@ -38,28 +76,42 @@ function applyBrandingToRoot(branding) {
|
|
|
38
76
|
return;
|
|
39
77
|
const root = document.documentElement;
|
|
40
78
|
if (!branding) {
|
|
41
|
-
// Remove any previously applied overrides
|
|
42
79
|
Object.values(BRANDING_CSS_MAP).forEach((cssVar) => {
|
|
43
80
|
if (cssVar)
|
|
44
81
|
root.style.removeProperty(cssVar);
|
|
45
82
|
});
|
|
46
|
-
// Also remove aliased vars
|
|
47
83
|
root.style.removeProperty('--accent-primary');
|
|
48
84
|
root.style.removeProperty('--app-primary');
|
|
49
85
|
root.style.removeProperty('--app-accent');
|
|
86
|
+
root.style.removeProperty('--accent-secondary');
|
|
50
87
|
root.removeAttribute('data-branding-loaded');
|
|
51
88
|
return;
|
|
52
89
|
}
|
|
53
90
|
if (branding.primary) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
if (!isValidOklch(branding.primary)) {
|
|
92
|
+
if (process.env.NODE_ENV === 'development') {
|
|
93
|
+
console.warn(`[BrandingProvider] Rejected primary colour "${branding.primary}" — must be oklch(). ` +
|
|
94
|
+
'Only oklch() values are accepted. Hex/rgb/hsl are not permitted in the token system.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
root.style.setProperty('--primary', branding.primary);
|
|
99
|
+
root.style.setProperty('--accent-primary', branding.primary);
|
|
100
|
+
root.style.setProperty('--app-primary', branding.primary);
|
|
101
|
+
root.style.setProperty('--ring', branding.primary);
|
|
102
|
+
}
|
|
58
103
|
}
|
|
59
104
|
if (branding.accent) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
if (!isValidOklch(branding.accent)) {
|
|
106
|
+
if (process.env.NODE_ENV === 'development') {
|
|
107
|
+
console.warn(`[BrandingProvider] Rejected accent colour "${branding.accent}" — must be oklch().`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
root.style.setProperty('--accent', branding.accent);
|
|
112
|
+
root.style.setProperty('--accent-secondary', branding.accent);
|
|
113
|
+
root.style.setProperty('--app-accent', branding.accent);
|
|
114
|
+
}
|
|
63
115
|
}
|
|
64
116
|
if (branding.logo_url) {
|
|
65
117
|
root.style.setProperty('--logo-url', `url(${branding.logo_url})`);
|
|
@@ -72,6 +124,20 @@ function applyBrandingToRoot(branding) {
|
|
|
72
124
|
}
|
|
73
125
|
root.setAttribute('data-branding-loaded', 'true');
|
|
74
126
|
}
|
|
127
|
+
// Warn in dev if a caller tries to set a protected key
|
|
128
|
+
function warnIfProtectedKeyAttempted(branding) {
|
|
129
|
+
if (process.env.NODE_ENV !== 'development')
|
|
130
|
+
return;
|
|
131
|
+
// Check for any attempt to set error/destructive via unexpected RPC fields
|
|
132
|
+
const raw = branding;
|
|
133
|
+
for (const key of Object.keys(raw)) {
|
|
134
|
+
const cssVar = `--${key.replace(/_/g, '-')}`;
|
|
135
|
+
if (!OVERRIDABLE_ROLE_KEYS.has(cssVar) && cssVar.includes('error')) {
|
|
136
|
+
console.warn(`[BrandingProvider] Blocked attempt to override "${cssVar}" from tenant payload. ` +
|
|
137
|
+
'Error/destructive colours are protected by the colourblind policy.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
75
141
|
function persistBranding(branding) {
|
|
76
142
|
try {
|
|
77
143
|
if (branding) {
|
|
@@ -82,7 +148,7 @@ function persistBranding(branding) {
|
|
|
82
148
|
}
|
|
83
149
|
}
|
|
84
150
|
catch {
|
|
85
|
-
// localStorage not available
|
|
151
|
+
// localStorage not available
|
|
86
152
|
}
|
|
87
153
|
}
|
|
88
154
|
function loadPersistedBranding() {
|
|
@@ -98,7 +164,6 @@ function loadPersistedBranding() {
|
|
|
98
164
|
}
|
|
99
165
|
function isBrandingEnabled() {
|
|
100
166
|
try {
|
|
101
|
-
// Vite env var — only present in client bundles that set it
|
|
102
167
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
168
|
const flag = import.meta.env;
|
|
104
169
|
const value = flag?.VITE_ENABLE_BRANDING_OVERRIDE ?? flag?.['VITE_ENABLE_BRANDING_OVERRIDE'];
|
|
@@ -106,15 +171,23 @@ function isBrandingEnabled() {
|
|
|
106
171
|
return false;
|
|
107
172
|
}
|
|
108
173
|
catch {
|
|
109
|
-
// Not in a Vite context — default
|
|
174
|
+
// Not in a Vite context — default enabled
|
|
110
175
|
}
|
|
111
176
|
return true;
|
|
112
177
|
}
|
|
113
|
-
export function BrandingProvider({ children, supabaseClient, applyPersistedOnMount = true, }) {
|
|
178
|
+
export function BrandingProvider({ children, supabaseClient, brand = 'bsuite', applyPersistedOnMount = true, }) {
|
|
114
179
|
const [branding, setBranding] = useState(null);
|
|
115
|
-
const [isLoading, setIsLoading] = useState(
|
|
180
|
+
const [isLoading, setIsLoading] = useState(brand !== 'braden');
|
|
116
181
|
const [error, setError] = useState(null);
|
|
117
182
|
const channelRef = useRef(null);
|
|
183
|
+
// ── Braden short-circuit ──────────────────────────────────────────────────
|
|
184
|
+
// Braden corporate uses static CSS from @bsuite/theme/braden-css — no runtime
|
|
185
|
+
// tenant overrides needed. Skip all Supabase calls entirely.
|
|
186
|
+
if (brand === 'braden') {
|
|
187
|
+
const staticValue = useMemo(() => ({ branding: null, isLoading: false, error: null, refresh: async () => { } }), []);
|
|
188
|
+
return (_jsx(BrandingContext.Provider, { value: staticValue, children: children }));
|
|
189
|
+
}
|
|
190
|
+
// ── BSuite / D2C runtime branding ─────────────────────────────────────────
|
|
118
191
|
const fetchBranding = useCallback(async () => {
|
|
119
192
|
if (!isBrandingEnabled()) {
|
|
120
193
|
setIsLoading(false);
|
|
@@ -124,17 +197,17 @@ export function BrandingProvider({ children, supabaseClient, applyPersistedOnMou
|
|
|
124
197
|
setError(null);
|
|
125
198
|
try {
|
|
126
199
|
const { data, error: rpcError } = await supabaseClient.rpc('branding_json_for_tenant');
|
|
127
|
-
if (rpcError)
|
|
200
|
+
if (rpcError)
|
|
128
201
|
throw new Error(rpcError.message);
|
|
129
|
-
}
|
|
130
202
|
const resolved = data ?? null;
|
|
203
|
+
if (resolved)
|
|
204
|
+
warnIfProtectedKeyAttempted(resolved);
|
|
131
205
|
setBranding(resolved);
|
|
132
206
|
applyBrandingToRoot(resolved);
|
|
133
207
|
persistBranding(resolved);
|
|
134
208
|
}
|
|
135
209
|
catch (err) {
|
|
136
210
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
137
|
-
// On error keep any persisted branding applied (already done at mount)
|
|
138
211
|
}
|
|
139
212
|
finally {
|
|
140
213
|
setIsLoading(false);
|
|
@@ -154,21 +227,13 @@ export function BrandingProvider({ children, supabaseClient, applyPersistedOnMou
|
|
|
154
227
|
useEffect(() => {
|
|
155
228
|
void fetchBranding();
|
|
156
229
|
}, [fetchBranding]);
|
|
157
|
-
// Subscribe to Realtime tenant row changes
|
|
230
|
+
// Subscribe to Realtime tenant row changes
|
|
158
231
|
useEffect(() => {
|
|
159
232
|
if (!isBrandingEnabled())
|
|
160
233
|
return;
|
|
161
234
|
const channel = supabaseClient
|
|
162
235
|
.channel('tenant-branding-updates')
|
|
163
|
-
.on('postgres_changes', {
|
|
164
|
-
event: 'UPDATE',
|
|
165
|
-
schema: 'public',
|
|
166
|
-
table: 'tenants',
|
|
167
|
-
// filter is applied server-side via RLS — only own tenant rows are received
|
|
168
|
-
}, (_payload) => {
|
|
169
|
-
// Re-fetch to get the full branding_json_for_tenant RPC result
|
|
170
|
-
void fetchBranding();
|
|
171
|
-
})
|
|
236
|
+
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'tenants' }, (_payload) => { void fetchBranding(); })
|
|
172
237
|
.subscribe();
|
|
173
238
|
channelRef.current = channel;
|
|
174
239
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://github.com/GaryOcean428/bsuite.git",
|
|
12
12
|
"directory": "packages/theme"
|
|
13
13
|
},
|
|
14
|
-
"description": "BSuite Universal D2C Neon Electric
|
|
14
|
+
"description": "BSuite Universal theme \u2014 D2C Neon Electric + Braden Corporate baselines. Tailwind v4 preset, CSS vars (5-layer), role aliases, colourblind-safe tokens, eye-strain-safe text scale, BrandingProvider (runtime white-label). Used by BSU, CRM7, conduit, R80.3, throughput (D2C) and braden.com.au (Corporate).",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"src/css",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"./preset-v4.css": "./src/preset-v4.css",
|
|
38
38
|
"./css": "./src/css/index.css",
|
|
39
39
|
"./vars.css": "./src/css/vars.css",
|
|
40
|
-
"./utilities.css": "./src/css/utilities.css"
|
|
40
|
+
"./utilities.css": "./src/css/utilities.css",
|
|
41
|
+
"./braden-css": "./src/css/braden.css"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsc -p tsconfig.build.json",
|
|
44
45
|
"typecheck": "tsc --noEmit",
|
|
45
|
-
"test": "vitest run"
|
|
46
|
-
"prepublishOnly": "pnpm build"
|
|
46
|
+
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@supabase/supabase-js": ">=2.0.0",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @bsuite/theme — Braden Corporate brand baseline
|
|
3
|
+
* Version 0.3.0
|
|
4
|
+
*
|
|
5
|
+
* Usage (braden.com.au only):
|
|
6
|
+
* @import '@bsuite/theme/braden-css';
|
|
7
|
+
*
|
|
8
|
+
* This file is EXCLUSIVELY for the braden.com.au Corporate brand.
|
|
9
|
+
* NEVER import this into D2C webapp projects (crm7, conduit, BSU, R80.3).
|
|
10
|
+
* NEVER apply Neon Electric colours to braden.com.au surfaces.
|
|
11
|
+
*
|
|
12
|
+
* Architecture — same 5-layer structure as vars.css:
|
|
13
|
+
* 1. Palette — --braden-red-*, --braden-gold-*, --braden-navy-*
|
|
14
|
+
* 2. Surface — --braden-light-*, --braden-dark-*
|
|
15
|
+
* 3. Role — --role-* (rebinds D2C role tokens to Braden values)
|
|
16
|
+
* 4. Shadcn — --background, --foreground, etc. (same keys, Braden values)
|
|
17
|
+
* 5. App — --app-primary, --app-accent
|
|
18
|
+
*
|
|
19
|
+
* Colourblind policy is PRESERVED:
|
|
20
|
+
* --role-error / --role-destructive = PURPLE (oklch(0.568 0.202 283.1))
|
|
21
|
+
* Same as D2C — purple error is a cross-brand system requirement.
|
|
22
|
+
*
|
|
23
|
+
* Eye-strain policy is PRESERVED:
|
|
24
|
+
* Dark text capped at L=0.94. Light text capped at L=0.22 (navy hue 250).
|
|
25
|
+
*
|
|
26
|
+
* OKLCH format mandatory for all new tokens. HSL values below are for
|
|
27
|
+
* legacy Tailwind v3 interop only, marked with LEGACY comments.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/* ============================================================
|
|
31
|
+
LAYER 1 — BRADEN CORPORATE PALETTE PRIMITIVES
|
|
32
|
+
============================================================ */
|
|
33
|
+
:root {
|
|
34
|
+
/* Primary — Braden Red scale */
|
|
35
|
+
--braden-red-50: oklch(0.965 0.015 17);
|
|
36
|
+
--braden-red-100: oklch(0.930 0.035 17);
|
|
37
|
+
--braden-red-200: oklch(0.870 0.070 18);
|
|
38
|
+
--braden-red-300: oklch(0.780 0.110 18);
|
|
39
|
+
--braden-red-400: oklch(0.660 0.145 18);
|
|
40
|
+
--braden-red-500: oklch(0.550 0.175 18); /* main brand red */
|
|
41
|
+
--braden-red-600: oklch(0.488 0.170 17.6); /* canonical –- oklch spec */
|
|
42
|
+
--braden-red-700: oklch(0.420 0.150 17);
|
|
43
|
+
--braden-red-800: oklch(0.340 0.120 16);
|
|
44
|
+
--braden-red-900: oklch(0.260 0.090 15);
|
|
45
|
+
|
|
46
|
+
/* Accent — Braden Gold scale */
|
|
47
|
+
--braden-gold-50: oklch(0.980 0.015 88);
|
|
48
|
+
--braden-gold-100: oklch(0.955 0.035 89);
|
|
49
|
+
--braden-gold-200: oklch(0.910 0.060 90);
|
|
50
|
+
--braden-gold-300: oklch(0.860 0.078 91);
|
|
51
|
+
--braden-gold-400: oklch(0.829 0.073 91.8); /* light gold */
|
|
52
|
+
--braden-gold-500: oklch(0.769 0.096 90.9); /* canonical accent */
|
|
53
|
+
--braden-gold-600: oklch(0.711 0.115 89.5); /* bronze */
|
|
54
|
+
--braden-gold-700: oklch(0.630 0.110 88);
|
|
55
|
+
--braden-gold-800: oklch(0.520 0.090 86);
|
|
56
|
+
--braden-gold-900: oklch(0.400 0.065 84);
|
|
57
|
+
|
|
58
|
+
/* Navy — secondary brand colour */
|
|
59
|
+
--braden-navy-50: oklch(0.960 0.008 249);
|
|
60
|
+
--braden-navy-100: oklch(0.920 0.015 249);
|
|
61
|
+
--braden-navy-200: oklch(0.840 0.025 249);
|
|
62
|
+
--braden-navy-300: oklch(0.720 0.030 249);
|
|
63
|
+
--braden-navy-400: oklch(0.580 0.033 249);
|
|
64
|
+
--braden-navy-500: oklch(0.450 0.035 249);
|
|
65
|
+
--braden-navy-600: oklch(0.356 0.039 249); /* canonical navy */
|
|
66
|
+
--braden-navy-700: oklch(0.280 0.030 248);
|
|
67
|
+
--braden-navy-800: oklch(0.200 0.022 248);
|
|
68
|
+
--braden-navy-900: oklch(0.130 0.015 248);
|
|
69
|
+
|
|
70
|
+
/* Neutrals */
|
|
71
|
+
--braden-slate: oklch(0.710 0.018 201.5);
|
|
72
|
+
--braden-sky: oklch(0.653 0.135 242.7);
|
|
73
|
+
--braden-forest: oklch(0.663 0.160 152.4);
|
|
74
|
+
--braden-lavender:oklch(0.577 0.153 315.3);
|
|
75
|
+
|
|
76
|
+
/* ============================================================
|
|
77
|
+
LAYER 2 — BRADEN SURFACE PRIMITIVES (light mode)
|
|
78
|
+
============================================================ */
|
|
79
|
+
--braden-light-bg-body: oklch(0.980 0.002 50); /* warm white */
|
|
80
|
+
--braden-light-bg-surface: oklch(0.996 0.001 50); /* near white */
|
|
81
|
+
--braden-light-bg-panel: oklch(1.000 0 0); /* white */
|
|
82
|
+
--braden-light-bg-input: oklch(0.972 0.003 250); /* cool light grey */
|
|
83
|
+
|
|
84
|
+
/* Braden light text scale — hue 250 (navy cast), capped L=0.22 */
|
|
85
|
+
--braden-light-text-primary: oklch(0.22 0.020 250); /* 13.2:1 ✓ AAA */
|
|
86
|
+
--braden-light-text-secondary: oklch(0.38 0.018 250); /* 8.1:1 ✓ AAA */
|
|
87
|
+
--braden-light-text-muted: oklch(0.52 0.018 250); /* 4.9:1 ✓ AA */
|
|
88
|
+
--braden-light-text-subtle: oklch(0.60 0.012 250); /* 3.8:1 ✓ AA large */
|
|
89
|
+
--braden-light-text-disabled: oklch(0.72 0.010 250);
|
|
90
|
+
|
|
91
|
+
--braden-light-border: oklch(0.900 0.008 248);
|
|
92
|
+
--braden-light-hover: oklch(0.950 0.005 248);
|
|
93
|
+
|
|
94
|
+
/* ============================================================
|
|
95
|
+
LAYER 3 — ROLE ALIASES (Braden light-mode rebind)
|
|
96
|
+
Reuses the same --role-* names as vars.css so components
|
|
97
|
+
are brand-agnostic at the role layer.
|
|
98
|
+
============================================================ */
|
|
99
|
+
--role-primary: var(--braden-red-600); /* oklch(0.488 0.170 17.6) */
|
|
100
|
+
--role-secondary: var(--braden-navy-600); /* oklch(0.356 0.039 249) */
|
|
101
|
+
--role-accent: var(--braden-gold-500); /* oklch(0.769 0.096 90.9) */
|
|
102
|
+
|
|
103
|
+
/* Colourblind policy: SAME purple as D2C — cross-brand requirement */
|
|
104
|
+
--role-error: oklch(0.568 0.202 283.1);
|
|
105
|
+
--role-destructive: oklch(0.568 0.202 283.1);
|
|
106
|
+
--role-warning: oklch(0.728 0.168 22.5);
|
|
107
|
+
--role-success: oklch(0.663 0.160 152.4); /* braden forest green */
|
|
108
|
+
--role-info: var(--braden-sky);
|
|
109
|
+
|
|
110
|
+
/* Text roles */
|
|
111
|
+
--role-text-body: var(--braden-light-text-primary);
|
|
112
|
+
--role-text-secondary: var(--braden-light-text-secondary);
|
|
113
|
+
--role-text-muted: var(--braden-light-text-muted);
|
|
114
|
+
--role-text-subtle: var(--braden-light-text-subtle);
|
|
115
|
+
--role-text-disabled: var(--braden-light-text-disabled);
|
|
116
|
+
--role-text-heading: oklch(0.19 0.025 252); /* near-navy — 14.8:1 ✓ AAA */
|
|
117
|
+
|
|
118
|
+
/* Surface roles */
|
|
119
|
+
--role-bg-body: var(--braden-light-bg-body);
|
|
120
|
+
--role-bg-surface: var(--braden-light-bg-surface);
|
|
121
|
+
--role-bg-panel: var(--braden-light-bg-panel);
|
|
122
|
+
--role-bg-input: var(--braden-light-bg-input);
|
|
123
|
+
--role-border: var(--braden-light-border);
|
|
124
|
+
|
|
125
|
+
/* Inverse text on coloured fills */
|
|
126
|
+
--text-on-primary: oklch(0.99 0 0); /* on red L≈0.49 — 7.8:1 ✓ AA */
|
|
127
|
+
--text-on-accent: oklch(0.17 0.02 250); /* on gold L≈0.77 — dark text */
|
|
128
|
+
--text-on-error: oklch(0.99 0 0); /* on purple */
|
|
129
|
+
--text-on-success: oklch(0.17 0.02 250);
|
|
130
|
+
--text-on-warning: oklch(0.17 0.02 250);
|
|
131
|
+
|
|
132
|
+
/* ============================================================
|
|
133
|
+
LAYER 4 — SHADCN BRIDGE (Braden values)
|
|
134
|
+
============================================================ */
|
|
135
|
+
--background: var(--braden-light-bg-body);
|
|
136
|
+
--foreground: var(--braden-light-text-primary);
|
|
137
|
+
--card: var(--braden-light-bg-panel);
|
|
138
|
+
--card-foreground: var(--braden-light-text-primary);
|
|
139
|
+
--popover: var(--braden-light-bg-panel);
|
|
140
|
+
--popover-foreground: var(--braden-light-text-primary);
|
|
141
|
+
--primary: var(--braden-red-600);
|
|
142
|
+
--primary-foreground: oklch(0.99 0 0);
|
|
143
|
+
--secondary: var(--braden-navy-600);
|
|
144
|
+
--secondary-foreground: oklch(0.99 0 0);
|
|
145
|
+
--muted: var(--braden-light-bg-input);
|
|
146
|
+
--muted-foreground: var(--braden-light-text-muted);
|
|
147
|
+
--accent: var(--braden-gold-500);
|
|
148
|
+
--accent-foreground: oklch(0.17 0.02 250);
|
|
149
|
+
--destructive: oklch(0.568 0.202 283.1);
|
|
150
|
+
--destructive-foreground: oklch(0.99 0 0);
|
|
151
|
+
--border: var(--braden-light-border);
|
|
152
|
+
--input: var(--braden-light-border);
|
|
153
|
+
--ring: var(--braden-red-600);
|
|
154
|
+
|
|
155
|
+
/* ============================================================
|
|
156
|
+
LAYER 5 — APP SHORTCUTS
|
|
157
|
+
============================================================ */
|
|
158
|
+
--app-primary: var(--braden-red-600);
|
|
159
|
+
--app-accent: var(--braden-gold-500);
|
|
160
|
+
|
|
161
|
+
/* ============================================================
|
|
162
|
+
SHADOWS — corporate subtle, no neon glow
|
|
163
|
+
============================================================ */
|
|
164
|
+
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.08);
|
|
165
|
+
--shadow-md: 0 4px 6px oklch(0 0 0 / 0.10), 0 2px 4px oklch(0 0 0 / 0.06);
|
|
166
|
+
--shadow-lg: 0 10px 15px oklch(0 0 0 / 0.10), 0 4px 6px oklch(0 0 0 / 0.05);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ============================================================
|
|
170
|
+
BRADEN DARK MODE
|
|
171
|
+
============================================================ */
|
|
172
|
+
.dark {
|
|
173
|
+
/* Dark surfaces */
|
|
174
|
+
--braden-dark-bg-body: oklch(0.118 0.010 252);
|
|
175
|
+
--braden-dark-bg-surface: oklch(0.165 0.015 251);
|
|
176
|
+
--braden-dark-bg-panel: oklch(0.200 0.018 250);
|
|
177
|
+
--braden-dark-bg-input: oklch(0.240 0.020 250);
|
|
178
|
+
|
|
179
|
+
/* Dark text scale — hue 250, capped L=0.93 (Braden uses navy cast) */
|
|
180
|
+
--braden-dark-text-primary: oklch(0.93 0.014 250); /* ~13.1:1 ✓ AAA */
|
|
181
|
+
--braden-dark-text-secondary: oklch(0.80 0.016 250); /* ~9.0:1 ✓ AAA */
|
|
182
|
+
--braden-dark-text-muted: oklch(0.66 0.018 250); /* ~5.2:1 ✓ AA */
|
|
183
|
+
--braden-dark-text-subtle: oklch(0.55 0.015 250); /* ~3.5:1 ✓ AA large */
|
|
184
|
+
--braden-dark-text-disabled: oklch(0.43 0.010 250);
|
|
185
|
+
|
|
186
|
+
/* Rebind roles */
|
|
187
|
+
--role-text-body: var(--braden-dark-text-primary);
|
|
188
|
+
--role-text-secondary: var(--braden-dark-text-secondary);
|
|
189
|
+
--role-text-muted: var(--braden-dark-text-muted);
|
|
190
|
+
--role-text-subtle: var(--braden-dark-text-subtle);
|
|
191
|
+
--role-text-disabled: var(--braden-dark-text-disabled);
|
|
192
|
+
--role-text-heading: oklch(0.91 0.016 250);
|
|
193
|
+
|
|
194
|
+
--role-bg-body: var(--braden-dark-bg-body);
|
|
195
|
+
--role-bg-surface: var(--braden-dark-bg-surface);
|
|
196
|
+
--role-bg-panel: var(--braden-dark-bg-panel);
|
|
197
|
+
--role-bg-input: var(--braden-dark-bg-input);
|
|
198
|
+
--role-border: oklch(0.30 0.020 250);
|
|
199
|
+
|
|
200
|
+
/* Shadcn bridge picks up via var() chain */
|
|
201
|
+
--background: var(--braden-dark-bg-body);
|
|
202
|
+
--foreground: var(--braden-dark-text-primary);
|
|
203
|
+
--card: var(--braden-dark-bg-panel);
|
|
204
|
+
--card-foreground: var(--braden-dark-text-primary);
|
|
205
|
+
--muted: var(--braden-dark-bg-input);
|
|
206
|
+
--muted-foreground:var(--braden-dark-text-muted);
|
|
207
|
+
--border: oklch(0.30 0.020 250);
|
|
208
|
+
--input: oklch(0.30 0.020 250);
|
|
209
|
+
}
|
package/src/css/index.css
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* @bsuite/theme — root CSS entry point
|
|
2
|
+
* @bsuite/theme — root CSS entry point (D2C Neon Electric brand)
|
|
3
|
+
* Version 0.3.0
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* DUAL-BRAND ENTRY POINTS:
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
+
* D2C Neon Electric (crm7, conduit, BSU, R80.3, throughput):
|
|
8
|
+
* @import '@bsuite/theme/css'; ← this file
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Braden Corporate (braden.com.au only):
|
|
11
|
+
* @import '@bsuite/theme/braden-css';
|
|
12
|
+
*
|
|
13
|
+
* DO NOT import both in the same app. Each file fully defines
|
|
14
|
+
* the --role-* and shadcn bridge layers for its own brand.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
@import './vars.css';
|
package/src/css/vars.css
CHANGED
|
@@ -1,86 +1,237 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @bsuite/theme — CSS custom properties (:root + .dark)
|
|
3
|
+
* Version 0.3.0
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Architecture — 5 layers (inner → outer):
|
|
6
|
+
* 1. Palette — frozen oklch primitives (--neon-electric-*, --braden-*)
|
|
7
|
+
* 2. Surface — light/dark bg + text primitives (--light-*, --dark-*)
|
|
8
|
+
* 3. Role -- semantic aliases consumed by Tailwind + components (--role-*)
|
|
9
|
+
* 4. Shadcn — shadcn/ui bridge mapping (--background, --foreground, etc.)
|
|
10
|
+
* 5. App -- per-app overrides (--app-primary, --app-accent)
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* Consumers should bind to Layer 3 (roles) or Layer 4 (shadcn) — never
|
|
13
|
+
* directly to palette primitives. Roles make white-labelling safe.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* Colourblind policy (WCAG 1.4.1 + audit constraint):
|
|
16
|
+
* --role-error / --role-destructive = PURPLE, never red-vs-green.
|
|
17
|
+
* Only flag where colour is the SOLE signifier of state.
|
|
18
|
+
*
|
|
19
|
+
* Eye-strain policy:
|
|
20
|
+
* Dark-mode text capped at L=0.94. Pure white (L=1.0) is banned as a
|
|
21
|
+
* text token. 5-tier scale verified WCAG AA minimum throughout.
|
|
22
|
+
*
|
|
23
|
+
* OKLCH format is MANDATORY for all new tokens. No hex/rgb/hsl.
|
|
24
|
+
* Syntax: oklch(L C H) — L=lightness 0–1, C=chroma 0–0.4, H=hue 0–360.
|
|
13
25
|
*/
|
|
14
26
|
|
|
27
|
+
/* ============================================================
|
|
28
|
+
LAYER 1 — PALETTE PRIMITIVES (frozen — do not edit)
|
|
29
|
+
============================================================ */
|
|
15
30
|
:root {
|
|
16
|
-
/*
|
|
17
|
-
--neon-electric-blue:
|
|
18
|
-
--neon-electric-cyan:
|
|
19
|
-
--neon-electric-indigo:
|
|
20
|
-
--neon-electric-purple:
|
|
21
|
-
--neon-electric-magenta:
|
|
22
|
-
--neon-electric-pink:
|
|
23
|
-
--neon-electric-coral:
|
|
24
|
-
--neon-electric-orange:
|
|
25
|
-
--neon-electric-yellow:
|
|
26
|
-
--neon-electric-green:
|
|
31
|
+
/* D2C Neon Electric — 11 colours (FROZEN per D2C spec v1.00A) */
|
|
32
|
+
--neon-electric-blue: oklch(0.546 0.215 262.9);
|
|
33
|
+
--neon-electric-cyan: oklch(0.769 0.132 191.7);
|
|
34
|
+
--neon-electric-indigo: oklch(0.511 0.23 277);
|
|
35
|
+
--neon-electric-purple: oklch(0.568 0.202 283.1);
|
|
36
|
+
--neon-electric-magenta: oklch(0.742 0.167 359.5);
|
|
37
|
+
--neon-electric-pink: oklch(0.656 0.212 354.3);
|
|
38
|
+
--neon-electric-coral: oklch(0.669 0.219 20.9);
|
|
39
|
+
--neon-electric-orange: oklch(0.728 0.168 22.5);
|
|
40
|
+
--neon-electric-yellow: oklch(0.868 0.125 81.4);
|
|
41
|
+
--neon-electric-green: oklch(0.723 0.192 149.6);
|
|
27
42
|
--neon-electric-lavender: oklch(0.736 0.141 285.6);
|
|
28
43
|
|
|
29
|
-
/*
|
|
30
|
-
|
|
44
|
+
/* ============================================================
|
|
45
|
+
LAYER 2 — SURFACE PRIMITIVES (light mode)
|
|
46
|
+
============================================================ */
|
|
47
|
+
|
|
48
|
+
/* Backgrounds */
|
|
49
|
+
--light-bg-primary: oklch(0.961 0 0.5);
|
|
31
50
|
--light-bg-secondary: oklch(0.982 0.002 248);
|
|
32
|
-
--light-bg-tertiary:
|
|
33
|
-
--light-bg-accent:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--light-text-
|
|
51
|
+
--light-bg-tertiary: oklch(0.963 0.003 228.9);
|
|
52
|
+
--light-bg-accent: oklch(1 0 0.5);
|
|
53
|
+
|
|
54
|
+
/* D2C light text scale — hue 260 (cool blue-grey), capped L=0.22.
|
|
55
|
+
Verified WCAG AA/AAA on --light-bg-primary oklch(0.961 0 0.5). */
|
|
56
|
+
--light-text-primary: oklch(0.22 0.015 260); /* 13.2:1 ✓ AAA */
|
|
57
|
+
--light-text-secondary: oklch(0.38 0.018 260); /* 8.1:1 ✓ AAA */
|
|
58
|
+
--light-text-muted: oklch(0.52 0.018 260); /* 4.9:1 ✓ AA */
|
|
59
|
+
--light-text-subtle: oklch(0.60 0.012 260); /* 3.8:1 ✓ AA large */
|
|
60
|
+
--light-text-disabled: oklch(0.72 0.010 260); /* pair with icon cue */
|
|
61
|
+
/* Legacy aliases (kept for consumers that haven't migrated) */
|
|
62
|
+
--light-text-tertiary: var(--light-text-muted);
|
|
63
|
+
--light-text-quaternary: var(--light-text-subtle);
|
|
64
|
+
|
|
38
65
|
--light-border: oklch(0.942 0.005 247.9);
|
|
39
|
-
--light-hover:
|
|
40
|
-
|
|
41
|
-
/*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/*
|
|
55
|
-
--
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
--
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
--
|
|
66
|
+
--light-hover: oklch(0.963 0.003 228.9);
|
|
67
|
+
|
|
68
|
+
/* ============================================================
|
|
69
|
+
LAYER 2 — SURFACE PRIMITIVES (dark mode — defined in .dark)
|
|
70
|
+
Values declared in .dark below.
|
|
71
|
+
============================================================ */
|
|
72
|
+
|
|
73
|
+
/* ============================================================
|
|
74
|
+
LAYER 3 — ROLE ALIASES (semantic, light-mode defaults)
|
|
75
|
+
Consumers must only bind to these — never to palette names.
|
|
76
|
+
Roles are what make white-labelling safe.
|
|
77
|
+
============================================================ */
|
|
78
|
+
|
|
79
|
+
/* Action colours */
|
|
80
|
+
--role-primary: var(--neon-electric-blue); /* oklch(0.546 0.215 262.9) */
|
|
81
|
+
--role-secondary: var(--neon-electric-indigo); /* oklch(0.511 0.23 277) */
|
|
82
|
+
--role-accent: var(--neon-electric-cyan); /* oklch(0.769 0.132 191.7) */
|
|
83
|
+
|
|
84
|
+
/* ⚠️ COLOURBLIND POLICY: error = PURPLE — never red/coral.
|
|
85
|
+
Red-vs-green is the most common colour-vision deficiency (~8% M, ~0.5% F).
|
|
86
|
+
Purple (hue 283) is unambiguous to deuteranopes and protanopes.
|
|
87
|
+
TENANT OVERRIDE BLOCKED: BrandingProvider rejects --role-error changes. */
|
|
88
|
+
--role-error: var(--neon-electric-purple); /* oklch(0.568 0.202 283.1) */
|
|
89
|
+
--role-destructive: var(--neon-electric-purple); /* synonym — same value */
|
|
90
|
+
--role-warning: var(--neon-electric-orange); /* oklch(0.728 0.168 22.5) */
|
|
91
|
+
--role-success: var(--neon-electric-green); /* oklch(0.723 0.192 149.6) */
|
|
92
|
+
--role-info: var(--neon-electric-cyan); /* oklch(0.769 0.132 191.7) */
|
|
93
|
+
|
|
94
|
+
/* Text roles */
|
|
95
|
+
--role-text-body: var(--light-text-primary);
|
|
96
|
+
--role-text-secondary: var(--light-text-secondary);
|
|
97
|
+
--role-text-muted: var(--light-text-muted);
|
|
98
|
+
--role-text-subtle: var(--light-text-subtle);
|
|
99
|
+
--role-text-disabled: var(--light-text-disabled);
|
|
100
|
+
--role-text-heading: oklch(0.20 0.018 263); /* slightly darker + bluer — 14.5:1 ✓ AAA */
|
|
101
|
+
|
|
102
|
+
/* Surface roles */
|
|
103
|
+
--role-bg-body: var(--light-bg-primary);
|
|
104
|
+
--role-bg-surface: var(--light-bg-secondary);
|
|
105
|
+
--role-bg-panel: var(--light-bg-accent);
|
|
106
|
+
--role-bg-input: var(--light-bg-tertiary);
|
|
107
|
+
--role-border: var(--light-border);
|
|
108
|
+
|
|
109
|
+
/* Inverse text — safe on coloured fills (mid-lightness L≈0.5–0.7) */
|
|
110
|
+
--text-on-primary: oklch(0.99 0 0); /* on blue L≈0.55 — 5.9:1 ✓ AA */
|
|
111
|
+
--text-on-accent: oklch(0.17 0.02 260); /* on cyan L≈0.77 — 6.1:1 ✓ AA */
|
|
112
|
+
--text-on-error: oklch(0.99 0 0); /* on purple L≈0.57 */
|
|
113
|
+
--text-on-success: oklch(0.17 0.02 260); /* on green L≈0.72 — dark text */
|
|
114
|
+
--text-on-warning: oklch(0.17 0.02 260); /* on amber L≈0.82 — dark text */
|
|
115
|
+
|
|
116
|
+
/* ============================================================
|
|
117
|
+
LAYER 3 — SEMANTIC STATUS (legacy: keeps --status-* names)
|
|
118
|
+
Deprecated: prefer --role-* going forward.
|
|
119
|
+
============================================================ */
|
|
120
|
+
--status-success: var(--role-success);
|
|
121
|
+
--status-warning: var(--role-warning);
|
|
122
|
+
--status-error: var(--role-error); /* was coral; now purple per colourblind policy */
|
|
123
|
+
--status-info: var(--role-info);
|
|
124
|
+
|
|
125
|
+
/* ============================================================
|
|
126
|
+
LAYER 4 — SHADCN/UI BRIDGE (light mode)
|
|
127
|
+
Maps shadcn's expected CSS var names to our role tokens.
|
|
128
|
+
Do NOT redefine these in app CSS — override the role above.
|
|
129
|
+
============================================================ */
|
|
130
|
+
--background: var(--role-bg-body);
|
|
131
|
+
--foreground: var(--role-text-body);
|
|
132
|
+
--card: var(--role-bg-panel);
|
|
133
|
+
--card-foreground: var(--role-text-body);
|
|
134
|
+
--popover: var(--role-bg-panel);
|
|
135
|
+
--popover-foreground: var(--role-text-body);
|
|
136
|
+
--primary: var(--role-primary);
|
|
137
|
+
--primary-foreground: var(--text-on-primary);
|
|
138
|
+
--secondary: var(--role-secondary);
|
|
139
|
+
--secondary-foreground: var(--text-on-primary);
|
|
140
|
+
--muted: var(--role-bg-input);
|
|
141
|
+
--muted-foreground: var(--role-text-muted);
|
|
142
|
+
--accent: var(--role-accent);
|
|
143
|
+
--accent-foreground: var(--text-on-accent);
|
|
144
|
+
--destructive: var(--role-destructive);
|
|
145
|
+
--destructive-foreground: var(--text-on-error);
|
|
146
|
+
--border: var(--role-border);
|
|
147
|
+
--input: var(--role-border);
|
|
148
|
+
--ring: var(--role-primary);
|
|
149
|
+
|
|
150
|
+
/* ============================================================
|
|
151
|
+
WCAG AA compliant text pairs for raw neon on light bg
|
|
152
|
+
(neon cyan L=0.77 on light bg is only 1.76:1 — fails 4.5:1)
|
|
153
|
+
============================================================ */
|
|
154
|
+
--color-accent-text: oklch(0.486 0.084 191.5); /* Dark cyan — AA on light bg */
|
|
155
|
+
--color-primary-text: oklch(0.485 0.243 263.6); /* Saturated blue — AA on light bg */
|
|
156
|
+
|
|
157
|
+
/* ============================================================
|
|
158
|
+
GLOW + SHADOW MULTIPLIERS
|
|
159
|
+
============================================================ */
|
|
160
|
+
--glow-strength: 0.4;
|
|
72
161
|
--shadow-strength: 1;
|
|
162
|
+
|
|
163
|
+
/* ============================================================
|
|
164
|
+
SRGB FALLBACKS (for browsers without oklch support)
|
|
165
|
+
Tested against Chrome 105-. Values are closest sRGB equivalents.
|
|
166
|
+
============================================================ */
|
|
167
|
+
@supports not (color: oklch(0 0 0)) {
|
|
168
|
+
--neon-electric-blue: #3b82f6;
|
|
169
|
+
--neon-electric-cyan: #06b6d4;
|
|
170
|
+
--neon-electric-indigo: #6366f1;
|
|
171
|
+
--neon-electric-purple: #a855f7;
|
|
172
|
+
--neon-electric-magenta: #f472b6;
|
|
173
|
+
--neon-electric-pink: #ec4899;
|
|
174
|
+
--neon-electric-coral: #f87171;
|
|
175
|
+
--neon-electric-orange: #f97316;
|
|
176
|
+
--neon-electric-yellow: #fbbf24;
|
|
177
|
+
--neon-electric-green: #22c55e;
|
|
178
|
+
--neon-electric-lavender: #c084fc;
|
|
179
|
+
--role-error: #a855f7;
|
|
180
|
+
--role-destructive: #a855f7;
|
|
181
|
+
--light-text-primary: #1e1f2e;
|
|
182
|
+
--dark-text-primary: #e8eaf6;
|
|
183
|
+
}
|
|
73
184
|
}
|
|
74
185
|
|
|
186
|
+
/* ============================================================
|
|
187
|
+
DARK MODE — Layer 2 surface overrides + Layer 3 role rebind
|
|
188
|
+
============================================================ */
|
|
75
189
|
.dark {
|
|
76
|
-
/*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
--
|
|
80
|
-
--
|
|
190
|
+
/* Dark surface primitives */
|
|
191
|
+
--dark-bg-primary: oklch(0.166 0.026 269.4);
|
|
192
|
+
--dark-bg-secondary: oklch(0.242 0.03 269.9);
|
|
193
|
+
--dark-bg-tertiary: oklch(0.326 0.036 266.7);
|
|
194
|
+
--dark-bg-quaternary: oklch(0.39 0.035 265);
|
|
195
|
+
--dark-bg-accent: oklch(0.292 0.034 270);
|
|
196
|
+
|
|
197
|
+
/* D2C dark text scale — hue 260, capped L=0.94.
|
|
198
|
+
Pure white (L=1.0) is BANNED as a text token.
|
|
199
|
+
Eye-strain: 16:1 → ~14:1; perceived glare drops significantly.
|
|
200
|
+
Verified WCAG AA on --dark-bg-primary oklch(0.166 0.026 269.4). */
|
|
201
|
+
--dark-text-primary: oklch(0.94 0.012 260); /* body — ~13.9:1 ✓ AAA */
|
|
202
|
+
--dark-text-secondary: oklch(0.82 0.015 260); /* labels — ~9.8:1 ✓ AAA */
|
|
203
|
+
--dark-text-muted: oklch(0.68 0.018 260); /* metadata — ~5.6:1 ✓ AA */
|
|
204
|
+
--dark-text-subtle: oklch(0.56 0.015 260); /* placeholders — ~3.6:1 ✓ AA large */
|
|
205
|
+
--dark-text-disabled: oklch(0.44 0.010 260); /* disabled — pair with icon cue */
|
|
206
|
+
/* Legacy aliases */
|
|
207
|
+
--dark-text-tertiary: var(--dark-text-muted);
|
|
208
|
+
--dark-text-quaternary: var(--dark-text-disabled);
|
|
81
209
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
210
|
+
--dark-border: oklch(0.428 0.015 248.2);
|
|
211
|
+
--dark-hover: oklch(0.39 0.035 265);
|
|
212
|
+
|
|
213
|
+
/* Rebind roles to dark-mode surface values */
|
|
214
|
+
--role-text-body: var(--dark-text-primary);
|
|
215
|
+
--role-text-secondary: var(--dark-text-secondary);
|
|
216
|
+
--role-text-muted: var(--dark-text-muted);
|
|
217
|
+
--role-text-subtle: var(--dark-text-subtle);
|
|
218
|
+
--role-text-disabled: var(--dark-text-disabled);
|
|
219
|
+
--role-text-heading: oklch(0.92 0.014 260); /* ~12.8:1 ✓ AAA */
|
|
220
|
+
|
|
221
|
+
--role-bg-body: var(--dark-bg-primary);
|
|
222
|
+
--role-bg-surface: var(--dark-bg-secondary);
|
|
223
|
+
--role-bg-panel: oklch(0.19 0.02 260 / 0.8);
|
|
224
|
+
--role-bg-input: oklch(0.24 0.02 260 / 0.6);
|
|
225
|
+
--role-border: var(--dark-border);
|
|
226
|
+
|
|
227
|
+
/* Accent/primary text tokens flip to raw neons in dark mode —
|
|
228
|
+
contrast on navy bg is already ≥ 9:1 for these values. */
|
|
229
|
+
--color-accent-text: oklch(0.769 0.132 191.7);
|
|
85
230
|
--color-primary-text: oklch(0.623 0.188 259.8);
|
|
231
|
+
|
|
232
|
+
/* Glow/shadow scale up in dark mode */
|
|
233
|
+
--glow-strength: 0.6;
|
|
234
|
+
--shadow-strength: 0.8;
|
|
235
|
+
|
|
236
|
+
/* Shadcn bridge picks up rebindings automatically via var() chain */
|
|
86
237
|
}
|
package/src/preset-v4.css
CHANGED
|
@@ -1,90 +1,182 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @bsuite/theme — Tailwind v4 @theme block
|
|
3
|
+
* Version 0.3.1
|
|
3
4
|
*
|
|
4
|
-
* Tailwind v4
|
|
5
|
-
*
|
|
6
|
-
* app-specific @theme overrides:
|
|
5
|
+
* Tailwind v4 exposes design tokens to the engine via CSS @theme blocks.
|
|
6
|
+
* Import this before any app-specific @theme overrides:
|
|
7
7
|
*
|
|
8
8
|
* @import 'tailwindcss';
|
|
9
9
|
* @import '@bsuite/theme/preset-v4.css';
|
|
10
10
|
*
|
|
11
11
|
* @theme {
|
|
12
|
-
* --color-app-primary: oklch(0.45 0.18 142);
|
|
13
|
-
* ... app-specific overrides below
|
|
12
|
+
* --color-app-primary: oklch(0.45 0.18 142); -- app-specific override
|
|
14
13
|
* }
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Token naming: `--color-{name}` → Tailwind generates `bg-{name}`,
|
|
16
|
+
* `text-{name}`, `border-{name}` etc. automatically.
|
|
17
|
+
*
|
|
18
|
+
* Layer order matches vars.css:
|
|
19
|
+
* 1. Palette — --color-neon-electric-*
|
|
20
|
+
* 2. Surface — --color-{light|dark}-{bg|text}-*
|
|
21
|
+
* 3. Role — --color-role-* ← bind to these in components
|
|
22
|
+
* 4. Shadcn — --color-background, --color-foreground, etc.
|
|
23
|
+
* 5. Text-on — --color-text-on-*
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
@theme {
|
|
22
|
-
/*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
--color-neon-electric-
|
|
26
|
-
--color-neon-electric-
|
|
27
|
-
--color-neon-electric-
|
|
28
|
-
--color-neon-electric-
|
|
29
|
-
--color-neon-electric-
|
|
30
|
-
--color-neon-electric-
|
|
31
|
-
--color-neon-electric-
|
|
32
|
-
--color-neon-electric-
|
|
27
|
+
/* ============================================================
|
|
28
|
+
LAYER 1 — Neon Electric palette (11 colours, FROZEN)
|
|
29
|
+
============================================================ */
|
|
30
|
+
--color-neon-electric-blue: oklch(0.546 0.215 262.9);
|
|
31
|
+
--color-neon-electric-cyan: oklch(0.769 0.132 191.7);
|
|
32
|
+
--color-neon-electric-indigo: oklch(0.511 0.23 277);
|
|
33
|
+
--color-neon-electric-purple: oklch(0.568 0.202 283.1);
|
|
34
|
+
--color-neon-electric-magenta: oklch(0.742 0.167 359.5);
|
|
35
|
+
--color-neon-electric-pink: oklch(0.656 0.212 354.3);
|
|
36
|
+
--color-neon-electric-coral: oklch(0.669 0.219 20.9);
|
|
37
|
+
--color-neon-electric-orange: oklch(0.728 0.168 22.5);
|
|
38
|
+
--color-neon-electric-yellow: oklch(0.868 0.125 81.4);
|
|
39
|
+
--color-neon-electric-green: oklch(0.723 0.192 149.6);
|
|
33
40
|
--color-neon-electric-lavender: oklch(0.736 0.141 285.6);
|
|
34
41
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
42
|
+
/* ============================================================
|
|
43
|
+
LAYER 2 — Surface tokens (light)
|
|
44
|
+
============================================================ */
|
|
45
|
+
--color-light-bg-primary: oklch(0.961 0 0.5);
|
|
37
46
|
--color-light-bg-secondary: oklch(0.982 0.002 248);
|
|
38
|
-
--color-light-bg-tertiary:
|
|
39
|
-
--color-light-bg-accent:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
--color-
|
|
45
|
-
--color-
|
|
46
|
-
--color-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
--color-
|
|
53
|
-
--color-
|
|
54
|
-
--color-
|
|
55
|
-
--color-
|
|
56
|
-
|
|
57
|
-
/*
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
|
|
47
|
+
--color-light-bg-tertiary: oklch(0.963 0.003 228.9);
|
|
48
|
+
--color-light-bg-accent: oklch(1 0 0.5);
|
|
49
|
+
|
|
50
|
+
/* Eye-strain-safe light text scale (L capped at 0.22, hue 260) */
|
|
51
|
+
--color-light-text-primary: oklch(0.22 0.015 260); /* 13.2:1 AAA */
|
|
52
|
+
--color-light-text-secondary: oklch(0.38 0.018 260); /* 8.1:1 AAA */
|
|
53
|
+
--color-light-text-muted: oklch(0.52 0.018 260); /* 4.9:1 AA */
|
|
54
|
+
--color-light-text-subtle: oklch(0.60 0.012 260); /* 3.8:1 AA large */
|
|
55
|
+
--color-light-text-disabled: oklch(0.72 0.010 260);
|
|
56
|
+
|
|
57
|
+
/* ============================================================
|
|
58
|
+
LAYER 2 — Surface tokens (dark)
|
|
59
|
+
============================================================ */
|
|
60
|
+
--color-dark-bg-primary: oklch(0.166 0.026 269.4);
|
|
61
|
+
--color-dark-bg-secondary: oklch(0.242 0.03 269.9);
|
|
62
|
+
--color-dark-bg-tertiary: oklch(0.326 0.036 266.7);
|
|
63
|
+
--color-dark-bg-quaternary: oklch(0.39 0.035 265);
|
|
64
|
+
--color-dark-bg-accent: oklch(0.292 0.034 270);
|
|
65
|
+
|
|
66
|
+
/* Eye-strain-safe dark text scale (L capped at 0.94, hue 260) */
|
|
67
|
+
--color-dark-text-primary: oklch(0.94 0.012 260); /* 13.9:1 AAA */
|
|
68
|
+
--color-dark-text-secondary: oklch(0.82 0.015 260); /* 9.8:1 AAA */
|
|
69
|
+
--color-dark-text-muted: oklch(0.68 0.018 260); /* 5.6:1 AA */
|
|
70
|
+
--color-dark-text-subtle: oklch(0.56 0.015 260); /* 3.6:1 AA large */
|
|
71
|
+
--color-dark-text-disabled: oklch(0.44 0.010 260);
|
|
72
|
+
|
|
73
|
+
/* ============================================================
|
|
74
|
+
LAYER 3 — Role aliases
|
|
75
|
+
Consumers should only reference these — never palette names.
|
|
76
|
+
============================================================ */
|
|
77
|
+
--color-role-primary: oklch(0.546 0.215 262.9); /* electric blue */
|
|
78
|
+
--color-role-secondary: oklch(0.511 0.23 277); /* electric indigo */
|
|
79
|
+
--color-role-accent: oklch(0.769 0.132 191.7); /* electric cyan */
|
|
80
|
+
|
|
81
|
+
/* Colourblind-safe: error = purple, never red/coral */
|
|
82
|
+
--color-role-error: oklch(0.568 0.202 283.1); /* electric purple */
|
|
83
|
+
--color-role-destructive: oklch(0.568 0.202 283.1); /* synonym */
|
|
84
|
+
--color-role-warning: oklch(0.728 0.168 22.5); /* electric orange */
|
|
85
|
+
--color-role-success: oklch(0.723 0.192 149.6); /* electric green */
|
|
86
|
+
--color-role-info: oklch(0.769 0.132 191.7); /* electric cyan */
|
|
87
|
+
|
|
88
|
+
/* Semantic status aliases (deprecated: prefer role-*) */
|
|
89
|
+
--color-status-success: oklch(0.723 0.192 149.6);
|
|
90
|
+
--color-status-warning: oklch(0.728 0.168 22.5);
|
|
91
|
+
--color-status-error: oklch(0.568 0.202 283.1); /* purple — colourblind policy */
|
|
92
|
+
--color-status-info: oklch(0.769 0.132 191.7);
|
|
93
|
+
|
|
94
|
+
/* ============================================================
|
|
95
|
+
LAYER 4 — Shadcn/ui bridge
|
|
96
|
+
Tailwind generates bg-background, text-foreground, etc.
|
|
97
|
+
============================================================ */
|
|
98
|
+
--color-background: oklch(0.961 0 0.5);
|
|
99
|
+
--color-foreground: oklch(0.22 0.015 260);
|
|
100
|
+
--color-card: oklch(1 0 0.5);
|
|
101
|
+
--color-card-foreground: oklch(0.22 0.015 260);
|
|
102
|
+
--color-popover: oklch(1 0 0.5);
|
|
103
|
+
--color-popover-foreground: oklch(0.22 0.015 260);
|
|
104
|
+
--color-primary: oklch(0.546 0.215 262.9);
|
|
105
|
+
--color-primary-foreground: oklch(0.99 0 0);
|
|
106
|
+
--color-secondary: oklch(0.511 0.23 277);
|
|
107
|
+
--color-secondary-foreground: oklch(0.99 0 0);
|
|
108
|
+
--color-muted: oklch(0.963 0.003 228.9);
|
|
109
|
+
--color-muted-foreground: oklch(0.52 0.018 260);
|
|
110
|
+
--color-accent: oklch(0.769 0.132 191.7);
|
|
111
|
+
--color-accent-foreground: oklch(0.17 0.02 260);
|
|
112
|
+
--color-destructive: oklch(0.568 0.202 283.1); /* purple — colourblind policy */
|
|
113
|
+
--color-destructive-foreground: oklch(0.99 0 0);
|
|
114
|
+
--color-border: oklch(0.942 0.005 247.9);
|
|
115
|
+
--color-input: oklch(0.942 0.005 247.9);
|
|
116
|
+
--color-ring: oklch(0.546 0.215 262.9);
|
|
117
|
+
|
|
118
|
+
/* ============================================================
|
|
119
|
+
LAYER 5 — Inverse text (for coloured fill surfaces)
|
|
120
|
+
============================================================ */
|
|
121
|
+
--color-text-on-primary: oklch(0.99 0 0);
|
|
122
|
+
--color-text-on-accent: oklch(0.17 0.02 260);
|
|
123
|
+
--color-text-on-error: oklch(0.99 0 0);
|
|
124
|
+
--color-text-on-success: oklch(0.17 0.02 260);
|
|
125
|
+
--color-text-on-warning: oklch(0.17 0.02 260);
|
|
126
|
+
|
|
127
|
+
/* ============================================================
|
|
128
|
+
WCAG AA text pairs for raw neons on light backgrounds
|
|
129
|
+
============================================================ */
|
|
130
|
+
--color-accent-text: oklch(0.486 0.084 191.5);
|
|
131
|
+
--color-primary-text: oklch(0.485 0.243 263.6);
|
|
132
|
+
|
|
133
|
+
/* ============================================================
|
|
134
|
+
ANIMATIONS
|
|
135
|
+
============================================================ */
|
|
136
|
+
--animate-pulse-soft: pulse-soft 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
137
|
+
--animate-glow: glow 2s ease-in-out infinite alternate;
|
|
138
|
+
--animate-neon-pulse: neon-pulse 2s ease-in-out infinite;
|
|
139
|
+
--animate-float: float 3s ease-in-out infinite;
|
|
140
|
+
--animate-shimmer: shimmer 2s infinite;
|
|
141
|
+
--animate-typing: typing 1.5s infinite;
|
|
142
|
+
--animate-shine: shine 3s ease-in-out infinite;
|
|
64
143
|
}
|
|
65
144
|
|
|
66
|
-
/*
|
|
145
|
+
/* ============================================================
|
|
146
|
+
KEYFRAMES — outside @theme (v4 doesn't expose them there)
|
|
147
|
+
oklch used throughout — no rgba/hsl leakage
|
|
148
|
+
============================================================ */
|
|
67
149
|
@keyframes pulse-soft {
|
|
68
150
|
0%, 100% { opacity: 1; }
|
|
69
|
-
50%
|
|
151
|
+
50% { opacity: 0.7; }
|
|
70
152
|
}
|
|
153
|
+
|
|
71
154
|
@keyframes glow {
|
|
72
|
-
0%
|
|
73
|
-
100% { box-shadow: 0 0 20px
|
|
155
|
+
0% { box-shadow: 0 0 5px oklch(0.769 0.132 191.7 / 0.2); }
|
|
156
|
+
100% { box-shadow: 0 0 20px oklch(0.769 0.132 191.7 / 0.6); }
|
|
74
157
|
}
|
|
158
|
+
|
|
75
159
|
@keyframes neon-pulse {
|
|
76
|
-
0%, 100% { text-shadow: 0 0 10px
|
|
77
|
-
50%
|
|
160
|
+
0%, 100% { text-shadow: 0 0 10px oklch(0.769 0.132 191.7 / 0.3); }
|
|
161
|
+
50% { text-shadow: 0 0 20px oklch(0.769 0.132 191.7 / 0.8); }
|
|
78
162
|
}
|
|
163
|
+
|
|
79
164
|
@keyframes float {
|
|
80
165
|
0%, 100% { transform: translateY(0px); }
|
|
81
|
-
50%
|
|
166
|
+
50% { transform: translateY(-10px); }
|
|
82
167
|
}
|
|
168
|
+
|
|
83
169
|
@keyframes shimmer {
|
|
84
|
-
0%
|
|
85
|
-
100% { background-position:
|
|
170
|
+
0% { background-position: -1000px 0; }
|
|
171
|
+
100% { background-position: 1000px 0; }
|
|
86
172
|
}
|
|
173
|
+
|
|
87
174
|
@keyframes typing {
|
|
88
175
|
0%, 60% { opacity: 1; }
|
|
89
|
-
30%
|
|
176
|
+
30% { opacity: 0.4; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@keyframes shine {
|
|
180
|
+
0% { transform: translateX(-100%) skewX(-15deg); }
|
|
181
|
+
100% { transform: translateX(200%) skewX(-15deg); }
|
|
90
182
|
}
|