import { createContext, useContext } from 'react'; import { StyleSheet } from 'react-native'; import { fontFamily, fontWeight, semanticTokens, TYPE_RAMP, type SemanticTokens, } from './tokens'; /** * Theming for the Superagent native package. * * Styles are authored in the original dark hex palette. Each authored hex is * treated as a *semantic role key*: theme.ts maps it onto the matching Base44 * design-system semantic token (see ./tokens.ts) and resolves that token for * the active color scheme. Both the dark and light StyleSheets are therefore * rendered from DS values, so the native package tracks the web design system * in both modes. Unmapped hexes fall through unchanged, so partial coverage is * always safe. * * `createThemedStyles` builds both StyleSheets up front and returns a proxy * that resolves against the active scheme on every property read, so existing * `styles.foo` call sites keep working unchanged. The scheme lives in module * state synced from the provider in SuperagentHomeScreen during render. */ export type SuperagentColorScheme = 'light' | 'dark'; let currentScheme: SuperagentColorScheme = 'light'; export function getSuperagentColorScheme(): SuperagentColorScheme { return currentScheme; } export function syncSuperagentColorScheme(scheme: SuperagentColorScheme) { currentScheme = scheme; } export const SuperagentThemeContext = createContext<{ scheme: SuperagentColorScheme; setScheme: (scheme: SuperagentColorScheme) => void; }>({ scheme: 'light', setScheme: () => {} }); export function useSuperagentTheme() { return useContext(SuperagentThemeContext); } // ── Authored hex → DS semantic role ────────────────────────────────────── // Keys are UPPERCASE. The same hex may carry different roles depending on the // property it sits on (surface vs border vs foreground), so the three maps are // kept separate and consulted by property name in toSchemeStyle(). type SurfaceRole = keyof SemanticTokens['surface']; type ForegroundRole = keyof SemanticTokens['fg']; type BorderRole = keyof SemanticTokens['border']; const SURFACE_ROLE: Record = { '#000000': 'canvas', // Sunken (recessed: code/inputs/tool output) '#050505': 'sunken', '#070707': 'sunken', '#080808': 'sunken', '#0A0A0A': 'sunken', // Standard cards / panels '#0B0B0C': 'surface', '#0F0F10': 'surface', '#101010': 'surface', '#101012': 'surface', '#111111': 'surface', '#111113': 'surface', // Raised / muted surfaces (chips, secondary buttons, inputs, icon tiles) '#151515': 'muted', '#171717': 'muted', '#181818': 'muted', '#1B1B1B': 'muted', '#1C1C1E': 'muted', '#1F1F22': 'muted', '#1F1F23': 'muted', '#252528': 'muted', '#262626': 'muted', '#2A2A2A': 'muted', '#2C2C2E': 'muted', // No-agents hero canvas — light warm-stone authored hex; maps to the DS // "dark-stone" muted surface so it stays warm-light in light mode and goes // dark-neutral in dark mode (otherwise the white hero title sits on light stone). '#F1EEEA': 'muted', // Divider lines rendered as a 1px background '#242424': 'divider', '#3A3A3E': 'divider', // Elevated/muted that doubles as a status-neutral surface '#242427': 'muted', // Decorative dots '#BDBDC2': 'dotMuted', // Brand connector tile stays white in both modes '#FFFFFF': 'white', // Brand accent fills '#FF5A1F': 'brand', // Inverse / action-primary button fills + cream CTAs '#F4F4F5': 'actionPrimary', '#F4F0E6': 'actionPrimary', '#F7F3E7': 'actionPrimary', // Status / accent soft fills '#11251A': 'successSoft', '#13231D': 'successSoft', '#351516': 'dangerSoft', '#241111': 'dangerSoft', '#1C0F10': 'dangerSoft', '#21110A': 'premiumSoft', '#1D120D': 'premiumSoft', '#102036': 'infoSoft', '#1E1B4B': 'accentSoft', '#312E81': 'accentStrong', // Continue-card message preview (web cream panel + user bubble) and modal input card '#121214': 'surface', '#161614': 'muted', '#141414': 'surface', // DS chip/filter fills (authored as the dark-scheme values). '#505050': 'chipFill', // neutral-600 dark / stone-200 light '#929292': 'chipFillActive', // neutral-400 dark / stone-400 light '#312D2A': 'interactiveStone', // stone-900 dark / stone-50 light }; const BORDER_ROLE: Record = { '#101012': 'default', '#1F1F23': 'default', '#242427': 'default', '#242429': 'default', '#252529': 'default', '#27272A': 'default', '#29292E': 'default', '#2A2A2A': 'default', '#2A2A2D': 'default', '#2A2A2E': 'default', '#2C2C2E': 'default', '#303035': 'default', '#333333': 'default', '#333338': 'default', '#34343A': 'default', '#353535': 'default', '#353539': 'default', '#38383D': 'default', '#3A3A3C': 'default', '#3A3A3E': 'default', '#3F3F46': 'strong', // Active "inverse" button outline tracks the action-primary fill '#F4F4F5': 'inverse', // Status borders '#5F2424': 'danger', '#552022': 'danger', '#3B2222': 'danger', '#4A2413': 'warning', '#3A2317': 'warning', // Continue-card message preview borders (panel + user bubble) '#1B1B1D': 'default', '#201E1A': 'default', }; const FOREGROUND_ROLE: Record = { '#FFFFFF': 'default', '#FAFAFA': 'default', '#F7F7F7': 'default', '#F4F4F5': 'default', // Labels on inverse / action-primary surfaces '#111111': 'onAction', '#0B0B0C': 'onAction', // Dark text on the always-white connector tile '#111827': 'onWhite', // Text tiers '#E4E4E7': 'secondary', '#DADADA': 'secondary', '#D4D4D8': 'secondary', '#C7C7CC': 'muted', '#BDBDC2': 'muted', '#A1A1AA': 'muted', '#8E8E93': 'subtle', '#73737A': 'subtle', '#71717A': 'subtle', '#6F6F76': 'subtle', // Brand / accent '#FF5A1F': 'accent', '#FF8A4C': 'accent', // Status foregrounds '#FCA5A5': 'danger', '#FECACA': 'danger', '#FF6B6B': 'danger', '#34D399': 'success', '#86EFAC': 'success', '#FDE68A': 'warning', '#F8D77B': 'warning', '#FDE047': 'warning', '#FBBF24': 'warning', '#F59E0B': 'warning', '#8AB4F8': 'info', '#93C5FD': 'info', '#A5B4FC': 'info', '#E0E7FF': 'info', '#67E8F9': 'info', '#8B5CF6': 'info', }; function resolveSurface(hex: string, scheme: SuperagentColorScheme): string { const role = SURFACE_ROLE[hex.toUpperCase()]; return role ? semanticTokens[scheme].surface[role] : hex; } function resolveBorder(hex: string, scheme: SuperagentColorScheme): string { const role = BORDER_ROLE[hex.toUpperCase()]; return role ? semanticTokens[scheme].border[role] : hex; } function resolveForeground(hex: string, scheme: SuperagentColorScheme): string { const role = FOREGROUND_ROLE[hex.toUpperCase()]; return role ? semanticTokens[scheme].fg[role] : hex; } /** DS foreground (text / icon) color for an authored hex, for the active scheme. */ export function themedColor(authored: string): string { return resolveForeground(authored, currentScheme); } /** DS surface (background) color for an authored hex, for the active scheme. */ export function themedSurface(authored: string): string { return resolveSurface(authored, currentScheme); } /** DS border color for an authored hex, for the active scheme. */ export function themedBorder(authored: string): string { return resolveBorder(authored, currentScheme); } // ── Typography ─────────────────────────────────────────────────────────── // Every text style is snapped onto the design-system type ramp: the authored // font size resolves to the nearest DS size and its paired line height, the // brand body family is applied, and the original heavy weights are mapped onto // the DS font-weight scale (regular/medium/semibold/bold) to match the web // app's lighter look. Native consumers must bundle the "Wix Madefor Text" // family. const FONT_WEIGHT_MAP: Record = { '900': fontWeight.semibold, '800': fontWeight.medium, '700': fontWeight.medium, '600': fontWeight.regular, '500': fontWeight.medium, '400': fontWeight.regular, bold: fontWeight.semibold, }; /** Snap an authored font size to the nearest DS size + its line height. */ function snapType(size: number): { fontSize: number; lineHeight: number } { for (const [upper, fontSize, lh] of TYPE_RAMP) { if (size <= upper) { return { fontSize, lineHeight: lh }; } } const last = TYPE_RAMP[TYPE_RAMP.length - 1]; return { fontSize: last[1], lineHeight: last[2] }; } function applyTypography(style: Record) { const isText = 'fontSize' in style || 'fontWeight' in style || 'lineHeight' in style; if (!isText) { return style; } const next = { ...style }; if (!next.fontFamily) { next.fontFamily = fontFamily.body; } // Size + line height from the DS ramp. Line height is always derived from // the (snapped) size so authored off-scale leading is replaced by the DS // pairing. Styles with only a line height (no size) keep it untouched. if (typeof next.fontSize === 'number') { const snapped = snapType(next.fontSize); next.fontSize = snapped.fontSize; next.lineHeight = snapped.lineHeight; } const weight = next.fontWeight; if (typeof weight === 'string' && FONT_WEIGHT_MAP[weight]) { next.fontWeight = FONT_WEIGHT_MAP[weight]; } return next; } const BG_KEYS = new Set(['backgroundColor']); const BORDER_KEYS = new Set([ 'borderColor', 'borderTopColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', ]); const TEXT_KEYS = new Set(['color', 'textDecorationColor', 'tintColor']); // shadowColor stays dark in both schemes. function toSchemeStyle(style: Record, scheme: SuperagentColorScheme) { const out: Record = {}; for (const [key, value] of Object.entries(style)) { if (typeof value === 'string' && value.startsWith('#')) { if (BG_KEYS.has(key)) { out[key] = resolveSurface(value, scheme); continue; } if (BORDER_KEYS.has(key)) { out[key] = resolveBorder(value, scheme); continue; } if (TEXT_KEYS.has(key)) { out[key] = resolveForeground(value, scheme); continue; } } out[key] = value; } return out; } /** * Drop-in replacement for StyleSheet.create: takes dark-authored styles and * returns a proxy resolving each style against the active color scheme, with * every color mapped onto the matching design-system semantic token. */ export function createThemedStyles>(base: T): T { const typographyBase = Object.fromEntries( Object.entries(base).map(([name, style]) => [name, applyTypography(style as Record)]), ); const dark = StyleSheet.create( Object.fromEntries( Object.entries(typographyBase).map(([name, style]) => [name, toSchemeStyle(style as Record, 'dark')]), ) as unknown as T, ); const light = StyleSheet.create( Object.fromEntries( Object.entries(typographyBase).map(([name, style]) => [name, toSchemeStyle(style as Record, 'light')]), ) as unknown as T, ); return new Proxy(dark, { get(_, property) { const sheets = currentScheme === 'dark' ? dark : light; return sheets[property as keyof T]; }, }); }