@bsuite/ui 0.1.0 → 0.3.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 CHANGED
@@ -30,6 +30,113 @@ import { DotPattern } from '@bsuite/ui'
30
30
  Colour control via Tailwind `text-*` utilities or inline `[color:var(...)]`
31
31
  (dots use `fill="currentColor"`).
32
32
 
33
+ ### `Logo`
34
+
35
+ Slot-aware, theme-aware, branding-aware logo component for all BSuite apps.
36
+
37
+ ```tsx
38
+ import { Logo } from '@bsuite/ui'
39
+
40
+ <Logo slot="header" colorScheme="auto" />
41
+ ```
42
+
43
+ ### `D2CDefaultLogo`
44
+
45
+ Inline D2C Neon Electric SVG logo (no asset dependency).
46
+
47
+ ```tsx
48
+ import { D2CDefaultLogo } from '@bsuite/ui'
49
+
50
+ <D2CDefaultLogo className="h-8 w-auto" />
51
+ ```
52
+
53
+ ## Branding Components (v0.3.0+)
54
+
55
+ ### `OklchColorPicker`
56
+
57
+ A color input component for OKLCH color format (BSuite standard).
58
+
59
+ ```tsx
60
+ import { OklchColorPicker } from '@bsuite/ui'
61
+
62
+ <OklchColorPicker
63
+ value="oklch(0.546 0.215 262.9)"
64
+ onChange={(value) => console.log(value)}
65
+ label="Primary Color"
66
+ description="Your brand's primary color in OKLCH format"
67
+ />
68
+ ```
69
+
70
+ **Features:**
71
+ - Live color preview
72
+ - Input fields for L (lightness), C (chroma), H (hue)
73
+ - Slider controls for easy adjustment
74
+ - Validation of OKLCH format
75
+ - Accessible labels and error messages
76
+
77
+ ### `ColorEditorSheet`
78
+
79
+ A sheet/dialog for editing branding colors with a slot-based color editor.
80
+
81
+ ```tsx
82
+ import { ColorEditorSheet, OklchColorPicker } from '@bsuite/ui'
83
+
84
+ <ColorEditorSheet
85
+ open={isOpen}
86
+ onOpenChange={setIsOpen}
87
+ title="Edit Primary Color"
88
+ description="Adjust the OKLCH color values"
89
+ value="oklch(0.546 0.215 262.9)"
90
+ onSave={(value) => updateBranding('primary', value)}
91
+ renderEditor={(props) => <OklchColorPicker {...props} />}
92
+ />
93
+ ```
94
+
95
+ **Features:**
96
+ - Slide-out sheet interface
97
+ - Slot-based color editor (inject your own picker)
98
+ - Save/Cancel actions
99
+ - Responsive design
100
+ - Accessible dialog implementation
101
+
102
+ ### `BrandingCard`
103
+
104
+ A card component for displaying and editing tenant branding configuration.
105
+
106
+ ```tsx
107
+ import { BrandingCard, OklchColorPicker } from '@bsuite/ui'
108
+
109
+ <BrandingCard
110
+ branding={{
111
+ primary: "oklch(0.546 0.215 262.9)",
112
+ accent: "oklch(0.769 0.132 191.7)",
113
+ logo_url: "https://example.com/logo.svg",
114
+ company_name: "Acme Corp"
115
+ }}
116
+ onUpdate={async (key, value) => {
117
+ await updateTenantBranding(key, value)
118
+ }}
119
+ renderColorEditor={(props) => <OklchColorPicker {...props} />}
120
+ />
121
+ ```
122
+
123
+ **Features:**
124
+ - Display current branding values (colors, logos, company name)
125
+ - Edit buttons for each branding property
126
+ - Integrated color editing with ColorEditorSheet
127
+ - Image preview for logos
128
+ - Accessible and responsive design
129
+ - Support for light/dark mode logo variants
130
+
131
+ **Branding Properties:**
132
+ - `primary` - Primary action color (OKLCH)
133
+ - `accent` - Accent/secondary color (OKLCH)
134
+ - `logo_url` - Main logo URL
135
+ - `logo_light_url` - Light mode logo variant (optional)
136
+ - `logo_dark_url` - Dark mode logo variant (optional)
137
+ - `favicon_url` - Favicon URL (16x16 or 32x32)
138
+ - `company_name` - Company name for alt text
139
+
33
140
  ## Utils
34
141
 
35
142
  ### `cn(...classes)`
@@ -38,6 +145,27 @@ shadcn-style className merger — `clsx` + `tailwind-merge`. Available for
38
145
  consumers that want to compose `@bsuite/ui` components with local overrides
39
146
  without pulling shadcn again.
40
147
 
148
+ ## Module Exports
149
+
150
+ The package provides subpath exports for better tree-shaking:
151
+
152
+ ```tsx
153
+ // Main exports
154
+ import { Logo, BrandingCard } from '@bsuite/ui'
155
+
156
+ // Dot pattern only
157
+ import { DotPattern } from '@bsuite/ui/dot-pattern'
158
+
159
+ // Utils only
160
+ import { cn } from '@bsuite/ui/utils'
161
+
162
+ // Branding components only
163
+ import { BrandingCard, ColorEditorSheet, OklchColorPicker } from '@bsuite/ui/branding'
164
+
165
+ // SVG asset
166
+ import logoSvg from '@bsuite/ui/assets/d2c-default-logo.svg'
167
+ ```
168
+
41
169
  ## Releases
42
170
 
43
171
  1. Edit source under `src/`.
