@bsuite/theme 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,7 +43,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
43
43
  * </BrandingProvider>
44
44
  */
45
45
  import { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
46
- import { sanitizeBrandingUrl, sanitizeFontFamily, toCssUrl, } from './branding-sanitize';
46
+ import { sanitizeBrandingUrl, sanitizeFontFamily, toCssUrl, } from './branding-sanitize.js';
47
47
  export const BRANDING_STORAGE_KEY = 'bsuite_tenant_branding';
48
48
  export const BRANDING_OVERRIDE_FLAG = 'VITE_ENABLE_BRANDING_OVERRIDE';
49
49
  export const BrandingContext = createContext(undefined);
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import { type ThemeContextValue, type ThemeMode } from '../index';
2
+ import { type ThemeContextValue, type ThemeMode } from '../index.js';
3
3
  export declare const ThemeContext: import("react").Context<ThemeContextValue | undefined>;
4
4
  export interface ThemeProviderProps {
5
5
  children: ReactNode;
@@ -1 +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,+BA4DpB"}
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,aAAa,CAAA;AAE3G,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,+BA4DpB"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
3
- import { THEME_STORAGE_KEY } from '../index';
3
+ import { THEME_STORAGE_KEY } from '../index.js';
4
4
  export const ThemeContext = createContext(undefined);
5
5
  /**
6
6
  * Resolve a stored ThemeMode to the effective ResolvedTheme.
@@ -30,8 +30,11 @@ export type BadgeVariant =
30
30
  | 'error'
31
31
  /** Blue — Draft, New, Scheduled */
32
32
  | 'info'
33
- /** Purple — Converted, Promoted */
34
- | 'purple'
33
+ /** Deep blue — Converted, Promoted, Exported, Paid: terminal states that
34
+ * have moved past success. Was `purple`, renamed 0.7.0: the variant was
35
+ * named for a colour rather than a role, and that colour is quarantined
36
+ * from semantics (purple collapses onto primary blue under protanopia). */
37
+ | 'secondary'
35
38
  /** Gray — Inactive, Archived */
36
39
  | 'neutral';
37
40
  export interface StatusBadgeProps {
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/StatusBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3D,MAAM,MAAM,YAAY;AACtB,yCAAyC;AACvC,SAAS;AACX,mCAAmC;GACjC,SAAS;AACX,sCAAsC;GACpC,OAAO;AACT,mCAAmC;GACjC,MAAM;AACR,mCAAmC;GACjC,QAAQ;AACV,gCAAgC;GAC9B,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAqBD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,KAAK,EACL,IAAW,EACX,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,GACtB,EAAE,gBAAgB,GAAG,SAAS,CAc9B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAChD,YAAY,CAoCd;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASxD;AAED,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,OAAO,CAAC;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,KAAK,EACL,gBAAgB,EAChB,GAAG,IAAI,EACR,EAAE,oBAAoB,GAAG,SAAS,CAIlC"}
1
+ {"version":3,"file":"StatusBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/StatusBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3D,MAAM,MAAM,YAAY;AACtB,yCAAyC;AACvC,SAAS;AACX,mCAAmC;GACjC,SAAS;AACX,sCAAsC;GACpC,OAAO;AACT,mCAAmC;GACjC,MAAM;AACR;;;4EAG4E;GAC1E,WAAW;AACb,gCAAgC;GAC9B,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA2BD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,KAAK,EACL,IAAW,EACX,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,GACtB,EAAE,gBAAgB,GAAG,SAAS,CAc9B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAChD,YAAY,CAoCd;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASxD;AAED,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,OAAO,CAAC;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,KAAK,EACL,gBAAgB,EAChB,GAAG,IAAI,EACR,EAAE,oBAAoB,GAAG,SAAS,CAIlC"}
@@ -1,10 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Role-bound, not palette-bound. Until 0.7.0 these were raw Tailwind palette
4
+ * classes (`bg-emerald-50`, `bg-red-50`, …), which meant the design-system
5
+ * component every app imports was invisible to tenant white-labelling —
6
+ * BrandingProvider mutates the role layer, and these bound below it.
7
+ *
8
+ * Tint is derived from the role colour with a slash-opacity fill plus the
9
+ * AA-safe `*-text` variant for type, so one definition covers light and dark
10
+ * and no `dark:` duplicate is needed.
11
+ */
2
12
  const variantClasses = {
3
- success: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800',
4
- warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800',
5
- error: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800',
6
- info: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:border-sky-800',
7
- purple: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800',
13
+ success: 'bg-role-success/10 text-success-text border-role-success/30',
14
+ warning: 'bg-role-warning/10 text-warning-text border-role-warning/30',
15
+ error: 'bg-role-error/10 text-error-text border-role-error/30',
16
+ info: 'bg-role-info/10 text-accent-text border-role-info/30',
17
+ secondary: 'bg-role-secondary/10 text-primary-text border-role-secondary/30',
8
18
  neutral: 'bg-muted text-muted-foreground border-border',
9
19
  };
10
20
  const sizeClasses = {
@@ -57,7 +67,7 @@ export function getStatusVariant(status, overrides) {
57
67
  'draft', 'new', 'scheduled', 'open',
58
68
  'applicant', 'interviewing', 'reported',
59
69
  ];
60
- const PURPLE = ['converted', 'promoted', 'shortlisted'];
70
+ const SECONDARY = ['converted', 'promoted', 'shortlisted', 'exported', 'paid', 'qualified'];
61
71
  if (SUCCESS.includes(normalized))
62
72
  return 'success';
63
73
  if (WARNING.includes(normalized))
@@ -66,8 +76,8 @@ export function getStatusVariant(status, overrides) {
66
76
  return 'error';
67
77
  if (INFO.includes(normalized))
68
78
  return 'info';
69
- if (PURPLE.includes(normalized))
70
- return 'purple';
79
+ if (SECONDARY.includes(normalized))
80
+ return 'secondary';
71
81
  return 'neutral';
72
82
  }
73
83
  /**
@@ -1,13 +1,13 @@
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
- export { BrandingProvider, BrandingContext, BRANDING_STORAGE_KEY, BRANDING_OVERRIDE_FLAG, } from './BrandingProvider';
7
- export type { TenantBranding, BrandingContextValue } from './BrandingProvider';
8
- export { useBranding } from './useBranding';
9
- export { resolvePlatformLogo, usePlatformLogo } from './usePlatformLogo';
10
- export type { PlatformLogoHookResult, PlatformLogoOptions, PlatformLogoScheme, PlatformLogoSlot, PlatformLogoSource, ResolvedPlatformLogo, } from './usePlatformLogo';
11
- export { StatusBadge, AutoStatusBadge, getStatusVariant, formatStatusLabel, } from './components/StatusBadge';
12
- export type { BadgeVariant, StatusBadgeProps, AutoStatusBadgeProps, } from './components/StatusBadge';
1
+ export { ThemeProvider } from './ThemeProvider.js';
2
+ export type { ThemeProviderProps } from './ThemeProvider.js';
3
+ export { useTheme } from './useTheme.js';
4
+ export type { ThemeContextValue, ThemeMode, ResolvedTheme } from '../index.js';
5
+ export { THEME_STORAGE_KEY } from '../index.js';
6
+ export { BrandingProvider, BrandingContext, BRANDING_STORAGE_KEY, BRANDING_OVERRIDE_FLAG, } from './BrandingProvider.js';
7
+ export type { TenantBranding, BrandingContextValue } from './BrandingProvider.js';
8
+ export { useBranding } from './useBranding.js';
9
+ export { resolvePlatformLogo, usePlatformLogo } from './usePlatformLogo.js';
10
+ export type { PlatformLogoHookResult, PlatformLogoOptions, PlatformLogoScheme, PlatformLogoSlot, PlatformLogoSource, ResolvedPlatformLogo, } from './usePlatformLogo.js';
11
+ export { StatusBadge, AutoStatusBadge, getStatusVariant, formatStatusLabel, } from './components/StatusBadge.js';
12
+ export type { BadgeVariant, StatusBadgeProps, AutoStatusBadgeProps, } from './components/StatusBadge.js';
13
13
  //# 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;AAC5C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EACV,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3E,YAAY,EACV,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,6BAA6B,CAAA;AACpC,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,6BAA6B,CAAA"}
@@ -1,8 +1,8 @@
1
- export { ThemeProvider } from './ThemeProvider';
2
- export { useTheme } from './useTheme';
3
- export { THEME_STORAGE_KEY } from '../index';
4
- export { BrandingProvider, BrandingContext, BRANDING_STORAGE_KEY, BRANDING_OVERRIDE_FLAG, } from './BrandingProvider';
5
- export { useBranding } from './useBranding';
6
- export { resolvePlatformLogo, usePlatformLogo } from './usePlatformLogo';
1
+ export { ThemeProvider } from './ThemeProvider.js';
2
+ export { useTheme } from './useTheme.js';
3
+ export { THEME_STORAGE_KEY } from '../index.js';
4
+ export { BrandingProvider, BrandingContext, BRANDING_STORAGE_KEY, BRANDING_OVERRIDE_FLAG, } from './BrandingProvider.js';
5
+ export { useBranding } from './useBranding.js';
6
+ export { resolvePlatformLogo, usePlatformLogo } from './usePlatformLogo.js';
7
7
  // Components — see ./components/<Name>.tsx
8
- export { StatusBadge, AutoStatusBadge, getStatusVariant, formatStatusLabel, } from './components/StatusBadge';
8
+ export { StatusBadge, AutoStatusBadge, getStatusVariant, formatStatusLabel, } from './components/StatusBadge.js';
@@ -7,5 +7,5 @@
7
7
  * const { branding, isLoading } = useBranding()
8
8
  * const logoUrl = branding?.logo_url ?? '/logos/default-logo.svg'
9
9
  */
10
- export declare function useBranding(): import("./BrandingProvider").BrandingContextValue;
10
+ export declare function useBranding(): import("./BrandingProvider.js").BrandingContextValue;
11
11
  //# sourceMappingURL=useBranding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBranding.d.ts","sourceRoot":"","sources":["../../src/react/useBranding.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,sDAM1B"}
1
+ {"version":3,"file":"useBranding.d.ts","sourceRoot":"","sources":["../../src/react/useBranding.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,yDAM1B"}
@@ -1,5 +1,5 @@
1
1
  import { useContext } from 'react';
2
- import { BrandingContext } from './BrandingProvider';
2
+ import { BrandingContext } from './BrandingProvider.js';
3
3
  /**
4
4
  * Access the current tenant branding values and loading state.
5
5
  *
@@ -1,4 +1,4 @@
1
- import type { TenantBranding } from './BrandingProvider';
1
+ import type { TenantBranding } from './BrandingProvider.js';
2
2
  export type PlatformLogoSlot = 'header' | 'sidebar' | 'auth' | 'favicon' | 'mark';
3
3
  export type PlatformLogoScheme = 'light' | 'dark';
4
4
  export type PlatformLogoSource = 'branding' | 'fallback' | 'none';
@@ -1 +1 @@
1
- {"version":3,"file":"usePlatformLogo.d.ts","sourceRoot":"","sources":["../../src/react/usePlatformLogo.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGxD,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AACjF,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAA;AACjD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,gBAAgB,CAAA;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;IACjE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAClE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAqDD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,OAAO,GAAE,mBAAwB,GAChC,oBAAoB,CAiBtB;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAczF"}
1
+ {"version":3,"file":"usePlatformLogo.d.ts","sourceRoot":"","sources":["../../src/react/usePlatformLogo.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAG3D,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AACjF,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAA;AACjD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,gBAAgB,CAAA;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;IACjE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAClE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAqDD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,OAAO,GAAE,mBAAwB,GAChC,oBAAoB,CAiBtB;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAczF"}
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from 'react';
2
- import { useBranding } from './useBranding';
2
+ import { useBranding } from './useBranding.js';
3
3
  function cleanUrl(value) {
4
4
  const trimmed = value?.trim();
5
5
  return trimmed ? trimmed : null;
@@ -1,4 +1,4 @@
1
- import type { ThemeContextValue } from '../index';
1
+ import type { ThemeContextValue } from '../index.js';
2
2
  /**
3
3
  * Hook to read + update the current theme.
4
4
  *
@@ -1 +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"}
1
+ {"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/react/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAGpD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAU5C"}
@@ -1,5 +1,5 @@
1
1
  import { useContext } from 'react';
2
- import { ThemeContext } from './ThemeProvider';
2
+ import { ThemeContext } from './ThemeProvider.js';
3
3
  /**
4
4
  * Hook to read + update the current theme.
5
5
  *
package/dist/ssr/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { THEME_STORAGE_KEY } from '../index';
1
+ import { THEME_STORAGE_KEY } from '../index.js';
2
2
  /**
3
3
  * Returns a self-contained JavaScript string that — when executed before
4
4
  * your React app hydrates — applies the correct `.dark` or `.light` class
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/theme",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -15,7 +15,6 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "src/css",
18
- "src/tailwind-preset.js",
19
18
  "src/preset-v4.css"
20
19
  ],
21
20
  "main": "dist/index.js",
@@ -33,7 +32,6 @@
33
32
  "import": "./dist/ssr/index.js",
34
33
  "types": "./dist/ssr/index.d.ts"
35
34
  },
36
- "./tailwind-preset": "./src/tailwind-preset.js",
37
35
  "./preset-v4.css": "./src/preset-v4.css",
38
36
  "./css": "./src/css/index.css",
39
37
  "./vars.css": "./src/css/vars.css",
@@ -78,7 +78,7 @@
78
78
  ============================================================ */
79
79
  --braden-light-bg-body: oklch(0.980 0.002 50); /* warm white */
80
80
  --braden-light-bg-surface: oklch(0.996 0.001 50); /* near white */
81
- --braden-light-bg-panel: oklch(1.000 0 0); /* white */
81
+ --braden-light-bg-panel: oklch(0.994 0.002 250); /* near-white; pure white is banned in every role */
82
82
  --braden-light-bg-input: oklch(0.972 0.003 250); /* cool light grey */
83
83
 
84
84
  /* Braden light text scale — hue 250 (navy cast), capped L=0.22 */
@@ -161,9 +161,9 @@
161
161
  /* ============================================================
162
162
  SHADOWS — corporate subtle, no neon glow
163
163
  ============================================================ */
164
- --shadow-sm: 0 1px 2px oklch(0 0 0 / 0.08);
165
- --shadow-md: 0 4px 6px oklch(0 0 0 / 0.10), 0 2px 4px oklch(0 0 0 / 0.06);
166
- --shadow-lg: 0 10px 15px oklch(0 0 0 / 0.10), 0 4px 6px oklch(0 0 0 / 0.05);
164
+ --shadow-sm: 0 1px 2px oklch(0.30 0.03 250 / 0.08);
165
+ --shadow-md: 0 4px 6px oklch(0.30 0.03 250 / 0.10), 0 2px 4px oklch(0.30 0.03 250 / 0.06);
166
+ --shadow-lg: 0 10px 15px oklch(0.30 0.03 250 / 0.10), 0 4px 6px oklch(0.30 0.03 250 / 0.05);
167
167
 
168
168
  /* Card glow is a dark-mode-only device (see .dark below); light mode
169
169
  stays border-only, consistent with the corporate "no neon glow" rule. */
@@ -74,18 +74,21 @@
74
74
  (braden/src/index.css) so the corporate and D2C marketing surfaces
75
75
  share one visual language for this specific device.
76
76
  ============================================================ */
77
+ /* Grid lines are hue-matched to the surface, never pure black/white — the
78
+ contract bans both endpoints in every role, alpha forms included. Light
79
+ mode stays neutral and quiet; dark mode takes a faint accent cast, which
80
+ is where the cyber register lives. 32px = the pre-auth marketing tile. */
77
81
  .bsuite-hero-grid {
78
82
  position: absolute;
79
83
  inset: 0;
84
+ --grid-line: oklch(0.40 0.030 258 / 0.07);
80
85
  background-image:
81
- linear-gradient(oklch(0 0 0 / 0.03) 1px, transparent 1px),
82
- linear-gradient(90deg, oklch(0 0 0 / 0.03) 1px, transparent 1px);
86
+ linear-gradient(var(--grid-line) 1px, transparent 1px),
87
+ linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
83
88
  background-size: 32px 32px;
84
89
  }
85
90
  .dark .bsuite-hero-grid {
86
- background-image:
87
- linear-gradient(oklch(1 0 0 / 0.03) 1px, transparent 1px),
88
- linear-gradient(90deg, oklch(1 0 0 / 0.03) 1px, transparent 1px);
91
+ --grid-line: oklch(0.82 0.020 262 / 0.055);
89
92
  }
90
93
 
91
94
  /* ===== prefers-reduced-motion — cap animation duration ===== */
package/src/css/vars.css CHANGED
@@ -12,13 +12,27 @@
12
12
  * Consumers should bind to Layer 3 (roles) or Layer 4 (shadcn) — never
13
13
  * directly to palette primitives. Roles make white-labelling safe.
14
14
  *
15
- * Colourblind policy (WCAG 1.4.1 + audit constraint):
16
- * --role-error / --role-destructive = PURPLE, never red-vs-green.
17
- * Only flag where colour is the SOLE signifier of state.
15
+ * Colourblind policy (WCAG 1.4.1) REWRITTEN 2026-08-02, contract 0.7.0:
16
+ * The invariant is SEPARATION, not a fixed hue. --role-error must be
17
+ * maximally separated from --role-primary under deuteranopia and
18
+ * protanopia. D2C's primary is blue, so error is RED.
19
+ * The previous "error = purple" rule was actively harmful: under
20
+ * protanopia purple measured DeltaE 0.006 against primary blue — the
21
+ * destructive colour and the primary action colour were the SAME swatch.
22
+ * Purple and indigo are therefore quarantined from semantics entirely.
23
+ * Warm states (red/amber) separate by LIGHTNESS, not hue — under
24
+ * deuteranopia everything warm collapses to yellow, so error is the dark
25
+ * one and warning the light one. Never put two cool hues in opposition.
26
+ * Colour is never the sole signifier: shape + label always.
27
+ * TENANT OVERRIDE BLOCKED on --role-error / --role-destructive.
18
28
  *
19
- * Eye-strain policy:
20
- * Dark-mode text capped at L=0.94. Pure white (L=1.0) is banned as a
21
- * text token. 5-tier scale verified WCAG AA minimum throughout.
29
+ * Eye-strain / pure-endpoint policy:
30
+ * Pure white (L=1.0) and pure black (L=0) are BANNED in EVERY role —
31
+ * text, surface, border, ring and shadow alike (operator ruling
32
+ * 2026-08-02). The prior text-only scoping of this rule is what let
33
+ * --light-bg-accent ship as oklch(1 0 0.5) and render every shadcn card
34
+ * and popover pure white. "White" here means oklch(0.982 0.002 248)
35
+ * (#f8f9fa); the lightest surface is bg-panel at L=0.994.
22
36
  *
23
37
  * OKLCH format is MANDATORY for all new tokens. No hex/rgb/hsl.
24
38
  * Syntax: oklch(L C H) — L=lightness 0–1, C=chroma 0–0.4, H=hue 0–360.
@@ -41,6 +55,17 @@
41
55
  --neon-electric-green: oklch(0.723 0.192 149.6);
42
56
  --neon-electric-lavender: oklch(0.736 0.141 285.6);
43
57
 
58
+ /* Semantic hues — added 2026-08-02 (contract 0.7.0). The 11 above were
59
+ chosen for looks; these four were chosen for SEPARATION, measured via
60
+ Vienot-Brettel dichromat simulation. Worst-case DeltaE(OKLab) across
61
+ normal/deuteranopia/protanopia for every semantic pair is >= 0.156;
62
+ the pairing these replace scored 0.006. Never bind a component to
63
+ these directly — bind to the --role-* alias. */
64
+ --neon-electric-red: oklch(0.580 0.230 25); /* error / destructive */
65
+ --neon-electric-amber: oklch(0.800 0.150 75); /* warning */
66
+ --neon-electric-teal: oklch(0.600 0.130 195); /* success */
67
+ --neon-electric-deep: oklch(0.380 0.140 270); /* secondary action */
68
+
44
69
  /* ============================================================
45
70
  LAYER 2 — SURFACE PRIMITIVES (light mode)
46
71
  ============================================================ */
@@ -49,7 +74,10 @@
49
74
  --light-bg-primary: oklch(0.961 0 0.5);
50
75
  --light-bg-secondary: oklch(0.982 0.002 248);
51
76
  --light-bg-tertiary: oklch(0.963 0.003 228.9);
52
- --light-bg-accent: oklch(1 0 0.5);
77
+ /* bg-panel. Was oklch(1 0 0.5) — pure white, and since --card and
78
+ --popover bridge to it, every shadcn card and popover in all five D2C
79
+ apps rendered pure white in light mode from this one line. */
80
+ --light-bg-accent: oklch(0.994 0.002 260);
53
81
 
54
82
  /* D2C light text scale — hue 260 (cool blue-grey), capped L=0.22.
55
83
  Measured WCAG contrast vs --light-bg-primary oklch(0.961 0 0.5), OKLCH ->
@@ -63,9 +91,6 @@
63
91
  --light-text-muted: oklch(0.52 0.018 260); /* 4.92:1 ✓ AA (normal, 4.5:1 floor) */
64
92
  --light-text-subtle: oklch(0.60 0.012 260); /* 3.52:1 ✓ AA large only (3:1) — ✗ AA normal */
65
93
  --light-text-disabled: oklch(0.72 0.010 260); /* 2.21:1 ✗ fails AA at any size — MUST pair with icon cue */
66
- /* Legacy aliases (kept for consumers that haven't migrated) */
67
- --light-text-tertiary: var(--light-text-muted);
68
- --light-text-quaternary: var(--light-text-subtle);
69
94
 
70
95
  --light-border: oklch(0.942 0.005 247.9);
71
96
  --light-hover: oklch(0.963 0.003 228.9);
@@ -83,19 +108,33 @@
83
108
 
84
109
  /* Action colours */
85
110
  --role-primary: var(--neon-electric-blue); /* oklch(0.546 0.215 262.9) */
86
- --role-secondary: var(--neon-electric-indigo); /* oklch(0.511 0.23 277) */
111
+ --role-secondary: var(--neon-electric-deep); /* was indigo — 0.037 from primary */
87
112
  --role-accent: var(--neon-electric-cyan); /* oklch(0.769 0.132 191.7) */
88
113
 
89
- /* ⚠️ COLOURBLIND POLICY: error = PURPLE never red/coral.
90
- Red-vs-green is the most common colour-vision deficiency (~8% M, ~0.5% F).
91
- Purple (hue 283) is unambiguous to deuteranopes and protanopes.
114
+ /* ⚠️ SEPARATION POLICY (contract 0.7.0). The invariant is NOT a fixed hue —
115
+ it is that error must be maximally separated from PRIMARY under
116
+ deuteranopia and protanopia. D2C's primary is blue, so error is RED.
117
+ Purple scored DeltaE 0.006 against primary under protanopia: the
118
+ destructive colour and the primary action colour were the same swatch.
119
+ Purple and indigo are quarantined from semantics entirely.
120
+ Warm states separate by LIGHTNESS (under deuteranopia red and amber
121
+ both read yellow — error is the dark one, warning the light one).
92
122
  TENANT OVERRIDE BLOCKED: BrandingProvider rejects --role-error changes. */
93
- --role-error: var(--neon-electric-purple); /* oklch(0.568 0.202 283.1) */
94
- --role-destructive: var(--neon-electric-purple); /* synonym — same value */
95
- --role-warning: var(--neon-electric-orange); /* oklch(0.728 0.168 22.5) */
96
- --role-success: var(--neon-electric-green); /* oklch(0.723 0.192 149.6) */
123
+ --role-error: var(--neon-electric-red); /* was purple — 0.006 from primary */
124
+ --role-destructive: var(--neon-electric-red); /* synonym — same value */
125
+ --role-warning: var(--neon-electric-amber); /* was orange h22.5 0.059 from red */
126
+ --role-success: var(--neon-electric-teal); /* was green — 0.043 from red */
97
127
  --role-info: var(--neon-electric-cyan); /* oklch(0.769 0.132 191.7) */
98
128
 
129
+ /* AA-safe text renderings of the saturated roles on light surfaces.
130
+ A saturated fill colour is not a legible text colour; these are the
131
+ text variants and are the ONLY correct token for coloured type. */
132
+ --role-primary-text: oklch(0.541 0.215 262.9); /* 4.71:1 ✓ AA */
133
+ --role-accent-text: oklch(0.499 0.132 191.7); /* 4.70:1 ✓ AA */
134
+ --role-error-text: oklch(0.556 0.230 25); /* 4.69:1 ✓ AA */
135
+ --role-warning-text: oklch(0.535 0.150 75); /* 4.70:1 ✓ AA */
136
+ --role-success-text: oklch(0.501 0.130 195); /* 4.70:1 ✓ AA */
137
+
99
138
  /* Text roles — six-tier scale (W3 §3.1 contract). Every tier below is
100
139
  measured against --role-bg-body (the conservative floor); all tiers
101
140
  score HIGHER against --role-bg-panel/--role-bg-surface. */
@@ -107,27 +146,54 @@
107
146
  --role-text-disabled: var(--light-text-disabled); /* 2.21:1 ✗ icon cue required */
108
147
 
109
148
  /* Surface roles */
110
- --role-bg-body: var(--light-bg-primary);
111
- --role-bg-surface: var(--light-bg-secondary);
112
- --role-bg-panel: var(--light-bg-accent);
113
- --role-bg-input: var(--light-bg-tertiary);
114
- --role-border: var(--light-border);
115
-
116
- /* Inverse text — safe on coloured fills (mid-lightness L≈0.5–0.7) */
117
- --text-on-primary: oklch(0.99 0 0); /* on blue L≈0.55 — 5.9:1 ✓ AA */
118
- --text-on-accent: oklch(0.17 0.02 260); /* on cyan L≈0.77 — 6.1:1 ✓ AA */
119
- --text-on-error: oklch(0.99 0 0); /* on purple L≈0.57 */
120
- --text-on-success: oklch(0.17 0.02 260); /* on green L≈0.72 — dark text */
121
- --text-on-warning: oklch(0.17 0.02 260); /* on amber L≈0.82 — dark text */
149
+ --role-bg-body: var(--light-bg-primary);
150
+ --role-bg-surface: var(--light-bg-secondary);
151
+ --role-bg-panel: var(--light-bg-accent);
152
+ --role-bg-input: var(--light-bg-tertiary);
153
+ --role-bg-sunken: oklch(0.938 0.004 250);
154
+ --role-border: var(--light-border);
155
+ --role-border-strong: oklch(0.880 0.008 250);
122
156
 
123
- /* ============================================================
124
- LAYER 3 SEMANTIC STATUS (legacy: keeps --status-* names)
125
- Deprecated: prefer --role-* going forward.
126
- ============================================================ */
127
- --status-success: var(--role-success);
128
- --status-warning: var(--role-warning);
129
- --status-error: var(--role-error); /* was coral; now purple per colourblind policy */
130
- --status-info: var(--role-info);
157
+ /* Scrim — the veil behind a modal, and the reason `bg-black/N` survived
158
+ everywhere else. It was the single most duplicated pure endpoint in the
159
+ tree (9 sites across nav-core, page-builder and schema-builder) for one
160
+ reason: no overlay role existed, so every author reached for the only
161
+ thing that was always available. A banned value with no sanctioned
162
+ replacement does not get obeyed, it gets re-typed.
163
+
164
+ VALUE AND SHAPE ADOPTED FROM crm7, which had already solved this locally
165
+ (crm7/src/index.css --color-overlay, with the note "pure black banned").
166
+ Lifting their value rather than minting a competing one is the whole
167
+ point: two scrim tokens with different values is the duplication this
168
+ change exists to remove, and theirs was here first.
169
+
170
+ OPAQUE — the alpha belongs at the CALL SITE, not in the token. I first
171
+ baked 0.55 in to stop the /40-vs-/50 drift between page-builder and
172
+ schema-builder, which would have silently halved crm7's existing
173
+ `bg-overlay/50` to 0.275 by compounding. Call-site alpha is also simply
174
+ more useful: a full-page modal scrim and a caption strip over a photo
175
+ want the same colour at different strengths.
176
+
177
+ Declared ONCE — no .dark override. A scrim is not a surface: it veils
178
+ whatever is behind it, and the dialog above it is --role-bg-panel, which
179
+ already flips. Giving it a dark twin would move the veil and the panel
180
+ together and cancel out the separation this exists to create. */
181
+ --role-overlay: oklch(0.145 0.022 268);
182
+
183
+ /* Inverse text on coloured fills. Two values only — the fill is either
184
+ light enough to need dark text, or dark enough to need light text.
185
+ Neither is a pure endpoint. */
186
+ --text-on-light-fill: oklch(0.98 0.006 260);
187
+ --text-on-dark-fill: oklch(0.19 0.020 255);
188
+ --text-on-primary: var(--text-on-light-fill); /* on blue L≈0.55 — 4.88:1 ✓ AA */
189
+ --text-on-error: var(--text-on-light-fill); /* on red L≈0.58 — 4.55:1 ✓ AA */
190
+ --text-on-accent: var(--text-on-dark-fill); /* on cyan L≈0.77 — 9.38:1 ✓ AAA */
191
+ --text-on-overlay: var(--text-on-light-fill); /* on scrim L≈0.15 — the scrim is dark in BOTH modes,
192
+ so this never flips. crm7 declared it as a bare
193
+ oklch(0.99 0 0); routed through the shared token
194
+ instead so there is one near-white, not two. */
195
+ --text-on-warning: var(--text-on-dark-fill); /* on amber L≈0.80 — 9.68:1 ✓ AAA */
196
+ --text-on-success: var(--text-on-dark-fill); /* on teal L≈0.60 — 5.17:1 ✓ AA */
131
197
 
132
198
  /* ============================================================
133
199
  LAYER 4 — SHADCN/UI BRIDGE (light mode)
@@ -154,13 +220,6 @@
154
220
  --input: var(--role-border);
155
221
  --ring: var(--role-primary);
156
222
 
157
- /* ============================================================
158
- WCAG AA compliant text pairs for raw neon on light bg
159
- (neon cyan L=0.77 on light bg is only 1.76:1 — fails 4.5:1)
160
- ============================================================ */
161
- --color-accent-text: oklch(0.486 0.084 191.5); /* Dark cyan — AA on light bg */
162
- --color-primary-text: oklch(0.485 0.243 263.6); /* Saturated blue — AA on light bg */
163
-
164
223
  /* ============================================================
165
224
  CANONICAL ACCENT GRADIENT (W3 §3.1) — exactly ONE gradient
166
225
  exists suite-wide. Marketing/pre-auth heroes ONLY — never on
@@ -190,44 +249,72 @@
190
249
  }
191
250
 
192
251
  /* ============================================================
193
- SRGB FALLBACKS (for browsers without oklch support) W3 §3.4
194
- True-source hexes, each re-derived directly from its oklch token
195
- (OKLCH -> OKLab -> linear-sRGB -> gamma-encoded sRGB) rather than
196
- eyeballed against a Tailwind palette. Round-trip-verified: every
197
- value below computes ΔE(OKLab) < 0.01 against its oklch token —
198
- at least twice inside the < 0.02 acceptance bar (most values are
199
- one to two orders of magnitude inside it; see docs/TOKEN-MAPPING.md
200
- §8 for the full per-token ΔE table). The prior Tailwind-palette-
201
- derived hexes were ΔE 0.03-0.10 off.
202
-
203
- Deliberately declared at the TOP LEVEL, not nested inside another
204
- `:root { }` block: engines that lack `oklch()` support predate CSS
205
- nesting too, so a `@supports` block nested inside `:root` would be
206
- silently dropped by exactly the browsers it's meant to serve when
207
- this file is consumed raw (unbundled). It works when bundled
208
- through the Tailwind v4 / LightningCSS pipeline only because that
209
- pipeline hoists nested at-rules — this top-level form is correct
210
- for both the raw and the bundled case.
252
+ LAYER 5PER-APP ACCENT
253
+ Added 2026-08-02: the 0.7.0 contract specifies these blocks but the
254
+ package had never shipped them only a `var(--app-accent, )` fallback
255
+ reference, so every app resolved to the shared cyan and the per-app
256
+ identity existed on paper only.
257
+
258
+ Set `data-app` on <body>. The FILL is the brand hue; the TEXT variants
259
+ are solved per mode, because a saturated fill colour is not a legible
260
+ text colour and the accent must never render below AA.
211
261
  ============================================================ */
212
- @supports not (color: oklch(0 0 0)) {
213
- :root {
214
- --neon-electric-blue: #2563eb;
215
- --neon-electric-cyan: #00cec9;
216
- --neon-electric-indigo: #4f46e5;
217
- --neon-electric-purple: #6c5ce7;
218
- --neon-electric-magenta: #fd79a8;
219
- --neon-electric-pink: #ec4899;
220
- --neon-electric-coral: #ff4757;
221
- --neon-electric-orange: #ff7675;
222
- --neon-electric-yellow: #fdcb6e;
223
- --neon-electric-green: #22c55e;
224
- --neon-electric-lavender: #a29bfe;
225
- --role-error: #6c5ce7;
226
- --role-destructive: #6c5ce7;
227
- --light-text-primary: #171b22;
228
- --dark-text-primary: #e8eaf6;
229
- }
262
+ [data-app="crm7"] {
263
+ --app-accent: oklch(0.769 0.132 191.7);
264
+ --app-accent-text-l: oklch(0.505 0.132 191.7);
265
+ --app-accent-text-d: oklch(0.769 0.132 191.7);
230
266
  }
267
+ [data-app="bsu"] {
268
+ --app-accent: oklch(0.541 0.247 293);
269
+ --app-accent-text-l: oklch(0.564 0.247 293);
270
+ --app-accent-text-d: oklch(0.745 0.247 293);
271
+ }
272
+ [data-app="conduit"] {
273
+ --app-accent: oklch(0.723 0.192 149.6);
274
+ --app-accent-text-l: oklch(0.505 0.192 149.6);
275
+ --app-accent-text-d: oklch(0.723 0.192 149.6);
276
+ }
277
+ [data-app="r80"] {
278
+ --app-accent: oklch(0.666 0.157 58.3);
279
+ --app-accent-text-l: oklch(0.546 0.157 58.3);
280
+ --app-accent-text-d: oklch(0.703 0.157 58.3);
281
+ }
282
+ /* R8.04 — the rates engine replacing R80.3. Deliberately REUSES r80's
283
+ orange: it serves the same users doing the same job, and a rates engine
284
+ is not where people should be re-learning which app they are in.
285
+ Requested by the r804 lane 2026-08-02. */
286
+ [data-app="r804"] {
287
+ --app-accent: oklch(0.666 0.157 58.3);
288
+ --app-accent-text-l: oklch(0.546 0.157 58.3);
289
+ --app-accent-text-d: oklch(0.703 0.157 58.3);
290
+ }
291
+ [data-app="throughput"] {
292
+ --app-accent: oklch(0.656 0.212 354.3);
293
+ --app-accent-text-l: oklch(0.564 0.212 354.3);
294
+ --app-accent-text-d: oklch(0.731 0.212 354.3);
295
+ }
296
+ /* Light/dark rebinding of the text variant. */
297
+ :root { --app-accent-text: var(--app-accent-text-l); }
298
+ .dark { --app-accent-text: var(--app-accent-text-d); }
299
+
300
+ /* ============================================================
301
+ NO sRGB FALLBACK — DELIBERATELY REMOVED 2026-08-02.
302
+ Precedent: precedent__bsuite__20260802__theme_conformance_is_absolute.
303
+
304
+ The `@supports not (color: oklch(0 0 0))` block that stood here shipped
305
+ a parallel hex palette. It is gone, and must not be reinstated:
306
+
307
+ · OKLCH has been Baseline-available in every evergreen engine since
308
+ Firefox 113 (May 2023), after Safari 15.4 (2022) and Chrome 111
309
+ (Mar 2023). It guarded a population that no longer exists.
310
+ · It was a second source of truth for the same colours, so it drifted:
311
+ it still bound --role-error to purple after the semantic set moved
312
+ to red. A fallback that disagrees with the tokens is worse than none.
313
+
314
+ If a colour needs to exist in hex (email HTML, PDF, canvas, manifest),
315
+ derive it at that call site from the oklch token — do not re-introduce a
316
+ second palette here.
317
+ ============================================================ */
231
318
 
232
319
  /* ============================================================
233
320
  DARK MODE — Layer 2 surface overrides + Layer 3 role rebind
@@ -253,9 +340,6 @@
253
340
  --dark-text-muted: oklch(0.68 0.018 260); /* metadata — 6.69:1 ✓ AA (normal) */
254
341
  --dark-text-subtle: oklch(0.56 0.015 260); /* placeholders — 4.14:1 ✓ AA large only — ✗ AA normal */
255
342
  --dark-text-disabled: oklch(0.44 0.010 260); /* disabled — 2.48:1 ✗ fails AA at any size — MUST pair with icon cue */
256
- /* Legacy aliases */
257
- --dark-text-tertiary: var(--dark-text-muted);
258
- --dark-text-quaternary: var(--dark-text-disabled);
259
343
 
260
344
  --dark-border: oklch(0.428 0.015 248.2);
261
345
  --dark-hover: oklch(0.39 0.035 265);
@@ -270,14 +354,17 @@
270
354
 
271
355
  --role-bg-body: var(--dark-bg-primary);
272
356
  --role-bg-surface: var(--dark-bg-secondary);
273
- --role-bg-panel: oklch(0.19 0.02 260 / 0.8);
274
- --role-bg-input: oklch(0.24 0.02 260 / 0.6);
357
+ --role-bg-panel: oklch(0.190 0.020 260);
358
+ --role-bg-input: oklch(0.240 0.020 260);
359
+ --role-bg-sunken: oklch(0.145 0.022 268);
275
360
  --role-border: var(--dark-border);
276
361
 
277
- /* Accent/primary text tokens flip to raw neons in dark mode
278
- contrast on navy bg is already ≥ 9:1 for these values. */
279
- --color-accent-text: oklch(0.769 0.132 191.7);
280
- --color-primary-text: oklch(0.623 0.188 259.8);
362
+ /* Role text variants rebind for the dark surface all AAA on navy. */
363
+ --role-primary-text: oklch(0.721 0.215 262.9); /* 7.01:1 AAA */
364
+ --role-accent-text: var(--neon-electric-cyan); /* 9.78:1 ✓ AAA */
365
+ --role-error-text: oklch(0.786 0.230 25); /* 7.00:1 ✓ AAA */
366
+ --role-warning-text: var(--neon-electric-amber);/* 10.10:1 ✓ AAA */
367
+ --role-success-text: oklch(0.671 0.130 195); /* 6.99:1 ✓ AAA */
281
368
 
282
369
  /* Glow/shadow scale up in dark mode */
283
370
  --glow-strength: 0.6;
package/src/preset-v4.css CHANGED
@@ -1,134 +1,148 @@
1
1
  /*
2
- * @bsuite/theme — Tailwind v4 @theme block
3
- * Version 0.3.1
2
+ * @bsuite/theme — Tailwind v4 @theme bridge
3
+ * Version 0.7.0
4
4
  *
5
- * Tailwind v4 exposes design tokens to the engine via CSS @theme blocks.
6
- * Import this before any app-specific @theme overrides:
5
+ * Usage (every D2C app; braden imports braden-css instead of css):
7
6
  *
8
7
  * @import 'tailwindcss';
9
- * @import '@bsuite/theme/preset-v4.css';
8
+ * @import '@bsuite/theme/css'; ← defines --role-* (vars.css)
9
+ * @import '@bsuite/theme/preset-v4.css'; ← this file, maps them to utilities
10
10
  *
11
- * @theme {
12
- * --color-app-primary: oklch(0.45 0.18 142); -- app-specific override
13
- * }
11
+ * ─────────────────────────────────────────────────────────────────────────
12
+ * WHY `@theme inline` AND NOT `@theme`
13
+ *
14
+ * Until 0.7.0 this file used a plain `@theme` block containing hardcoded
15
+ * oklch literals — a full second copy of every value in vars.css. Two
16
+ * consequences, both real bugs:
17
+ *
18
+ * 1. DARK MODE DID NOT REACH THESE UTILITIES. Plain `@theme` emits
19
+ * `--color-card: <literal>` at :root and makes `bg-card` resolve to
20
+ * `var(--color-card)`. vars.css rebinds `--role-bg-panel` under
21
+ * `.dark`, but nothing rebound `--color-card`, so `bg-card` stayed on
22
+ * its light-mode literal in dark mode.
23
+ * 2. IT DRIFTED. The duplicate copy still said `--color-card: oklch(1 0
24
+ * 0.5)` (pure white) and `--color-destructive: <purple>` after the
25
+ * token source had moved on from both.
26
+ *
27
+ * `@theme inline` emits the `var()` reference into the utility itself, so
28
+ * `bg-card` resolves through `--role-bg-panel` and follows every `.dark`
29
+ * and BrandingProvider rebinding for free. vars.css is the ONLY source of
30
+ * truth for values; this file contains no colour literals at all.
31
+ *
32
+ * Adding a colour literal to this file re-creates both bugs. Don't.
33
+ * ─────────────────────────────────────────────────────────────────────────
14
34
  *
15
35
  * Token naming: `--color-{name}` → Tailwind generates `bg-{name}`,
16
36
  * `text-{name}`, `border-{name}` etc. automatically.
17
- *
18
- * Layer order matches vars.css:
19
- * 1. Palette — --color-neon-electric-*
20
- * 2. Surface — --color-{light|dark}-{bg|text}-*
21
- * 3. Role — --color-role-* ← bind to these in components
22
- * 4. Shadcn — --color-background, --color-foreground, etc.
23
- * 5. Text-on — --color-text-on-*
24
37
  */
25
38
 
26
- @theme {
27
- /* ============================================================
28
- LAYER 1 — Neon Electric palette (11 colours, FROZEN)
29
- ============================================================ */
30
- --color-neon-electric-blue: oklch(0.546 0.215 262.9);
31
- --color-neon-electric-cyan: oklch(0.769 0.132 191.7);
32
- --color-neon-electric-indigo: oklch(0.511 0.23 277);
33
- --color-neon-electric-purple: oklch(0.568 0.202 283.1);
34
- --color-neon-electric-magenta: oklch(0.742 0.167 359.5);
35
- --color-neon-electric-pink: oklch(0.656 0.212 354.3);
36
- --color-neon-electric-coral: oklch(0.669 0.219 20.9);
37
- --color-neon-electric-orange: oklch(0.728 0.168 22.5);
38
- --color-neon-electric-yellow: oklch(0.868 0.125 81.4);
39
- --color-neon-electric-green: oklch(0.723 0.192 149.6);
40
- --color-neon-electric-lavender: oklch(0.736 0.141 285.6);
41
-
39
+ @theme inline {
42
40
  /* ============================================================
43
- LAYER 2Surface tokens (light)
41
+ LAYER 1Palette. Exposed for decorative use only.
42
+ Bind components to the role tokens below, never to these:
43
+ white-labelling mutates roles, so a palette-bound component
44
+ silently ignores tenant branding.
44
45
  ============================================================ */
45
- --color-light-bg-primary: oklch(0.961 0 0.5);
46
- --color-light-bg-secondary: oklch(0.982 0.002 248);
47
- --color-light-bg-tertiary: oklch(0.963 0.003 228.9);
48
- --color-light-bg-accent: oklch(1 0 0.5);
49
-
50
- /* Eye-strain-safe light text scale (L capped at 0.22, hue 260) */
51
- --color-light-text-primary: oklch(0.22 0.015 260); /* 13.2:1 AAA */
52
- --color-light-text-secondary: oklch(0.38 0.018 260); /* 8.1:1 AAA */
53
- --color-light-text-muted: oklch(0.52 0.018 260); /* 4.9:1 AA */
54
- --color-light-text-subtle: oklch(0.60 0.012 260); /* 3.8:1 AA large */
55
- --color-light-text-disabled: oklch(0.72 0.010 260);
46
+ --color-neon-electric-blue: var(--neon-electric-blue);
47
+ --color-neon-electric-cyan: var(--neon-electric-cyan);
48
+ --color-neon-electric-indigo: var(--neon-electric-indigo);
49
+ --color-neon-electric-purple: var(--neon-electric-purple);
50
+ --color-neon-electric-magenta: var(--neon-electric-magenta);
51
+ --color-neon-electric-pink: var(--neon-electric-pink);
52
+ --color-neon-electric-coral: var(--neon-electric-coral);
53
+ --color-neon-electric-orange: var(--neon-electric-orange);
54
+ --color-neon-electric-yellow: var(--neon-electric-yellow);
55
+ --color-neon-electric-green: var(--neon-electric-green);
56
+ --color-neon-electric-lavender: var(--neon-electric-lavender);
57
+ --color-neon-electric-red: var(--neon-electric-red);
58
+ --color-neon-electric-amber: var(--neon-electric-amber);
59
+ --color-neon-electric-teal: var(--neon-electric-teal);
60
+ --color-neon-electric-deep: var(--neon-electric-deep);
56
61
 
57
62
  /* ============================================================
58
- LAYER 2 — Surface tokens (dark)
63
+ LAYER 2 — Surfaces. Role-bound, so they flip with .dark.
59
64
  ============================================================ */
60
- --color-dark-bg-primary: oklch(0.166 0.026 269.4);
61
- --color-dark-bg-secondary: oklch(0.242 0.03 269.9);
62
- --color-dark-bg-tertiary: oklch(0.326 0.036 266.7);
63
- --color-dark-bg-quaternary: oklch(0.39 0.035 265);
64
- --color-dark-bg-accent: oklch(0.292 0.034 270);
65
-
66
- /* Eye-strain-safe dark text scale (L capped at 0.94, hue 260) */
67
- --color-dark-text-primary: oklch(0.94 0.012 260); /* 13.9:1 AAA */
68
- --color-dark-text-secondary: oklch(0.82 0.015 260); /* 9.8:1 AAA */
69
- --color-dark-text-muted: oklch(0.68 0.018 260); /* 5.6:1 AA */
70
- --color-dark-text-subtle: oklch(0.56 0.015 260); /* 3.6:1 AA large */
71
- --color-dark-text-disabled: oklch(0.44 0.010 260);
65
+ --color-bg-body: var(--role-bg-body);
66
+ --color-bg-surface: var(--role-bg-surface);
67
+ --color-bg-panel: var(--role-bg-panel);
68
+ --color-bg-input: var(--role-bg-input);
69
+ --color-bg-sunken: var(--role-bg-sunken);
70
+ --color-border-strong: var(--role-border-strong);
72
71
 
73
72
  /* ============================================================
74
- LAYER 3 — Role aliases
75
- Consumers should only reference these — never palette names.
73
+ LAYER 3 — Text scale
76
74
  ============================================================ */
77
- --color-role-primary: oklch(0.546 0.215 262.9); /* electric blue */
78
- --color-role-secondary: oklch(0.511 0.23 277); /* electric indigo */
79
- --color-role-accent: oklch(0.769 0.132 191.7); /* electric cyan */
80
-
81
- /* Colourblind-safe: error = purple, never red/coral */
82
- --color-role-error: oklch(0.568 0.202 283.1); /* electric purple */
83
- --color-role-destructive: oklch(0.568 0.202 283.1); /* synonym */
84
- --color-role-warning: oklch(0.728 0.168 22.5); /* electric orange */
85
- --color-role-success: oklch(0.723 0.192 149.6); /* electric green */
86
- --color-role-info: oklch(0.769 0.132 191.7); /* electric cyan */
87
-
88
- /* Semantic status aliases (deprecated: prefer role-*) */
89
- --color-status-success: oklch(0.723 0.192 149.6);
90
- --color-status-warning: oklch(0.728 0.168 22.5);
91
- --color-status-error: oklch(0.568 0.202 283.1); /* purple — colourblind policy */
92
- --color-status-info: oklch(0.769 0.132 191.7);
75
+ --color-text-heading: var(--role-text-heading);
76
+ --color-text-body: var(--role-text-body);
77
+ --color-text-secondary: var(--role-text-secondary);
78
+ --color-text-muted: var(--role-text-muted);
79
+ --color-text-subtle: var(--role-text-subtle);
80
+ --color-text-disabled: var(--role-text-disabled);
93
81
 
94
82
  /* ============================================================
95
- LAYER 4Shadcn/ui bridge
96
- Tailwind generates bg-background, text-foreground, etc.
83
+ LAYER 3Semantic roles.
84
+ Error is RED because D2C's primary is blue and error must be
85
+ maximally separated from primary under dichromacy — see the
86
+ separation policy in vars.css. Not a fixed-hue rule.
97
87
  ============================================================ */
98
- --color-background: oklch(0.961 0 0.5);
99
- --color-foreground: oklch(0.22 0.015 260);
100
- --color-card: oklch(1 0 0.5);
101
- --color-card-foreground: oklch(0.22 0.015 260);
102
- --color-popover: oklch(1 0 0.5);
103
- --color-popover-foreground: oklch(0.22 0.015 260);
104
- --color-primary: oklch(0.546 0.215 262.9);
105
- --color-primary-foreground: oklch(0.99 0 0);
106
- --color-secondary: oklch(0.511 0.23 277);
107
- --color-secondary-foreground: oklch(0.99 0 0);
108
- --color-muted: oklch(0.963 0.003 228.9);
109
- --color-muted-foreground: oklch(0.52 0.018 260);
110
- --color-accent: oklch(0.769 0.132 191.7);
111
- --color-accent-foreground: oklch(0.17 0.02 260);
112
- --color-destructive: oklch(0.568 0.202 283.1); /* purple — colourblind policy */
113
- --color-destructive-foreground: oklch(0.99 0 0);
114
- --color-border: oklch(0.942 0.005 247.9);
115
- --color-input: oklch(0.942 0.005 247.9);
116
- --color-ring: oklch(0.546 0.215 262.9);
88
+ --color-role-primary: var(--role-primary);
89
+ --color-role-secondary: var(--role-secondary);
90
+ --color-role-accent: var(--role-accent);
91
+ --color-role-error: var(--role-error);
92
+ --color-role-destructive: var(--role-destructive);
93
+ --color-role-warning: var(--role-warning);
94
+ --color-role-success: var(--role-success);
95
+ --color-role-info: var(--role-info);
96
+
97
+ /* AA-safe text variants — the only correct token for coloured type.
98
+ A saturated fill colour is not a legible text colour. */
99
+ --color-primary-text: var(--role-primary-text);
100
+ --color-accent-text: var(--role-accent-text);
101
+ --color-error-text: var(--role-error-text);
102
+ --color-warning-text: var(--role-warning-text);
103
+ --color-success-text: var(--role-success-text);
104
+
105
+ /* Per-app accent (Layer 5). Resolves through [data-app] on <body>,
106
+ falling back to the shared accent when no app is declared. */
107
+ --color-app-accent: var(--app-accent, var(--role-accent));
108
+ --color-app-accent-text: var(--app-accent-text, var(--role-accent-text));
117
109
 
118
110
  /* ============================================================
119
- LAYER 5Inverse text (for coloured fill surfaces)
111
+ LAYER 4shadcn/ui bridge
120
112
  ============================================================ */
121
- --color-text-on-primary: oklch(0.99 0 0);
122
- --color-text-on-accent: oklch(0.17 0.02 260);
123
- --color-text-on-error: oklch(0.99 0 0);
124
- --color-text-on-success: oklch(0.17 0.02 260);
125
- --color-text-on-warning: oklch(0.17 0.02 260);
113
+ --color-background: var(--role-bg-body);
114
+ --color-foreground: var(--role-text-body);
115
+ --color-card: var(--role-bg-panel);
116
+ --color-card-foreground: var(--role-text-body);
117
+ --color-popover: var(--role-bg-panel);
118
+ --color-popover-foreground: var(--role-text-body);
119
+ --color-primary: var(--role-primary);
120
+ --color-primary-foreground: var(--text-on-primary);
121
+ --color-secondary: var(--role-secondary);
122
+ --color-secondary-foreground: var(--text-on-primary);
123
+ --color-muted: var(--role-bg-input);
124
+ --color-muted-foreground: var(--role-text-muted);
125
+ --color-accent: var(--role-accent);
126
+ --color-accent-foreground: var(--text-on-accent);
127
+ --color-destructive: var(--role-destructive);
128
+ --color-destructive-foreground: var(--text-on-error);
129
+ --color-border: var(--role-border);
130
+ --color-input: var(--role-border);
131
+ --color-ring: var(--role-primary);
132
+ /* Modal scrim. OPAQUE — always write an explicit alpha at the call site
133
+ (`bg-overlay/50`), never bare `bg-overlay`, which would hide the page
134
+ entirely. Pairs with `text-text-on-overlay`. */
135
+ --color-overlay: var(--role-overlay);
136
+ --color-text-on-overlay: var(--text-on-overlay);
126
137
 
127
138
  /* ============================================================
128
- WCAG AA text pairs for raw neons on light backgrounds
139
+ LAYER 5 Inverse text on coloured fills
129
140
  ============================================================ */
130
- --color-accent-text: oklch(0.486 0.084 191.5);
131
- --color-primary-text: oklch(0.485 0.243 263.6);
141
+ --color-text-on-primary: var(--text-on-primary);
142
+ --color-text-on-accent: var(--text-on-accent);
143
+ --color-text-on-error: var(--text-on-error);
144
+ --color-text-on-success: var(--text-on-success);
145
+ --color-text-on-warning: var(--text-on-warning);
132
146
 
133
147
  /* ============================================================
134
148
  ANIMATIONS
@@ -143,27 +157,28 @@
143
157
  }
144
158
 
145
159
  /* ============================================================
146
- KEYFRAMES — outside @theme (v4 doesn't expose them there)
147
- oklch used throughout no rgba/hsl leakage
160
+ KEYFRAMES — outside @theme (v4 doesn't expose them there).
161
+ Colour-bearing frames derive from --role-accent, so they follow
162
+ per-app accent and tenant branding instead of pinning cyan.
148
163
  ============================================================ */
149
164
  @keyframes pulse-soft {
150
165
  0%, 100% { opacity: 1; }
151
- 50% { opacity: 0.7; }
166
+ 50% { opacity: 0.7; }
152
167
  }
153
168
 
154
169
  @keyframes glow {
155
- 0% { box-shadow: 0 0 5px oklch(0.769 0.132 191.7 / 0.2); }
156
- 100% { box-shadow: 0 0 20px oklch(0.769 0.132 191.7 / 0.6); }
170
+ 0% { box-shadow: 0 0 5px oklch(from var(--role-accent) l c h / 0.2); }
171
+ 100% { box-shadow: 0 0 20px oklch(from var(--role-accent) l c h / 0.6); }
157
172
  }
158
173
 
159
174
  @keyframes neon-pulse {
160
- 0%, 100% { text-shadow: 0 0 10px oklch(0.769 0.132 191.7 / 0.3); }
161
- 50% { text-shadow: 0 0 20px oklch(0.769 0.132 191.7 / 0.8); }
175
+ 0%, 100% { text-shadow: 0 0 10px oklch(from var(--role-accent) l c h / 0.3); }
176
+ 50% { text-shadow: 0 0 20px oklch(from var(--role-accent) l c h / 0.8); }
162
177
  }
163
178
 
164
179
  @keyframes float {
165
180
  0%, 100% { transform: translateY(0px); }
166
- 50% { transform: translateY(-10px); }
181
+ 50% { transform: translateY(-10px); }
167
182
  }
168
183
 
169
184
  @keyframes shimmer {
@@ -173,7 +188,7 @@
173
188
 
174
189
  @keyframes typing {
175
190
  0%, 60% { opacity: 1; }
176
- 30% { opacity: 0.4; }
191
+ 30% { opacity: 0.4; }
177
192
  }
178
193
 
179
194
  @keyframes shine {
@@ -1,150 +0,0 @@
1
- /**
2
- * @bsuite/theme — Tailwind v3 preset (oklch edition)
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
- * 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.
24
- *
25
- * NOTE: Tailwind v4 apps should import `@bsuite/theme/preset-v4.css` instead.
26
- * v4 removed the preset system — presets exist only for v3.
27
- */
28
-
29
- /** @type {import('tailwindcss').Config} */
30
- module.exports = {
31
- darkMode: ['class'],
32
- theme: {
33
- extend: {
34
- colors: {
35
- neon: {
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>)',
47
- },
48
- light: {
49
- bg: {
50
- primary: 'var(--light-bg-primary)',
51
- secondary: 'var(--light-bg-secondary)',
52
- tertiary: 'var(--light-bg-tertiary)',
53
- accent: 'var(--light-bg-accent)',
54
- },
55
- text: {
56
- primary: 'var(--light-text-primary)',
57
- secondary: 'var(--light-text-secondary)',
58
- tertiary: 'var(--light-text-tertiary)',
59
- quaternary: 'var(--light-text-quaternary)',
60
- },
61
- border: 'var(--light-border)',
62
- hover: 'var(--light-hover)',
63
- },
64
- dark: {
65
- bg: {
66
- primary: 'var(--dark-bg-primary)',
67
- secondary: 'var(--dark-bg-secondary)',
68
- tertiary: 'var(--dark-bg-tertiary)',
69
- quaternary: 'var(--dark-bg-quaternary)',
70
- accent: 'var(--dark-bg-accent)',
71
- },
72
- text: {
73
- primary: 'var(--dark-text-primary)',
74
- secondary: 'var(--dark-text-secondary)',
75
- tertiary: 'var(--dark-text-tertiary)',
76
- quaternary: 'var(--dark-text-quaternary)',
77
- },
78
- border: 'var(--dark-border)',
79
- hover: 'var(--dark-hover)',
80
- },
81
- status: {
82
- success: 'var(--status-success)',
83
- warning: 'var(--status-warning)',
84
- error: 'var(--status-error)',
85
- info: 'var(--status-info)',
86
- },
87
- },
88
-
89
- backgroundImage: {
90
- 'gradient-brand':
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%)',
92
- 'gradient-electric':
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%)',
94
- 'gradient-neon':
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%)',
96
- 'gradient-chat-user':
97
- 'linear-gradient(135deg, oklch(0.636 0.153 271.3) 0%, oklch(0.466 0.168 296.3) 100%)',
98
- 'gradient-chat-agent':
99
- 'linear-gradient(135deg, oklch(0.546 0.215 262.9) 0%, oklch(0.656 0.212 354.3) 100%)',
100
- 'gradient-neural':
101
- 'radial-gradient(circle at center, oklch(0.769 0.132 191.7 / 0.1) 0%, transparent 50%)',
102
- },
103
-
104
- boxShadow: {
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))',
111
- },
112
-
113
- animation: {
114
- 'pulse-soft': 'pulse-soft 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
115
- glow: 'glow 2s ease-in-out infinite alternate',
116
- typing: 'typing 1.5s infinite',
117
- shimmer: 'shimmer 2s infinite',
118
- float: 'float 3s ease-in-out infinite',
119
- 'neon-pulse': 'neon-pulse 2s ease-in-out infinite',
120
- },
121
-
122
- keyframes: {
123
- 'pulse-soft': {
124
- '0%, 100%': { opacity: '1' },
125
- '50%': { opacity: '0.7' },
126
- },
127
- glow: {
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)' },
130
- },
131
- typing: {
132
- '0%, 60%': { opacity: '1' },
133
- '30%': { opacity: '0.4' },
134
- },
135
- shimmer: {
136
- '0%': { backgroundPosition: '-1000px 0' },
137
- '100%': { backgroundPosition: '1000px 0' },
138
- },
139
- float: {
140
- '0%, 100%': { transform: 'translateY(0px)' },
141
- '50%': { transform: 'translateY(-10px)' },
142
- },
143
- 'neon-pulse': {
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)' },
146
- },
147
- },
148
- },
149
- },
150
- }