@syncagent/js
Advanced tools
+47
-1
@@ -472,2 +472,48 @@ interface ToolParameter { | ||
| export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, detectPageContext, generateGuestIdentifier, validateEmail, validateGuestForm, validateName }; | ||
| /** | ||
| * Theme computation for customer chat components. | ||
| * | ||
| * Generates all color tokens from an accent color and dark mode flag, | ||
| * ensuring WCAG AA compliance: | ||
| * - 4.5:1 contrast ratio for text against backgrounds | ||
| * - 3:1 contrast ratio for focus indicators against adjacent colors | ||
| */ | ||
| interface ThemeColors { | ||
| background: string; | ||
| surface: string; | ||
| text: string; | ||
| textSecondary: string; | ||
| border: string; | ||
| accent: string; | ||
| accentHover: string; | ||
| userBubble: string; | ||
| userBubbleText: string; | ||
| assistantBubble: string; | ||
| assistantBubbleText: string; | ||
| inputBackground: string; | ||
| inputBorder: string; | ||
| } | ||
| /** | ||
| * Compute relative luminance per WCAG 2.1. | ||
| * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance | ||
| */ | ||
| declare function relativeLuminance(r: number, g: number, b: number): number; | ||
| /** | ||
| * Compute contrast ratio between two colors (each as [r, g, b]). | ||
| * Returns a value between 1 and 21. | ||
| */ | ||
| declare function contrastRatio(color1: [number, number, number], color2: [number, number, number]): number; | ||
| /** | ||
| * Compute a complete theme color palette from an accent color and dark mode flag. | ||
| * | ||
| * Ensures: | ||
| * - WCAG AA 4.5:1 contrast ratio for text colors against their backgrounds | ||
| * - 3:1 contrast ratio for focus indicators (accent against background) | ||
| * | ||
| * @param accentColor - CSS color string (hex, rgb, or hsl). Falls back to default if invalid. | ||
| * @param darkMode - Whether to use dark color scheme. | ||
| * @returns Complete ThemeColors object with all tokens. | ||
| */ | ||
| declare function computeTheme(accentColor?: string, darkMode?: boolean): ThemeColors; | ||
| export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ThemeColors, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, computeTheme, contrastRatio, detectPageContext, generateGuestIdentifier, relativeLuminance, validateEmail, validateGuestForm, validateName }; |
+47
-1
@@ -472,2 +472,48 @@ interface ToolParameter { | ||
| export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, detectPageContext, generateGuestIdentifier, validateEmail, validateGuestForm, validateName }; | ||
| /** | ||
| * Theme computation for customer chat components. | ||
| * | ||
| * Generates all color tokens from an accent color and dark mode flag, | ||
| * ensuring WCAG AA compliance: | ||
| * - 4.5:1 contrast ratio for text against backgrounds | ||
| * - 3:1 contrast ratio for focus indicators against adjacent colors | ||
| */ | ||
| interface ThemeColors { | ||
| background: string; | ||
| surface: string; | ||
| text: string; | ||
| textSecondary: string; | ||
| border: string; | ||
| accent: string; | ||
| accentHover: string; | ||
| userBubble: string; | ||
| userBubbleText: string; | ||
| assistantBubble: string; | ||
| assistantBubbleText: string; | ||
| inputBackground: string; | ||
| inputBorder: string; | ||
| } | ||
| /** | ||
| * Compute relative luminance per WCAG 2.1. | ||
| * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance | ||
| */ | ||
| declare function relativeLuminance(r: number, g: number, b: number): number; | ||
| /** | ||
| * Compute contrast ratio between two colors (each as [r, g, b]). | ||
| * Returns a value between 1 and 21. | ||
| */ | ||
| declare function contrastRatio(color1: [number, number, number], color2: [number, number, number]): number; | ||
| /** | ||
| * Compute a complete theme color palette from an accent color and dark mode flag. | ||
| * | ||
| * Ensures: | ||
| * - WCAG AA 4.5:1 contrast ratio for text colors against their backgrounds | ||
| * - 3:1 contrast ratio for focus indicators (accent against background) | ||
| * | ||
| * @param accentColor - CSS color string (hex, rgb, or hsl). Falls back to default if invalid. | ||
| * @param darkMode - Whether to use dark color scheme. | ||
| * @returns Complete ThemeColors object with all tokens. | ||
| */ | ||
| declare function computeTheme(accentColor?: string, darkMode?: boolean): ThemeColors; | ||
| export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ThemeColors, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, computeTheme, contrastRatio, detectPageContext, generateGuestIdentifier, relativeLuminance, validateEmail, validateGuestForm, validateName }; |
+226
-0
@@ -26,4 +26,7 @@ "use strict"; | ||
| SyncAgentClient: () => SyncAgentClient, | ||
| computeTheme: () => computeTheme, | ||
| contrastRatio: () => contrastRatio, | ||
| detectPageContext: () => detectPageContext, | ||
| generateGuestIdentifier: () => generateGuestIdentifier, | ||
| relativeLuminance: () => relativeLuminance, | ||
| validateEmail: () => validateEmail, | ||
@@ -631,2 +634,222 @@ validateGuestForm: () => validateGuestForm, | ||
| } | ||
| // src/theme.ts | ||
| function parseHex(hex) { | ||
| const h = hex.replace("#", ""); | ||
| if (h.length === 3) { | ||
| const r = parseInt(h[0] + h[0], 16); | ||
| const g = parseInt(h[1] + h[1], 16); | ||
| const b = parseInt(h[2] + h[2], 16); | ||
| if (isNaN(r) || isNaN(g) || isNaN(b)) return null; | ||
| return [r, g, b]; | ||
| } | ||
| if (h.length === 6) { | ||
| const r = parseInt(h.slice(0, 2), 16); | ||
| const g = parseInt(h.slice(2, 4), 16); | ||
| const b = parseInt(h.slice(4, 6), 16); | ||
| if (isNaN(r) || isNaN(g) || isNaN(b)) return null; | ||
| return [r, g, b]; | ||
| } | ||
| return null; | ||
| } | ||
| function parseRgb(color) { | ||
| const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); | ||
| if (!m) return null; | ||
| return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])]; | ||
| } | ||
| function parseHsl(color) { | ||
| const m = color.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/); | ||
| if (!m) return null; | ||
| const h = parseFloat(m[1]) / 360; | ||
| const s = parseFloat(m[2]) / 100; | ||
| const l = parseFloat(m[3]) / 100; | ||
| return hslToRgb(h, s, l); | ||
| } | ||
| function hslToRgb(h, s, l) { | ||
| if (s === 0) { | ||
| const v = Math.round(l * 255); | ||
| return [v, v, v]; | ||
| } | ||
| const hue2rgb = (p2, q2, t) => { | ||
| if (t < 0) t += 1; | ||
| if (t > 1) t -= 1; | ||
| if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t; | ||
| if (t < 1 / 2) return q2; | ||
| if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6; | ||
| return p2; | ||
| }; | ||
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; | ||
| const p = 2 * l - q; | ||
| return [ | ||
| Math.round(hue2rgb(p, q, h + 1 / 3) * 255), | ||
| Math.round(hue2rgb(p, q, h) * 255), | ||
| Math.round(hue2rgb(p, q, h - 1 / 3) * 255) | ||
| ]; | ||
| } | ||
| function rgbToHsl(r, g, b) { | ||
| r /= 255; | ||
| g /= 255; | ||
| b /= 255; | ||
| const max = Math.max(r, g, b), min = Math.min(r, g, b); | ||
| let h = 0, s = 0; | ||
| const l = (max + min) / 2; | ||
| if (max !== min) { | ||
| const d = max - min; | ||
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | ||
| switch (max) { | ||
| case r: | ||
| h = ((g - b) / d + (g < b ? 6 : 0)) / 6; | ||
| break; | ||
| case g: | ||
| h = ((b - r) / d + 2) / 6; | ||
| break; | ||
| case b: | ||
| h = ((r - g) / d + 4) / 6; | ||
| break; | ||
| } | ||
| } | ||
| return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; | ||
| } | ||
| function parseColor(color) { | ||
| const trimmed = color.trim().toLowerCase(); | ||
| if (trimmed.startsWith("#")) return parseHex(trimmed); | ||
| if (trimmed.startsWith("rgb")) return parseRgb(trimmed); | ||
| if (trimmed.startsWith("hsl")) return parseHsl(trimmed); | ||
| return null; | ||
| } | ||
| function toHex(r, g, b) { | ||
| return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`; | ||
| } | ||
| function relativeLuminance(r, g, b) { | ||
| const [rs, gs, bs] = [r / 255, g / 255, b / 255].map( | ||
| (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) | ||
| ); | ||
| return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; | ||
| } | ||
| function contrastRatio(color1, color2) { | ||
| const l1 = relativeLuminance(...color1); | ||
| const l2 = relativeLuminance(...color2); | ||
| const lighter = Math.max(l1, l2); | ||
| const darker = Math.min(l1, l2); | ||
| return (lighter + 0.05) / (darker + 0.05); | ||
| } | ||
| function clamp(value, min, max) { | ||
| return Math.min(max, Math.max(min, value)); | ||
| } | ||
| function adjustLightness(rgb, delta) { | ||
| const [h, s, l] = rgbToHsl(...rgb); | ||
| const newL = clamp(l + delta, 0, 100); | ||
| return hslToRgb(h / 360, s / 100, newL / 100); | ||
| } | ||
| function ensureContrast(textRgb, bgRgb, targetRatio) { | ||
| let current = textRgb; | ||
| const bgLum = relativeLuminance(...bgRgb); | ||
| const direction = bgLum < 0.5 ? 1 : -1; | ||
| for (let i = 0; i < 50; i++) { | ||
| if (contrastRatio(current, bgRgb) >= targetRatio) { | ||
| return current; | ||
| } | ||
| current = adjustLightness(current, direction * 5); | ||
| } | ||
| return bgLum < 0.5 ? [255, 255, 255] : [0, 0, 0]; | ||
| } | ||
| var DEFAULT_ACCENT = "#6366f1"; | ||
| function computeTheme(accentColor, darkMode = false) { | ||
| let accentRgb = parseColor(accentColor || DEFAULT_ACCENT); | ||
| if (!accentRgb) { | ||
| accentRgb = parseColor(DEFAULT_ACCENT); | ||
| } | ||
| if (darkMode) { | ||
| return computeDarkTheme(accentRgb); | ||
| } | ||
| return computeLightTheme(accentRgb); | ||
| } | ||
| function ensureBubbleContrast(bubbleRgb) { | ||
| const white = [255, 255, 255]; | ||
| const black = [0, 0, 0]; | ||
| if (contrastRatio(white, bubbleRgb) >= 4.5) { | ||
| return { bubble: bubbleRgb, text: white }; | ||
| } | ||
| let darkened = bubbleRgb; | ||
| for (let i = 0; i < 30; i++) { | ||
| darkened = adjustLightness(darkened, -3); | ||
| if (contrastRatio(white, darkened) >= 4.5) { | ||
| return { bubble: darkened, text: white }; | ||
| } | ||
| } | ||
| if (contrastRatio(black, bubbleRgb) >= 4.5) { | ||
| return { bubble: bubbleRgb, text: black }; | ||
| } | ||
| return { bubble: bubbleRgb, text: white }; | ||
| } | ||
| function computeLightTheme(accentRgb) { | ||
| const background = [255, 255, 255]; | ||
| const surface = [249, 250, 251]; | ||
| const border = [229, 231, 235]; | ||
| const inputBackground = [255, 255, 255]; | ||
| const inputBorder = [209, 213, 219]; | ||
| let text = [17, 24, 39]; | ||
| let textSecondary = [107, 114, 128]; | ||
| text = ensureContrast(text, background, 4.5); | ||
| textSecondary = ensureContrast(textSecondary, background, 4.5); | ||
| let accent = accentRgb; | ||
| if (contrastRatio(accent, background) < 3) { | ||
| accent = ensureContrast(accent, background, 3); | ||
| } | ||
| const accentHover = adjustLightness(accent, -8); | ||
| const { bubble: userBubble, text: userBubbleText } = ensureBubbleContrast(accent); | ||
| const assistantBubble = [243, 244, 246]; | ||
| let assistantBubbleText = [31, 41, 55]; | ||
| assistantBubbleText = ensureContrast(assistantBubbleText, assistantBubble, 4.5); | ||
| return { | ||
| background: toHex(...background), | ||
| surface: toHex(...surface), | ||
| text: toHex(...text), | ||
| textSecondary: toHex(...textSecondary), | ||
| border: toHex(...border), | ||
| accent: toHex(...accent), | ||
| accentHover: toHex(...accentHover), | ||
| userBubble: toHex(...userBubble), | ||
| userBubbleText: toHex(...userBubbleText), | ||
| assistantBubble: toHex(...assistantBubble), | ||
| assistantBubbleText: toHex(...assistantBubbleText), | ||
| inputBackground: toHex(...inputBackground), | ||
| inputBorder: toHex(...inputBorder) | ||
| }; | ||
| } | ||
| function computeDarkTheme(accentRgb) { | ||
| const background = [24, 24, 27]; | ||
| const surface = [39, 39, 42]; | ||
| const border = [63, 63, 70]; | ||
| const inputBackground = [39, 39, 42]; | ||
| const inputBorder = [63, 63, 70]; | ||
| let text = [244, 244, 245]; | ||
| let textSecondary = [161, 161, 170]; | ||
| text = ensureContrast(text, background, 4.5); | ||
| textSecondary = ensureContrast(textSecondary, background, 4.5); | ||
| let accent = accentRgb; | ||
| if (contrastRatio(accent, background) < 3) { | ||
| accent = ensureContrast(accent, background, 3); | ||
| } | ||
| const accentHover = adjustLightness(accent, 8); | ||
| const { bubble: userBubble, text: userBubbleText } = ensureBubbleContrast(accent); | ||
| const assistantBubble = [52, 52, 56]; | ||
| let assistantBubbleText = [228, 228, 231]; | ||
| assistantBubbleText = ensureContrast(assistantBubbleText, assistantBubble, 4.5); | ||
| return { | ||
| background: toHex(...background), | ||
| surface: toHex(...surface), | ||
| text: toHex(...text), | ||
| textSecondary: toHex(...textSecondary), | ||
| border: toHex(...border), | ||
| accent: toHex(...accent), | ||
| accentHover: toHex(...accentHover), | ||
| userBubble: toHex(...userBubble), | ||
| userBubbleText: toHex(...userBubbleText), | ||
| assistantBubble: toHex(...assistantBubble), | ||
| assistantBubbleText: toHex(...assistantBubbleText), | ||
| inputBackground: toHex(...inputBackground), | ||
| inputBorder: toHex(...inputBorder) | ||
| }; | ||
| } | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
@@ -637,4 +860,7 @@ 0 && (module.exports = { | ||
| SyncAgentClient, | ||
| computeTheme, | ||
| contrastRatio, | ||
| detectPageContext, | ||
| generateGuestIdentifier, | ||
| relativeLuminance, | ||
| validateEmail, | ||
@@ -641,0 +867,0 @@ validateGuestForm, |
+223
-0
@@ -597,2 +597,222 @@ // src/stream.ts | ||
| } | ||
| // src/theme.ts | ||
| function parseHex(hex) { | ||
| const h = hex.replace("#", ""); | ||
| if (h.length === 3) { | ||
| const r = parseInt(h[0] + h[0], 16); | ||
| const g = parseInt(h[1] + h[1], 16); | ||
| const b = parseInt(h[2] + h[2], 16); | ||
| if (isNaN(r) || isNaN(g) || isNaN(b)) return null; | ||
| return [r, g, b]; | ||
| } | ||
| if (h.length === 6) { | ||
| const r = parseInt(h.slice(0, 2), 16); | ||
| const g = parseInt(h.slice(2, 4), 16); | ||
| const b = parseInt(h.slice(4, 6), 16); | ||
| if (isNaN(r) || isNaN(g) || isNaN(b)) return null; | ||
| return [r, g, b]; | ||
| } | ||
| return null; | ||
| } | ||
| function parseRgb(color) { | ||
| const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); | ||
| if (!m) return null; | ||
| return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])]; | ||
| } | ||
| function parseHsl(color) { | ||
| const m = color.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/); | ||
| if (!m) return null; | ||
| const h = parseFloat(m[1]) / 360; | ||
| const s = parseFloat(m[2]) / 100; | ||
| const l = parseFloat(m[3]) / 100; | ||
| return hslToRgb(h, s, l); | ||
| } | ||
| function hslToRgb(h, s, l) { | ||
| if (s === 0) { | ||
| const v = Math.round(l * 255); | ||
| return [v, v, v]; | ||
| } | ||
| const hue2rgb = (p2, q2, t) => { | ||
| if (t < 0) t += 1; | ||
| if (t > 1) t -= 1; | ||
| if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t; | ||
| if (t < 1 / 2) return q2; | ||
| if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6; | ||
| return p2; | ||
| }; | ||
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; | ||
| const p = 2 * l - q; | ||
| return [ | ||
| Math.round(hue2rgb(p, q, h + 1 / 3) * 255), | ||
| Math.round(hue2rgb(p, q, h) * 255), | ||
| Math.round(hue2rgb(p, q, h - 1 / 3) * 255) | ||
| ]; | ||
| } | ||
| function rgbToHsl(r, g, b) { | ||
| r /= 255; | ||
| g /= 255; | ||
| b /= 255; | ||
| const max = Math.max(r, g, b), min = Math.min(r, g, b); | ||
| let h = 0, s = 0; | ||
| const l = (max + min) / 2; | ||
| if (max !== min) { | ||
| const d = max - min; | ||
| s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | ||
| switch (max) { | ||
| case r: | ||
| h = ((g - b) / d + (g < b ? 6 : 0)) / 6; | ||
| break; | ||
| case g: | ||
| h = ((b - r) / d + 2) / 6; | ||
| break; | ||
| case b: | ||
| h = ((r - g) / d + 4) / 6; | ||
| break; | ||
| } | ||
| } | ||
| return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; | ||
| } | ||
| function parseColor(color) { | ||
| const trimmed = color.trim().toLowerCase(); | ||
| if (trimmed.startsWith("#")) return parseHex(trimmed); | ||
| if (trimmed.startsWith("rgb")) return parseRgb(trimmed); | ||
| if (trimmed.startsWith("hsl")) return parseHsl(trimmed); | ||
| return null; | ||
| } | ||
| function toHex(r, g, b) { | ||
| return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`; | ||
| } | ||
| function relativeLuminance(r, g, b) { | ||
| const [rs, gs, bs] = [r / 255, g / 255, b / 255].map( | ||
| (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) | ||
| ); | ||
| return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; | ||
| } | ||
| function contrastRatio(color1, color2) { | ||
| const l1 = relativeLuminance(...color1); | ||
| const l2 = relativeLuminance(...color2); | ||
| const lighter = Math.max(l1, l2); | ||
| const darker = Math.min(l1, l2); | ||
| return (lighter + 0.05) / (darker + 0.05); | ||
| } | ||
| function clamp(value, min, max) { | ||
| return Math.min(max, Math.max(min, value)); | ||
| } | ||
| function adjustLightness(rgb, delta) { | ||
| const [h, s, l] = rgbToHsl(...rgb); | ||
| const newL = clamp(l + delta, 0, 100); | ||
| return hslToRgb(h / 360, s / 100, newL / 100); | ||
| } | ||
| function ensureContrast(textRgb, bgRgb, targetRatio) { | ||
| let current = textRgb; | ||
| const bgLum = relativeLuminance(...bgRgb); | ||
| const direction = bgLum < 0.5 ? 1 : -1; | ||
| for (let i = 0; i < 50; i++) { | ||
| if (contrastRatio(current, bgRgb) >= targetRatio) { | ||
| return current; | ||
| } | ||
| current = adjustLightness(current, direction * 5); | ||
| } | ||
| return bgLum < 0.5 ? [255, 255, 255] : [0, 0, 0]; | ||
| } | ||
| var DEFAULT_ACCENT = "#6366f1"; | ||
| function computeTheme(accentColor, darkMode = false) { | ||
| let accentRgb = parseColor(accentColor || DEFAULT_ACCENT); | ||
| if (!accentRgb) { | ||
| accentRgb = parseColor(DEFAULT_ACCENT); | ||
| } | ||
| if (darkMode) { | ||
| return computeDarkTheme(accentRgb); | ||
| } | ||
| return computeLightTheme(accentRgb); | ||
| } | ||
| function ensureBubbleContrast(bubbleRgb) { | ||
| const white = [255, 255, 255]; | ||
| const black = [0, 0, 0]; | ||
| if (contrastRatio(white, bubbleRgb) >= 4.5) { | ||
| return { bubble: bubbleRgb, text: white }; | ||
| } | ||
| let darkened = bubbleRgb; | ||
| for (let i = 0; i < 30; i++) { | ||
| darkened = adjustLightness(darkened, -3); | ||
| if (contrastRatio(white, darkened) >= 4.5) { | ||
| return { bubble: darkened, text: white }; | ||
| } | ||
| } | ||
| if (contrastRatio(black, bubbleRgb) >= 4.5) { | ||
| return { bubble: bubbleRgb, text: black }; | ||
| } | ||
| return { bubble: bubbleRgb, text: white }; | ||
| } | ||
| function computeLightTheme(accentRgb) { | ||
| const background = [255, 255, 255]; | ||
| const surface = [249, 250, 251]; | ||
| const border = [229, 231, 235]; | ||
| const inputBackground = [255, 255, 255]; | ||
| const inputBorder = [209, 213, 219]; | ||
| let text = [17, 24, 39]; | ||
| let textSecondary = [107, 114, 128]; | ||
| text = ensureContrast(text, background, 4.5); | ||
| textSecondary = ensureContrast(textSecondary, background, 4.5); | ||
| let accent = accentRgb; | ||
| if (contrastRatio(accent, background) < 3) { | ||
| accent = ensureContrast(accent, background, 3); | ||
| } | ||
| const accentHover = adjustLightness(accent, -8); | ||
| const { bubble: userBubble, text: userBubbleText } = ensureBubbleContrast(accent); | ||
| const assistantBubble = [243, 244, 246]; | ||
| let assistantBubbleText = [31, 41, 55]; | ||
| assistantBubbleText = ensureContrast(assistantBubbleText, assistantBubble, 4.5); | ||
| return { | ||
| background: toHex(...background), | ||
| surface: toHex(...surface), | ||
| text: toHex(...text), | ||
| textSecondary: toHex(...textSecondary), | ||
| border: toHex(...border), | ||
| accent: toHex(...accent), | ||
| accentHover: toHex(...accentHover), | ||
| userBubble: toHex(...userBubble), | ||
| userBubbleText: toHex(...userBubbleText), | ||
| assistantBubble: toHex(...assistantBubble), | ||
| assistantBubbleText: toHex(...assistantBubbleText), | ||
| inputBackground: toHex(...inputBackground), | ||
| inputBorder: toHex(...inputBorder) | ||
| }; | ||
| } | ||
| function computeDarkTheme(accentRgb) { | ||
| const background = [24, 24, 27]; | ||
| const surface = [39, 39, 42]; | ||
| const border = [63, 63, 70]; | ||
| const inputBackground = [39, 39, 42]; | ||
| const inputBorder = [63, 63, 70]; | ||
| let text = [244, 244, 245]; | ||
| let textSecondary = [161, 161, 170]; | ||
| text = ensureContrast(text, background, 4.5); | ||
| textSecondary = ensureContrast(textSecondary, background, 4.5); | ||
| let accent = accentRgb; | ||
| if (contrastRatio(accent, background) < 3) { | ||
| accent = ensureContrast(accent, background, 3); | ||
| } | ||
| const accentHover = adjustLightness(accent, 8); | ||
| const { bubble: userBubble, text: userBubbleText } = ensureBubbleContrast(accent); | ||
| const assistantBubble = [52, 52, 56]; | ||
| let assistantBubbleText = [228, 228, 231]; | ||
| assistantBubbleText = ensureContrast(assistantBubbleText, assistantBubble, 4.5); | ||
| return { | ||
| background: toHex(...background), | ||
| surface: toHex(...surface), | ||
| text: toHex(...text), | ||
| textSecondary: toHex(...textSecondary), | ||
| border: toHex(...border), | ||
| accent: toHex(...accent), | ||
| accentHover: toHex(...accentHover), | ||
| userBubble: toHex(...userBubble), | ||
| userBubbleText: toHex(...userBubbleText), | ||
| assistantBubble: toHex(...assistantBubble), | ||
| assistantBubbleText: toHex(...assistantBubbleText), | ||
| inputBackground: toHex(...inputBackground), | ||
| inputBorder: toHex(...inputBorder) | ||
| }; | ||
| } | ||
| export { | ||
@@ -602,4 +822,7 @@ GuestIdentificationRequiredError, | ||
| SyncAgentClient, | ||
| computeTheme, | ||
| contrastRatio, | ||
| detectPageContext, | ||
| generateGuestIdentifier, | ||
| relativeLuminance, | ||
| validateEmail, | ||
@@ -606,0 +829,0 @@ validateGuestForm, |
+1
-1
| { | ||
| "name": "@syncagent/js", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "description": "SyncAgent JavaScript SDK — AI database agent for any app", | ||
@@ -5,0 +5,0 @@ "homepage": "https://syncagentdev.vercel.app/docs", |
+181
-1
@@ -237,6 +237,15 @@ # @syncagent/js | ||
| ## Customer Agent Mode | ||
| ## Customer Chat | ||
| Route messages through the customer support pipeline — with persona, knowledge base, conversation flows, escalation, and AI fallback — instead of the direct database agent. | ||
| The customer chat API surface includes: | ||
| - **`client.customerChat()`** — send messages through the support pipeline | ||
| - **`client.rateConversation()`** — submit satisfaction ratings | ||
| - **Guest identification** — `getGuestIdentity()`, `setGuestIdentity()`, `validateGuestForm()`, `generateGuestIdentifier()` | ||
| - **Theme engine** — `computeTheme()` for WCAG-compliant color generation | ||
| - **Types** — `CustomerChatResult`, `CustomerChatOptions`, `GuestIdentity`, `GuestFormConfig`, `FieldValidationResult` | ||
| ### Initialization | ||
| ```typescript | ||
@@ -253,2 +262,74 @@ import { SyncAgentClient } from "@syncagent/js"; | ||
| ### Full Lifecycle Example | ||
| ```typescript | ||
| import { SyncAgentClient } from "@syncagent/js"; | ||
| // 1. Initialize client in customer mode | ||
| const client = new SyncAgentClient({ | ||
| apiKey: "sa_your_api_key", | ||
| externalUserId: "customer_123", | ||
| }); | ||
| // 2. Send a message | ||
| const result = await client.customerChat("How do I reset my password?"); | ||
| console.log(result.response); | ||
| // "To reset your password, go to Settings > Security > Reset Password..." | ||
| // 3. Continue the conversation | ||
| const followUp = await client.customerChat("That didn't work", { | ||
| conversationId: result.conversationId, | ||
| onEscalated: () => { | ||
| console.log("Conversation escalated to a human agent"); | ||
| // Connect to Pusher for real-time agent messages | ||
| }, | ||
| onResolved: (conversationId) => { | ||
| console.log(`Conversation ${conversationId} resolved`); | ||
| }, | ||
| }); | ||
| // 4. Rate the conversation after resolution | ||
| if (followUp.resolved) { | ||
| await client.rateConversation(followUp.conversationId, 5); | ||
| } | ||
| ``` | ||
| ### Guest Mode (No External User ID) | ||
| For anonymous visitors, initialize with `customerMode: true` and no `externalUserId`. The SDK will require guest identification before chatting: | ||
| ```typescript | ||
| import { SyncAgentClient, generateGuestIdentifier, validateGuestForm } from "@syncagent/js"; | ||
| const client = new SyncAgentClient({ | ||
| apiKey: "sa_your_api_key", | ||
| customerMode: true, | ||
| guestForm: { | ||
| title: "Welcome!", | ||
| subtitle: "Tell us who you are to get started", | ||
| submitButtonText: "Start Chat", | ||
| }, | ||
| }); | ||
| // Validate form data before submitting | ||
| const validation = validateGuestForm({ | ||
| name: "Jane Doe", | ||
| email: "jane@example.com", | ||
| }); | ||
| if (validation.valid) { | ||
| // Set guest identity — persists to localStorage | ||
| client.setGuestIdentity({ | ||
| name: "Jane Doe", | ||
| email: "jane@example.com", | ||
| phone: null, | ||
| guestId: generateGuestIdentifier("jane@example.com"), | ||
| }); | ||
| // Now customerChat() will work | ||
| const result = await client.customerChat("I need help with my order"); | ||
| console.log(result.response); | ||
| } | ||
| ``` | ||
| ### Configuration | ||
@@ -293,2 +374,3 @@ | ||
| | `onResolved` | `(conversationId: string) => void` | Called when the conversation is resolved | | ||
| | `onGuestIdentified` | `(identity: GuestIdentity) => void` | Called when a guest completes identification (JS-only usage) | | ||
@@ -593,2 +675,86 @@ #### `CustomerChatResult` | ||
| ## Theme Engine | ||
| The `computeTheme()` function generates a complete, WCAG-compliant color palette from an accent color and dark mode flag. Used internally by the pre-built `<SyncAgentCustomerChat>` components across all framework packages (React, Angular, Vue). | ||
| ### `computeTheme(accentColor?, darkMode?)` | ||
| ```typescript | ||
| import { computeTheme } from "@syncagent/js"; | ||
| const theme = computeTheme("#6366f1", false); | ||
| // ThemeColors object with all color tokens | ||
| ``` | ||
| **Parameters** | ||
| | Parameter | Type | Default | Description | | ||
| |-----------|------|---------|-------------| | ||
| | `accentColor` | `string` | `"#6366f1"` | CSS color string (hex `#RGB`/`#RRGGBB`, `rgb()`, or `hsl()`). Falls back to default if invalid. | | ||
| | `darkMode` | `boolean` | `false` | Whether to generate a dark color scheme | | ||
| **Returns** `ThemeColors` | ||
| ### `ThemeColors` | ||
| | Field | Type | Description | | ||
| |-------|------|-------------| | ||
| | `background` | `string` | Main background color | | ||
| | `surface` | `string` | Elevated surface color (cards, panels) | | ||
| | `text` | `string` | Primary text color (4.5:1 contrast against background) | | ||
| | `textSecondary` | `string` | Secondary text color (4.5:1 contrast against background) | | ||
| | `border` | `string` | Border color for dividers and outlines | | ||
| | `accent` | `string` | Primary accent color (3:1 contrast against background) | | ||
| | `accentHover` | `string` | Accent color on hover state | | ||
| | `userBubble` | `string` | User message bubble background | | ||
| | `userBubbleText` | `string` | User message bubble text (4.5:1 contrast against userBubble) | | ||
| | `assistantBubble` | `string` | Assistant message bubble background | | ||
| | `assistantBubbleText` | `string` | Assistant message bubble text (4.5:1 contrast against assistantBubble) | | ||
| | `inputBackground` | `string` | Text input background color | | ||
| | `inputBorder` | `string` | Text input border color | | ||
| ### WCAG Contrast Guarantees | ||
| `computeTheme()` ensures the following minimum contrast ratios for any valid input: | ||
| - **4.5:1** — `text` against `background` | ||
| - **4.5:1** — `textSecondary` against `background` | ||
| - **4.5:1** — `userBubbleText` against `userBubble` | ||
| - **4.5:1** — `assistantBubbleText` against `assistantBubble` | ||
| - **3:1** — `accent` against `background` | ||
| ### Contrast Utilities | ||
| ```typescript | ||
| import { relativeLuminance, contrastRatio } from "@syncagent/js"; | ||
| // Compute relative luminance (WCAG 2.1) for an RGB color | ||
| const lum = relativeLuminance(99, 102, 241); // 0-1 | ||
| // Compute contrast ratio between two RGB colors | ||
| const ratio = contrastRatio([255, 255, 255], [24, 24, 27]); // 1-21 | ||
| ``` | ||
| | Function | Signature | Description | | ||
| |----------|-----------|-------------| | ||
| | `relativeLuminance` | `(r: number, g: number, b: number) => number` | WCAG 2.1 relative luminance (0–1) for an RGB color (0–255 per channel) | | ||
| | `contrastRatio` | `(color1: [r,g,b], color2: [r,g,b]) => number` | Contrast ratio (1–21) between two RGB colors | | ||
| ### Example: Custom Theme Integration | ||
| ```typescript | ||
| import { computeTheme, type ThemeColors } from "@syncagent/js"; | ||
| // Light mode with custom brand color | ||
| const lightTheme: ThemeColors = computeTheme("#e11d48", false); | ||
| // Dark mode | ||
| const darkTheme: ThemeColors = computeTheme("#e11d48", true); | ||
| // Apply to your UI | ||
| document.documentElement.style.setProperty("--chat-bg", lightTheme.background); | ||
| document.documentElement.style.setProperty("--chat-text", lightTheme.text); | ||
| document.documentElement.style.setProperty("--chat-accent", lightTheme.accent); | ||
| ``` | ||
| ## Unified Dual Mode | ||
@@ -667,5 +833,19 @@ | ||
| DualChatReturn, DualChatOptions, UnifiedConfig, | ||
| GuestIdentity, GuestFormConfig, FieldValidationResult, | ||
| } from "@syncagent/js"; | ||
| import type { ThemeColors } from "@syncagent/js"; | ||
| ``` | ||
| ### Customer Chat Types | ||
| | Type | Description | | ||
| |------|-------------| | ||
| | `CustomerChatResult` | Response from `customerChat()` — contains `conversationId`, `response`, `escalated`, `resolved`, `flowActive`, `welcomeMessage`, `sources`, `flowSession` | | ||
| | `CustomerChatOptions` | Options for `customerChat()` — includes `conversationId`, `metadata`, `onEscalated`, `onResolved`, `onGuestIdentified` | | ||
| | `GuestIdentity` | Guest user data — `name`, `email`, `phone`, `guestId` | | ||
| | `GuestFormConfig` | Guest form customization — `title`, `subtitle`, `submitButtonText`, placeholders, `className`, `onSubmit` | | ||
| | `FieldValidationResult` | Validation result — `valid` boolean and optional `error` message | | ||
| | `ThemeColors` | Complete color palette from `computeTheme()` — 13 color tokens for backgrounds, text, accents, and bubbles | | ||
| ### New Types Reference | ||
@@ -672,0 +852,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
130540
23.84%2193
28.92%882
25.64%4
33.33%