package/dist/Logo.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ export type LogoSlot = 'favicon' | 'sidebar' | 'auth' | 'marketing' | 'header';
2
+ export type LogoColorScheme = 'light' | 'dark' | 'auto';
3
+ export type AppSlug = 'crm7' | 'bsu' | 'conduit' | 'r80' | 'throughput' | 'braden';
4
+ /**
5
+ * Shape consumed by the resolver. Each field corresponds to a branding tier;
6
+ * `null` / `undefined` signals "not set at this tier — fall through".
7
+ *
8
+ * The Tier-0 sub-org walk happens server-side in
9
+ * `branding_json_for_tenant()` so by the time the client sees these values
10
+ * the parent_tenant_id chain has already collapsed into Tier 2/3 fields.
11
+ * The dedicated `app_*` fields keep Tier 3 (per-app override) distinct from
12
+ * Tier 2 (per-tenant) so the resolver can express the four-step precedence.
13
+ */
14
+ export interface LogoBranding {
15
+ /** App-specific override logo URL (any scheme). */
16
+ app_logo_url?: string | null;
17
+ /** App-specific override logo URL for light mode. */
18
+ app_logo_light_url?: string | null;
19
+ /** App-specific override logo URL for dark mode. */
20
+ app_logo_dark_url?: string | null;
21
+ /** Tenant-level logo URL (any scheme). */
22
+ logo_url?: string | null;
23
+ /** Tenant-level logo URL for light mode. */
24
+ logo_light_url?: string | null;
25
+ /** Tenant-level logo URL for dark mode. */
26
+ logo_dark_url?: string | null;
27
+ /** Tenant-level favicon URL (square mark). */
28
+ favicon_url?: string | null;
29
+ /** Tenant-level mark / icon (square). */
30
+ mark_url?: string | null;
31
+ /** Platform default logo URL (any scheme). */
32
+ platform_logo_url?: string | null;
33
+ /** Platform default light-mode logo URL. */
34
+ platform_logo_light_url?: string | null;
35
+ /** Platform default dark-mode logo URL. */
36
+ platform_logo_dark_url?: string | null;
37
+ /** Platform default mark/icon URL. */
38
+ platform_mark_url?: string | null;
39
+ /** Platform default favicon URL. */
40
+ platform_favicon_url?: string | null;
41
+ /** Display name used as alt-text fallback when no `alt` prop is supplied. */
42
+ company_name?: string | null;
43
+ }
44
+ export interface ResolvedLogo {
45
+ /** Final URL, or `null` if every tier produced no candidate. */
46
+ src: string | null;
47
+ /** Which tier produced the URL — `'default'` means the SVG fallback. */
48
+ tier: 'app' | 'tenant' | 'platform' | 'default';
49
+ /** Resolved colour scheme (`'auto'` collapsed to concrete value). */
50
+ scheme: 'light' | 'dark';
51
+ }
52
+ export interface LogoProps {
53
+ /** Layout slot. Drives default dimensions. */
54
+ slot?: LogoSlot;
55
+ /** Colour scheme. `'auto'` reads `<html class="dark">` at render time. */
56
+ colorScheme?: LogoColorScheme;
57
+ /** Optional className for the rendered element. */
58
+ className?: string;
59
+ /** Image alt text. Defaults to `branding.company_name ?? 'BSuite'`. */
60
+ alt?: string;
61
+ /** Explicit width override (px). */
62
+ width?: number;
63
+ /** Explicit height override (px). */
64
+ height?: number;
65
+ /**
66
+ * Fully-resolved 4-tier branding shape. Pass `null` / `undefined` to render
67
+ * the default SVG mark. Consumers typically wire this from their app-level
68
+ * `useBranding({tenantId, appSlug})` hook.
69
+ */
70
+ branding?: LogoBranding | null;
71
+ /**
72
+ * Optional app slug — used as a tie-breaker when picking a Tier-1 platform
73
+ * default that may have app-specific entries in the future. Currently
74
+ * informational only (kept for forward-compat with bsuite#320 sub-org work).
75
+ */
76
+ appSlug?: AppSlug;
77
+ }
78
+ /**
79
+ * Resolve the final logo URL across all four tiers.
80
+ *
81
+ * Pure function. Stable, deterministic, and tested for 100% branch coverage.
82
+ *
83
+ * @param branding — merged 4-tier branding shape (server-resolved sub-org chain).
84
+ * @param slot — layout slot. `favicon` prefers mark/favicon fields.
85
+ * @param scheme — concrete colour scheme (callers should pre-resolve `'auto'`).
86
+ */
87
+ export declare function resolveLogoUrl(branding: LogoBranding | null | undefined, slot: LogoSlot, scheme: 'light' | 'dark'): ResolvedLogo;
88
+ /**
89
+ * Slot-aware, theme-aware, branding-aware logo component.
90
+ *
91
+ * Renders an `<img>` for resolved URLs and the inline D2C default SVG when
92
+ * no URL is available at any tier. Memoised because the same component
93
+ * frequently mounts in app shells (header, sidebar, auth gate) and the
94
+ * resolver inputs are stable.
95
+ */
96
+ export declare const Logo: import("react").MemoExoticComponent<({ slot, colorScheme, className, alt, width, height, branding, appSlug, }: LogoProps) => import("react/jsx-runtime").JSX.Element>;
97
+ //# sourceMappingURL=Logo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Logo.d.ts","sourceRoot":"","sources":["../src/Logo.tsx"],"names":[],"mappings":"AAgCA,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAA;AAC9E,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,QAAQ,CAAA;AAElF;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAE3B,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,qDAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAGjC,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAGxB,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,4CAA4C;IAC5C,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,2CAA2C;IAC3C,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oCAAoC;IACpC,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAGpC,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,wEAAwE;IACxE,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAA;IAC/C,qEAAqE;IACrE,MAAM,EAAE,OAAO,GAAG,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,0EAA0E;IAC1E,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,EACzC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,OAAO,GAAG,MAAM,GACvB,YAAY,CA2Cd;AAaD;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,iHASd,SAAS,6CAuCV,CAAA"}
package/dist/Logo.js ADDED
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * <Logo /> — slot-aware, theme-aware, branding-aware logo for all BSuite apps.
4
+ *
5
+ * Resolves the rendered image URL across **four tiers**, in order:
6
+ *
7
+ * Tier 0 sub-org — server-side walks parent_tenant_id chain via the
8
+ * `branding_json_for_tenant()` SECURITY DEFINER RPC,
9
+ * so by the time the value arrives client-side it has
10
+ * already been resolved through the sub-org chain
11
+ * and surfaces in the `app_*` / `logo_*` fields below.
12
+ * Tier 3 app override — `app_logo_url` / `app_logo_light_url` /
13
+ * `app_logo_dark_url` from `tenant_app_branding`
14
+ * (highest precedence when supplied).
15
+ * Tier 2 enterprise — `logo_url` / `logo_light_url` / `logo_dark_url`
16
+ * from `tenant_branding`.
17
+ * Tier 1 platform — `platform_logo_url` / variants from
18
+ * `platform_branding` (D2C defaults).
19
+ * Default — Inline D2C Neon Electric SVG (no asset dependency).
20
+ *
21
+ * The resolver is a pure function (`resolveLogoUrl`) — unit-tested for 100%
22
+ * branch coverage. The component is a thin shell that calls the resolver and
23
+ * picks the SVG default when the URL is null.
24
+ *
25
+ * To keep `@bsuite/ui` portable (it must work in BSU, CRM7, conduit, R80.3,
26
+ * throughput AND braden, which all have different branding hooks), the
27
+ * component itself does NOT call any `useBranding` hook. It reads from props.
28
+ * Each consumer wires its own hook in a thin wrapper (BSU's `BSULogo.tsx`).
29
+ */
30
+ import { memo } from 'react';
31
+ import { D2CDefaultLogo } from './default-logo.js';
32
+ import { cn } from './utils.js';
33
+ /** Default dimensions per slot. Matches BSU's existing BSULogo contract. */
34
+ const SLOT_SIZES = {
35
+ favicon: { width: 32, height: 32 },
36
+ sidebar: { width: 112, height: 28 },
37
+ auth: { width: 160, height: 40 },
38
+ marketing: { width: 200, height: 50 },
39
+ header: { width: 120, height: 30 },
40
+ };
41
+ function pickFirstString(...candidates) {
42
+ for (const value of candidates) {
43
+ if (typeof value === 'string') {
44
+ const trimmed = value.trim();
45
+ if (trimmed.length > 0)
46
+ return trimmed;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Resolve the final logo URL across all four tiers.
53
+ *
54
+ * Pure function. Stable, deterministic, and tested for 100% branch coverage.
55
+ *
56
+ * @param branding — merged 4-tier branding shape (server-resolved sub-org chain).
57
+ * @param slot — layout slot. `favicon` prefers mark/favicon fields.
58
+ * @param scheme — concrete colour scheme (callers should pre-resolve `'auto'`).
59
+ */
60
+ export function resolveLogoUrl(branding, slot, scheme) {
61
+ if (!branding) {
62
+ return { src: null, tier: 'default', scheme };
63
+ }
64
+ // Favicon slot has its own field family — mark/favicon, not the wordmark.
65
+ if (slot === 'favicon') {
66
+ const appTier = pickFirstString(branding.app_logo_url);
67
+ if (appTier)
68
+ return { src: appTier, tier: 'app', scheme };
69
+ const tenantTier = pickFirstString(branding.favicon_url, branding.mark_url, branding.logo_url);
70
+ if (tenantTier)
71
+ return { src: tenantTier, tier: 'tenant', scheme };
72
+ const platformTier = pickFirstString(branding.platform_favicon_url, branding.platform_mark_url, branding.platform_logo_url);
73
+ if (platformTier)
74
+ return { src: platformTier, tier: 'platform', scheme };
75
+ return { src: null, tier: 'default', scheme };
76
+ }
77
+ // Wordmark slots: prefer scheme-specific, then scheme-neutral, at each tier.
78
+ const appCandidates = scheme === 'dark'
79
+ ? [branding.app_logo_dark_url, branding.app_logo_url, branding.app_logo_light_url]
80
+ : [branding.app_logo_light_url, branding.app_logo_url, branding.app_logo_dark_url];
81
+ const appTier = pickFirstString(...appCandidates);
82
+ if (appTier)
83
+ return { src: appTier, tier: 'app', scheme };
84
+ const tenantCandidates = scheme === 'dark'
85
+ ? [branding.logo_dark_url, branding.logo_url, branding.logo_light_url]
86
+ : [branding.logo_light_url, branding.logo_url, branding.logo_dark_url];
87
+ const tenantTier = pickFirstString(...tenantCandidates);
88
+ if (tenantTier)
89
+ return { src: tenantTier, tier: 'tenant', scheme };
90
+ const platformCandidates = scheme === 'dark'
91
+ ? [branding.platform_logo_dark_url, branding.platform_logo_url, branding.platform_logo_light_url]
92
+ : [branding.platform_logo_light_url, branding.platform_logo_url, branding.platform_logo_dark_url];
93
+ const platformTier = pickFirstString(...platformCandidates);
94
+ if (platformTier)
95
+ return { src: platformTier, tier: 'platform', scheme };
96
+ return { src: null, tier: 'default', scheme };
97
+ }
98
+ /**
99
+ * Collapse the `'auto'` colour scheme to a concrete value by reading the
100
+ * `dark` class on `<html>`. SSR-safe: returns `'light'` when document is
101
+ * undefined.
102
+ */
103
+ function resolveAutoScheme(scheme) {
104
+ if (scheme !== 'auto')
105
+ return scheme;
106
+ if (typeof document === 'undefined')
107
+ return 'light';
108
+ return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
109
+ }
110
+ /**
111
+ * Slot-aware, theme-aware, branding-aware logo component.
112
+ *
113
+ * Renders an `<img>` for resolved URLs and the inline D2C default SVG when
114
+ * no URL is available at any tier. Memoised because the same component
115
+ * frequently mounts in app shells (header, sidebar, auth gate) and the
116
+ * resolver inputs are stable.
117
+ */
118
+ export const Logo = memo(function Logo({ slot = 'sidebar', colorScheme = 'auto', className, alt, width, height, branding, appSlug, }) {
119
+ const resolvedScheme = resolveAutoScheme(colorScheme);
120
+ const { src, tier } = resolveLogoUrl(branding, slot, resolvedScheme);
121
+ const dims = SLOT_SIZES[slot];
122
+ const finalWidth = width ?? dims.width;
123
+ const finalHeight = height ?? dims.height;
124
+ const finalAlt = alt ?? (typeof branding?.company_name === 'string' ? branding.company_name : 'BSuite');
125
+ if (src) {
126
+ return (_jsx("img", { src: src, alt: finalAlt, width: finalWidth, height: finalHeight, className: cn('inline-block object-contain', className), style: { width: finalWidth, height: finalHeight }, loading: "eager", "data-slot": slot, "data-tier": tier, "data-scheme": resolvedScheme, "data-app": appSlug }));
127
+ }
128
+ return (_jsx(D2CDefaultLogo, { title: finalAlt, width: finalWidth, height: finalHeight, className: cn('inline-block', className), "data-slot": slot, "data-tier": tier, "data-scheme": resolvedScheme, "data-app": appSlug }));
129
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * BrandingCard — A card component for displaying and editing branding settings
3
+ *
4
+ * This component provides a UI for viewing and editing tenant branding
5
+ * configuration, including colors, logos, and other brand properties.
6
+ *
7
+ * Features:
8
+ * - Display current branding values
9
+ * - Edit buttons for each branding property
10
+ * - Integration with ColorEditorSheet for color editing
11
+ * - Accessible and responsive design
12
+ * - Supports OKLCH color format
13
+ *
14
+ * Usage:
15
+ * <BrandingCard
16
+ * branding={{
17
+ * primary: "oklch(0.546 0.215 262.9)",
18
+ * accent: "oklch(0.769 0.132 191.7)",
19
+ * logo_url: "https://example.com/logo.svg"
20
+ * }}
21
+ * onUpdate={(key, value) => console.log('Updated:', key, value)}
22
+ * renderColorEditor={(props) => <OklchColorPicker {...props} />}
23
+ * />
24
+ */
25
+ import { type ReactNode } from 'react';
26
+ export interface BrandingCardProps {
27
+ /** Current branding configuration */
28
+ branding: {
29
+ primary?: string;
30
+ accent?: string;
31
+ logo_url?: string;
32
+ logo_light_url?: string;
33
+ logo_dark_url?: string;
34
+ favicon_url?: string;
35
+ company_name?: string;
36
+ font_stack?: string | null;
37
+ };
38
+ /** Callback when a branding property is updated */
39
+ onUpdate: (key: string, value: string) => void | Promise<void>;
40
+ /** Render function for the color editor component */
41
+ renderColorEditor: (props: {
42
+ value: string;
43
+ onChange: (value: string) => void;
44
+ disabled?: boolean;
45
+ }) => ReactNode;
46
+ /** Optional className for the card */
47
+ className?: string;
48
+ /** Whether editing is disabled */
49
+ disabled?: boolean;
50
+ /** Optional title for the card */
51
+ title?: string;
52
+ /** Optional description for the card */
53
+ description?: string;
54
+ }
55
+ export declare function BrandingCard({ branding, onUpdate, renderColorEditor, className, disabled, title, description, }: BrandingCardProps): import("react/jsx-runtime").JSX.Element;
56
+ //# sourceMappingURL=BrandingCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrandingCard.d.ts","sourceRoot":"","sources":["../../src/branding/BrandingCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGhD,MAAM,WAAW,iBAAiB;IAChC,qCAAqC;IACrC,QAAQ,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,CAAA;IACD,mDAAmD;IACnD,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9D,qDAAqD;IACrD,iBAAiB,EAAE,CAAC,KAAK,EAAE;QACzB,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;QACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,KAAK,SAAS,CAAA;IACf,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,QAAgB,EAChB,KAAwB,EACxB,WAAW,GACZ,EAAE,iBAAiB,2CA8QnB"}
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * BrandingCard — A card component for displaying and editing branding settings
4
+ *
5
+ * This component provides a UI for viewing and editing tenant branding
6
+ * configuration, including colors, logos, and other brand properties.
7
+ *
8
+ * Features:
9
+ * - Display current branding values
10
+ * - Edit buttons for each branding property
11
+ * - Integration with ColorEditorSheet for color editing
12
+ * - Accessible and responsive design
13
+ * - Supports OKLCH color format
14
+ *
15
+ * Usage:
16
+ * <BrandingCard
17
+ * branding={{
18
+ * primary: "oklch(0.546 0.215 262.9)",
19
+ * accent: "oklch(0.769 0.132 191.7)",
20
+ * logo_url: "https://example.com/logo.svg"
21
+ * }}
22
+ * onUpdate={(key, value) => console.log('Updated:', key, value)}
23
+ * renderColorEditor={(props) => <OklchColorPicker {...props} />}
24
+ * />
25
+ */
26
+ import { useState } from 'react';
27
+ import { cn } from '../utils.js';
28
+ export function BrandingCard({ branding, onUpdate, renderColorEditor, className, disabled = false, title = 'Brand Settings', description, }) {
29
+ const [colorEditor, setColorEditor] = useState({
30
+ open: false,
31
+ field: '',
32
+ label: '',
33
+ value: '',
34
+ });
35
+ const openColorEditor = (field, label, currentValue) => {
36
+ setColorEditor({
37
+ open: true,
38
+ field,
39
+ label,
40
+ value: currentValue || '',
41
+ });
42
+ };
43
+ const closeColorEditor = () => {
44
+ setColorEditor(prev => ({ ...prev, open: false }));
45
+ };
46
+ const handleColorSave = async (value) => {
47
+ await onUpdate(colorEditor.field, value);
48
+ closeColorEditor();
49
+ };
50
+ const handleTextUpdate = async (field, value) => {
51
+ await onUpdate(field, value);
52
+ };
53
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn('rounded-lg border border-input bg-card text-card-foreground shadow-sm', className), children: [_jsxs("div", { className: "border-b border-border px-6 py-4", children: [_jsx("h3", { className: "text-lg font-semibold", children: title }), description && (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description }))] }), _jsxs("div", { className: "divide-y divide-border", children: [_jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "h-10 w-10 rounded border border-input shadow-sm", style: { backgroundColor: branding.primary || 'oklch(0.546 0.215 262.9)' }, "aria-label": "Primary color preview" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Primary Color" }), _jsx("div", { className: "text-sm text-muted-foreground", children: branding.primary || 'Not set' })] })] }), _jsx("button", { type: "button", onClick: () => openColorEditor('primary', 'Primary Color', branding.primary || 'oklch(0.546 0.215 262.9)'), disabled: disabled, className: cn('rounded-md border border-input bg-background px-4 py-2 text-sm font-medium', 'hover:bg-accent hover:text-accent-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50'), children: "Edit" })] }), _jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "h-10 w-10 rounded border border-input shadow-sm", style: { backgroundColor: branding.accent || 'oklch(0.769 0.132 191.7)' }, "aria-label": "Accent color preview" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: "Accent Color" }), _jsx("div", { className: "text-sm text-muted-foreground", children: branding.accent || 'Not set' })] })] }), _jsx("button", { type: "button", onClick: () => openColorEditor('accent', 'Accent Color', branding.accent || 'oklch(0.769 0.132 191.7)'), disabled: disabled, className: cn('rounded-md border border-input bg-background px-4 py-2 text-sm font-medium', 'hover:bg-accent hover:text-accent-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50'), children: "Edit" })] }), _jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [_jsx("label", { htmlFor: "company-name", className: "font-medium", children: "Company Name" }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("input", { id: "company-name", type: "text", value: branding.company_name || '', onChange: (e) => handleTextUpdate('company_name', e.target.value), disabled: disabled, placeholder: "Enter company name", className: cn('flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50') }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Used in logo alt text and other branding contexts" })] }), _jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [_jsx("label", { htmlFor: "logo-url", className: "font-medium", children: "Logo URL" }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("input", { id: "logo-url", type: "url", value: branding.logo_url || '', onChange: (e) => handleTextUpdate('logo_url', e.target.value), disabled: disabled, placeholder: "https://example.com/logo.svg", className: cn('flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50') }) }), branding.logo_url && (_jsx("div", { className: "mt-2 flex items-center gap-2", children: _jsx("img", { src: branding.logo_url, alt: "Logo preview", className: "h-12 object-contain", onError: (e) => {
54
+ e.currentTarget.style.display = 'none';
55
+ } }) }))] }), _jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [_jsx("label", { htmlFor: "logo-light-url", className: "font-medium", children: "Logo URL (Light Mode)" }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("input", { id: "logo-light-url", type: "url", value: branding.logo_light_url || '', onChange: (e) => handleTextUpdate('logo_light_url', e.target.value), disabled: disabled, placeholder: "https://example.com/logo-light.svg", className: cn('flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50') }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Optional: Use a different logo for light mode" })] }), _jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [_jsx("label", { htmlFor: "logo-dark-url", className: "font-medium", children: "Logo URL (Dark Mode)" }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("input", { id: "logo-dark-url", type: "url", value: branding.logo_dark_url || '', onChange: (e) => handleTextUpdate('logo_dark_url', e.target.value), disabled: disabled, placeholder: "https://example.com/logo-dark.svg", className: cn('flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50') }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Optional: Use a different logo for dark mode" })] }), _jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [_jsx("label", { htmlFor: "favicon-url", className: "font-medium", children: "Favicon URL" }), _jsx("div", { className: "flex items-center gap-2", children: _jsx("input", { id: "favicon-url", type: "url", value: branding.favicon_url || '', onChange: (e) => handleTextUpdate('favicon_url', e.target.value), disabled: disabled, placeholder: "https://example.com/favicon.ico", className: cn('flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50') }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Square icon shown in browser tabs (16x16 or 32x32)" })] })] })] }), colorEditor.open && (_jsx(ColorEditorSheetWrapper, { open: colorEditor.open, title: `Edit ${colorEditor.label}`, description: "Adjust the OKLCH color values using the sliders below", value: colorEditor.value, onSave: handleColorSave, onCancel: closeColorEditor, renderEditor: renderColorEditor }))] }));
56
+ }
57
+ function ColorEditorSheetWrapper({ open, title, description, value, onSave, onCancel, renderEditor, }) {
58
+ const [editValue, setEditValue] = useState(value);
59
+ const [hasChanges, setHasChanges] = useState(false);
60
+ const handleChange = (newValue) => {
61
+ setEditValue(newValue);
62
+ setHasChanges(newValue !== value);
63
+ };
64
+ const handleSave = () => {
65
+ onSave(editValue);
66
+ };
67
+ if (!open)
68
+ return null;
69
+ return (_jsxs("div", { className: "fixed inset-0 z-50 flex", role: "dialog", "aria-modal": "true", "aria-labelledby": "color-editor-title", "aria-describedby": "color-editor-description", children: [_jsx("div", { className: "fixed inset-0 bg-background/80 backdrop-blur-sm", onClick: onCancel, "aria-hidden": "true" }), _jsxs("div", { className: "fixed inset-y-0 right-0 z-50 flex w-full flex-col border-l bg-background shadow-lg sm:max-w-md", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-6 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h2", { id: "color-editor-title", className: "text-lg font-semibold text-foreground", children: title }), _jsx("p", { id: "color-editor-description", className: "text-sm text-muted-foreground", children: description })] }), _jsx("button", { type: "button", onClick: onCancel, className: cn('rounded-sm opacity-70 ring-offset-background transition-opacity', 'hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'), "aria-label": "Close", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }) })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-6 py-6", children: renderEditor({
70
+ value: editValue,
71
+ onChange: handleChange,
72
+ }) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t px-6 py-4", children: [_jsx("button", { type: "button", onClick: onCancel, className: cn('inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium', 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'), children: "Cancel" }), _jsx("button", { type: "button", onClick: handleSave, disabled: !hasChanges, className: cn('inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-text-on-primary hover:bg-primary/90', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50'), children: "Save Changes" })] })] })] }));
73
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ColorEditorSheet — A sheet/dialog for editing branding colors
3
+ *
4
+ * This component provides a slide-out sheet interface for editing
5
+ * color values. It uses a slot-based design for the color editor
6
+ * to allow consumers to inject their own color picker implementation
7
+ * (e.g., OklchColorPicker, or a custom one).
8
+ *
9
+ * Features:
10
+ * - Slot-based color editor (renderEditor prop)
11
+ * - Save/Cancel actions
12
+ * - Accessible dialog implementation
13
+ * - Responsive design
14
+ *
15
+ * Usage:
16
+ * <ColorEditorSheet
17
+ * open={isOpen}
18
+ * onOpenChange={setIsOpen}
19
+ * title="Edit Primary Color"
20
+ * value="oklch(0.546 0.215 262.9)"
21
+ * onSave={(value) => console.log('Saved:', value)}
22
+ * renderEditor={(props) => <OklchColorPicker {...props} />}
23
+ * />
24
+ *
25
+ * Note: Consumers must have shadcn Sheet component installed:
26
+ * npx shadcn@latest add sheet
27
+ */
28
+ import { type ReactNode } from 'react';
29
+ export interface ColorEditorSheetProps {
30
+ /** Whether the sheet is open */
31
+ open: boolean;
32
+ /** Callback when open state changes */
33
+ onOpenChange: (open: boolean) => void;
34
+ /** Sheet title */
35
+ title?: string;
36
+ /** Description text */
37
+ description?: string;
38
+ /** Current color value */
39
+ value: string;
40
+ /** Callback when Save is clicked */
41
+ onSave: (value: string) => void;
42
+ /** Callback when Cancel is clicked (optional, defaults to closing sheet) */
43
+ onCancel?: () => void;
44
+ /** Render function for the color editor component */
45
+ renderEditor: (props: {
46
+ value: string;
47
+ onChange: (value: string) => void;
48
+ disabled?: boolean;
49
+ }) => ReactNode;
50
+ /** Optional className for the sheet content */
51
+ className?: string;
52
+ /** Whether save button should be disabled */
53
+ disabled?: boolean;
54
+ }
55
+ /**
56
+ * ColorEditorSheet - Generic sheet for editing colors
57
+ *
58
+ * This is a "bring your own Sheet" component. Consumers must:
59
+ * 1. Have shadcn Sheet installed in their app
60
+ * 2. Pass this component to their app-specific Sheet wrapper
61
+ *
62
+ * For consumers WITHOUT shadcn Sheet, they can use this as a
63
+ * regular dialog/modal by wrapping it in their own dialog primitive.
64
+ *
65
+ * Internal implementation uses basic HTML + Tailwind for flexibility.
66
+ */
67
+ export declare function ColorEditorSheet({ open, onOpenChange, title, description, value, onSave, onCancel, renderEditor, className, disabled, }: ColorEditorSheetProps): import("react/jsx-runtime").JSX.Element | null;
68
+ //# sourceMappingURL=ColorEditorSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorEditorSheet.d.ts","sourceRoot":"","sources":["../../src/branding/ColorEditorSheet.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG3D,MAAM,WAAW,qBAAqB;IACpC,gCAAgC;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,uCAAuC;IACvC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,oCAAoC;IACpC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE;QACpB,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;QACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,KAAK,SAAS,CAAA;IACf,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,KAAoB,EACpB,WAAW,EACX,KAAK,EACL,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAgB,GACjB,EAAE,qBAAqB,kDAuIvB"}
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ColorEditorSheet — A sheet/dialog for editing branding colors
4
+ *
5
+ * This component provides a slide-out sheet interface for editing
6
+ * color values. It uses a slot-based design for the color editor
7
+ * to allow consumers to inject their own color picker implementation
8
+ * (e.g., OklchColorPicker, or a custom one).
9
+ *
10
+ * Features:
11
+ * - Slot-based color editor (renderEditor prop)
12
+ * - Save/Cancel actions
13
+ * - Accessible dialog implementation
14
+ * - Responsive design
15
+ *
16
+ * Usage:
17
+ * <ColorEditorSheet
18
+ * open={isOpen}
19
+ * onOpenChange={setIsOpen}
20
+ * title="Edit Primary Color"
21
+ * value="oklch(0.546 0.215 262.9)"
22
+ * onSave={(value) => console.log('Saved:', value)}
23
+ * renderEditor={(props) => <OklchColorPicker {...props} />}
24
+ * />
25
+ *
26
+ * Note: Consumers must have shadcn Sheet component installed:
27
+ * npx shadcn@latest add sheet
28
+ */
29
+ import { useState, useEffect } from 'react';
30
+ import { cn } from '../utils.js';
31
+ /**
32
+ * ColorEditorSheet - Generic sheet for editing colors
33
+ *
34
+ * This is a "bring your own Sheet" component. Consumers must:
35
+ * 1. Have shadcn Sheet installed in their app
36
+ * 2. Pass this component to their app-specific Sheet wrapper
37
+ *
38
+ * For consumers WITHOUT shadcn Sheet, they can use this as a
39
+ * regular dialog/modal by wrapping it in their own dialog primitive.
40
+ *
41
+ * Internal implementation uses basic HTML + Tailwind for flexibility.
42
+ */
43
+ export function ColorEditorSheet({ open, onOpenChange, title = 'Edit Color', description, value, onSave, onCancel, renderEditor, className, disabled = false, }) {
44
+ const [editValue, setEditValue] = useState(value);
45
+ const [hasChanges, setHasChanges] = useState(false);
46
+ useEffect(() => {
47
+ if (open) {
48
+ setEditValue(value);
49
+ setHasChanges(false);
50
+ }
51
+ }, [open, value]);
52
+ const handleChange = (newValue) => {
53
+ setEditValue(newValue);
54
+ setHasChanges(newValue !== value);
55
+ };
56
+ const handleSave = () => {
57
+ onSave(editValue);
58
+ onOpenChange(false);
59
+ };
60
+ const handleCancel = () => {
61
+ if (onCancel) {
62
+ onCancel();
63
+ }
64
+ onOpenChange(false);
65
+ };
66
+ if (!open)
67
+ return null;
68
+ return (_jsxs("div", { className: "fixed inset-0 z-50 flex", role: "dialog", "aria-modal": "true", "aria-labelledby": "color-editor-title", "aria-describedby": description ? 'color-editor-description' : undefined, children: [_jsx("div", { className: "fixed inset-0 bg-background/80 backdrop-blur-sm", onClick: handleCancel, "aria-hidden": "true" }), _jsxs("div", { className: "fixed inset-y-0 right-0 z-50 flex w-full flex-col border-l bg-background shadow-lg sm:max-w-md", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-6 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h2", { id: "color-editor-title", className: "text-lg font-semibold text-foreground", children: title }), description && (_jsx("p", { id: "color-editor-description", className: "text-sm text-muted-foreground", children: description }))] }), _jsx("button", { type: "button", onClick: handleCancel, className: cn('rounded-sm opacity-70 ring-offset-background transition-opacity', 'hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:pointer-events-none'), "aria-label": "Close", disabled: disabled, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }) })] }), _jsx("div", { className: cn('flex-1 overflow-y-auto px-6 py-6', className), children: renderEditor({
69
+ value: editValue,
70
+ onChange: handleChange,
71
+ disabled,
72
+ }) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t px-6 py-4", children: [_jsx("button", { type: "button", onClick: handleCancel, disabled: disabled, className: cn('inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium', 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50'), children: "Cancel" }), _jsx("button", { type: "button", onClick: handleSave, disabled: disabled || !hasChanges, className: cn('inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-text-on-primary hover:bg-primary/90', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'disabled:cursor-not-allowed disabled:opacity-50'), children: "Save Changes" })] })] })] }));
73
+ }
@@ -0,0 +1,18 @@
1
+ export interface OklchColorPickerProps {
2
+ /** Current OKLCH color value, e.g. "oklch(0.546 0.215 262.9)" */
3
+ value?: string;
4
+ /** Callback when color changes */
5
+ onChange?: (value: string) => void;
6
+ /** Label for the color picker */
7
+ label?: string;
8
+ /** Whether the input is disabled */
9
+ disabled?: boolean;
10
+ /** Optional className for the container */
11
+ className?: string;
12
+ /** Optional description/helper text */
13
+ description?: string;
14
+ /** Optional error message */
15
+ error?: string;
16
+ }
17
+ export declare function OklchColorPicker({ value, onChange, label, disabled, className, description, error, }: OklchColorPickerProps): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=OklchColorPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OklchColorPicker.d.ts","sourceRoot":"","sources":["../../src/branding/OklchColorPicker.tsx"],"names":[],"mappings":"AAyBA,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kCAAkC;IAClC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA+BD,wBAAgB,gBAAgB,CAAC,EAC/B,KAA0B,EAC1B,QAAQ,EACR,KAAK,EACL,QAAgB,EAChB,SAAS,EACT,WAAW,EACX,KAAK,GACN,EAAE,qBAAqB,2CA8LvB"}
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * OklchColorPicker — A color input component for OKLCH color format
4
+ *
5
+ * OKLCH is the BSuite standard color format for the D2C Neon Electric theme.
6
+ * This component provides a user-friendly interface for editing OKLCH colors
7
+ * with visual preview and validation.
8
+ *
9
+ * Features:
10
+ * - Live preview of the selected color
11
+ * - Input fields for L (lightness), C (chroma), H (hue)
12
+ * - Validation of OKLCH format
13
+ * - Accessible labels and error messages
14
+ *
15
+ * Usage:
16
+ * <OklchColorPicker
17
+ * value="oklch(0.546 0.215 262.9)"
18
+ * onChange={(value) => console.log(value)}
19
+ * label="Primary Color"
20
+ * />
21
+ */
22
+ import { useState, useEffect } from 'react';
23
+ import { cn } from '../utils.js';
24
+ const OKLCH_RE = /^oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+%?(?:\s*\/\s*[\d.]+%?)?\s*\)$/i;
25
+ function parseOklch(value) {
26
+ if (!value || !OKLCH_RE.test(value))
27
+ return null;
28
+ const match = value.match(/oklch\(\s*([\d.]+)%?\s+([\d.]+)%?\s+([\d.]+)%?(?:\s*\/\s*([\d.]+)%?)?\s*\)/i);
29
+ if (!match)
30
+ return null;
31
+ return {
32
+ l: parseFloat(match[1]),
33
+ c: parseFloat(match[2]),
34
+ h: parseFloat(match[3]),
35
+ alpha: match[4] ? parseFloat(match[4]) : undefined,
36
+ };
37
+ }
38
+ function formatOklch(components) {
39
+ const { l, c, h, alpha } = components;
40
+ if (alpha !== undefined && alpha !== 1) {
41
+ return `oklch(${l} ${c} ${h} / ${alpha})`;
42
+ }
43
+ return `oklch(${l} ${c} ${h})`;
44
+ }
45
+ export function OklchColorPicker({ value = 'oklch(0.5 0.1 0)', onChange, label, disabled = false, className, description, error, }) {
46
+ const parsed = parseOklch(value);
47
+ const [components, setComponents] = useState(parsed || { l: 0.5, c: 0.1, h: 0 });
48
+ const [inputValue, setInputValue] = useState(value);
49
+ const [isValid, setIsValid] = useState(true);
50
+ useEffect(() => {
51
+ const newParsed = parseOklch(value);
52
+ if (newParsed) {
53
+ setComponents(newParsed);
54
+ setInputValue(value);
55
+ setIsValid(true);
56
+ }
57
+ }, [value]);
58
+ const handleComponentChange = (key, val) => {
59
+ const numVal = parseFloat(val);
60
+ if (isNaN(numVal))
61
+ return;
62
+ const newComponents = { ...components, [key]: numVal };
63
+ setComponents(newComponents);
64
+ const formatted = formatOklch(newComponents);
65
+ setInputValue(formatted);
66
+ setIsValid(true);
67
+ onChange?.(formatted);
68
+ };
69
+ const handleRawInputChange = (e) => {
70
+ const newValue = e.target.value;
71
+ setInputValue(newValue);
72
+ const newParsed = parseOklch(newValue);
73
+ if (newParsed) {
74
+ setComponents(newParsed);
75
+ setIsValid(true);
76
+ onChange?.(newValue);
77
+ }
78
+ else {
79
+ setIsValid(false);
80
+ }
81
+ };
82
+ return (_jsxs("div", { className: cn('space-y-3', className), children: [label && (_jsx("label", { className: "text-sm font-medium text-foreground", children: label })), description && (_jsx("p", { className: "text-sm text-muted-foreground", children: description })), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-12 w-12 rounded-md border-2 border-input shadow-sm", style: { backgroundColor: inputValue }, "aria-label": "Color preview" }), _jsx("div", { className: "flex-1", children: _jsx("input", { type: "text", value: inputValue, onChange: handleRawInputChange, disabled: disabled, className: cn('w-full rounded-md border border-input bg-background px-3 py-2 text-sm', 'focus:outline-none focus:ring-2 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50', !isValid && 'border-destructive'), placeholder: "oklch(0.5 0.1 0)", "aria-label": "OKLCH color value", "aria-invalid": !isValid }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-[80px_1fr_60px] items-center gap-2", children: [_jsx("label", { htmlFor: "oklch-l", className: "text-sm text-muted-foreground", children: "L (Light)" }), _jsx("input", { id: "oklch-l", type: "range", min: "0", max: "1", step: "0.01", value: components.l, onChange: (e) => handleComponentChange('l', e.target.value), disabled: disabled, className: "cursor-pointer disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Lightness" }), _jsx("input", { type: "number", min: "0", max: "1", step: "0.01", value: components.l.toFixed(2), onChange: (e) => handleComponentChange('l', e.target.value), disabled: disabled, className: cn('w-full rounded border border-input bg-background px-2 py-1 text-sm', 'focus:outline-none focus:ring-1 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50'), "aria-label": "Lightness value" })] }), _jsxs("div", { className: "grid grid-cols-[80px_1fr_60px] items-center gap-2", children: [_jsx("label", { htmlFor: "oklch-c", className: "text-sm text-muted-foreground", children: "C (Chroma)" }), _jsx("input", { id: "oklch-c", type: "range", min: "0", max: "0.4", step: "0.001", value: components.c, onChange: (e) => handleComponentChange('c', e.target.value), disabled: disabled, className: "cursor-pointer disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Chroma" }), _jsx("input", { type: "number", min: "0", max: "0.4", step: "0.001", value: components.c.toFixed(3), onChange: (e) => handleComponentChange('c', e.target.value), disabled: disabled, className: cn('w-full rounded border border-input bg-background px-2 py-1 text-sm', 'focus:outline-none focus:ring-1 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50'), "aria-label": "Chroma value" })] }), _jsxs("div", { className: "grid grid-cols-[80px_1fr_60px] items-center gap-2", children: [_jsx("label", { htmlFor: "oklch-h", className: "text-sm text-muted-foreground", children: "H (Hue)" }), _jsx("input", { id: "oklch-h", type: "range", min: "0", max: "360", step: "0.1", value: components.h, onChange: (e) => handleComponentChange('h', e.target.value), disabled: disabled, className: "cursor-pointer disabled:cursor-not-allowed disabled:opacity-50", "aria-label": "Hue" }), _jsx("input", { type: "number", min: "0", max: "360", step: "0.1", value: components.h.toFixed(1), onChange: (e) => handleComponentChange('h', e.target.value), disabled: disabled, className: cn('w-full rounded border border-input bg-background px-2 py-1 text-sm', 'focus:outline-none focus:ring-1 focus:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50'), "aria-label": "Hue value" })] })] }), (error || !isValid) && (_jsx("p", { className: "text-sm text-destructive", children: error || 'Invalid OKLCH format. Expected: oklch(L C H) where L is 0-1, C is 0-0.4, H is 0-360' }))] }));
83
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @bsuite/ui/branding — branding components
3
+ */
4
+ export { BrandingCard, type BrandingCardProps } from './BrandingCard.js';
5
+ export { ColorEditorSheet, type ColorEditorSheetProps } from './ColorEditorSheet.js';
6
+ export { OklchColorPicker, type OklchColorPickerProps } from './OklchColorPicker.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/branding/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AACpF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @bsuite/ui/branding — branding components
3
+ */
4
+ export { BrandingCard } from './BrandingCard.js';
5
+ export { ColorEditorSheet } from './ColorEditorSheet.js';
6
+ export { OklchColorPicker } from './OklchColorPicker.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Built-in D2C Neon Electric default mark.
3
+ *
4
+ * Inlined as a React SVG component so the package ships zero asset
5
+ * dependencies — no /public/ path required at the consumer site, no
6
+ * CDN, no broken `<img src>` if the consumer forgets to copy the asset.
7
+ *
8
+ * Source values are the canonical role tokens from @bsuite/theme:
9
+ * Electric Blue oklch(0.546 0.215 262.9)
10
+ * Electric Cyan oklch(0.769 0.132 191.7)
11
+ *
12
+ * The same shape is also available as a static SVG file at
13
+ * `assets/d2c-default-logo.svg` for non-React consumers (favicons,
14
+ * email templates, marketing surfaces).
15
+ */
16
+ import type { SVGProps } from 'react';
17
+ export interface D2CDefaultLogoProps extends SVGProps<SVGSVGElement> {
18
+ /** Title for screen readers — defaults to "BSuite". */
19
+ title?: string;
20
+ }
21
+ export declare function D2CDefaultLogo({ title, width, height, role, 'aria-label': ariaLabel, ...rest }: D2CDefaultLogoProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=default-logo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-logo.d.ts","sourceRoot":"","sources":["../src/default-logo.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CAAC,aAAa,CAAC;IAClE,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAgB,EAChB,KAAU,EACV,MAAW,EACX,IAAY,EACZ,YAAY,EAAE,SAAS,EACvB,GAAG,IAAI,EACR,EAAE,mBAAmB,2CAsBrB"}
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function D2CDefaultLogo({ title = 'BSuite', width = 64, height = 64, role = 'img', 'aria-label': ariaLabel, ...rest }) {
3
+ return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 64 64", width: width, height: height, role: role, "aria-label": ariaLabel ?? title, ...rest, children: [_jsx("title", { children: title }), _jsx("defs", { children: _jsxs("linearGradient", { id: "bsuite-d2c-ring", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [_jsx("stop", { offset: "0%", stopColor: "oklch(0.546 0.215 262.9)" }), _jsx("stop", { offset: "100%", stopColor: "oklch(0.769 0.132 191.7)" })] }) }), _jsx("circle", { cx: "32", cy: "32", r: "26", fill: "none", stroke: "url(#bsuite-d2c-ring)", strokeWidth: "5" }), _jsx("circle", { cx: "32", cy: "32", r: "11", fill: "oklch(0.769 0.132 191.7)" })] }));
4
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { DotPattern, type DotPatternProps } from './dot-pattern.js';
2
2
  export { cn } from './utils.js';
3
+ export { Logo, resolveLogoUrl, type LogoProps, type LogoSlot, type LogoColorScheme, type LogoBranding, type ResolvedLogo, type AppSlug, } from './Logo.js';
4
+ export { D2CDefaultLogo, type D2CDefaultLogoProps } from './default-logo.js';
5
+ export { BrandingCard, ColorEditorSheet, OklchColorPicker, type BrandingCardProps, type ColorEditorSheetProps, type OklchColorPickerProps, } from './branding/index.js';
3
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAA;AAC/B,OAAO,EACL,IAAI,EACJ,cAAc,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,OAAO,GACb,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAG5E,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
1
  export { DotPattern } from './dot-pattern.js';
2
2
  export { cn } from './utils.js';
3
+ export { Logo, resolveLogoUrl, } from './Logo.js';
4
+ export { D2CDefaultLogo } from './default-logo.js';
5
+ // Branding components (v0.3.0+)
6
+ export { BrandingCard, ColorEditorSheet, OklchColorPicker, } from './branding/index.js';
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@bsuite/ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
6
9
  "description": "Shared UI primitives for the BSuite apps (D2C Neon Electric + Corporate brands).",
7
10
  "repository": {
8
11
  "type": "git",
@@ -10,7 +13,8 @@
10
13
  "directory": "packages/ui"
11
14
  },
12
15
  "files": [
13
- "dist"
16
+ "dist",
17
+ "src/assets"
14
18
  ],
15
19
  "main": "dist/index.js",
16
20
  "types": "dist/index.d.ts",
@@ -26,7 +30,12 @@
26
30
  "./utils": {
27
31
  "import": "./dist/utils.js",
28
32
  "types": "./dist/utils.d.ts"
29
- }
33
+ },
34
+ "./branding": {
35
+ "import": "./dist/branding/index.js",
36
+ "types": "./dist/branding/index.d.ts"
37
+ },
38
+ "./assets/d2c-default-logo.svg": "./src/assets/d2c-default-logo.svg"
30
39
  },
31
40
  "scripts": {
32
41
  "build": "tsc -p tsconfig.build.json",
@@ -35,7 +44,7 @@
35
44
  },
36
45
  "dependencies": {
37
46
  "clsx": "^2.1.1",
38
- "tailwind-merge": "^2.5.5"
47
+ "tailwind-merge": "^3.5.0"
39
48
  },
40
49
  "peerDependencies": {
41
50
  "react": "^18.3.0 || ^19.0.0",
@@ -44,13 +53,14 @@
44
53
  "devDependencies": {
45
54
  "@testing-library/jest-dom": "^6.9.1",
46
55
  "@testing-library/react": "^16.3.2",
47
- "@types/react": "^18.3.28",
48
- "@types/react-dom": "^18.3.6",
49
- "@vitejs/plugin-react": "^5.2.0",
50
- "jsdom": "^28.1.0",
51
- "react": "^18.3.1",
52
- "react-dom": "^18.3.1",
53
- "typescript": "~5.7.3",
54
- "vitest": "^3.2.4"
56
+ "@types/react": "^19.2.14",
57
+ "@types/react-dom": "^19.2.3",
58
+ "@vitejs/plugin-react": "^6.0.1",
59
+ "jsdom": "^29.1.1",
60
+ "react": "^19.2.5",
61
+ "react-dom": "^19.2.5",
62
+ "typescript": "~5.9.3",
63
+ "vite": "^8.0.10",
64
+ "vitest": "^4.1.5"
55
65
  }
56
66
  }
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="BSuite default mark">
3
+ <title>BSuite</title>
4
+ <!--
5
+ D2C Neon Electric default mark.
6
+ Electric Blue ring + Electric Cyan inner accent.
7
+ Values are the canonical role tokens from @bsuite/theme:
8
+ Electric Blue oklch(0.546 0.215 262.9)
9
+ Electric Cyan oklch(0.769 0.132 191.7)
10
+ Inline as raw SVG so the package has no runtime asset dependency.
11
+ -->
12
+ <defs>
13
+ <linearGradient id="bsuite-d2c-ring" x1="0%" y1="0%" x2="100%" y2="100%">
14
+ <stop offset="0%" stop-color="oklch(0.546 0.215 262.9)" />
15
+ <stop offset="100%" stop-color="oklch(0.769 0.132 191.7)" />
16
+ </linearGradient>
17
+ </defs>
18
+ <circle cx="32" cy="32" r="26" fill="none" stroke="url(#bsuite-d2c-ring)" stroke-width="5" />
19
+ <circle cx="32" cy="32" r="11" fill="oklch(0.769 0.132 191.7)" />
20
+ </svg>