learning_ai_common_plat/dashboards/admin-web/src/components/ThemeEditor.tsx
saravanakumardb1 4ca9b73d75 fix(platform): build react-native-platform-sdk + fix admin-web typecheck errors
- react-native-platform-sdk: add tsconfig.json + 13 source files (core, auth, telemetry, feature-flags, kill-switch, broadcasts, surveys)
- react-native-platform-sdk: React hooks + providers wrapping platform-service APIs via fetch
- admin-web: fix ThemeEditor.tsx + themes/active/route.ts lysnrai token type access
- tracker-web: product-context import path fix (pre-existing)
2026-03-12 16:36:13 -07:00

228 lines
7.4 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Theme, PlatformTheme } from '@/types/theme';
import tokensJson from '@bytelyst/design-tokens/tokens.json';
function ColorInput({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
return (
<div className="flex items-center gap-2">
<label className="text-sm font-medium w-32">{label}</label>
<input
type="color"
value={value}
onChange={e => onChange(e.target.value)}
className="w-12 h-8 border rounded cursor-pointer"
/>
<input
type="text"
value={value}
onChange={e => onChange(e.target.value)}
className="flex-1 px-2 py-1 border rounded text-sm font-mono"
placeholder="#00000000"
/>
</div>
);
}
function PlatformSection({
title,
colors,
onChange,
}: {
title: string;
colors: PlatformTheme;
onChange: (key: keyof PlatformTheme, value: string) => void;
}) {
return (
<div className="bg-white rounded-lg border p-4">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="space-y-2">
<ColorInput label="Primary" value={colors.primary} onChange={v => onChange('primary', v)} />
<ColorInput
label="Secondary"
value={colors.secondary}
onChange={v => onChange('secondary', v)}
/>
<ColorInput label="Accent" value={colors.accent} onChange={v => onChange('accent', v)} />
<ColorInput
label="Background"
value={colors.background}
onChange={v => onChange('background', v)}
/>
<ColorInput label="Surface" value={colors.surface} onChange={v => onChange('surface', v)} />
<ColorInput label="Error" value={colors.error} onChange={v => onChange('error', v)} />
<ColorInput label="Warning" value={colors.warning} onChange={v => onChange('warning', v)} />
<ColorInput label="Success" value={colors.success} onChange={v => onChange('success', v)} />
{title === 'Desktop' && (
<>
<ColorInput
label="Idle"
value={colors.idle || tokensJson.color.semantic.dark.success}
onChange={v => onChange('idle', v)}
/>
<ColorInput
label="Listening"
value={
colors.listening ||
((tokensJson.color as unknown as Record<string, Record<string, string>>).lysnrai
?.hotkeyActive ??
'#5A8CFF')
}
onChange={v => onChange('listening', v)}
/>
<ColorInput
label="Processing"
value={colors.processing || tokensJson.color.semantic.dark.warning}
onChange={v => onChange('processing', v)}
/>
<ColorInput
label="Offline"
value={colors.offline || tokensJson.color.semantic.dark.textSecondary}
onChange={v => onChange('offline', v)}
/>
</>
)}
</div>
</div>
);
}
interface ThemeEditorProps {
theme?: Theme;
onSave: (theme: Partial<Theme>) => void;
onCancel: () => void;
}
export default function ThemeEditor({ theme, onSave, onCancel }: ThemeEditorProps) {
const [name, setName] = useState(theme?.name || '');
const [description, setDescription] = useState(theme?.description || '');
const [iosColors, setIosColors] = useState<PlatformTheme>(
theme?.ios || {
primary: tokensJson.color.semantic.dark.success,
secondary: tokensJson.color.palette.neutral['700'],
accent: tokensJson.color.semantic.dark.accentPrimary,
background: tokensJson.color.palette.neutral['0'],
surface: tokensJson.color.palette.neutral['50'],
error: tokensJson.color.semantic.dark.danger,
warning: tokensJson.color.semantic.dark.warning,
success: tokensJson.color.semantic.dark.success,
}
);
const [androidColors, setAndroidColors] = useState<PlatformTheme>(theme?.android || iosColors);
const [desktopColors, setDesktopColors] = useState<PlatformTheme>(
theme?.desktop || {
...iosColors,
idle: tokensJson.color.semantic.dark.success,
listening:
(tokensJson.color as unknown as Record<string, Record<string, string>>).lysnrai
?.hotkeyActive ?? '#5A8CFF',
processing: tokensJson.color.semantic.dark.warning,
offline: tokensJson.color.semantic.dark.textSecondary,
}
);
const handleColorChange = (
platform: 'ios' | 'android' | 'desktop',
colorKey: keyof PlatformTheme,
value: string
) => {
// Validate hex color format
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
if (value && !hexPattern.test(value)) {
return; // Don't update if invalid
}
const setter =
platform === 'ios'
? setIosColors
: platform === 'android'
? setAndroidColors
: setDesktopColors;
setter(prev => ({ ...prev, [colorKey]: value }));
};
const handleSave = () => {
onSave({
name,
description,
ios: iosColors,
android: androidColors,
desktop: desktopColors,
});
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto m-4">
<div className="p-6">
<h2 className="text-2xl font-bold mb-6">{theme ? 'Edit Theme' : 'Create New Theme'}</h2>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium mb-1">Theme Name</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
placeholder="Enter theme name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
rows={2}
placeholder="Enter theme description (optional)"
/>
</div>
</div>
<div className="space-y-4 mb-6">
<PlatformSection
title="iOS"
colors={iosColors}
onChange={(key, value) => handleColorChange('ios', key, value)}
/>
<PlatformSection
title="Android"
colors={androidColors}
onChange={(key, value) => handleColorChange('android', key, value)}
/>
<PlatformSection
title="Desktop"
colors={desktopColors}
onChange={(key, value) => handleColorChange('desktop', key, value)}
/>
</div>
<div className="flex justify-end gap-2">
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancel
</button>
<button
onClick={handleSave}
disabled={!name}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{theme ? 'Update' : 'Create'}
</button>
</div>
</div>
</div>
</div>
);
}