@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 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.
@@ -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,3 @@
1
+ export { ThemeProvider } from './ThemeProvider';
2
+ export { useTheme } from './useTheme';
3
+ export { THEME_STORAGE_KEY } from '../index';
@@ -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
+ }
@@ -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
+ }