@bsuite/theme 0.1.1 → 0.2.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/dist/react/BrandingProvider.d.ts +63 -0
- package/dist/react/BrandingProvider.d.ts.map +1 -0
- package/dist/react/BrandingProvider.js +183 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/useBranding.d.ts +11 -0
- package/dist/react/useBranding.d.ts.map +1 -0
- package/dist/react/useBranding.js +18 -0
- package/package.json +7 -1
- package/src/css/utilities.css +12 -12
- package/src/css/vars.css +50 -63
- package/src/preset-v4.css +27 -27
- package/src/tailwind-preset.js +37 -31
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrandingProvider — runtime enterprise white-labelling
|
|
3
|
+
*
|
|
4
|
+
* On mount:
|
|
5
|
+
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
6
|
+
* 2. Applies each key as a CSS custom property on document.documentElement.
|
|
7
|
+
* 3. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
8
|
+
* 4. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
9
|
+
* propagate without a page reload (within ~1 second).
|
|
10
|
+
*
|
|
11
|
+
* Environment flags:
|
|
12
|
+
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set to 'false' to disable
|
|
13
|
+
* entirely without code changes (emergency kill switch).
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* Wrap BrandingProvider inside ThemeProvider so branding CSS vars override theme defaults:
|
|
17
|
+
*
|
|
18
|
+
* <ThemeProvider>
|
|
19
|
+
* <BrandingProvider supabaseClient={supabase}>
|
|
20
|
+
* {children}
|
|
21
|
+
* </BrandingProvider>
|
|
22
|
+
* </ThemeProvider>
|
|
23
|
+
*/
|
|
24
|
+
import type { ReactNode } from 'react';
|
|
25
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
26
|
+
export declare const BRANDING_STORAGE_KEY = "bsuite_tenant_branding";
|
|
27
|
+
export declare const BRANDING_OVERRIDE_FLAG = "VITE_ENABLE_BRANDING_OVERRIDE";
|
|
28
|
+
/** Shape returned by the branding_json_for_tenant RPC */
|
|
29
|
+
export interface TenantBranding {
|
|
30
|
+
/** Primary action colour as an OKLCH string, e.g. "oklch(0.55 0.22 265)" */
|
|
31
|
+
primary?: string;
|
|
32
|
+
/** Accent / secondary colour as an OKLCH string */
|
|
33
|
+
accent?: string;
|
|
34
|
+
/** Full logo URL (SVG or raster) */
|
|
35
|
+
logo_url?: string;
|
|
36
|
+
/** Mark / icon logo URL */
|
|
37
|
+
mark_url?: string;
|
|
38
|
+
/** Optional CSS font-family stack string */
|
|
39
|
+
font_stack?: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface BrandingContextValue {
|
|
42
|
+
/** Current resolved branding, or null if no tenant override */
|
|
43
|
+
branding: TenantBranding | null;
|
|
44
|
+
/** True while the initial RPC call is in flight */
|
|
45
|
+
isLoading: boolean;
|
|
46
|
+
/** Any error from the RPC call */
|
|
47
|
+
error: Error | null;
|
|
48
|
+
/** Manually trigger a branding refresh */
|
|
49
|
+
refresh: () => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export declare const BrandingContext: import("react").Context<BrandingContextValue | undefined>;
|
|
52
|
+
export interface BrandingProviderProps {
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
/** Authenticated Supabase client from the consuming app */
|
|
55
|
+
supabaseClient: SupabaseClient;
|
|
56
|
+
/**
|
|
57
|
+
* If true, apply persisted branding immediately on mount (before the RPC
|
|
58
|
+
* resolves) to prevent FOUC. Default: true.
|
|
59
|
+
*/
|
|
60
|
+
applyPersistedOnMount?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export declare function BrandingProvider({ children, supabaseClient, applyPersistedOnMount, }: BrandingProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
//# sourceMappingURL=BrandingProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrandingProvider.d.ts","sourceRoot":"","sources":["../../src/react/BrandingProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mDAAmD;IACnD,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,+DAA+D;IAC/D,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAA;IAClB,kCAAkC;IAClC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,eAAO,MAAM,eAAe,2DAA6D,CAAA;AAqFzF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAA;IACnB,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAA;IAC9B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,cAAc,EACd,qBAA4B,GAC7B,EAAE,qBAAqB,2CAyFvB"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BrandingProvider — runtime enterprise white-labelling
|
|
4
|
+
*
|
|
5
|
+
* On mount:
|
|
6
|
+
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
7
|
+
* 2. Applies each key as a CSS custom property on document.documentElement.
|
|
8
|
+
* 3. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
9
|
+
* 4. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
10
|
+
* propagate without a page reload (within ~1 second).
|
|
11
|
+
*
|
|
12
|
+
* Environment flags:
|
|
13
|
+
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set to 'false' to disable
|
|
14
|
+
* entirely without code changes (emergency kill switch).
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* Wrap BrandingProvider inside ThemeProvider so branding CSS vars override theme defaults:
|
|
18
|
+
*
|
|
19
|
+
* <ThemeProvider>
|
|
20
|
+
* <BrandingProvider supabaseClient={supabase}>
|
|
21
|
+
* {children}
|
|
22
|
+
* </BrandingProvider>
|
|
23
|
+
* </ThemeProvider>
|
|
24
|
+
*/
|
|
25
|
+
import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
26
|
+
export const BRANDING_STORAGE_KEY = 'bsuite_tenant_branding';
|
|
27
|
+
export const BRANDING_OVERRIDE_FLAG = 'VITE_ENABLE_BRANDING_OVERRIDE';
|
|
28
|
+
export const BrandingContext = createContext(undefined);
|
|
29
|
+
const BRANDING_CSS_MAP = {
|
|
30
|
+
primary: '--primary',
|
|
31
|
+
accent: '--accent',
|
|
32
|
+
logo_url: '--logo-url',
|
|
33
|
+
mark_url: '--mark-url',
|
|
34
|
+
font_stack: '--font-stack',
|
|
35
|
+
};
|
|
36
|
+
function applyBrandingToRoot(branding) {
|
|
37
|
+
if (typeof document === 'undefined')
|
|
38
|
+
return;
|
|
39
|
+
const root = document.documentElement;
|
|
40
|
+
if (!branding) {
|
|
41
|
+
// Remove any previously applied overrides
|
|
42
|
+
Object.values(BRANDING_CSS_MAP).forEach((cssVar) => {
|
|
43
|
+
if (cssVar)
|
|
44
|
+
root.style.removeProperty(cssVar);
|
|
45
|
+
});
|
|
46
|
+
// Also remove aliased vars
|
|
47
|
+
root.style.removeProperty('--accent-primary');
|
|
48
|
+
root.style.removeProperty('--app-primary');
|
|
49
|
+
root.style.removeProperty('--app-accent');
|
|
50
|
+
root.removeAttribute('data-branding-loaded');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (branding.primary) {
|
|
54
|
+
root.style.setProperty('--primary', branding.primary);
|
|
55
|
+
root.style.setProperty('--accent-primary', branding.primary);
|
|
56
|
+
root.style.setProperty('--app-primary', branding.primary);
|
|
57
|
+
root.style.setProperty('--ring', branding.primary);
|
|
58
|
+
}
|
|
59
|
+
if (branding.accent) {
|
|
60
|
+
root.style.setProperty('--accent', branding.accent);
|
|
61
|
+
root.style.setProperty('--accent-secondary', branding.accent);
|
|
62
|
+
root.style.setProperty('--app-accent', branding.accent);
|
|
63
|
+
}
|
|
64
|
+
if (branding.logo_url) {
|
|
65
|
+
root.style.setProperty('--logo-url', `url(${branding.logo_url})`);
|
|
66
|
+
}
|
|
67
|
+
if (branding.mark_url) {
|
|
68
|
+
root.style.setProperty('--mark-url', `url(${branding.mark_url})`);
|
|
69
|
+
}
|
|
70
|
+
if (branding.font_stack) {
|
|
71
|
+
root.style.setProperty('--font-stack', branding.font_stack);
|
|
72
|
+
}
|
|
73
|
+
root.setAttribute('data-branding-loaded', 'true');
|
|
74
|
+
}
|
|
75
|
+
function persistBranding(branding) {
|
|
76
|
+
try {
|
|
77
|
+
if (branding) {
|
|
78
|
+
localStorage.setItem(BRANDING_STORAGE_KEY, JSON.stringify(branding));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
localStorage.removeItem(BRANDING_STORAGE_KEY);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// localStorage not available — silently ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function loadPersistedBranding() {
|
|
89
|
+
try {
|
|
90
|
+
const raw = localStorage.getItem(BRANDING_STORAGE_KEY);
|
|
91
|
+
if (!raw)
|
|
92
|
+
return null;
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isBrandingEnabled() {
|
|
100
|
+
try {
|
|
101
|
+
// Vite env var — only present in client bundles that set it
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
const flag = import.meta.env;
|
|
104
|
+
const value = flag?.VITE_ENABLE_BRANDING_OVERRIDE ?? flag?.['VITE_ENABLE_BRANDING_OVERRIDE'];
|
|
105
|
+
if (value === 'false' || value === '0')
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Not in a Vite context — default to enabled
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
export function BrandingProvider({ children, supabaseClient, applyPersistedOnMount = true, }) {
|
|
114
|
+
const [branding, setBranding] = useState(null);
|
|
115
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
116
|
+
const [error, setError] = useState(null);
|
|
117
|
+
const channelRef = useRef(null);
|
|
118
|
+
const fetchBranding = useCallback(async () => {
|
|
119
|
+
if (!isBrandingEnabled()) {
|
|
120
|
+
setIsLoading(false);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setIsLoading(true);
|
|
124
|
+
setError(null);
|
|
125
|
+
try {
|
|
126
|
+
const { data, error: rpcError } = await supabaseClient.rpc('branding_json_for_tenant');
|
|
127
|
+
if (rpcError) {
|
|
128
|
+
throw new Error(rpcError.message);
|
|
129
|
+
}
|
|
130
|
+
const resolved = data ?? null;
|
|
131
|
+
setBranding(resolved);
|
|
132
|
+
applyBrandingToRoot(resolved);
|
|
133
|
+
persistBranding(resolved);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
137
|
+
// On error keep any persisted branding applied (already done at mount)
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
setIsLoading(false);
|
|
141
|
+
}
|
|
142
|
+
}, [supabaseClient]);
|
|
143
|
+
// Apply persisted branding immediately to prevent FOUC
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (applyPersistedOnMount && isBrandingEnabled()) {
|
|
146
|
+
const persisted = loadPersistedBranding();
|
|
147
|
+
if (persisted) {
|
|
148
|
+
applyBrandingToRoot(persisted);
|
|
149
|
+
setBranding(persisted);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}, [applyPersistedOnMount]);
|
|
153
|
+
// Fetch fresh branding on mount
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
void fetchBranding();
|
|
156
|
+
}, [fetchBranding]);
|
|
157
|
+
// Subscribe to Realtime tenant row changes so branding propagates without reload
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!isBrandingEnabled())
|
|
160
|
+
return;
|
|
161
|
+
const channel = supabaseClient
|
|
162
|
+
.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
|
+
})
|
|
172
|
+
.subscribe();
|
|
173
|
+
channelRef.current = channel;
|
|
174
|
+
return () => {
|
|
175
|
+
if (channelRef.current) {
|
|
176
|
+
void supabaseClient.removeChannel(channelRef.current);
|
|
177
|
+
channelRef.current = null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [supabaseClient, fetchBranding]);
|
|
181
|
+
const value = useMemo(() => ({ branding, isLoading, error, refresh: fetchBranding }), [branding, isLoading, error, fetchBranding]);
|
|
182
|
+
return _jsx(BrandingContext.Provider, { value: value, children: children });
|
|
183
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export type { ThemeProviderProps } from './ThemeProvider';
|
|
|
3
3
|
export { useTheme } from './useTheme';
|
|
4
4
|
export type { ThemeContextValue, ThemeMode, ResolvedTheme } from '../index';
|
|
5
5
|
export { THEME_STORAGE_KEY } from '../index';
|
|
6
|
+
export { BrandingProvider, BrandingContext, BRANDING_STORAGE_KEY, BRANDING_OVERRIDE_FLAG } from './BrandingProvider';
|
|
7
|
+
export type { TenantBranding, BrandingContextValue } from './BrandingProvider';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACpH,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/react/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access the current tenant branding values and loading state.
|
|
3
|
+
*
|
|
4
|
+
* Must be used inside a <BrandingProvider>.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { branding, isLoading } = useBranding()
|
|
8
|
+
* const logoUrl = branding?.logo_url ?? '/logos/default-logo.svg'
|
|
9
|
+
*/
|
|
10
|
+
export declare function useBranding(): import("./BrandingProvider").BrandingContextValue;
|
|
11
|
+
//# sourceMappingURL=useBranding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBranding.d.ts","sourceRoot":"","sources":["../../src/react/useBranding.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,sDAM1B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { BrandingContext } from './BrandingProvider';
|
|
3
|
+
/**
|
|
4
|
+
* Access the current tenant branding values and loading state.
|
|
5
|
+
*
|
|
6
|
+
* Must be used inside a <BrandingProvider>.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { branding, isLoading } = useBranding()
|
|
10
|
+
* const logoUrl = branding?.logo_url ?? '/logos/default-logo.svg'
|
|
11
|
+
*/
|
|
12
|
+
export function useBranding() {
|
|
13
|
+
const ctx = useContext(BrandingContext);
|
|
14
|
+
if (!ctx) {
|
|
15
|
+
throw new Error('useBranding must be used within a <BrandingProvider>');
|
|
16
|
+
}
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -46,18 +46,24 @@
|
|
|
46
46
|
"prepublishOnly": "pnpm build"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
+
"@supabase/supabase-js": ">=2.0.0",
|
|
49
50
|
"react": ">=18.0.0",
|
|
50
51
|
"react-dom": ">=18.0.0",
|
|
51
52
|
"tailwindcss": ">=3.4.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependenciesMeta": {
|
|
55
|
+
"@supabase/supabase-js": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
54
58
|
"tailwindcss": {
|
|
55
59
|
"optional": true
|
|
56
60
|
}
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
63
|
+
"@supabase/supabase-js": "^2.49.4",
|
|
59
64
|
"@testing-library/jest-dom": "^6.9.1",
|
|
60
65
|
"@testing-library/react": "^16.3.2",
|
|
66
|
+
"@types/node": "^20.0.0",
|
|
61
67
|
"@types/react": "^18.3.28",
|
|
62
68
|
"@vitejs/plugin-react": "^5.2.0",
|
|
63
69
|
"jsdom": "^28.1.0",
|
package/src/css/utilities.css
CHANGED
|
@@ -5,37 +5,37 @@
|
|
|
5
5
|
* consumer regardless of their Tailwind version (v3 or v4).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/* ===== Glow effects (
|
|
8
|
+
/* ===== Glow effects (oklch with alpha scaled by glow-strength) ===== */
|
|
9
9
|
.glow-electric-blue {
|
|
10
|
-
box-shadow: 0 0 20px
|
|
10
|
+
box-shadow: 0 0 20px oklch(0.546 0.215 262.9 / var(--glow-strength));
|
|
11
11
|
}
|
|
12
12
|
.glow-electric-cyan {
|
|
13
|
-
box-shadow: 0 0 20px
|
|
13
|
+
box-shadow: 0 0 20px oklch(0.769 0.132 191.7 / var(--glow-strength));
|
|
14
14
|
}
|
|
15
15
|
.glow-electric-indigo {
|
|
16
|
-
box-shadow: 0 0 20px
|
|
16
|
+
box-shadow: 0 0 20px oklch(0.511 0.23 277 / var(--glow-strength));
|
|
17
17
|
}
|
|
18
18
|
.glow-electric-purple {
|
|
19
|
-
box-shadow: 0 0 20px
|
|
19
|
+
box-shadow: 0 0 20px oklch(0.568 0.202 283.1 / var(--glow-strength));
|
|
20
20
|
}
|
|
21
21
|
.glow-electric-pink {
|
|
22
|
-
box-shadow: 0 0 20px
|
|
22
|
+
box-shadow: 0 0 20px oklch(0.656 0.212 354.3 / var(--glow-strength));
|
|
23
23
|
}
|
|
24
24
|
.glow-electric-coral {
|
|
25
|
-
box-shadow: 0 0 20px
|
|
25
|
+
box-shadow: 0 0 20px oklch(0.669 0.219 20.9 / var(--glow-strength));
|
|
26
26
|
}
|
|
27
27
|
.glow-electric-magenta {
|
|
28
|
-
box-shadow: 0 0 20px
|
|
28
|
+
box-shadow: 0 0 20px oklch(0.742 0.167 359.5 / var(--glow-strength));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/* ===== Neon text shadows ===== */
|
|
32
32
|
.neon-text-cyan {
|
|
33
|
-
text-shadow: 0 0 10px
|
|
34
|
-
0 0 20px
|
|
33
|
+
text-shadow: 0 0 10px oklch(0.769 0.132 191.7 / 0.5),
|
|
34
|
+
0 0 20px oklch(0.769 0.132 191.7 / 0.3);
|
|
35
35
|
}
|
|
36
36
|
.neon-text-electric {
|
|
37
|
-
text-shadow: 0 0 10px
|
|
38
|
-
0 0 20px
|
|
37
|
+
text-shadow: 0 0 10px oklch(0.546 0.215 262.9 / 0.5),
|
|
38
|
+
0 0 20px oklch(0.546 0.215 262.9 / 0.3);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/* ===== Smooth theme transition ===== */
|
package/src/css/vars.css
CHANGED
|
@@ -1,84 +1,71 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @bsuite/theme — CSS custom properties (:root + .dark)
|
|
3
3
|
*
|
|
4
|
-
* Canonical D2C Neon Electric palette
|
|
5
|
-
*
|
|
4
|
+
* Canonical D2C Neon Electric palette in oklch. Tailwind v4 supports
|
|
5
|
+
* native alpha modifiers on oklch values, so RGB triplets are no longer
|
|
6
|
+
* needed:
|
|
6
7
|
*
|
|
7
8
|
* color: var(--neon-electric-blue);
|
|
8
|
-
* background:
|
|
9
|
+
* background: oklch(from var(--neon-electric-blue) l c h / 0.4);
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* resolve correctly.
|
|
11
|
+
* In Tailwind v4 @theme blocks, oklch values automatically support
|
|
12
|
+
* alpha modifiers: bg-neon-electric-blue/40 works natively.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
:root {
|
|
16
16
|
/* ===== Neon Electric (11 colours — FROZEN per D2C spec v1.00A) ===== */
|
|
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:
|
|
27
|
-
--neon-electric-lavender:
|
|
28
|
-
|
|
29
|
-
/* RGB triplets for rgba() use and Tailwind alpha-value substitution */
|
|
30
|
-
--neon-electric-blue-rgb: 37 99 235;
|
|
31
|
-
--neon-electric-cyan-rgb: 0 206 201;
|
|
32
|
-
--neon-electric-indigo-rgb: 79 70 229;
|
|
33
|
-
--neon-electric-purple-rgb: 108 92 231;
|
|
34
|
-
--neon-electric-magenta-rgb: 253 121 168;
|
|
35
|
-
--neon-electric-pink-rgb: 236 72 153;
|
|
36
|
-
--neon-electric-coral-rgb: 255 71 87;
|
|
37
|
-
--neon-electric-orange-rgb: 255 118 117;
|
|
38
|
-
--neon-electric-yellow-rgb: 253 203 110;
|
|
39
|
-
--neon-electric-green-rgb: 34 197 94;
|
|
40
|
-
--neon-electric-lavender-rgb: 162 155 254;
|
|
17
|
+
--neon-electric-blue: oklch(0.546 0.215 262.9);
|
|
18
|
+
--neon-electric-cyan: oklch(0.769 0.132 191.7);
|
|
19
|
+
--neon-electric-indigo: oklch(0.511 0.23 277);
|
|
20
|
+
--neon-electric-purple: oklch(0.568 0.202 283.1);
|
|
21
|
+
--neon-electric-magenta: oklch(0.742 0.167 359.5);
|
|
22
|
+
--neon-electric-pink: oklch(0.656 0.212 354.3);
|
|
23
|
+
--neon-electric-coral: oklch(0.669 0.219 20.9);
|
|
24
|
+
--neon-electric-orange: oklch(0.728 0.168 22.5);
|
|
25
|
+
--neon-electric-yellow: oklch(0.868 0.125 81.4);
|
|
26
|
+
--neon-electric-green: oklch(0.723 0.192 149.6);
|
|
27
|
+
--neon-electric-lavender: oklch(0.736 0.141 285.6);
|
|
41
28
|
|
|
42
29
|
/* ===== Light theme surfaces ===== */
|
|
43
|
-
--light-bg-primary:
|
|
44
|
-
--light-bg-secondary:
|
|
45
|
-
--light-bg-tertiary:
|
|
46
|
-
--light-bg-accent:
|
|
47
|
-
--light-text-primary:
|
|
48
|
-
--light-text-secondary:
|
|
49
|
-
--light-text-tertiary:
|
|
50
|
-
--light-text-quaternary:
|
|
51
|
-
--light-border:
|
|
52
|
-
--light-hover:
|
|
30
|
+
--light-bg-primary: oklch(0.961 0 0.5);
|
|
31
|
+
--light-bg-secondary: oklch(0.982 0.002 248);
|
|
32
|
+
--light-bg-tertiary: oklch(0.963 0.003 228.9);
|
|
33
|
+
--light-bg-accent: oklch(1 0 0.5);
|
|
34
|
+
--light-text-primary: oklch(0.319 0.01 216.8);
|
|
35
|
+
--light-text-secondary: oklch(0.53 0.015 221.6);
|
|
36
|
+
--light-text-tertiary: oklch(0.768 0.123 250);
|
|
37
|
+
--light-text-quaternary: oklch(0.748 0.017 239.2);
|
|
38
|
+
--light-border: oklch(0.942 0.005 247.9);
|
|
39
|
+
--light-hover: oklch(0.963 0.003 228.9);
|
|
53
40
|
|
|
54
41
|
/* ===== Dark theme surfaces ===== */
|
|
55
|
-
--dark-bg-primary:
|
|
56
|
-
--dark-bg-secondary:
|
|
57
|
-
--dark-bg-tertiary:
|
|
58
|
-
--dark-bg-quaternary:
|
|
59
|
-
--dark-bg-accent:
|
|
60
|
-
--dark-text-primary:
|
|
61
|
-
--dark-text-secondary:
|
|
62
|
-
--dark-text-tertiary:
|
|
63
|
-
--dark-text-quaternary:
|
|
64
|
-
--dark-border:
|
|
65
|
-
--dark-hover:
|
|
42
|
+
--dark-bg-primary: oklch(0.166 0.026 269.4);
|
|
43
|
+
--dark-bg-secondary: oklch(0.242 0.03 269.9);
|
|
44
|
+
--dark-bg-tertiary: oklch(0.326 0.036 266.7);
|
|
45
|
+
--dark-bg-quaternary: oklch(0.39 0.035 265);
|
|
46
|
+
--dark-bg-accent: oklch(0.292 0.034 270);
|
|
47
|
+
--dark-text-primary: oklch(0.982 0.002 248);
|
|
48
|
+
--dark-text-secondary: oklch(0.769 0.015 248);
|
|
49
|
+
--dark-text-tertiary: oklch(0.558 0.016 244.9);
|
|
50
|
+
--dark-text-quaternary: oklch(0.428 0.015 248.2);
|
|
51
|
+
--dark-border: oklch(0.428 0.015 248.2);
|
|
52
|
+
--dark-hover: oklch(0.39 0.035 265);
|
|
66
53
|
|
|
67
54
|
/* ===== Semantic status colours ===== */
|
|
68
|
-
--status-success:
|
|
69
|
-
--status-warning:
|
|
70
|
-
--status-error:
|
|
71
|
-
--status-info:
|
|
55
|
+
--status-success: oklch(0.697 0.135 172.1);
|
|
56
|
+
--status-warning: oklch(0.868 0.125 81.4);
|
|
57
|
+
--status-error: oklch(0.669 0.219 20.9);
|
|
58
|
+
--status-info: oklch(0.769 0.132 191.7);
|
|
72
59
|
|
|
73
60
|
/* ===== WCAG AA compliant text pairs =====
|
|
74
61
|
*
|
|
75
|
-
* Raw neon cyan
|
|
62
|
+
* Raw neon cyan on light bg is 1.76:1 — fails WCAG AA 4.5:1.
|
|
76
63
|
* These tokens are the accessible text equivalents for the same visual
|
|
77
64
|
* identity. Use these for any text, border, or icon that needs to pass
|
|
78
65
|
* contrast checks; use raw --neon-electric-cyan for backgrounds and
|
|
79
66
|
* decorative elements only. */
|
|
80
|
-
--color-accent-text:
|
|
81
|
-
--color-primary-text:
|
|
67
|
+
--color-accent-text: oklch(0.486 0.084 191.5); /* Dark cyan — passes AA on light bg */
|
|
68
|
+
--color-primary-text: oklch(0.485 0.243 263.6); /* Saturated blue — passes AA on light bg */
|
|
82
69
|
|
|
83
70
|
/* ===== Glow + shadow strength multipliers ===== */
|
|
84
71
|
--glow-strength: 0.4;
|
|
@@ -86,14 +73,14 @@
|
|
|
86
73
|
}
|
|
87
74
|
|
|
88
75
|
.dark {
|
|
89
|
-
/* Neon palette is identical in dark mode — same
|
|
90
|
-
*
|
|
76
|
+
/* Neon palette is identical in dark mode — same oklch values.
|
|
77
|
+
* What changes is the intensity of glow/shadow effects, which
|
|
91
78
|
* we scale down to suit dark surfaces. */
|
|
92
79
|
--glow-strength: 0.6;
|
|
93
80
|
--shadow-strength: 0.8;
|
|
94
81
|
|
|
95
82
|
/* Accent/primary text tokens flip to the raw neons in dark mode because
|
|
96
|
-
* contrast on
|
|
97
|
-
--color-accent-text:
|
|
98
|
-
--color-primary-text:
|
|
83
|
+
* contrast on navy bg is already ≥ 9:1 for those values. */
|
|
84
|
+
--color-accent-text: oklch(0.769 0.132 191.7);
|
|
85
|
+
--color-primary-text: oklch(0.623 0.188 259.8);
|
|
99
86
|
}
|
package/src/preset-v4.css
CHANGED
|
@@ -20,39 +20,39 @@
|
|
|
20
20
|
|
|
21
21
|
@theme {
|
|
22
22
|
/* ===== Neon Electric palette (11 colours, canonical D2C) ===== */
|
|
23
|
-
--color-neon-electric-blue:
|
|
24
|
-
--color-neon-electric-cyan:
|
|
25
|
-
--color-neon-electric-indigo:
|
|
26
|
-
--color-neon-electric-purple:
|
|
27
|
-
--color-neon-electric-magenta:
|
|
28
|
-
--color-neon-electric-pink:
|
|
29
|
-
--color-neon-electric-coral:
|
|
30
|
-
--color-neon-electric-orange:
|
|
31
|
-
--color-neon-electric-yellow:
|
|
32
|
-
--color-neon-electric-green:
|
|
33
|
-
--color-neon-electric-lavender:
|
|
23
|
+
--color-neon-electric-blue: oklch(0.546 0.215 262.9);
|
|
24
|
+
--color-neon-electric-cyan: oklch(0.769 0.132 191.7);
|
|
25
|
+
--color-neon-electric-indigo: oklch(0.511 0.23 277);
|
|
26
|
+
--color-neon-electric-purple: oklch(0.568 0.202 283.1);
|
|
27
|
+
--color-neon-electric-magenta: oklch(0.742 0.167 359.5);
|
|
28
|
+
--color-neon-electric-pink: oklch(0.656 0.212 354.3);
|
|
29
|
+
--color-neon-electric-coral: oklch(0.669 0.219 20.9);
|
|
30
|
+
--color-neon-electric-orange: oklch(0.728 0.168 22.5);
|
|
31
|
+
--color-neon-electric-yellow: oklch(0.868 0.125 81.4);
|
|
32
|
+
--color-neon-electric-green: oklch(0.723 0.192 149.6);
|
|
33
|
+
--color-neon-electric-lavender: oklch(0.736 0.141 285.6);
|
|
34
34
|
|
|
35
35
|
/* ===== Light surface tokens ===== */
|
|
36
|
-
--color-light-bg-primary:
|
|
37
|
-
--color-light-bg-secondary:
|
|
38
|
-
--color-light-bg-tertiary:
|
|
39
|
-
--color-light-bg-accent:
|
|
40
|
-
--color-light-text-primary:
|
|
41
|
-
--color-light-text-secondary:
|
|
36
|
+
--color-light-bg-primary: oklch(0.961 0 0.5);
|
|
37
|
+
--color-light-bg-secondary: oklch(0.982 0.002 248);
|
|
38
|
+
--color-light-bg-tertiary: oklch(0.963 0.003 228.9);
|
|
39
|
+
--color-light-bg-accent: oklch(1 0 0.5);
|
|
40
|
+
--color-light-text-primary: oklch(0.319 0.01 216.8);
|
|
41
|
+
--color-light-text-secondary: oklch(0.53 0.015 221.6);
|
|
42
42
|
|
|
43
43
|
/* ===== Dark surface tokens ===== */
|
|
44
|
-
--color-dark-bg-primary:
|
|
45
|
-
--color-dark-bg-secondary:
|
|
46
|
-
--color-dark-bg-tertiary:
|
|
47
|
-
--color-dark-bg-accent:
|
|
48
|
-
--color-dark-text-primary:
|
|
49
|
-
--color-dark-text-secondary:
|
|
44
|
+
--color-dark-bg-primary: oklch(0.166 0.026 269.4);
|
|
45
|
+
--color-dark-bg-secondary: oklch(0.242 0.03 269.9);
|
|
46
|
+
--color-dark-bg-tertiary: oklch(0.326 0.036 266.7);
|
|
47
|
+
--color-dark-bg-accent: oklch(0.292 0.034 270);
|
|
48
|
+
--color-dark-text-primary: oklch(0.982 0.002 248);
|
|
49
|
+
--color-dark-text-secondary: oklch(0.769 0.015 248);
|
|
50
50
|
|
|
51
51
|
/* ===== Semantic status ===== */
|
|
52
|
-
--color-status-success:
|
|
53
|
-
--color-status-warning:
|
|
54
|
-
--color-status-error:
|
|
55
|
-
--color-status-info:
|
|
52
|
+
--color-status-success: oklch(0.697 0.135 172.1);
|
|
53
|
+
--color-status-warning: oklch(0.868 0.125 81.4);
|
|
54
|
+
--color-status-error: oklch(0.669 0.219 20.9);
|
|
55
|
+
--color-status-info: oklch(0.769 0.132 191.7);
|
|
56
56
|
|
|
57
57
|
/* ===== Animations (Tailwind v4 exposes via --animate-*) ===== */
|
|
58
58
|
--animate-pulse-soft: pulse-soft 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
package/src/tailwind-preset.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @bsuite/theme — Tailwind v3 preset
|
|
2
|
+
* @bsuite/theme — Tailwind v3 preset (oklch edition)
|
|
3
3
|
*
|
|
4
4
|
* Consume via:
|
|
5
5
|
* // tailwind.config.js (CommonJS)
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
* surface tokens, semantic status colours, gradients, glow shadows, and
|
|
13
13
|
* the six canonical animations. Does not override Tailwind's default scale.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* CSS
|
|
15
|
+
* Colour values are inline oklch so the preset is self-contained — no
|
|
16
|
+
* dependency on any `--*-rgb` CSS custom property. Tailwind v3 injects
|
|
17
|
+
* `<alpha-value>` into any valid CSS colour function, and `oklch(L C H / A)`
|
|
18
|
+
* is valid CSS Color 4 syntax (supported in all evergreen browsers:
|
|
19
|
+
* Chrome ≥111, Safari ≥15.4, Firefox ≥113). Keeps bg-neon-electric-blue/40
|
|
20
|
+
* and friends working without the RGB-triplet variable indirection.
|
|
21
|
+
*
|
|
22
|
+
* Values are kept in sync with `preset-v4.css` — editing one without the
|
|
23
|
+
* other will cause v3 and v4 consumers to diverge. See README.
|
|
18
24
|
*
|
|
19
25
|
* NOTE: Tailwind v4 apps should import `@bsuite/theme/preset-v4.css` instead.
|
|
20
26
|
* v4 removed the preset system — presets exist only for v3.
|
|
@@ -27,17 +33,17 @@ module.exports = {
|
|
|
27
33
|
extend: {
|
|
28
34
|
colors: {
|
|
29
35
|
neon: {
|
|
30
|
-
'electric-blue': '
|
|
31
|
-
'electric-cyan': '
|
|
32
|
-
'electric-indigo': '
|
|
33
|
-
'electric-purple': '
|
|
34
|
-
'electric-magenta': '
|
|
35
|
-
'electric-pink': '
|
|
36
|
-
'electric-coral': '
|
|
37
|
-
'electric-orange': '
|
|
38
|
-
'electric-yellow': '
|
|
39
|
-
'electric-green': '
|
|
40
|
-
'electric-lavender': '
|
|
36
|
+
'electric-blue': 'oklch(0.546 0.215 262.9 / <alpha-value>)',
|
|
37
|
+
'electric-cyan': 'oklch(0.769 0.132 191.7 / <alpha-value>)',
|
|
38
|
+
'electric-indigo': 'oklch(0.511 0.23 277 / <alpha-value>)',
|
|
39
|
+
'electric-purple': 'oklch(0.568 0.202 283.1 / <alpha-value>)',
|
|
40
|
+
'electric-magenta': 'oklch(0.742 0.167 359.5 / <alpha-value>)',
|
|
41
|
+
'electric-pink': 'oklch(0.656 0.212 354.3 / <alpha-value>)',
|
|
42
|
+
'electric-coral': 'oklch(0.669 0.219 20.9 / <alpha-value>)',
|
|
43
|
+
'electric-orange': 'oklch(0.728 0.168 22.5 / <alpha-value>)',
|
|
44
|
+
'electric-yellow': 'oklch(0.868 0.125 81.4 / <alpha-value>)',
|
|
45
|
+
'electric-green': 'oklch(0.726 0.197 145.5 / <alpha-value>)',
|
|
46
|
+
'electric-lavender': 'oklch(0.749 0.115 288.4 / <alpha-value>)',
|
|
41
47
|
},
|
|
42
48
|
light: {
|
|
43
49
|
bg: {
|
|
@@ -82,26 +88,26 @@ module.exports = {
|
|
|
82
88
|
|
|
83
89
|
backgroundImage: {
|
|
84
90
|
'gradient-brand':
|
|
85
|
-
'linear-gradient(135deg,
|
|
91
|
+
'linear-gradient(135deg, oklch(0.669 0.219 20.9) 0%, oklch(0.728 0.168 22.5) 25%, oklch(0.868 0.125 81.4) 50%, oklch(0.769 0.132 191.7) 75%, oklch(0.749 0.115 288.4) 100%)',
|
|
86
92
|
'gradient-electric':
|
|
87
|
-
'linear-gradient(135deg,
|
|
93
|
+
'linear-gradient(135deg, oklch(0.546 0.215 262.9) 0%, oklch(0.769 0.132 191.7) 50%, oklch(0.656 0.212 354.3) 100%)',
|
|
88
94
|
'gradient-neon':
|
|
89
|
-
'linear-gradient(90deg,
|
|
95
|
+
'linear-gradient(90deg, oklch(0.769 0.132 191.7) 0%, oklch(0.568 0.202 283.1) 50%, oklch(0.669 0.219 20.9) 100%)',
|
|
90
96
|
'gradient-chat-user':
|
|
91
|
-
'linear-gradient(135deg,
|
|
97
|
+
'linear-gradient(135deg, oklch(0.636 0.153 271.3) 0%, oklch(0.466 0.168 296.3) 100%)',
|
|
92
98
|
'gradient-chat-agent':
|
|
93
|
-
'linear-gradient(135deg,
|
|
99
|
+
'linear-gradient(135deg, oklch(0.546 0.215 262.9) 0%, oklch(0.656 0.212 354.3) 100%)',
|
|
94
100
|
'gradient-neural':
|
|
95
|
-
'radial-gradient(circle at center,
|
|
101
|
+
'radial-gradient(circle at center, oklch(0.769 0.132 191.7 / 0.1) 0%, transparent 50%)',
|
|
96
102
|
},
|
|
97
103
|
|
|
98
104
|
boxShadow: {
|
|
99
|
-
'glow-electric-blue': '0 0 20px
|
|
100
|
-
'glow-electric-cyan': '0 0 20px
|
|
101
|
-
'glow-electric-purple': '0 0 20px
|
|
102
|
-
'glow-electric-pink': '0 0 20px
|
|
103
|
-
'glow-electric-coral': '0 0 20px
|
|
104
|
-
'glow-electric-magenta': '0 0 20px
|
|
105
|
+
'glow-electric-blue': '0 0 20px oklch(0.546 0.215 262.9 / var(--glow-strength))',
|
|
106
|
+
'glow-electric-cyan': '0 0 20px oklch(0.769 0.132 191.7 / var(--glow-strength))',
|
|
107
|
+
'glow-electric-purple': '0 0 20px oklch(0.568 0.202 283.1 / var(--glow-strength))',
|
|
108
|
+
'glow-electric-pink': '0 0 20px oklch(0.656 0.212 354.3 / var(--glow-strength))',
|
|
109
|
+
'glow-electric-coral': '0 0 20px oklch(0.669 0.219 20.9 / var(--glow-strength))',
|
|
110
|
+
'glow-electric-magenta': '0 0 20px oklch(0.742 0.167 359.5 / var(--glow-strength))',
|
|
105
111
|
},
|
|
106
112
|
|
|
107
113
|
animation: {
|
|
@@ -119,8 +125,8 @@ module.exports = {
|
|
|
119
125
|
'50%': { opacity: '0.7' },
|
|
120
126
|
},
|
|
121
127
|
glow: {
|
|
122
|
-
'0%': { boxShadow: '0 0 5px
|
|
123
|
-
'100%': { boxShadow: '0 0 20px
|
|
128
|
+
'0%': { boxShadow: '0 0 5px oklch(0.769 0.132 191.7 / 0.2)' },
|
|
129
|
+
'100%': { boxShadow: '0 0 20px oklch(0.769 0.132 191.7 / 0.6)' },
|
|
124
130
|
},
|
|
125
131
|
typing: {
|
|
126
132
|
'0%, 60%': { opacity: '1' },
|
|
@@ -135,8 +141,8 @@ module.exports = {
|
|
|
135
141
|
'50%': { transform: 'translateY(-10px)' },
|
|
136
142
|
},
|
|
137
143
|
'neon-pulse': {
|
|
138
|
-
'0%, 100%': { textShadow: '0 0 10px
|
|
139
|
-
'50%': { textShadow: '0 0 20px
|
|
144
|
+
'0%, 100%': { textShadow: '0 0 10px oklch(0.769 0.132 191.7 / 0.3)' },
|
|
145
|
+
'50%': { textShadow: '0 0 20px oklch(0.769 0.132 191.7 / 0.8)' },
|
|
140
146
|
},
|
|
141
147
|
},
|
|
142
148
|
},
|