@bsuite/theme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/react/ThemeProvider.d.ts +15 -0
- package/dist/react/ThemeProvider.d.ts.map +1 -0
- package/dist/react/ThemeProvider.js +86 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useTheme.d.ts +18 -0
- package/dist/react/useTheme.d.ts.map +1 -0
- package/dist/react/useTheme.js +26 -0
- package/dist/ssr/index.d.ts +43 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +42 -0
- package/package.json +66 -0
- package/src/css/index.css +13 -0
- package/src/css/utilities.css +59 -0
- package/src/css/vars.css +99 -0
- package/src/preset-v4.css +90 -0
- package/src/tailwind-preset.js +144 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @bsuite/theme
|
|
2
|
+
|
|
3
|
+
Universal D2C Neon Electric theme for the BSuite monorepo. Ships the canonical palette, Tailwind tokens, CSS variables, React ThemeProvider, and a framework-agnostic FOUC-prevention script.
|
|
4
|
+
|
|
5
|
+
## What's inside
|
|
6
|
+
|
|
7
|
+
- **CSS variables** — 11 neon electric colours + light/dark surface tokens + WCAG AA-compliant text tokens
|
|
8
|
+
- **Tailwind v3 preset** — drop-in `presets: [require('@bsuite/theme/tailwind-preset')]`
|
|
9
|
+
- **Tailwind v4 `@theme` block** — `@import '@bsuite/theme/preset-v4.css'`
|
|
10
|
+
- **`<ThemeProvider>`** + **`useTheme()`** with localStorage persistence + system preference
|
|
11
|
+
- **`getThemeInitScript()`** — stringified JS for inline `<script>` tags to prevent FOUC
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @bsuite/theme
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Consumer setup
|
|
20
|
+
|
|
21
|
+
### Vite (v4 Tailwind) — BSU / CRM7 / R80.3
|
|
22
|
+
|
|
23
|
+
**1.** `src/index.css` or equivalent global stylesheet:
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
@import 'tailwindcss';
|
|
27
|
+
@import '@bsuite/theme/preset-v4.css';
|
|
28
|
+
@import '@bsuite/theme/css';
|
|
29
|
+
|
|
30
|
+
/* your app-specific @theme {} overrides below */
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**2.** `src/main.tsx`:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { ThemeProvider } from '@bsuite/theme/react'
|
|
37
|
+
|
|
38
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
39
|
+
<ThemeProvider>
|
|
40
|
+
<App />
|
|
41
|
+
</ThemeProvider>
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**3.** `index.html` — FOUC prevention:
|
|
46
|
+
|
|
47
|
+
Paste the result of `getThemeInitScript()` into `<head>` before stylesheets.
|
|
48
|
+
|
|
49
|
+
### Next.js 16 (v4 Tailwind) — conduit
|
|
50
|
+
|
|
51
|
+
**1.** `src/app/globals.css`:
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
@import 'tailwindcss';
|
|
55
|
+
@import '@bsuite/theme/preset-v4.css';
|
|
56
|
+
@import '@bsuite/theme/css';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**2.** `src/app/layout.tsx` — inline theme-init script in `<head>`:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import Script from 'next/script'
|
|
63
|
+
import { getThemeInitScript } from '@bsuite/theme/ssr'
|
|
64
|
+
import { ThemeProvider } from '@bsuite/theme/react'
|
|
65
|
+
|
|
66
|
+
export default function RootLayout({ children }) {
|
|
67
|
+
return (
|
|
68
|
+
<html lang="en" suppressHydrationWarning>
|
|
69
|
+
<head>
|
|
70
|
+
<Script id="bsuite-theme-init" strategy="beforeInteractive">
|
|
71
|
+
{getThemeInitScript()}
|
|
72
|
+
</Script>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Vite (v3 Tailwind) — throughput
|
|
83
|
+
|
|
84
|
+
**1.** `tailwind.config.js`:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
module.exports = {
|
|
88
|
+
presets: [require('@bsuite/theme/tailwind-preset')],
|
|
89
|
+
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**2.** Global stylesheet:
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
@tailwind base;
|
|
97
|
+
@tailwind components;
|
|
98
|
+
@tailwind utilities;
|
|
99
|
+
@import '@bsuite/theme/css';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**3.** `src/main.tsx` + `index.html` — same as the v4 Vite setup above.
|
|
103
|
+
|
|
104
|
+
## Using the hook
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useTheme } from '@bsuite/theme/react'
|
|
108
|
+
|
|
109
|
+
export function ThemeToggleButton() {
|
|
110
|
+
const { theme, resolvedTheme, isDark, setTheme } = useTheme()
|
|
111
|
+
return (
|
|
112
|
+
<button onClick={() => setTheme(isDark ? 'light' : 'dark')}>
|
|
113
|
+
{isDark ? '☀️' : '🌙'}
|
|
114
|
+
</button>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `resolvedTheme` always returns `'light'` or `'dark'` — use this when you need the _effective_ theme (it resolves `'system'` for you).
|
|
120
|
+
|
|
121
|
+
## Palette
|
|
122
|
+
|
|
123
|
+
All 11 canonical electric colours are available via Tailwind classes:
|
|
124
|
+
|
|
125
|
+
| Token | Hex | Use |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `neon-electric-blue` | `#2563eb` | Primary, highlights |
|
|
128
|
+
| `neon-electric-cyan` | `#00cec9` | Accents, borders |
|
|
129
|
+
| `neon-electric-indigo` | `#4f46e5` | Secondary actions |
|
|
130
|
+
| `neon-electric-purple` | `#6c5ce7` | Gradients, effects |
|
|
131
|
+
| `neon-electric-magenta` | `#fd79a8` | Interactive |
|
|
132
|
+
| `neon-electric-pink` | `#ec4899` | Hover states |
|
|
133
|
+
| `neon-electric-coral` | `#ff4757` | Alerts, destructive |
|
|
134
|
+
| `neon-electric-orange` | `#ff7675` | Warnings |
|
|
135
|
+
| `neon-electric-yellow` | `#fdcb6e` | Info |
|
|
136
|
+
| `neon-electric-green` | `#22c55e` | Success |
|
|
137
|
+
| `neon-electric-lavender` | `#a29bfe` | Subtle accents |
|
|
138
|
+
|
|
139
|
+
WCAG AA compliance: use `text-color-accent-text` and `text-color-primary-text` for text on light backgrounds. These are the accessible equivalents of cyan and blue — same visual identity, 5:1+ contrast on `#f2f2f2`.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
UNLICENSED — internal BSuite use only.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bsuite/theme — root export
|
|
3
|
+
*
|
|
4
|
+
* Shared types + constants for the BSuite D2C Neon Electric theme.
|
|
5
|
+
*
|
|
6
|
+
* Most consumers want one of the subpath exports:
|
|
7
|
+
*
|
|
8
|
+
* import '@bsuite/theme/css' // CSS vars + utilities
|
|
9
|
+
* import { ThemeProvider, useTheme } from '@bsuite/theme/react'
|
|
10
|
+
* import { getThemeInitScript } from '@bsuite/theme/ssr'
|
|
11
|
+
* // + either require('@bsuite/theme/tailwind-preset') (TW v3)
|
|
12
|
+
* // or @import '@bsuite/theme/preset-v4.css' (TW v4)
|
|
13
|
+
*/
|
|
14
|
+
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
15
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
16
|
+
/**
|
|
17
|
+
* localStorage key used to persist the user's theme preference.
|
|
18
|
+
* Namespaced to avoid collision with apps that already store a plain
|
|
19
|
+
* 'theme' key.
|
|
20
|
+
*/
|
|
21
|
+
export declare const THEME_STORAGE_KEY = "bsuite_theme";
|
|
22
|
+
export interface ThemeContextValue {
|
|
23
|
+
/** The user's stored preference ('light' | 'dark' | 'system'). */
|
|
24
|
+
theme: ThemeMode;
|
|
25
|
+
/**
|
|
26
|
+
* The effective theme after resolving `'system'` — always `'light'` or
|
|
27
|
+
* `'dark'`. Use this to conditionally render light/dark variants.
|
|
28
|
+
*/
|
|
29
|
+
resolvedTheme: ResolvedTheme;
|
|
30
|
+
/** Shorthand: resolvedTheme === 'dark'. */
|
|
31
|
+
isDark: boolean;
|
|
32
|
+
/** Update the theme. Persists to localStorage and applies the class. */
|
|
33
|
+
setTheme: (mode: ThemeMode) => void;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AACnD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAA;AAE5C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iBAAiB,CAAA;AAE/C,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,aAAa,EAAE,aAAa,CAAA;IAC5B,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,wEAAwE;IACxE,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;CACpC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bsuite/theme — root export
|
|
3
|
+
*
|
|
4
|
+
* Shared types + constants for the BSuite D2C Neon Electric theme.
|
|
5
|
+
*
|
|
6
|
+
* Most consumers want one of the subpath exports:
|
|
7
|
+
*
|
|
8
|
+
* import '@bsuite/theme/css' // CSS vars + utilities
|
|
9
|
+
* import { ThemeProvider, useTheme } from '@bsuite/theme/react'
|
|
10
|
+
* import { getThemeInitScript } from '@bsuite/theme/ssr'
|
|
11
|
+
* // + either require('@bsuite/theme/tailwind-preset') (TW v3)
|
|
12
|
+
* // or @import '@bsuite/theme/preset-v4.css' (TW v4)
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* localStorage key used to persist the user's theme preference.
|
|
16
|
+
* Namespaced to avoid collision with apps that already store a plain
|
|
17
|
+
* 'theme' key.
|
|
18
|
+
*/
|
|
19
|
+
export const THEME_STORAGE_KEY = 'bsuite_theme';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type ThemeContextValue, type ThemeMode } from '../index';
|
|
3
|
+
export declare const ThemeContext: import("react").Context<ThemeContextValue | undefined>;
|
|
4
|
+
export interface ThemeProviderProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Initial theme if nothing is in localStorage yet. Default: 'system'. */
|
|
7
|
+
defaultTheme?: ThemeMode;
|
|
8
|
+
/**
|
|
9
|
+
* localStorage key override. Defaults to `THEME_STORAGE_KEY = 'bsuite_theme'`.
|
|
10
|
+
* Only set this during migrations from a legacy key.
|
|
11
|
+
*/
|
|
12
|
+
storageKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function ThemeProvider({ children, defaultTheme, storageKey, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=ThemeProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/react/ThemeProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAyC,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAExG,eAAO,MAAM,YAAY,wDAA0D,CAAA;AAEnF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,CAAA;IACnB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA6BD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,YAAuB,EACvB,UAA8B,GAC/B,EAAE,kBAAkB,2CA4DpB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { THEME_STORAGE_KEY } from '../index';
|
|
4
|
+
export const ThemeContext = createContext(undefined);
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a stored ThemeMode to the effective ResolvedTheme.
|
|
7
|
+
*
|
|
8
|
+
* In SSR / pre-hydration contexts (no `window`), returns 'dark' as a safe
|
|
9
|
+
* default — matches what the inline FOUC-prevention script sets when there's
|
|
10
|
+
* no stored preference.
|
|
11
|
+
*/
|
|
12
|
+
function resolveTheme(mode) {
|
|
13
|
+
if (typeof window === 'undefined')
|
|
14
|
+
return 'dark';
|
|
15
|
+
if (mode === 'system') {
|
|
16
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
17
|
+
}
|
|
18
|
+
return mode;
|
|
19
|
+
}
|
|
20
|
+
function applyThemeClass(resolved) {
|
|
21
|
+
if (typeof document === 'undefined')
|
|
22
|
+
return;
|
|
23
|
+
const root = document.documentElement;
|
|
24
|
+
if (resolved === 'dark') {
|
|
25
|
+
root.classList.add('dark');
|
|
26
|
+
root.classList.remove('light');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
root.classList.add('light');
|
|
30
|
+
root.classList.remove('dark');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function ThemeProvider({ children, defaultTheme = 'system', storageKey = THEME_STORAGE_KEY, }) {
|
|
34
|
+
// We use lazy init so the first render already has the correct theme when
|
|
35
|
+
// running in the browser — combined with the pre-React FOUC script, this
|
|
36
|
+
// means the class on <html> and the React state are in sync from mount.
|
|
37
|
+
const [theme, setThemeState] = useState(() => {
|
|
38
|
+
if (typeof window === 'undefined')
|
|
39
|
+
return defaultTheme;
|
|
40
|
+
try {
|
|
41
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
42
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system')
|
|
43
|
+
return stored;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// localStorage can throw in privacy modes; fall through to default
|
|
47
|
+
}
|
|
48
|
+
return defaultTheme;
|
|
49
|
+
});
|
|
50
|
+
const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(theme));
|
|
51
|
+
// Apply + re-apply whenever theme changes or (for 'system') the OS
|
|
52
|
+
// preference changes.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const next = resolveTheme(theme);
|
|
55
|
+
setResolvedTheme(next);
|
|
56
|
+
applyThemeClass(next);
|
|
57
|
+
}, [theme]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (theme !== 'system' || typeof window === 'undefined')
|
|
60
|
+
return;
|
|
61
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
62
|
+
const listener = () => {
|
|
63
|
+
const next = mq.matches ? 'dark' : 'light';
|
|
64
|
+
setResolvedTheme(next);
|
|
65
|
+
applyThemeClass(next);
|
|
66
|
+
};
|
|
67
|
+
mq.addEventListener('change', listener);
|
|
68
|
+
return () => mq.removeEventListener('change', listener);
|
|
69
|
+
}, [theme]);
|
|
70
|
+
const setTheme = useCallback((mode) => {
|
|
71
|
+
setThemeState(mode);
|
|
72
|
+
try {
|
|
73
|
+
window.localStorage.setItem(storageKey, mode);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Ignore — localStorage not available
|
|
77
|
+
}
|
|
78
|
+
}, [storageKey]);
|
|
79
|
+
const value = useMemo(() => ({
|
|
80
|
+
theme,
|
|
81
|
+
resolvedTheme,
|
|
82
|
+
isDark: resolvedTheme === 'dark',
|
|
83
|
+
setTheme,
|
|
84
|
+
}), [theme, resolvedTheme, setTheme]);
|
|
85
|
+
return _jsx(ThemeContext.Provider, { value: value, children: children });
|
|
86
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ThemeProvider } from './ThemeProvider';
|
|
2
|
+
export type { ThemeProviderProps } from './ThemeProvider';
|
|
3
|
+
export { useTheme } from './useTheme';
|
|
4
|
+
export type { ThemeContextValue, ThemeMode, ResolvedTheme } from '../index';
|
|
5
|
+
export { THEME_STORAGE_KEY } from '../index';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ThemeContextValue } from '../index';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to read + update the current theme.
|
|
4
|
+
*
|
|
5
|
+
* Must be called inside a tree that has a `<ThemeProvider>` mounted —
|
|
6
|
+
* throws an explanatory error otherwise (catches the "component rendered
|
|
7
|
+
* outside provider" footgun at dev time).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { theme, resolvedTheme, isDark, setTheme } = useTheme()
|
|
11
|
+
* return (
|
|
12
|
+
* <button onClick={() => setTheme(isDark ? 'light' : 'dark')}>
|
|
13
|
+
* {isDark ? 'Switch to light' : 'Switch to dark'}
|
|
14
|
+
* </button>
|
|
15
|
+
* )
|
|
16
|
+
*/
|
|
17
|
+
export declare function useTheme(): ThemeContextValue;
|
|
18
|
+
//# sourceMappingURL=useTheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/react/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAGjD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAU5C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ThemeContext } from './ThemeProvider';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to read + update the current theme.
|
|
5
|
+
*
|
|
6
|
+
* Must be called inside a tree that has a `<ThemeProvider>` mounted —
|
|
7
|
+
* throws an explanatory error otherwise (catches the "component rendered
|
|
8
|
+
* outside provider" footgun at dev time).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const { theme, resolvedTheme, isDark, setTheme } = useTheme()
|
|
12
|
+
* return (
|
|
13
|
+
* <button onClick={() => setTheme(isDark ? 'light' : 'dark')}>
|
|
14
|
+
* {isDark ? 'Switch to light' : 'Switch to dark'}
|
|
15
|
+
* </button>
|
|
16
|
+
* )
|
|
17
|
+
*/
|
|
18
|
+
export function useTheme() {
|
|
19
|
+
const ctx = useContext(ThemeContext);
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error('@bsuite/theme: useTheme() called outside <ThemeProvider>. ' +
|
|
22
|
+
'Wrap your app (typically in main.tsx or app/layout.tsx) with ' +
|
|
23
|
+
"<ThemeProvider> from '@bsuite/theme/react'.");
|
|
24
|
+
}
|
|
25
|
+
return ctx;
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface InitScriptOptions {
|
|
2
|
+
/** localStorage key to read. Defaults to `THEME_STORAGE_KEY`. */
|
|
3
|
+
storageKey?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns a self-contained JavaScript string that — when executed before
|
|
7
|
+
* your React app hydrates — applies the correct `.dark` or `.light` class
|
|
8
|
+
* to `<html>` based on the user's stored preference (or their system
|
|
9
|
+
* preference if they chose 'system', or 'dark' as the default).
|
|
10
|
+
*
|
|
11
|
+
* The returned string is produced by this package itself, not from user
|
|
12
|
+
* input, so it is safe to inline in `<head>` via the framework's inline
|
|
13
|
+
* script mechanism (e.g. Next.js `<Script strategy="beforeInteractive">`
|
|
14
|
+
* or an HTML `<script>` tag). It has no external dependencies and uses
|
|
15
|
+
* no identifiers that might collide with host-page globals (wrapped in
|
|
16
|
+
* an IIFE).
|
|
17
|
+
*
|
|
18
|
+
* Embed it in an inline `<script>` tag in `<head>` — **before** any
|
|
19
|
+
* stylesheet that uses `.dark` selectors. This is the only way to avoid
|
|
20
|
+
* FOUC: any script that runs after first paint will flash the wrong theme.
|
|
21
|
+
*
|
|
22
|
+
* @example Next.js App Router (src/app/layout.tsx)
|
|
23
|
+
* import Script from 'next/script'
|
|
24
|
+
* import { getThemeInitScript } from '@bsuite/theme/ssr'
|
|
25
|
+
* export default function RootLayout({ children }) {
|
|
26
|
+
* return (
|
|
27
|
+
* <html lang="en" suppressHydrationWarning>
|
|
28
|
+
* <head>
|
|
29
|
+
* <Script id="bsuite-theme-init" strategy="beforeInteractive">
|
|
30
|
+
* {getThemeInitScript()}
|
|
31
|
+
* </Script>
|
|
32
|
+
* </head>
|
|
33
|
+
* <body>{children}</body>
|
|
34
|
+
* </html>
|
|
35
|
+
* )
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example Vite (index.html)
|
|
39
|
+
* <!-- paste the exact output of getThemeInitScript() inline -->
|
|
40
|
+
* <script>(function(){ ... })();</script>
|
|
41
|
+
*/
|
|
42
|
+
export declare function getThemeInitScript(options?: InitScriptOptions): string;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssr/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAG1E"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { THEME_STORAGE_KEY } from '../index';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a self-contained JavaScript string that — when executed before
|
|
4
|
+
* your React app hydrates — applies the correct `.dark` or `.light` class
|
|
5
|
+
* to `<html>` based on the user's stored preference (or their system
|
|
6
|
+
* preference if they chose 'system', or 'dark' as the default).
|
|
7
|
+
*
|
|
8
|
+
* The returned string is produced by this package itself, not from user
|
|
9
|
+
* input, so it is safe to inline in `<head>` via the framework's inline
|
|
10
|
+
* script mechanism (e.g. Next.js `<Script strategy="beforeInteractive">`
|
|
11
|
+
* or an HTML `<script>` tag). It has no external dependencies and uses
|
|
12
|
+
* no identifiers that might collide with host-page globals (wrapped in
|
|
13
|
+
* an IIFE).
|
|
14
|
+
*
|
|
15
|
+
* Embed it in an inline `<script>` tag in `<head>` — **before** any
|
|
16
|
+
* stylesheet that uses `.dark` selectors. This is the only way to avoid
|
|
17
|
+
* FOUC: any script that runs after first paint will flash the wrong theme.
|
|
18
|
+
*
|
|
19
|
+
* @example Next.js App Router (src/app/layout.tsx)
|
|
20
|
+
* import Script from 'next/script'
|
|
21
|
+
* import { getThemeInitScript } from '@bsuite/theme/ssr'
|
|
22
|
+
* export default function RootLayout({ children }) {
|
|
23
|
+
* return (
|
|
24
|
+
* <html lang="en" suppressHydrationWarning>
|
|
25
|
+
* <head>
|
|
26
|
+
* <Script id="bsuite-theme-init" strategy="beforeInteractive">
|
|
27
|
+
* {getThemeInitScript()}
|
|
28
|
+
* </Script>
|
|
29
|
+
* </head>
|
|
30
|
+
* <body>{children}</body>
|
|
31
|
+
* </html>
|
|
32
|
+
* )
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example Vite (index.html)
|
|
36
|
+
* <!-- paste the exact output of getThemeInitScript() inline -->
|
|
37
|
+
* <script>(function(){ ... })();</script>
|
|
38
|
+
*/
|
|
39
|
+
export function getThemeInitScript(options = {}) {
|
|
40
|
+
const key = options.storageKey ?? THEME_STORAGE_KEY;
|
|
41
|
+
return `(function(){try{var s=localStorage.getItem('${key}');var t=s||'dark';var r=t;if(t==='system'){r=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}var c=document.documentElement.classList;if(r==='dark'){c.add('dark');c.remove('light');}else{c.add('light');c.remove('dark');}}catch(e){}})();`;
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bsuite/theme",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/GaryOcean428/bsuite.git",
|
|
9
|
+
"directory": "packages/theme"
|
|
10
|
+
},
|
|
11
|
+
"description": "BSuite Universal D2C Neon Electric theme — Tailwind preset, CSS vars, ThemeProvider, SSR init helper. Shared across BSU, CRM7, conduit, R80.3, throughput.",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src/css",
|
|
15
|
+
"src/tailwind-preset.js",
|
|
16
|
+
"src/preset-v4.css"
|
|
17
|
+
],
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./react": {
|
|
26
|
+
"import": "./dist/react/index.js",
|
|
27
|
+
"types": "./dist/react/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./ssr": {
|
|
30
|
+
"import": "./dist/ssr/index.js",
|
|
31
|
+
"types": "./dist/ssr/index.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./tailwind-preset": "./src/tailwind-preset.js",
|
|
34
|
+
"./preset-v4.css": "./src/preset-v4.css",
|
|
35
|
+
"./css": "./src/css/index.css",
|
|
36
|
+
"./vars.css": "./src/css/vars.css",
|
|
37
|
+
"./utilities.css": "./src/css/utilities.css"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"prepublishOnly": "pnpm build"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.0.0",
|
|
47
|
+
"react-dom": ">=18.0.0",
|
|
48
|
+
"tailwindcss": ">=3.4.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"tailwindcss": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/react": "^16.3.2",
|
|
58
|
+
"@types/react": "^18.3.28",
|
|
59
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
60
|
+
"jsdom": "^28.1.0",
|
|
61
|
+
"react": "^18.3.1",
|
|
62
|
+
"react-dom": "^18.3.1",
|
|
63
|
+
"typescript": "~5.7.3",
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @bsuite/theme — root CSS entry point
|
|
3
|
+
*
|
|
4
|
+
* Import this once at the top of your app's global stylesheet:
|
|
5
|
+
*
|
|
6
|
+
* @import '@bsuite/theme/css';
|
|
7
|
+
*
|
|
8
|
+
* Provides the full D2C Neon Electric palette as CSS custom properties
|
|
9
|
+
* plus reusable glow / neon-text utility classes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@import './vars.css';
|
|
13
|
+
@import './utilities.css';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @bsuite/theme — utility classes
|
|
3
|
+
*
|
|
4
|
+
* Pure CSS (no Tailwind @apply) so this file is safe to import from any
|
|
5
|
+
* consumer regardless of their Tailwind version (v3 or v4).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ===== Glow effects (use box-shadow with alpha scaled by glow-strength) ===== */
|
|
9
|
+
.glow-electric-blue {
|
|
10
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-blue-rgb) / var(--glow-strength));
|
|
11
|
+
}
|
|
12
|
+
.glow-electric-cyan {
|
|
13
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-cyan-rgb) / var(--glow-strength));
|
|
14
|
+
}
|
|
15
|
+
.glow-electric-indigo {
|
|
16
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-indigo-rgb) / var(--glow-strength));
|
|
17
|
+
}
|
|
18
|
+
.glow-electric-purple {
|
|
19
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-purple-rgb) / var(--glow-strength));
|
|
20
|
+
}
|
|
21
|
+
.glow-electric-pink {
|
|
22
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-pink-rgb) / var(--glow-strength));
|
|
23
|
+
}
|
|
24
|
+
.glow-electric-coral {
|
|
25
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-coral-rgb) / var(--glow-strength));
|
|
26
|
+
}
|
|
27
|
+
.glow-electric-magenta {
|
|
28
|
+
box-shadow: 0 0 20px rgb(var(--neon-electric-magenta-rgb) / var(--glow-strength));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ===== Neon text shadows ===== */
|
|
32
|
+
.neon-text-cyan {
|
|
33
|
+
text-shadow: 0 0 10px rgb(var(--neon-electric-cyan-rgb) / 0.5),
|
|
34
|
+
0 0 20px rgb(var(--neon-electric-cyan-rgb) / 0.3);
|
|
35
|
+
}
|
|
36
|
+
.neon-text-electric {
|
|
37
|
+
text-shadow: 0 0 10px rgb(var(--neon-electric-blue-rgb) / 0.5),
|
|
38
|
+
0 0 20px rgb(var(--neon-electric-blue-rgb) / 0.3);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ===== Smooth theme transition ===== */
|
|
42
|
+
.transition-theme {
|
|
43
|
+
transition:
|
|
44
|
+
background-color 300ms ease,
|
|
45
|
+
border-color 300ms ease,
|
|
46
|
+
color 300ms ease,
|
|
47
|
+
box-shadow 300ms ease;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ===== prefers-reduced-motion — cap animation duration ===== */
|
|
51
|
+
@media (prefers-reduced-motion: reduce) {
|
|
52
|
+
*,
|
|
53
|
+
*::before,
|
|
54
|
+
*::after {
|
|
55
|
+
animation-duration: 0.01ms !important;
|
|
56
|
+
animation-iteration-count: 1 !important;
|
|
57
|
+
transition-duration: 0.01ms !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/css/vars.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @bsuite/theme — CSS custom properties (:root + .dark)
|
|
3
|
+
*
|
|
4
|
+
* Canonical D2C Neon Electric palette. Both hex values and RGB triplets are
|
|
5
|
+
* exported so consumers can use either pattern:
|
|
6
|
+
*
|
|
7
|
+
* color: var(--neon-electric-blue);
|
|
8
|
+
* background: rgb(var(--neon-electric-blue-rgb) / 0.4);
|
|
9
|
+
*
|
|
10
|
+
* The RGB-triplet form is what the Tailwind preset uses under the hood so
|
|
11
|
+
* `<alpha-value>` modifiers in Tailwind classes (e.g. bg-neon-electric-blue/40)
|
|
12
|
+
* resolve correctly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* ===== Neon Electric (11 colours — FROZEN per D2C spec v1.00A) ===== */
|
|
17
|
+
--neon-electric-blue: #2563eb;
|
|
18
|
+
--neon-electric-cyan: #00cec9;
|
|
19
|
+
--neon-electric-indigo: #4f46e5;
|
|
20
|
+
--neon-electric-purple: #6c5ce7;
|
|
21
|
+
--neon-electric-magenta: #fd79a8;
|
|
22
|
+
--neon-electric-pink: #ec4899;
|
|
23
|
+
--neon-electric-coral: #ff4757;
|
|
24
|
+
--neon-electric-orange: #ff7675;
|
|
25
|
+
--neon-electric-yellow: #fdcb6e;
|
|
26
|
+
--neon-electric-green: #22c55e;
|
|
27
|
+
--neon-electric-lavender: #a29bfe;
|
|
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;
|
|
41
|
+
|
|
42
|
+
/* ===== Light theme surfaces ===== */
|
|
43
|
+
--light-bg-primary: #f2f2f2;
|
|
44
|
+
--light-bg-secondary: #f8f9fa;
|
|
45
|
+
--light-bg-tertiary: #f1f3f4;
|
|
46
|
+
--light-bg-accent: #ffffff;
|
|
47
|
+
--light-text-primary: #2d3436;
|
|
48
|
+
--light-text-secondary: #636e72;
|
|
49
|
+
--light-text-tertiary: #74b9ff;
|
|
50
|
+
--light-text-quaternary: #a4afb7;
|
|
51
|
+
--light-border: #e9ecef;
|
|
52
|
+
--light-hover: #f1f3f4;
|
|
53
|
+
|
|
54
|
+
/* ===== Dark theme surfaces ===== */
|
|
55
|
+
--dark-bg-primary: #0a0e1a;
|
|
56
|
+
--dark-bg-secondary: #1a1f2e;
|
|
57
|
+
--dark-bg-tertiary: #2c3447;
|
|
58
|
+
--dark-bg-quaternary: #3c4558;
|
|
59
|
+
--dark-bg-accent: #252b3d;
|
|
60
|
+
--dark-text-primary: #f8f9fa;
|
|
61
|
+
--dark-text-secondary: #adb5bd;
|
|
62
|
+
--dark-text-tertiary: #6c757d;
|
|
63
|
+
--dark-text-quaternary: #495057;
|
|
64
|
+
--dark-border: #495057;
|
|
65
|
+
--dark-hover: #3c4558;
|
|
66
|
+
|
|
67
|
+
/* ===== Semantic status colours ===== */
|
|
68
|
+
--status-success: #00b894;
|
|
69
|
+
--status-warning: #fdcb6e;
|
|
70
|
+
--status-error: #ff4757;
|
|
71
|
+
--status-info: #00cec9;
|
|
72
|
+
|
|
73
|
+
/* ===== WCAG AA compliant text pairs =====
|
|
74
|
+
*
|
|
75
|
+
* Raw neon cyan #00cec9 on light #f2f2f2 is 1.76:1 — fails WCAG AA 4.5:1.
|
|
76
|
+
* These tokens are the accessible text equivalents for the same visual
|
|
77
|
+
* identity. Use these for any text, border, or icon that needs to pass
|
|
78
|
+
* contrast checks; use raw --neon-electric-cyan for backgrounds and
|
|
79
|
+
* decorative elements only. */
|
|
80
|
+
--color-accent-text: #006e6b; /* Dark cyan — passes AA on light bg */
|
|
81
|
+
--color-primary-text: #0a47e5; /* Saturated blue — passes AA on light bg */
|
|
82
|
+
|
|
83
|
+
/* ===== Glow + shadow strength multipliers ===== */
|
|
84
|
+
--glow-strength: 0.4;
|
|
85
|
+
--shadow-strength: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dark {
|
|
89
|
+
/* Neon palette is identical in dark mode — same raw colours, same RGB
|
|
90
|
+
* triplets. What changes is the intensity of glow/shadow effects, which
|
|
91
|
+
* we scale down to suit dark surfaces. */
|
|
92
|
+
--glow-strength: 0.6;
|
|
93
|
+
--shadow-strength: 0.8;
|
|
94
|
+
|
|
95
|
+
/* Accent/primary text tokens flip to the raw neons in dark mode because
|
|
96
|
+
* contrast on #0a0e1a navy is already ≥ 9:1 for those values. */
|
|
97
|
+
--color-accent-text: #00cec9;
|
|
98
|
+
--color-primary-text: #3b82f6;
|
|
99
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @bsuite/theme — Tailwind v4 @theme block
|
|
3
|
+
*
|
|
4
|
+
* Tailwind v4 removed the preset system; design tokens are exposed to the
|
|
5
|
+
* Tailwind engine via CSS `@theme {}` blocks. Import this before any
|
|
6
|
+
* app-specific @theme overrides:
|
|
7
|
+
*
|
|
8
|
+
* @import 'tailwindcss';
|
|
9
|
+
* @import '@bsuite/theme/preset-v4.css';
|
|
10
|
+
*
|
|
11
|
+
* @theme {
|
|
12
|
+
* --color-app-primary: oklch(0.45 0.18 142);
|
|
13
|
+
* ... app-specific overrides below
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Tailwind v4 generates classes automatically from @theme variables. So
|
|
17
|
+
* `--color-neon-electric-blue` → `bg-neon-electric-blue`, `text-neon-electric-blue`,
|
|
18
|
+
* `border-neon-electric-blue`, etc.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@theme {
|
|
22
|
+
/* ===== Neon Electric palette (11 colours, canonical D2C) ===== */
|
|
23
|
+
--color-neon-electric-blue: #2563eb;
|
|
24
|
+
--color-neon-electric-cyan: #00cec9;
|
|
25
|
+
--color-neon-electric-indigo: #4f46e5;
|
|
26
|
+
--color-neon-electric-purple: #6c5ce7;
|
|
27
|
+
--color-neon-electric-magenta: #fd79a8;
|
|
28
|
+
--color-neon-electric-pink: #ec4899;
|
|
29
|
+
--color-neon-electric-coral: #ff4757;
|
|
30
|
+
--color-neon-electric-orange: #ff7675;
|
|
31
|
+
--color-neon-electric-yellow: #fdcb6e;
|
|
32
|
+
--color-neon-electric-green: #22c55e;
|
|
33
|
+
--color-neon-electric-lavender: #a29bfe;
|
|
34
|
+
|
|
35
|
+
/* ===== Light surface tokens ===== */
|
|
36
|
+
--color-light-bg-primary: #f2f2f2;
|
|
37
|
+
--color-light-bg-secondary: #f8f9fa;
|
|
38
|
+
--color-light-bg-tertiary: #f1f3f4;
|
|
39
|
+
--color-light-bg-accent: #ffffff;
|
|
40
|
+
--color-light-text-primary: #2d3436;
|
|
41
|
+
--color-light-text-secondary: #636e72;
|
|
42
|
+
|
|
43
|
+
/* ===== Dark surface tokens ===== */
|
|
44
|
+
--color-dark-bg-primary: #0a0e1a;
|
|
45
|
+
--color-dark-bg-secondary: #1a1f2e;
|
|
46
|
+
--color-dark-bg-tertiary: #2c3447;
|
|
47
|
+
--color-dark-bg-accent: #252b3d;
|
|
48
|
+
--color-dark-text-primary: #f8f9fa;
|
|
49
|
+
--color-dark-text-secondary: #adb5bd;
|
|
50
|
+
|
|
51
|
+
/* ===== Semantic status ===== */
|
|
52
|
+
--color-status-success: #00b894;
|
|
53
|
+
--color-status-warning: #fdcb6e;
|
|
54
|
+
--color-status-error: #ff4757;
|
|
55
|
+
--color-status-info: #00cec9;
|
|
56
|
+
|
|
57
|
+
/* ===== Animations (Tailwind v4 exposes via --animate-*) ===== */
|
|
58
|
+
--animate-pulse-soft: pulse-soft 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
59
|
+
--animate-glow: glow 2s ease-in-out infinite alternate;
|
|
60
|
+
--animate-neon-pulse: neon-pulse 2s ease-in-out infinite;
|
|
61
|
+
--animate-float: float 3s ease-in-out infinite;
|
|
62
|
+
--animate-shimmer: shimmer 2s infinite;
|
|
63
|
+
--animate-typing: typing 1.5s infinite;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Keyframes live outside @theme because v4 doesn't expose them there. */
|
|
67
|
+
@keyframes pulse-soft {
|
|
68
|
+
0%, 100% { opacity: 1; }
|
|
69
|
+
50% { opacity: 0.7; }
|
|
70
|
+
}
|
|
71
|
+
@keyframes glow {
|
|
72
|
+
0% { box-shadow: 0 0 5px rgba(0, 206, 201, 0.2); }
|
|
73
|
+
100% { box-shadow: 0 0 20px rgba(0, 206, 201, 0.6); }
|
|
74
|
+
}
|
|
75
|
+
@keyframes neon-pulse {
|
|
76
|
+
0%, 100% { text-shadow: 0 0 10px rgba(0, 206, 201, 0.3); }
|
|
77
|
+
50% { text-shadow: 0 0 20px rgba(0, 206, 201, 0.8); }
|
|
78
|
+
}
|
|
79
|
+
@keyframes float {
|
|
80
|
+
0%, 100% { transform: translateY(0px); }
|
|
81
|
+
50% { transform: translateY(-10px); }
|
|
82
|
+
}
|
|
83
|
+
@keyframes shimmer {
|
|
84
|
+
0% { background-position: -1000px 0; }
|
|
85
|
+
100% { background-position: 1000px 0; }
|
|
86
|
+
}
|
|
87
|
+
@keyframes typing {
|
|
88
|
+
0%, 60% { opacity: 1; }
|
|
89
|
+
30% { opacity: 0.4; }
|
|
90
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bsuite/theme — Tailwind v3 preset
|
|
3
|
+
*
|
|
4
|
+
* Consume via:
|
|
5
|
+
* // tailwind.config.js (CommonJS)
|
|
6
|
+
* module.exports = {
|
|
7
|
+
* presets: [require('@bsuite/theme/tailwind-preset')],
|
|
8
|
+
* content: ['./src/**\/*.{js,ts,jsx,tsx}'],
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* Extends Tailwind's theme with the D2C Neon Electric palette, light/dark
|
|
12
|
+
* surface tokens, semantic status colours, gradients, glow shadows, and
|
|
13
|
+
* the six canonical animations. Does not override Tailwind's default scale.
|
|
14
|
+
*
|
|
15
|
+
* The `rgb(var(--*-rgb) / <alpha-value>)` pattern makes Tailwind alpha-value
|
|
16
|
+
* modifiers work (e.g. `bg-neon-electric-blue/40`). Requires the companion
|
|
17
|
+
* CSS vars to be loaded via `@import '@bsuite/theme/css'`.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: Tailwind v4 apps should import `@bsuite/theme/preset-v4.css` instead.
|
|
20
|
+
* v4 removed the preset system — presets exist only for v3.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @type {import('tailwindcss').Config} */
|
|
24
|
+
module.exports = {
|
|
25
|
+
darkMode: ['class'],
|
|
26
|
+
theme: {
|
|
27
|
+
extend: {
|
|
28
|
+
colors: {
|
|
29
|
+
neon: {
|
|
30
|
+
'electric-blue': 'rgb(var(--neon-electric-blue-rgb) / <alpha-value>)',
|
|
31
|
+
'electric-cyan': 'rgb(var(--neon-electric-cyan-rgb) / <alpha-value>)',
|
|
32
|
+
'electric-indigo': 'rgb(var(--neon-electric-indigo-rgb) / <alpha-value>)',
|
|
33
|
+
'electric-purple': 'rgb(var(--neon-electric-purple-rgb) / <alpha-value>)',
|
|
34
|
+
'electric-magenta': 'rgb(var(--neon-electric-magenta-rgb) / <alpha-value>)',
|
|
35
|
+
'electric-pink': 'rgb(var(--neon-electric-pink-rgb) / <alpha-value>)',
|
|
36
|
+
'electric-coral': 'rgb(var(--neon-electric-coral-rgb) / <alpha-value>)',
|
|
37
|
+
'electric-orange': 'rgb(var(--neon-electric-orange-rgb) / <alpha-value>)',
|
|
38
|
+
'electric-yellow': 'rgb(var(--neon-electric-yellow-rgb) / <alpha-value>)',
|
|
39
|
+
'electric-green': 'rgb(var(--neon-electric-green-rgb) / <alpha-value>)',
|
|
40
|
+
'electric-lavender': 'rgb(var(--neon-electric-lavender-rgb) / <alpha-value>)',
|
|
41
|
+
},
|
|
42
|
+
light: {
|
|
43
|
+
bg: {
|
|
44
|
+
primary: 'var(--light-bg-primary)',
|
|
45
|
+
secondary: 'var(--light-bg-secondary)',
|
|
46
|
+
tertiary: 'var(--light-bg-tertiary)',
|
|
47
|
+
accent: 'var(--light-bg-accent)',
|
|
48
|
+
},
|
|
49
|
+
text: {
|
|
50
|
+
primary: 'var(--light-text-primary)',
|
|
51
|
+
secondary: 'var(--light-text-secondary)',
|
|
52
|
+
tertiary: 'var(--light-text-tertiary)',
|
|
53
|
+
quaternary: 'var(--light-text-quaternary)',
|
|
54
|
+
},
|
|
55
|
+
border: 'var(--light-border)',
|
|
56
|
+
hover: 'var(--light-hover)',
|
|
57
|
+
},
|
|
58
|
+
dark: {
|
|
59
|
+
bg: {
|
|
60
|
+
primary: 'var(--dark-bg-primary)',
|
|
61
|
+
secondary: 'var(--dark-bg-secondary)',
|
|
62
|
+
tertiary: 'var(--dark-bg-tertiary)',
|
|
63
|
+
quaternary: 'var(--dark-bg-quaternary)',
|
|
64
|
+
accent: 'var(--dark-bg-accent)',
|
|
65
|
+
},
|
|
66
|
+
text: {
|
|
67
|
+
primary: 'var(--dark-text-primary)',
|
|
68
|
+
secondary: 'var(--dark-text-secondary)',
|
|
69
|
+
tertiary: 'var(--dark-text-tertiary)',
|
|
70
|
+
quaternary: 'var(--dark-text-quaternary)',
|
|
71
|
+
},
|
|
72
|
+
border: 'var(--dark-border)',
|
|
73
|
+
hover: 'var(--dark-hover)',
|
|
74
|
+
},
|
|
75
|
+
status: {
|
|
76
|
+
success: 'var(--status-success)',
|
|
77
|
+
warning: 'var(--status-warning)',
|
|
78
|
+
error: 'var(--status-error)',
|
|
79
|
+
info: 'var(--status-info)',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
backgroundImage: {
|
|
84
|
+
'gradient-brand':
|
|
85
|
+
'linear-gradient(135deg, #ff4757 0%, #ff7675 25%, #fdcb6e 50%, #00cec9 75%, #a29bfe 100%)',
|
|
86
|
+
'gradient-electric':
|
|
87
|
+
'linear-gradient(135deg, #2563eb 0%, #00cec9 50%, #ec4899 100%)',
|
|
88
|
+
'gradient-neon':
|
|
89
|
+
'linear-gradient(90deg, #00cec9 0%, #6c5ce7 50%, #ff4757 100%)',
|
|
90
|
+
'gradient-chat-user':
|
|
91
|
+
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
92
|
+
'gradient-chat-agent':
|
|
93
|
+
'linear-gradient(135deg, #2563eb 0%, #ec4899 100%)',
|
|
94
|
+
'gradient-neural':
|
|
95
|
+
'radial-gradient(circle at center, rgba(0, 206, 201, 0.1) 0%, transparent 50%)',
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
boxShadow: {
|
|
99
|
+
'glow-electric-blue': '0 0 20px rgb(var(--neon-electric-blue-rgb) / var(--glow-strength))',
|
|
100
|
+
'glow-electric-cyan': '0 0 20px rgb(var(--neon-electric-cyan-rgb) / var(--glow-strength))',
|
|
101
|
+
'glow-electric-purple': '0 0 20px rgb(var(--neon-electric-purple-rgb) / var(--glow-strength))',
|
|
102
|
+
'glow-electric-pink': '0 0 20px rgb(var(--neon-electric-pink-rgb) / var(--glow-strength))',
|
|
103
|
+
'glow-electric-coral': '0 0 20px rgb(var(--neon-electric-coral-rgb) / var(--glow-strength))',
|
|
104
|
+
'glow-electric-magenta': '0 0 20px rgb(var(--neon-electric-magenta-rgb) / var(--glow-strength))',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
animation: {
|
|
108
|
+
'pulse-soft': 'pulse-soft 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
109
|
+
glow: 'glow 2s ease-in-out infinite alternate',
|
|
110
|
+
typing: 'typing 1.5s infinite',
|
|
111
|
+
shimmer: 'shimmer 2s infinite',
|
|
112
|
+
float: 'float 3s ease-in-out infinite',
|
|
113
|
+
'neon-pulse': 'neon-pulse 2s ease-in-out infinite',
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
keyframes: {
|
|
117
|
+
'pulse-soft': {
|
|
118
|
+
'0%, 100%': { opacity: '1' },
|
|
119
|
+
'50%': { opacity: '0.7' },
|
|
120
|
+
},
|
|
121
|
+
glow: {
|
|
122
|
+
'0%': { boxShadow: '0 0 5px rgba(0, 206, 201, 0.2)' },
|
|
123
|
+
'100%': { boxShadow: '0 0 20px rgba(0, 206, 201, 0.6)' },
|
|
124
|
+
},
|
|
125
|
+
typing: {
|
|
126
|
+
'0%, 60%': { opacity: '1' },
|
|
127
|
+
'30%': { opacity: '0.4' },
|
|
128
|
+
},
|
|
129
|
+
shimmer: {
|
|
130
|
+
'0%': { backgroundPosition: '-1000px 0' },
|
|
131
|
+
'100%': { backgroundPosition: '1000px 0' },
|
|
132
|
+
},
|
|
133
|
+
float: {
|
|
134
|
+
'0%, 100%': { transform: 'translateY(0px)' },
|
|
135
|
+
'50%': { transform: 'translateY(-10px)' },
|
|
136
|
+
},
|
|
137
|
+
'neon-pulse': {
|
|
138
|
+
'0%, 100%': { textShadow: '0 0 10px rgba(0, 206, 201, 0.3)' },
|
|
139
|
+
'50%': { textShadow: '0 0 20px rgba(0, 206, 201, 0.8)' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|