- 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)
228 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|