@togo-framework/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 togo-framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @togo-framework/ui
2
+
3
+ The togo **UI kit** — the prism-style admin + auth component library extracted from the
4
+ Fort dashboard. Framework-agnostic (no Next.js / data-fetching coupling), **RTL-ready**,
5
+ dark-first, and self-hosting the **Lusail** typeface.
6
+
7
+ ```bash
8
+ npm i @togo-framework/ui lucide-react
9
+ ```
10
+
11
+ ```tsx
12
+ import "@togo-framework/ui/styles.css";
13
+ import { AdminShell, StatCard, DataTable, Button } from "@togo-framework/ui";
14
+ ```
15
+
16
+ Then copy the package's `public/fonts` into your app's served root, and add the package
17
+ to your Tailwind v4 content so its utility classes are generated:
18
+
19
+ ```css
20
+ /* app.css */ @import "tailwindcss"; @source "../node_modules/@togo-framework/ui/dist";
21
+ ```
22
+
23
+ ## Components
24
+
25
+ | Group | Components |
26
+ |---|---|
27
+ | **Layout** | `AdminShell`, `PageHeader`, `PlatformSwitcher`, `UserMenu`, `LangToggle`, `RealtimeDot`, `Toast` |
28
+ | **Data** | `StatCard`, `DataTable`, `DetailGrid` |
29
+ | **Charts** | `AreaChart`, `BarChart`, `Gauge` (dependency-free SVG) |
30
+ | **Overlays** | `Modal` |
31
+ | **Primitives** | `Button`, `Badge`, `StatusPill`, `Card`, `Input`, `SearchInput`, `Select`, `Switch`, `Checkbox`, `Field` |
32
+
33
+ All components are presentational: pass data and callbacks. RTL works by setting
34
+ `dir="rtl"` on a parent — the components use logical CSS (`ps/pe/ms/me/start/end`) and
35
+ flip automatically.
36
+
37
+ ## Develop
38
+
39
+ ```bash
40
+ npm install
41
+ npm run storybook # interactive component explorer (LTR/RTL toggle in the toolbar)
42
+ npm run build # emit dist/ (ESM + types + styles.css)
43
+ ```
44
+
45
+ MIT.
@@ -0,0 +1,211 @@
1
+ // src/theme/brand.ts
2
+ function isHSL(value) {
3
+ return /^\d+(\.\d+)?\s+\d+(\.\d+)?%\s+\d+(\.\d+)?%$/.test(value.trim());
4
+ }
5
+ function isValidColor(value) {
6
+ const t = value.trim();
7
+ return isHSL(t) || /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(t);
8
+ }
9
+ function hexToHSL(hex) {
10
+ let h = hex.replace("#", "").trim();
11
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
12
+ if (h.length !== 6) return "";
13
+ const r = parseInt(h.substring(0, 2), 16) / 255;
14
+ const g = parseInt(h.substring(2, 4), 16) / 255;
15
+ const b = parseInt(h.substring(4, 6), 16) / 255;
16
+ const max = Math.max(r, g, b);
17
+ const min = Math.min(r, g, b);
18
+ const l = (max + min) / 2;
19
+ if (max === min) return `0 0% ${Math.round(l * 100)}%`;
20
+ const d = max - min;
21
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
22
+ let hue = 0;
23
+ if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
24
+ else if (max === g) hue = ((b - r) / d + 2) / 6;
25
+ else hue = ((r - g) / d + 4) / 6;
26
+ return `${Math.round(hue * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
27
+ }
28
+ function toHSLSafe(color) {
29
+ if (!color) return null;
30
+ const t = color.trim();
31
+ if (isHSL(t)) return t;
32
+ const result = hexToHSL(t);
33
+ return result !== "" ? result : null;
34
+ }
35
+ function nudgeL(hsl, delta) {
36
+ const parts = hsl.trim().split(/\s+/);
37
+ if (parts.length < 3) return hsl;
38
+ const l = parseFloat(parts[2] ?? "0");
39
+ const newL = Math.max(0, Math.min(100, l + delta));
40
+ return `${parts[0]} ${parts[1]} ${Math.round(newL)}%`;
41
+ }
42
+ var SENTRA_BRAND = {
43
+ /** Brand primary: Sentra crimson (--primary 345 75% 33%). */
44
+ primaryHex: "#931535",
45
+ /** Brand accent: Tailwind violet-500. */
46
+ accentHex: "#daab4e",
47
+ /** Logo path — products serve this from their own /public directory. */
48
+ logoUrl: "/sentra-logo-full.png"
49
+ };
50
+ var SENTRA_PRIMARY_HSL = toHSLSafe(SENTRA_BRAND.primaryHex);
51
+ var SENTRA_ACCENT_HSL = toHSLSafe(SENTRA_BRAND.accentHex);
52
+ function applyBrand(root, tokens = {}) {
53
+ const primaryHSL = (tokens.primaryHex ? toHSLSafe(tokens.primaryHex) : null) ?? SENTRA_PRIMARY_HSL;
54
+ root.style.setProperty("--primary", primaryHSL);
55
+ root.style.setProperty("--brand-primary", primaryHSL);
56
+ root.style.setProperty("--brand-primary-glow", nudgeL(primaryHSL, 7));
57
+ root.style.setProperty("--ring", primaryHSL);
58
+ root.style.setProperty("--sidebar-primary", primaryHSL);
59
+ root.style.setProperty("--sidebar-ring", primaryHSL);
60
+ root.style.setProperty("--ai-glow", primaryHSL);
61
+ const accentHSL = (tokens.accentHex ? toHSLSafe(tokens.accentHex) : null) ?? SENTRA_ACCENT_HSL;
62
+ root.style.setProperty("--accent-muted", nudgeL(accentHSL, -13));
63
+ const rawLogo = tokens.logoUrl;
64
+ if (rawLogo && rawLogo.trim() !== "") {
65
+ root.style.setProperty("--logo-url", `url("${rawLogo}")`);
66
+ } else {
67
+ root.style.setProperty("--logo-url", "none");
68
+ }
69
+ }
70
+
71
+ // src/theme/BrandingProvider.tsx
72
+ import { createContext, useContext, useEffect } from "react";
73
+ import { jsx } from "react/jsx-runtime";
74
+ var BrandContext = createContext({
75
+ primaryHex: SENTRA_BRAND.primaryHex,
76
+ accentHex: SENTRA_BRAND.accentHex,
77
+ logoUrl: null,
78
+ iconName: null,
79
+ productName: ""
80
+ });
81
+ function useBrand() {
82
+ return useContext(BrandContext);
83
+ }
84
+ var BrandingProvider = ({
85
+ primaryHex,
86
+ accentHex,
87
+ logoUrl,
88
+ iconName,
89
+ productName,
90
+ children
91
+ }) => {
92
+ useEffect(() => {
93
+ if (typeof document === "undefined") return;
94
+ applyBrand(document.documentElement, { primaryHex, accentHex, logoUrl });
95
+ }, [primaryHex, accentHex, logoUrl]);
96
+ const contextValue = {
97
+ primaryHex: primaryHex ?? SENTRA_BRAND.primaryHex,
98
+ accentHex: accentHex ?? SENTRA_BRAND.accentHex,
99
+ // RAW logo: null when the tenant has none (empty/whitespace counts as none),
100
+ // so consumers fall back to the icon mark instead of the Sentra default PNG.
101
+ logoUrl: logoUrl && logoUrl.trim() !== "" ? logoUrl : null,
102
+ iconName: iconName ?? null,
103
+ productName: productName ?? ""
104
+ };
105
+ return /* @__PURE__ */ jsx(BrandContext.Provider, { value: contextValue, children });
106
+ };
107
+ BrandingProvider.displayName = "BrandingProvider";
108
+
109
+ // src/theme/ThemeProvider.tsx
110
+ import * as React from "react";
111
+
112
+ // src/theme/themes.ts
113
+ var themes = [
114
+ { id: "dark", label: "Dark", base: "dark" },
115
+ { id: "light", label: "Light", base: "light" }
116
+ ];
117
+ function themeBase(id) {
118
+ return themes.find((t) => t.id === id)?.base ?? "dark";
119
+ }
120
+ var STORAGE_KEY = "togo-theme";
121
+ var themeInitScript = `(function(){try{
122
+ var k=${JSON.stringify(STORAGE_KEY)};
123
+ var t=localStorage.getItem(k);
124
+ if(!t){t=window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';}
125
+ var d=document.documentElement;
126
+ d.setAttribute('data-theme',t);
127
+ d.classList.toggle('dark', t!=='light');
128
+ }catch(e){}})();`;
129
+
130
+ // src/theme/ThemeProvider.tsx
131
+ import { jsx as jsx2 } from "react/jsx-runtime";
132
+ var ThemeContext = React.createContext(null);
133
+ function applyToElement(el, theme, dir, overrides) {
134
+ el.setAttribute("data-theme", theme);
135
+ el.classList.toggle("dark", themeBase(theme) === "dark");
136
+ el.setAttribute("dir", dir);
137
+ if (overrides) for (const [k, v] of Object.entries(overrides)) el.style.setProperty(k, v);
138
+ }
139
+ function ThemeProvider({
140
+ theme: themeProp,
141
+ overrides,
142
+ dir: dirProp,
143
+ scope = "html",
144
+ themes: themes2 = themes,
145
+ persist = true,
146
+ className,
147
+ children
148
+ }) {
149
+ const [theme, setThemeState] = React.useState(themeProp ?? "dark");
150
+ const [dir, setDirState] = React.useState(dirProp ?? "ltr");
151
+ const selfRef = React.useRef(null);
152
+ React.useEffect(() => {
153
+ if (themeProp != null || !persist || typeof window === "undefined") return;
154
+ const stored = window.localStorage.getItem(STORAGE_KEY);
155
+ if (stored) setThemeState(stored);
156
+ else if (window.matchMedia?.("(prefers-color-scheme: light)").matches) setThemeState("light");
157
+ }, []);
158
+ React.useEffect(() => {
159
+ if (themeProp != null) setThemeState(themeProp);
160
+ }, [themeProp]);
161
+ React.useEffect(() => {
162
+ if (dirProp != null) setDirState(dirProp);
163
+ }, [dirProp]);
164
+ const setTheme = React.useCallback((t) => {
165
+ setThemeState(t);
166
+ if (persist && typeof window !== "undefined") window.localStorage.setItem(STORAGE_KEY, String(t));
167
+ }, [persist]);
168
+ const setDir = React.useCallback((d) => setDirState(d), []);
169
+ React.useEffect(() => {
170
+ if (typeof document === "undefined") return;
171
+ const el = scope === "self" ? selfRef.current : document.documentElement;
172
+ if (el) applyToElement(el, String(theme), dir, overrides);
173
+ }, [theme, dir, overrides, scope]);
174
+ const value = React.useMemo(() => ({ theme, setTheme, themes: themes2, dir, setDir }), [theme, setTheme, themes2, dir, setDir]);
175
+ return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: scope === "self" ? /* @__PURE__ */ jsx2(
176
+ "div",
177
+ {
178
+ ref: selfRef,
179
+ "data-theme": theme,
180
+ dir,
181
+ className: ["tg-root", themeBase(String(theme)) === "dark" ? "dark" : "", "bg-background text-foreground", className].filter(Boolean).join(" "),
182
+ style: overrides,
183
+ children
184
+ }
185
+ ) : children });
186
+ }
187
+ ThemeProvider.displayName = "ThemeProvider";
188
+ function useTheme() {
189
+ const ctx = React.useContext(ThemeContext);
190
+ if (!ctx) throw new Error("useTheme must be used within a <ThemeProvider>");
191
+ return ctx;
192
+ }
193
+
194
+ export {
195
+ isHSL,
196
+ isValidColor,
197
+ hexToHSL,
198
+ toHSLSafe,
199
+ nudgeL,
200
+ SENTRA_BRAND,
201
+ applyBrand,
202
+ useBrand,
203
+ BrandingProvider,
204
+ themes,
205
+ themeBase,
206
+ STORAGE_KEY,
207
+ themeInitScript,
208
+ ThemeProvider,
209
+ useTheme
210
+ };
211
+ //# sourceMappingURL=chunk-KD4MPGYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme/brand.ts","../src/theme/BrandingProvider.tsx","../src/theme/ThemeProvider.tsx","../src/theme/themes.ts"],"sourcesContent":["'use client'\n\n/**\n * brand.ts — Sentra dynamic theming primitives.\n *\n * Pure utilities: no React, no app context, no bridge dependency.\n * A product imports these and wires its own org-settings fetch,\n * then calls applyBrand() to override the CSS var baseline at runtime.\n *\n * CSS variables written (same set as app/src/components/BrandingProvider.tsx):\n * --primary HSL \"H S% L%\" — buttons, rings, active states\n * --brand-primary HSL \"H S% L%\" — alias kept for Situation Room glow\n * --brand-primary-glow HSL with +7 lightness — glow variant\n * --ring same as --primary\n * --sidebar-primary same as --primary\n * --sidebar-ring same as --primary\n * --ai-glow same as --primary\n * --accent-muted accent HSL with -13 lightness\n * --logo-url CSS url(\"…\") string for background-image consumers\n */\n\n// ── Colour helpers ─────────────────────────────────────────────────────────\n\n/** Returns true for the HSL string form \"H S% L%\". */\nexport function isHSL(value: string): boolean {\n return /^\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?%\\s+\\d+(\\.\\d+)?%$/.test(value.trim());\n}\n\n/** Returns true for \"#RRGGBB\" or \"#RGB\". */\nexport function isValidColor(value: string): boolean {\n const t = value.trim();\n return (\n isHSL(t) || /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(t)\n );\n}\n\n/**\n * Converts \"#RRGGBB\" or \"#RGB\" to the HSL string form \"H S% L%\".\n * Returns an empty string when the input is not a valid hex color.\n */\nexport function hexToHSL(hex: string): string {\n let h = hex.replace(\"#\", \"\").trim();\n if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n if (h.length !== 6) return \"\";\n\n const r = parseInt(h.substring(0, 2), 16) / 255;\n const g = parseInt(h.substring(2, 4), 16) / 255;\n const b = parseInt(h.substring(4, 6), 16) / 255;\n\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const l = (max + min) / 2;\n\n if (max === min) return `0 0% ${Math.round(l * 100)}%`;\n\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let hue = 0;\n if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;\n else if (max === g) hue = ((b - r) / d + 2) / 6;\n else hue = ((r - g) / d + 4) / 6;\n\n return `${Math.round(hue * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;\n}\n\n/**\n * Normalises a hex or HSL value to the \"H S% L%\" form.\n * Returns null when the value is unrecognised — callers should skip setting\n * the var rather than writing a broken value.\n */\nexport function toHSLSafe(color: string): string | null {\n if (!color) return null;\n const t = color.trim();\n if (isHSL(t)) return t;\n const result = hexToHSL(t);\n return result !== \"\" ? result : null;\n}\n\n/**\n * Nudges the lightness of an \"H S% L%\" string by `delta` percentage points,\n * clamped to [0, 100].\n */\nexport function nudgeL(hsl: string, delta: number): string {\n const parts = hsl.trim().split(/\\s+/);\n if (parts.length < 3) return hsl;\n const l = parseFloat(parts[2] ?? \"0\");\n const newL = Math.max(0, Math.min(100, l + delta));\n return `${parts[0]} ${parts[1]} ${Math.round(newL)}%`;\n}\n\n// ── Sentra platform defaults ───────────────────────────────────────────────\n\n/**\n * SENTRA_BRAND — the platform's own brand tokens.\n *\n * These are the values used when no tenant branding is configured.\n * BrandingProvider applies them on first render so the first paint is always\n * branded; a tenant override writes over them once org-settings resolves.\n */\nexport const SENTRA_BRAND = {\n /** Brand primary: Sentra crimson (--primary 345 75% 33%). */\n primaryHex: \"#931535\",\n /** Brand accent: Tailwind violet-500. */\n accentHex: \"#daab4e\",\n /** Logo path — products serve this from their own /public directory. */\n logoUrl: \"/sentra-logo-full.png\",\n} as const;\n\n// ── Pre-computed HSL values for the defaults ──────────────────────────────\n\nconst SENTRA_PRIMARY_HSL = toHSLSafe(SENTRA_BRAND.primaryHex)!; // \"160 84% 39%\"\nconst SENTRA_ACCENT_HSL = toHSLSafe(SENTRA_BRAND.accentHex)!; // \"263 70% 66%\"\n\n// ── Brand application ──────────────────────────────────────────────────────\n\nexport interface BrandTokens {\n /** Hex string (\"#RRGGBB\") or HSL string (\"H S% L%\") for the primary color. */\n primaryHex?: string;\n /** Hex string (\"#RRGGBB\") or HSL string (\"H S% L%\") for the accent color. */\n accentHex?: string;\n /** Absolute URL or relative path to the org logo image. */\n logoUrl?: string;\n}\n\n/**\n * applyBrand — writes brand CSS custom properties directly onto `root`\n * (typically `document.documentElement`).\n *\n * Call this inside a useEffect whenever org-settings data changes.\n * When a field is absent or invalid, the corresponding Sentra default fills\n * the gap — the product always has a styled baseline.\n *\n * @param root The element to set properties on (usually document.documentElement).\n * @param tokens The brand tokens from org-settings; all fields are optional.\n */\nexport function applyBrand(root: HTMLElement, tokens: BrandTokens = {}): void {\n // ── Primary ──────────────────────────────────────────────────────────────\n const primaryHSL =\n (tokens.primaryHex ? toHSLSafe(tokens.primaryHex) : null) ?? SENTRA_PRIMARY_HSL;\n\n root.style.setProperty(\"--primary\", primaryHSL);\n root.style.setProperty(\"--brand-primary\", primaryHSL);\n root.style.setProperty(\"--brand-primary-glow\", nudgeL(primaryHSL, 7));\n root.style.setProperty(\"--ring\", primaryHSL);\n root.style.setProperty(\"--sidebar-primary\", primaryHSL);\n root.style.setProperty(\"--sidebar-ring\", primaryHSL);\n root.style.setProperty(\"--ai-glow\", primaryHSL);\n\n // ── Accent ───────────────────────────────────────────────────────────────\n const accentHSL =\n (tokens.accentHex ? toHSLSafe(tokens.accentHex) : null) ?? SENTRA_ACCENT_HSL;\n\n root.style.setProperty(\"--accent-muted\", nudgeL(accentHSL, -13));\n\n // ── Logo ─────────────────────────────────────────────────────────────────\n // Only set --logo-url when the tenant actually has a logo. Do NOT fall back to\n // the Sentra default PNG (operator 2026-06-05: empty DB logo was rendering\n // /sentra-logo-full.png everywhere instead of the platform icon). When there's\n // no logo, set --logo-url: none so background-image consumers show nothing and\n // icon-based marks (AuthCard crest, AdminLayout brand mark) take over.\n const rawLogo = tokens.logoUrl;\n if (rawLogo && rawLogo.trim() !== \"\") {\n root.style.setProperty(\"--logo-url\", `url(\"${rawLogo}\")`);\n } else {\n root.style.setProperty(\"--logo-url\", \"none\");\n }\n}","'use client'\n\n/**\n * BrandingProvider.tsx — React wrapper for dynamic Sentra branding.\n *\n * Design contract (prop-driven, no app context):\n * A product fetches its own org-settings and passes the values as props.\n * BrandingProvider calls applyBrand() in a useEffect whenever the values\n * change, writing the CSS custom properties onto document.documentElement.\n *\n * The tokens.css vars (--primary, --brand-primary, etc.) are the static\n * fallback baseline. applyBrand() overrides them at runtime with the org's\n * palette, making the UI theme dynamic per tenant.\n *\n * Placement: wrap the root of your product's component tree AFTER your\n * ThemeProvider (light/dark), so the dark-class is already on <html> when\n * the effect fires.\n *\n * Usage:\n * import { BrandingProvider } from '@prism/ui'\n *\n * // In your product's root layout / providers component:\n * const { data: org } = useOrgSettings() // your own fetch\n *\n * return (\n * <BrandingProvider\n * primaryHex={org?.primaryColor}\n * accentHex={org?.accentColor}\n * logoUrl={org?.logoUrl}\n * >\n * {children}\n * </BrandingProvider>\n * )\n *\n * SSR note: applyBrand() only runs inside useEffect (client-side). On the\n * server the CSS var defaults from tokens.css are used — no flash for the\n * initial paint because the default palette is set in the stylesheet.\n */\nimport { createContext, useContext, useEffect, type ReactNode } from \"react\";\nimport { applyBrand, SENTRA_BRAND, type BrandTokens } from \"./brand\";\n\n// ── BrandContext (optional — lets deeply-nested components read the current tokens) ──\n\ninterface BrandContextValue extends Omit<BrandTokens, 'logoUrl'> {\n /** The resolved primary hex/HSL that is currently active (may be the Sentra default). */\n primaryHex: string;\n /** The resolved accent hex/HSL that is currently active (may be the Sentra default). */\n accentHex: string;\n /**\n * The RAW logo URL the tenant set, or null when none is configured.\n * Intentionally NOT defaulted to the Sentra logo: consumers (AdminLayout,\n * AuthCard) use null to decide \"no logo → render the icon mark instead\".\n * (operator 2026-06-05: everything was showing /sentra-logo-full.png — the\n * default — instead of the platform icon when no DB logo exists.)\n * The `--logo-url` CSS var (set by applyBrand) still falls back to the Sentra\n * default for pure background-image consumers; this context value does not.\n */\n logoUrl: string | null;\n /** Lucide icon name (e.g. 'ShieldCheck') for the platform mark. Null if unset. */\n iconName: string | null;\n /** Resolved product/platform name (for the sidebar title). Empty if unset. */\n productName: string;\n}\n\nconst BrandContext = createContext<BrandContextValue>({\n primaryHex: SENTRA_BRAND.primaryHex,\n accentHex: SENTRA_BRAND.accentHex,\n logoUrl: null,\n iconName: null,\n productName: \"\",\n});\n\n/**\n * useBrand — reads the currently-active brand tokens from context.\n *\n * Returns the Sentra defaults when no BrandingProvider is in the tree.\n * Only use this when a component genuinely needs to read the color values\n * (e.g. to pass them to a canvas API). For standard CSS theming the CSS\n * vars (hsl(var(--primary)) etc.) are always preferred.\n */\nexport function useBrand(): BrandContextValue {\n return useContext(BrandContext);\n}\n\n// ── BrandingProvider ───────────────────────────────────────────────────────\n\ninterface BrandingProviderProps extends BrandTokens {\n /** Lucide icon name for the platform mark (from Fort branding.icon). */\n iconName?: string | null;\n /** Product/platform display name (from Fort branding.product_name_*). */\n productName?: string;\n children: ReactNode;\n}\n\n/**\n * BrandingProvider — hot-applies org branding CSS vars and exposes the\n * active tokens via BrandContext.\n *\n * Renders no additional DOM nodes — it is a pure side-effect + context provider.\n *\n * Props (all optional — Sentra defaults fill any missing value):\n * primaryHex Hex \"#RRGGBB\" or HSL \"H S% L%\" for the brand primary color.\n * accentHex Hex \"#RRGGBB\" or HSL \"H S% L%\" for the brand accent color.\n * logoUrl Absolute URL or relative path to the org logo.\n */\nconst BrandingProvider = ({\n primaryHex,\n accentHex,\n logoUrl,\n iconName,\n productName,\n children,\n}: BrandingProviderProps) => {\n // Apply CSS vars on mount and whenever the brand tokens change.\n useEffect(() => {\n if (typeof document === \"undefined\") return; // SSR guard\n applyBrand(document.documentElement, { primaryHex, accentHex, logoUrl });\n }, [primaryHex, accentHex, logoUrl]);\n\n const contextValue: BrandContextValue = {\n primaryHex: primaryHex ?? SENTRA_BRAND.primaryHex,\n accentHex: accentHex ?? SENTRA_BRAND.accentHex,\n // RAW logo: null when the tenant has none (empty/whitespace counts as none),\n // so consumers fall back to the icon mark instead of the Sentra default PNG.\n logoUrl: logoUrl && logoUrl.trim() !== '' ? logoUrl : null,\n iconName: iconName ?? null,\n productName: productName ?? \"\",\n };\n\n return (\n <BrandContext.Provider value={contextValue}>\n {children}\n </BrandContext.Provider>\n );\n};\n\nBrandingProvider.displayName = \"BrandingProvider\";\n\nexport { BrandingProvider };\nexport type { BrandingProviderProps, BrandContextValue };","\"use client\";\n\nimport * as React from \"react\";\nimport { themes as defaultThemes, themeBase, STORAGE_KEY, type ThemeDef } from \"./themes\";\n\nexport type TogoTheme = \"dark\" | \"light\" | (string & {});\nexport type Dir = \"ltr\" | \"rtl\";\n\n/** Per-app brand overrides: any `--togo-*` (or bridged) custom property → value. */\nexport type ThemeOverrides = Record<string, string>;\n\nexport interface ThemeContextValue {\n theme: TogoTheme;\n setTheme: (t: TogoTheme) => void;\n themes: ThemeDef[];\n dir: Dir;\n setDir: (d: Dir) => void;\n}\n\nconst ThemeContext = React.createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n theme?: TogoTheme;\n /** Per-app brand overrides applied as inline custom properties on the scope element. */\n overrides?: ThemeOverrides;\n dir?: Dir;\n /** \"html\" (default) themes <html>; \"self\" themes a wrapper div (so themes can coexist on one page). */\n scope?: \"html\" | \"self\";\n themes?: ThemeDef[];\n /** Persist theme/dir choice to localStorage and read it on first mount. */\n persist?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\nfunction applyToElement(el: HTMLElement, theme: string, dir: Dir, overrides?: ThemeOverrides) {\n el.setAttribute(\"data-theme\", theme);\n el.classList.toggle(\"dark\", themeBase(theme) === \"dark\");\n el.setAttribute(\"dir\", dir);\n if (overrides) for (const [k, v] of Object.entries(overrides)) el.style.setProperty(k, v);\n}\n\n/**\n * ThemeProvider — runtime theming. Sets `data-theme` + toggles `.dark` + `dir` on its\n * scope (the <html> element, or a wrapper when scope=\"self\"), applies per-app `overrides`\n * as inline custom properties, and exposes `useTheme()`. SSR-safe (no DOM access at module\n * top level; all mutations run in effects). For no-flash, also inline `themeInitScript`.\n */\nexport function ThemeProvider({\n theme: themeProp,\n overrides,\n dir: dirProp,\n scope = \"html\",\n themes = defaultThemes,\n persist = true,\n className,\n children,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = React.useState<TogoTheme>(themeProp ?? \"dark\");\n const [dir, setDirState] = React.useState<Dir>(dirProp ?? \"ltr\");\n const selfRef = React.useRef<HTMLDivElement>(null);\n\n // First mount: hydrate from localStorage / prefers-color-scheme (only when uncontrolled).\n React.useEffect(() => {\n if (themeProp != null || !persist || typeof window === \"undefined\") return;\n const stored = window.localStorage.getItem(STORAGE_KEY);\n if (stored) setThemeState(stored);\n else if (window.matchMedia?.(\"(prefers-color-scheme: light)\").matches) setThemeState(\"light\");\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Keep state in sync when controlled.\n React.useEffect(() => { if (themeProp != null) setThemeState(themeProp); }, [themeProp]);\n React.useEffect(() => { if (dirProp != null) setDirState(dirProp); }, [dirProp]);\n\n const setTheme = React.useCallback((t: TogoTheme) => {\n setThemeState(t);\n if (persist && typeof window !== \"undefined\") window.localStorage.setItem(STORAGE_KEY, String(t));\n }, [persist]);\n\n const setDir = React.useCallback((d: Dir) => setDirState(d), []);\n\n // Apply to the chosen scope.\n React.useEffect(() => {\n if (typeof document === \"undefined\") return;\n const el = scope === \"self\" ? selfRef.current : document.documentElement;\n if (el) applyToElement(el, String(theme), dir, overrides);\n }, [theme, dir, overrides, scope]);\n\n const value = React.useMemo<ThemeContextValue>(() => ({ theme, setTheme, themes, dir, setDir }), [theme, setTheme, themes, dir, setDir]);\n\n return (\n <ThemeContext.Provider value={value}>\n {scope === \"self\" ? (\n <div\n ref={selfRef}\n data-theme={theme}\n dir={dir}\n className={[\"tg-root\", themeBase(String(theme)) === \"dark\" ? \"dark\" : \"\", \"bg-background text-foreground\", className].filter(Boolean).join(\" \")}\n style={overrides as React.CSSProperties}\n >\n {children}\n </div>\n ) : (\n children\n )}\n </ThemeContext.Provider>\n );\n}\nThemeProvider.displayName = \"ThemeProvider\";\n\nexport function useTheme(): ThemeContextValue {\n const ctx = React.useContext(ThemeContext);\n if (!ctx) throw new Error(\"useTheme must be used within a <ThemeProvider>\");\n return ctx;\n}\n","// ToGO theme registry. A theme is a `data-theme` value + a dark/light base (so the\n// existing `.dark`-driven utilities stay in sync). Add a tenant/brand theme by adding\n// one block to tokens.semantic.css and one entry here — no component changes.\n\nexport type ThemeBase = \"dark\" | \"light\";\n\nexport interface ThemeDef {\n id: string;\n label: string;\n /** Whether this theme toggles the `.dark` class (keeps `dark:` utilities in sync). */\n base: ThemeBase;\n}\n\nexport const themes: ThemeDef[] = [\n { id: \"dark\", label: \"Dark\", base: \"dark\" },\n { id: \"light\", label: \"Light\", base: \"light\" },\n];\n\nexport function themeBase(id: string): ThemeBase {\n return themes.find((t) => t.id === id)?.base ?? \"dark\";\n}\n\nexport const STORAGE_KEY = \"togo-theme\";\n\n/**\n * Inline this string in a <script> in <head> to set the theme before first paint\n * (no flash of the wrong theme). The framework injects it server-side.\n *\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />\n */\nexport const themeInitScript = `(function(){try{\nvar k=${JSON.stringify(STORAGE_KEY)};\nvar t=localStorage.getItem(k);\nif(!t){t=window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';}\nvar d=document.documentElement;\nd.setAttribute('data-theme',t);\nd.classList.toggle('dark', t!=='light');\n}catch(e){}})();`;\n"],"mappings":"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","names":["jsx","themes"]}