feat: skin engine + Twenty CRM theme for iistwin
- Add client/src/themes/ with tokens, built-in skins (iistwin, twenty, omegaprom) - Add ThemeProvider with data-skin/data-theme and runtime CSS for custom skins - Add npm run skins:generate script for build-time CSS generation - Add organization_skins table, migration and CRUD API - Add activeSkinId to organizations and expose it in /api/auth/me - Add skin switcher in /profile and admin skin editor in /settings/appearance - Adapt index.css hardcoded colors to skin tokens - Update AGENTS.md and Obsidian notes
This commit is contained in:
218
client/src/themes/ThemeProvider.tsx
Normal file
218
client/src/themes/ThemeProvider.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* ThemeProvider с поддержкой скинов.
|
||||
* Управляет data-skin / data-theme на <html> и runtime CSS для пользовательских скинов.
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useEffect, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { Skin } from './tokens';
|
||||
import { BUILT_IN_SKINS, DEFAULT_SKIN_ID, getBuiltInSkinById, isBuiltInSkin } from './index';
|
||||
import { flattenTokens } from './tokens';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { apiRequest } from '@/lib/queryClient';
|
||||
|
||||
const THEME_STORAGE_KEY = 'theme';
|
||||
const SKIN_STORAGE_KEY = 'skin';
|
||||
|
||||
export type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
toggleTheme: () => void;
|
||||
skinId: string;
|
||||
setSkinId: (skinId: string) => void;
|
||||
availableSkins: Skin[];
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
function getSystemTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === 'light' || stored === 'dark') return stored;
|
||||
return getSystemTheme();
|
||||
}
|
||||
|
||||
function getStoredSkinId(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(SKIN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/** Вставляет или обновляет runtime CSS для пользовательского скина. */
|
||||
function applyRuntimeSkinCss(skinId: string, theme: Theme, skinTokens?: Skin['tokens']) {
|
||||
if (!skinTokens) {
|
||||
const style = document.getElementById('skin-runtime');
|
||||
if (style) style.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const vars = flattenTokens(skinTokens[theme]);
|
||||
vars['--radius'] = skinTokens[theme].radius.lg;
|
||||
const lines = Object.entries(vars)
|
||||
.map(([name, value]) => ` ${name}: ${value};`)
|
||||
.join('\n');
|
||||
const css = `[data-skin="${skinId}"][data-theme="${theme}"] {\n${lines}\n}`;
|
||||
|
||||
let style = document.getElementById('skin-runtime');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = 'skin-runtime';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
style.textContent = css;
|
||||
}
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
const { user } = useAuth();
|
||||
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
|
||||
const [skinId, setSkinIdState] = useState<string>(DEFAULT_SKIN_ID);
|
||||
const [customSkinsMap, setCustomSkinsMap] = useState<Record<string, Skin>>({});
|
||||
|
||||
const organizationSkinId = user?.organization?.activeSkinId;
|
||||
|
||||
// Загружаем пользовательские скины организации.
|
||||
const { data: customSkinsData } = useQuery({
|
||||
queryKey: ['/api/organization/skins'],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest('GET', '/api/organization/skins');
|
||||
return res.json() as Promise<{ success: boolean; skins: Array<{ id: string; dbId?: number; name: string; description?: string; isEditable: boolean; isBuiltIn: boolean }> }>;
|
||||
},
|
||||
enabled: !!user,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Загружаем детали текущего активного пользовательского скина, если он не встроенный.
|
||||
const isCurrentCustom = Boolean(skinId) && !isBuiltInSkin(skinId);
|
||||
const { data: currentSkinData } = useQuery({
|
||||
queryKey: ['/api/organization/skins', skinId],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest('GET', `/api/organization/skins/${encodeURIComponent(skinId)}`);
|
||||
return res.json() as Promise<{ success: boolean; skin: Skin }>;
|
||||
},
|
||||
enabled: !!user && isCurrentCustom,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!customSkinsData?.skins) return;
|
||||
const map: Record<string, Skin> = {};
|
||||
for (const item of customSkinsData.skins) {
|
||||
if (item.isBuiltIn) continue;
|
||||
map[item.id] = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
isEditable: item.isEditable,
|
||||
tokens: {} as Skin['tokens'], // tokens подгружаются отдельно при выборе
|
||||
};
|
||||
}
|
||||
setCustomSkinsMap(map);
|
||||
}, [customSkinsData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSkinData?.skin) {
|
||||
setCustomSkinsMap((prev) => ({ ...prev, [currentSkinData.skin.id]: currentSkinData.skin }));
|
||||
}
|
||||
}, [currentSkinData]);
|
||||
|
||||
// Инициализация skinId: организационный > localStorage > default.
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const allSkinIds = new Set([...BUILT_IN_SKINS.map((s) => s.id), ...Object.keys(customSkinsMap)]);
|
||||
const stored = getStoredSkinId();
|
||||
|
||||
if (stored && allSkinIds.has(stored)) {
|
||||
setSkinIdState(stored);
|
||||
} else if (organizationSkinId && allSkinIds.has(organizationSkinId)) {
|
||||
setSkinIdState(organizationSkinId);
|
||||
} else {
|
||||
setSkinIdState(DEFAULT_SKIN_ID);
|
||||
}
|
||||
}, [organizationSkinId, customSkinsData]);
|
||||
|
||||
const setTheme = useCallback((newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setSkinId = useCallback((newSkinId: string) => {
|
||||
setSkinIdState(newSkinId);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(SKIN_STORAGE_KEY, newSkinId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(theme === 'light' ? 'dark' : 'light');
|
||||
}, [theme, setTheme]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute('data-skin', skinId);
|
||||
root.setAttribute('data-theme', theme);
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(theme);
|
||||
|
||||
// Для встроенных скинов CSS уже есть в generated-skins.css.
|
||||
// Для пользовательских — генерируем runtime.
|
||||
const customSkin = customSkinsMap[skinId];
|
||||
applyRuntimeSkinCss(skinId, theme, customSkin?.tokens);
|
||||
}, [theme, skinId, customSkinsMap]);
|
||||
|
||||
// Слушаем системную тему, если нет явного сохранённого выбора.
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
|
||||
setThemeState(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
media.addEventListener('change', handler);
|
||||
return () => media.removeEventListener('change', handler);
|
||||
}, []);
|
||||
|
||||
const availableSkins = useMemo(() => {
|
||||
const custom = Object.values(customSkinsMap);
|
||||
return [...BUILT_IN_SKINS, ...custom];
|
||||
}, [customSkinsMap]);
|
||||
|
||||
const value: ThemeContextType = {
|
||||
theme,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
skinId,
|
||||
setSkinId,
|
||||
availableSkins,
|
||||
};
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/** Удобный хук только для текущего скина. */
|
||||
export function useSkin() {
|
||||
const { skinId, setSkinId, availableSkins } = useTheme();
|
||||
const skin = availableSkins.find((s) => s.id === skinId) ?? getBuiltInSkinById(DEFAULT_SKIN_ID)!;
|
||||
return { skinId, skin, setSkinId, availableSkins };
|
||||
}
|
||||
23
client/src/themes/index.ts
Normal file
23
client/src/themes/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Skin } from './tokens';
|
||||
import { iistwinSkin } from './skins/iistwin';
|
||||
import { omegapromSkin } from './skins/omegaprom';
|
||||
import { twentySkin } from './skins/twenty';
|
||||
|
||||
export * from './tokens';
|
||||
export { iistwinSkin, omegapromSkin, twentySkin };
|
||||
|
||||
/** Встроенные скины, доступные из коробки. */
|
||||
export const BUILT_IN_SKINS: Skin[] = [iistwinSkin, twentySkin, omegapromSkin];
|
||||
|
||||
/** Скин по умолчанию. */
|
||||
export const DEFAULT_SKIN_ID = 'iistwin';
|
||||
|
||||
/** Найти встроенный скин по id. */
|
||||
export function getBuiltInSkinById(id: string): Skin | undefined {
|
||||
return BUILT_IN_SKINS.find((skin) => skin.id === id);
|
||||
}
|
||||
|
||||
/** Проверить, является ли скин встроенным. */
|
||||
export function isBuiltInSkin(id: string): boolean {
|
||||
return BUILT_IN_SKINS.some((skin) => skin.id === id);
|
||||
}
|
||||
206
client/src/themes/skins/iistwin.ts
Normal file
206
client/src/themes/skins/iistwin.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import type { Skin } from '../tokens';
|
||||
|
||||
/**
|
||||
* Скин iistwin — текущий вид системы, fallback по умолчанию.
|
||||
*/
|
||||
export const iistwinSkin: Skin = {
|
||||
id: 'iistwin',
|
||||
name: 'iistwin Classic',
|
||||
description: 'Классический минималистичный вид iistwin.',
|
||||
author: 'iistwin',
|
||||
isEditable: false,
|
||||
tokens: {
|
||||
light: {
|
||||
colors: {
|
||||
background: 'hsl(0 0% 100%)',
|
||||
foreground: 'hsl(220 10% 15%)',
|
||||
card: 'hsl(0 0% 100%)',
|
||||
cardForeground: 'hsl(220 10% 15%)',
|
||||
popover: 'hsl(0 0% 100%)',
|
||||
popoverForeground: 'hsl(220 10% 15%)',
|
||||
primary: 'hsl(220 10% 15%)',
|
||||
primaryForeground: 'hsl(0 0% 100%)',
|
||||
secondary: 'hsl(220 5% 96%)',
|
||||
secondaryForeground: 'hsl(220 10% 15%)',
|
||||
muted: 'hsl(220 5% 96%)',
|
||||
mutedForeground: 'hsl(220 5% 45%)',
|
||||
accent: 'hsl(220 5% 93%)',
|
||||
accentForeground: 'hsl(220 10% 15%)',
|
||||
destructive: 'hsl(0 72% 51%)',
|
||||
destructiveForeground: 'hsl(0 0% 100%)',
|
||||
border: 'hsl(220 5% 90%)',
|
||||
input: 'hsl(0 0% 100%)',
|
||||
ring: 'hsl(220 10% 15%)',
|
||||
|
||||
backgroundSecondary: 'hsl(0 0% 98%)',
|
||||
backgroundTertiary: 'hsl(0 0% 96%)',
|
||||
backgroundTransparentPrimary: 'hsl(220 10% 15% / 0.04)',
|
||||
backgroundTransparentSecondary: 'hsl(220 10% 15% / 0.08)',
|
||||
backgroundTransparentTertiary: 'hsl(220 10% 15% / 0.12)',
|
||||
backgroundTransparentMedium: 'hsl(220 10% 15% / 0.06)',
|
||||
backgroundTransparentLight: 'hsl(220 10% 15% / 0.03)',
|
||||
backgroundTransparentLighter: 'hsl(220 10% 15% / 0.02)',
|
||||
backgroundNoisy: 'hsl(0 0% 100%)',
|
||||
|
||||
fontPrimary: 'hsl(220 10% 15%)',
|
||||
fontSecondary: 'hsl(220 5% 45%)',
|
||||
fontTertiary: 'hsl(220 5% 55%)',
|
||||
fontLight: 'hsl(220 5% 65%)',
|
||||
fontExtraLight: 'hsl(220 5% 75%)',
|
||||
fontInverted: 'hsl(0 0% 100%)',
|
||||
|
||||
borderLight: 'hsl(220 5% 94%)',
|
||||
borderMedium: 'hsl(220 5% 90%)',
|
||||
borderStrong: 'hsl(220 5% 80%)',
|
||||
|
||||
statusDone: 'hsl(142 50% 45%)',
|
||||
statusProgress: 'hsl(45 90% 50%)',
|
||||
statusOverdue: 'hsl(0 70% 55%)',
|
||||
statusPaused: 'hsl(220 5% 60%)',
|
||||
|
||||
chart1: 'hsl(220 10% 25%)',
|
||||
chart2: 'hsl(142 50% 45%)',
|
||||
chart3: 'hsl(45 90% 50%)',
|
||||
chart4: 'hsl(200 70% 50%)',
|
||||
chart5: 'hsl(0 70% 55%)',
|
||||
|
||||
sidebar: 'hsl(0 0% 100%)',
|
||||
sidebarForeground: 'hsl(220 10% 15%)',
|
||||
sidebarPrimary: 'hsl(220 10% 15%)',
|
||||
sidebarPrimaryForeground: 'hsl(0 0% 100%)',
|
||||
sidebarAccent: 'hsl(220 5% 96%)',
|
||||
sidebarAccentForeground: 'hsl(220 10% 15%)',
|
||||
sidebarBorder: 'hsl(220 5% 92%)',
|
||||
sidebarRing: 'hsl(220 10% 15%)',
|
||||
},
|
||||
radius: {
|
||||
xs: '0.125rem',
|
||||
sm: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
},
|
||||
shadows: {
|
||||
sm: 'none',
|
||||
md: '0 1px 2px 0 rgba(0, 0, 0, 0.03)',
|
||||
lg: '0 1px 3px 0 rgba(0, 0, 0, 0.05)',
|
||||
xl: '0 2px 4px 0 rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"SF Mono", "Monaco", "Inconsolata", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.75rem',
|
||||
md: '0.8125rem',
|
||||
lg: '1rem',
|
||||
xl: '1.25rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '0.25rem',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
background: 'hsl(220 10% 8%)',
|
||||
foreground: 'hsl(220 5% 92%)',
|
||||
card: 'hsl(220 10% 10%)',
|
||||
cardForeground: 'hsl(220 5% 92%)',
|
||||
popover: 'hsl(220 10% 10%)',
|
||||
popoverForeground: 'hsl(220 5% 92%)',
|
||||
primary: 'hsl(0 0% 100%)',
|
||||
primaryForeground: 'hsl(220 10% 8%)',
|
||||
secondary: 'hsl(220 10% 14%)',
|
||||
secondaryForeground: 'hsl(220 5% 92%)',
|
||||
muted: 'hsl(220 10% 14%)',
|
||||
mutedForeground: 'hsl(220 5% 55%)',
|
||||
accent: 'hsl(220 10% 16%)',
|
||||
accentForeground: 'hsl(220 5% 92%)',
|
||||
destructive: 'hsl(0 70% 55%)',
|
||||
destructiveForeground: 'hsl(0 0% 100%)',
|
||||
border: 'hsl(220 10% 18%)',
|
||||
input: 'hsl(220 10% 12%)',
|
||||
ring: 'hsl(0 0% 100%)',
|
||||
|
||||
backgroundSecondary: 'hsl(220 10% 10%)',
|
||||
backgroundTertiary: 'hsl(220 10% 14%)',
|
||||
backgroundTransparentPrimary: 'hsl(0 0% 100% / 0.04)',
|
||||
backgroundTransparentSecondary: 'hsl(0 0% 100% / 0.08)',
|
||||
backgroundTransparentTertiary: 'hsl(0 0% 100% / 0.12)',
|
||||
backgroundTransparentMedium: 'hsl(0 0% 100% / 0.06)',
|
||||
backgroundTransparentLight: 'hsl(0 0% 100% / 0.03)',
|
||||
backgroundTransparentLighter: 'hsl(0 0% 100% / 0.02)',
|
||||
backgroundNoisy: 'hsl(220 10% 8%)',
|
||||
|
||||
fontPrimary: 'hsl(220 5% 92%)',
|
||||
fontSecondary: 'hsl(220 5% 65%)',
|
||||
fontTertiary: 'hsl(220 5% 55%)',
|
||||
fontLight: 'hsl(220 5% 45%)',
|
||||
fontExtraLight: 'hsl(220 5% 35%)',
|
||||
fontInverted: 'hsl(220 10% 8%)',
|
||||
|
||||
borderLight: 'hsl(220 10% 16%)',
|
||||
borderMedium: 'hsl(220 10% 20%)',
|
||||
borderStrong: 'hsl(220 10% 30%)',
|
||||
|
||||
statusDone: 'hsl(142 50% 55%)',
|
||||
statusProgress: 'hsl(45 90% 55%)',
|
||||
statusOverdue: 'hsl(0 70% 60%)',
|
||||
statusPaused: 'hsl(220 5% 55%)',
|
||||
|
||||
chart1: 'hsl(220 5% 70%)',
|
||||
chart2: 'hsl(142 50% 55%)',
|
||||
chart3: 'hsl(45 90% 55%)',
|
||||
chart4: 'hsl(200 70% 55%)',
|
||||
chart5: 'hsl(0 70% 60%)',
|
||||
|
||||
sidebar: 'hsl(220 10% 8%)',
|
||||
sidebarForeground: 'hsl(220 5% 92%)',
|
||||
sidebarPrimary: 'hsl(0 0% 100%)',
|
||||
sidebarPrimaryForeground: 'hsl(220 10% 8%)',
|
||||
sidebarAccent: 'hsl(220 10% 14%)',
|
||||
sidebarAccentForeground: 'hsl(220 5% 92%)',
|
||||
sidebarBorder: 'hsl(220 10% 16%)',
|
||||
sidebarRing: 'hsl(0 0% 100%)',
|
||||
},
|
||||
radius: {
|
||||
xs: '0.125rem',
|
||||
sm: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
},
|
||||
shadows: {
|
||||
sm: 'none',
|
||||
md: '0 1px 2px 0 rgba(0, 0, 0, 0.15)',
|
||||
lg: '0 2px 4px 0 rgba(0, 0, 0, 0.2)',
|
||||
xl: '0 4px 6px -1px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"SF Mono", "Monaco", "Inconsolata", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.75rem',
|
||||
md: '0.8125rem',
|
||||
lg: '1rem',
|
||||
xl: '1.25rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '0.25rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
206
client/src/themes/skins/omegaprom.ts
Normal file
206
client/src/themes/skins/omegaprom.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import type { Skin } from '../tokens';
|
||||
|
||||
/**
|
||||
* Корпоративный скин ОмегаПром — синий акцент, нейтральные серые.
|
||||
*/
|
||||
export const omegapromSkin: Skin = {
|
||||
id: 'omegaprom',
|
||||
name: 'ОмегаПром',
|
||||
description: 'Корпоративный скин с синим акцентом.',
|
||||
author: 'iistwin',
|
||||
isEditable: false,
|
||||
tokens: {
|
||||
light: {
|
||||
colors: {
|
||||
background: 'hsl(0 0% 100%)',
|
||||
foreground: 'hsl(220 15% 18%)',
|
||||
card: 'hsl(0 0% 100%)',
|
||||
cardForeground: 'hsl(220 15% 18%)',
|
||||
popover: 'hsl(0 0% 100%)',
|
||||
popoverForeground: 'hsl(220 15% 18%)',
|
||||
primary: 'hsl(221 83% 53%)',
|
||||
primaryForeground: 'hsl(0 0% 100%)',
|
||||
secondary: 'hsl(217 91% 96%)',
|
||||
secondaryForeground: 'hsl(221 83% 40%)',
|
||||
muted: 'hsl(220 14% 96%)',
|
||||
mutedForeground: 'hsl(220 9% 46%)',
|
||||
accent: 'hsl(214 100% 92%)',
|
||||
accentForeground: 'hsl(221 83% 40%)',
|
||||
destructive: 'hsl(0 72% 51%)',
|
||||
destructiveForeground: 'hsl(0 0% 100%)',
|
||||
border: 'hsl(220 13% 90%)',
|
||||
input: 'hsl(0 0% 100%)',
|
||||
ring: 'hsl(221 83% 53%)',
|
||||
|
||||
backgroundSecondary: 'hsl(220 20% 98%)',
|
||||
backgroundTertiary: 'hsl(220 14% 96%)',
|
||||
backgroundTransparentPrimary: 'hsl(221 83% 53% / 0.04)',
|
||||
backgroundTransparentSecondary: 'hsl(221 83% 53% / 0.08)',
|
||||
backgroundTransparentTertiary: 'hsl(221 83% 53% / 0.12)',
|
||||
backgroundTransparentMedium: 'hsl(220 15% 18% / 0.06)',
|
||||
backgroundTransparentLight: 'hsl(220 15% 18% / 0.03)',
|
||||
backgroundTransparentLighter: 'hsl(220 15% 18% / 0.02)',
|
||||
backgroundNoisy: 'hsl(0 0% 100%)',
|
||||
|
||||
fontPrimary: 'hsl(220 15% 18%)',
|
||||
fontSecondary: 'hsl(220 9% 46%)',
|
||||
fontTertiary: 'hsl(220 9% 56%)',
|
||||
fontLight: 'hsl(220 9% 66%)',
|
||||
fontExtraLight: 'hsl(220 9% 76%)',
|
||||
fontInverted: 'hsl(0 0% 100%)',
|
||||
|
||||
borderLight: 'hsl(220 13% 94%)',
|
||||
borderMedium: 'hsl(220 13% 90%)',
|
||||
borderStrong: 'hsl(220 13% 80%)',
|
||||
|
||||
statusDone: 'hsl(142 55% 45%)',
|
||||
statusProgress: 'hsl(45 90% 50%)',
|
||||
statusOverdue: 'hsl(0 70% 55%)',
|
||||
statusPaused: 'hsl(220 9% 60%)',
|
||||
|
||||
chart1: 'hsl(221 83% 53%)',
|
||||
chart2: 'hsl(142 55% 45%)',
|
||||
chart3: 'hsl(45 90% 50%)',
|
||||
chart4: 'hsl(200 70% 50%)',
|
||||
chart5: 'hsl(0 70% 55%)',
|
||||
|
||||
sidebar: 'hsl(220 20% 98%)',
|
||||
sidebarForeground: 'hsl(220 15% 18%)',
|
||||
sidebarPrimary: 'hsl(221 83% 53%)',
|
||||
sidebarPrimaryForeground: 'hsl(0 0% 100%)',
|
||||
sidebarAccent: 'hsl(220 14% 96%)',
|
||||
sidebarAccentForeground: 'hsl(220 15% 18%)',
|
||||
sidebarBorder: 'hsl(220 13% 90%)',
|
||||
sidebarRing: 'hsl(221 83% 53%)',
|
||||
},
|
||||
radius: {
|
||||
xs: '0.125rem',
|
||||
sm: '0.25rem',
|
||||
md: '0.5rem',
|
||||
lg: '0.75rem',
|
||||
xl: '1rem',
|
||||
},
|
||||
shadows: {
|
||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.03)',
|
||||
md: '0 2px 4px 0 rgba(0, 0, 0, 0.05)',
|
||||
lg: '0 4px 8px -2px rgba(0, 0, 0, 0.06)',
|
||||
xl: '0 8px 16px -4px rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"SF Mono", "Monaco", "Inconsolata", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.75rem',
|
||||
md: '0.875rem',
|
||||
lg: '1rem',
|
||||
xl: '1.25rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '0.25rem',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
background: 'hsl(220 20% 8%)',
|
||||
foreground: 'hsl(220 10% 92%)',
|
||||
card: 'hsl(220 20% 10%)',
|
||||
cardForeground: 'hsl(220 10% 92%)',
|
||||
popover: 'hsl(220 20% 10%)',
|
||||
popoverForeground: 'hsl(220 10% 92%)',
|
||||
primary: 'hsl(217 91% 60%)',
|
||||
primaryForeground: 'hsl(220 20% 8%)',
|
||||
secondary: 'hsl(220 20% 16%)',
|
||||
secondaryForeground: 'hsl(220 10% 92%)',
|
||||
muted: 'hsl(220 20% 14%)',
|
||||
mutedForeground: 'hsl(220 10% 60%)',
|
||||
accent: 'hsl(220 20% 18%)',
|
||||
accentForeground: 'hsl(220 10% 92%)',
|
||||
destructive: 'hsl(0 70% 55%)',
|
||||
destructiveForeground: 'hsl(0 0% 100%)',
|
||||
border: 'hsl(220 20% 18%)',
|
||||
input: 'hsl(220 20% 12%)',
|
||||
ring: 'hsl(217 91% 60%)',
|
||||
|
||||
backgroundSecondary: 'hsl(220 20% 10%)',
|
||||
backgroundTertiary: 'hsl(220 20% 14%)',
|
||||
backgroundTransparentPrimary: 'hsl(217 91% 60% / 0.04)',
|
||||
backgroundTransparentSecondary: 'hsl(217 91% 60% / 0.08)',
|
||||
backgroundTransparentTertiary: 'hsl(217 91% 60% / 0.12)',
|
||||
backgroundTransparentMedium: 'hsl(0 0% 100% / 0.06)',
|
||||
backgroundTransparentLight: 'hsl(0 0% 100% / 0.03)',
|
||||
backgroundTransparentLighter: 'hsl(0 0% 100% / 0.02)',
|
||||
backgroundNoisy: 'hsl(220 20% 8%)',
|
||||
|
||||
fontPrimary: 'hsl(220 10% 92%)',
|
||||
fontSecondary: 'hsl(220 10% 65%)',
|
||||
fontTertiary: 'hsl(220 10% 55%)',
|
||||
fontLight: 'hsl(220 10% 45%)',
|
||||
fontExtraLight: 'hsl(220 10% 35%)',
|
||||
fontInverted: 'hsl(220 20% 8%)',
|
||||
|
||||
borderLight: 'hsl(220 20% 16%)',
|
||||
borderMedium: 'hsl(220 20% 20%)',
|
||||
borderStrong: 'hsl(220 20% 30%)',
|
||||
|
||||
statusDone: 'hsl(142 55% 55%)',
|
||||
statusProgress: 'hsl(45 90% 55%)',
|
||||
statusOverdue: 'hsl(0 70% 60%)',
|
||||
statusPaused: 'hsl(220 10% 55%)',
|
||||
|
||||
chart1: 'hsl(217 91% 60%)',
|
||||
chart2: 'hsl(142 55% 55%)',
|
||||
chart3: 'hsl(45 90% 55%)',
|
||||
chart4: 'hsl(200 70% 55%)',
|
||||
chart5: 'hsl(0 70% 60%)',
|
||||
|
||||
sidebar: 'hsl(220 20% 8%)',
|
||||
sidebarForeground: 'hsl(220 10% 92%)',
|
||||
sidebarPrimary: 'hsl(217 91% 60%)',
|
||||
sidebarPrimaryForeground: 'hsl(220 20% 8%)',
|
||||
sidebarAccent: 'hsl(220 20% 14%)',
|
||||
sidebarAccentForeground: 'hsl(220 10% 92%)',
|
||||
sidebarBorder: 'hsl(220 20% 16%)',
|
||||
sidebarRing: 'hsl(217 91% 60%)',
|
||||
},
|
||||
radius: {
|
||||
xs: '0.125rem',
|
||||
sm: '0.25rem',
|
||||
md: '0.5rem',
|
||||
lg: '0.75rem',
|
||||
xl: '1rem',
|
||||
},
|
||||
shadows: {
|
||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.2)',
|
||||
md: '0 2px 4px 0 rgba(0, 0, 0, 0.25)',
|
||||
lg: '0 4px 8px -2px rgba(0, 0, 0, 0.3)',
|
||||
xl: '0 8px 16px -4px rgba(0, 0, 0, 0.35)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"SF Mono", "Monaco", "Inconsolata", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.75rem',
|
||||
md: '0.875rem',
|
||||
lg: '1rem',
|
||||
xl: '1.25rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '0.25rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
208
client/src/themes/skins/twenty.ts
Normal file
208
client/src/themes/skins/twenty.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import type { Skin } from '../tokens';
|
||||
|
||||
/**
|
||||
* Скин в стиле Twenty CRM.
|
||||
* Референс: https://github.com/twentyhq/twenty
|
||||
* Акцентный цвет — indigo/blue, нейтральная серая палитра, мягкие тени.
|
||||
*/
|
||||
export const twentySkin: Skin = {
|
||||
id: 'twenty',
|
||||
name: 'Twenty',
|
||||
description: 'Минималистичный светлый/тёмный скин в стиле Twenty CRM.',
|
||||
author: 'iistwin',
|
||||
isEditable: false,
|
||||
tokens: {
|
||||
light: {
|
||||
colors: {
|
||||
background: '#ffffff',
|
||||
foreground: '#333333',
|
||||
card: '#ffffff',
|
||||
cardForeground: '#333333',
|
||||
popover: '#ffffff',
|
||||
popoverForeground: '#333333',
|
||||
primary: '#3e63dd',
|
||||
primaryForeground: '#ffffff',
|
||||
secondary: '#f4f4f5',
|
||||
secondaryForeground: '#333333',
|
||||
muted: '#f4f4f5',
|
||||
mutedForeground: '#666666',
|
||||
accent: '#e0e7ff',
|
||||
accentForeground: '#1e1b4b',
|
||||
destructive: '#dc2626',
|
||||
destructiveForeground: '#ffffff',
|
||||
border: '#e4e4e7',
|
||||
input: '#ffffff',
|
||||
ring: '#3e63dd',
|
||||
|
||||
backgroundSecondary: '#fcfcfc',
|
||||
backgroundTertiary: '#f4f4f5',
|
||||
backgroundTransparentPrimary: 'rgba(0, 0, 0, 0.04)',
|
||||
backgroundTransparentSecondary: 'rgba(0, 0, 0, 0.08)',
|
||||
backgroundTransparentTertiary: 'rgba(0, 0, 0, 0.12)',
|
||||
backgroundTransparentMedium: 'rgba(0, 0, 0, 0.06)',
|
||||
backgroundTransparentLight: 'rgba(0, 0, 0, 0.03)',
|
||||
backgroundTransparentLighter: 'rgba(0, 0, 0, 0.02)',
|
||||
backgroundNoisy: '#ffffff',
|
||||
|
||||
fontPrimary: '#333333',
|
||||
fontSecondary: '#666666',
|
||||
fontTertiary: '#999999',
|
||||
fontLight: '#a1a1aa',
|
||||
fontExtraLight: '#c4c4c7',
|
||||
fontInverted: '#ffffff',
|
||||
|
||||
borderLight: '#f4f4f5',
|
||||
borderMedium: '#e4e4e7',
|
||||
borderStrong: '#d4d4d8',
|
||||
|
||||
statusDone: '#22c55e',
|
||||
statusProgress: '#f59e0b',
|
||||
statusOverdue: '#ef4444',
|
||||
statusPaused: '#a1a1aa',
|
||||
|
||||
chart1: '#333333',
|
||||
chart2: '#22c55e',
|
||||
chart3: '#f59e0b',
|
||||
chart4: '#3b82f6',
|
||||
chart5: '#ef4444',
|
||||
|
||||
sidebar: '#fcfcfc',
|
||||
sidebarForeground: '#333333',
|
||||
sidebarPrimary: '#3e63dd',
|
||||
sidebarPrimaryForeground: '#ffffff',
|
||||
sidebarAccent: '#f4f4f5',
|
||||
sidebarAccentForeground: '#333333',
|
||||
sidebarBorder: '#e4e4e7',
|
||||
sidebarRing: '#3e63dd',
|
||||
},
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
lg: '16px',
|
||||
xl: '20px',
|
||||
},
|
||||
shadows: {
|
||||
sm: '0px 1px 2px 0px rgba(0, 0, 0, 0.04)',
|
||||
md: '0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
|
||||
lg: '2px 4px 16px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.04)',
|
||||
xl: '0px 0px 8px 0px rgba(0, 0, 0, 0.08), 0px 8px 32px -8px rgba(0, 0, 0, 0.12)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"DM Mono", "SF Mono", "Monaco", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.85rem',
|
||||
md: '0.92rem',
|
||||
lg: '1rem',
|
||||
xl: '1.23rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '4px',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
background: '#171717',
|
||||
foreground: '#ededed',
|
||||
card: '#1f1f1f',
|
||||
cardForeground: '#ededed',
|
||||
popover: '#1f1f1f',
|
||||
popoverForeground: '#ededed',
|
||||
primary: '#5b7ee3',
|
||||
primaryForeground: '#0a0a0a',
|
||||
secondary: '#262626',
|
||||
secondaryForeground: '#ededed',
|
||||
muted: '#262626',
|
||||
mutedForeground: '#a1a1aa',
|
||||
accent: '#312e81',
|
||||
accentForeground: '#e0e7ff',
|
||||
destructive: '#ef4444',
|
||||
destructiveForeground: '#ffffff',
|
||||
border: '#27272a',
|
||||
input: '#1f1f1f',
|
||||
ring: '#5b7ee3',
|
||||
|
||||
backgroundSecondary: '#1f1f1f',
|
||||
backgroundTertiary: '#262626',
|
||||
backgroundTransparentPrimary: 'rgba(255, 255, 255, 0.04)',
|
||||
backgroundTransparentSecondary: 'rgba(255, 255, 255, 0.08)',
|
||||
backgroundTransparentTertiary: 'rgba(255, 255, 255, 0.12)',
|
||||
backgroundTransparentMedium: 'rgba(255, 255, 255, 0.06)',
|
||||
backgroundTransparentLight: 'rgba(255, 255, 255, 0.03)',
|
||||
backgroundTransparentLighter: 'rgba(255, 255, 255, 0.02)',
|
||||
backgroundNoisy: '#171717',
|
||||
|
||||
fontPrimary: '#ededed',
|
||||
fontSecondary: '#a1a1aa',
|
||||
fontTertiary: '#71717a',
|
||||
fontLight: '#52525b',
|
||||
fontExtraLight: '#3f3f46',
|
||||
fontInverted: '#171717',
|
||||
|
||||
borderLight: '#262626',
|
||||
borderMedium: '#27272a',
|
||||
borderStrong: '#3f3f46',
|
||||
|
||||
statusDone: '#4ade80',
|
||||
statusProgress: '#fbbf24',
|
||||
statusOverdue: '#f87171',
|
||||
statusPaused: '#a1a1aa',
|
||||
|
||||
chart1: '#d4d4d8',
|
||||
chart2: '#4ade80',
|
||||
chart3: '#fbbf24',
|
||||
chart4: '#60a5fa',
|
||||
chart5: '#f87171',
|
||||
|
||||
sidebar: '#171717',
|
||||
sidebarForeground: '#ededed',
|
||||
sidebarPrimary: '#5b7ee3',
|
||||
sidebarPrimaryForeground: '#0a0a0a',
|
||||
sidebarAccent: '#262626',
|
||||
sidebarAccentForeground: '#ededed',
|
||||
sidebarBorder: '#27272a',
|
||||
sidebarRing: '#5b7ee3',
|
||||
},
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
lg: '16px',
|
||||
xl: '20px',
|
||||
},
|
||||
shadows: {
|
||||
sm: '0px 1px 2px 0px rgba(0, 0, 0, 0.2)',
|
||||
md: '0px 2px 4px 0px rgba(0, 0, 0, 0.2), 0px 0px 4px 0px rgba(0, 0, 0, 0.3)',
|
||||
lg: '2px 4px 16px 0px rgba(0, 0, 0, 0.35), 0px 2px 4px 0px rgba(0, 0, 0, 0.2)',
|
||||
xl: '0px 0px 8px 0px rgba(0, 0, 0, 0.3), 0px 8px 32px -8px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
font: {
|
||||
sans: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
mono: '"DM Mono", "SF Mono", "Monaco", monospace',
|
||||
size: {
|
||||
xs: '0.625rem',
|
||||
sm: '0.85rem',
|
||||
md: '0.92rem',
|
||||
lg: '1rem',
|
||||
xl: '1.23rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
unit: '4px',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
469
client/src/themes/styles/generated-skins.css
Normal file
469
client/src/themes/styles/generated-skins.css
Normal file
@@ -0,0 +1,469 @@
|
||||
/* Auto-generated from client/src/themes/skins/*.ts */
|
||||
|
||||
[data-skin="iistwin"][data-theme="light"] {
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(220 10% 15%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(220 10% 15%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(220 10% 15%);
|
||||
--primary: hsl(220 10% 15%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
--secondary: hsl(220 5% 96%);
|
||||
--secondary-foreground: hsl(220 10% 15%);
|
||||
--muted: hsl(220 5% 96%);
|
||||
--muted-foreground: hsl(220 5% 45%);
|
||||
--accent: hsl(220 5% 93%);
|
||||
--accent-foreground: hsl(220 10% 15%);
|
||||
--destructive: hsl(0 72% 51%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--border: hsl(220 5% 90%);
|
||||
--input: hsl(0 0% 100%);
|
||||
--ring: hsl(220 10% 15%);
|
||||
--tw-background-secondary: hsl(0 0% 98%);
|
||||
--tw-background-tertiary: hsl(0 0% 96%);
|
||||
--tw-background-transparent-primary: hsl(220 10% 15% / 0.04);
|
||||
--tw-background-transparent-secondary: hsl(220 10% 15% / 0.08);
|
||||
--tw-background-transparent-tertiary: hsl(220 10% 15% / 0.12);
|
||||
--tw-background-transparent-medium: hsl(220 10% 15% / 0.06);
|
||||
--tw-background-transparent-light: hsl(220 10% 15% / 0.03);
|
||||
--tw-background-transparent-lighter: hsl(220 10% 15% / 0.02);
|
||||
--tw-background-noisy: hsl(0 0% 100%);
|
||||
--tw-font-primary: hsl(220 10% 15%);
|
||||
--tw-font-secondary: hsl(220 5% 45%);
|
||||
--tw-font-tertiary: hsl(220 5% 55%);
|
||||
--tw-font-light: hsl(220 5% 65%);
|
||||
--tw-font-extra-light: hsl(220 5% 75%);
|
||||
--tw-font-inverted: hsl(0 0% 100%);
|
||||
--tw-border-light: hsl(220 5% 94%);
|
||||
--tw-border-medium: hsl(220 5% 90%);
|
||||
--tw-border-strong: hsl(220 5% 80%);
|
||||
--status-done: hsl(142 50% 45%);
|
||||
--status-progress: hsl(45 90% 50%);
|
||||
--status-overdue: hsl(0 70% 55%);
|
||||
--status-paused: hsl(220 5% 60%);
|
||||
--chart1: hsl(220 10% 25%);
|
||||
--chart2: hsl(142 50% 45%);
|
||||
--chart3: hsl(45 90% 50%);
|
||||
--chart4: hsl(200 70% 50%);
|
||||
--chart5: hsl(0 70% 55%);
|
||||
--sidebar: hsl(0 0% 100%);
|
||||
--sidebar-foreground: hsl(220 10% 15%);
|
||||
--sidebar-primary: hsl(220 10% 15%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(220 5% 96%);
|
||||
--sidebar-accent-foreground: hsl(220 10% 15%);
|
||||
--sidebar-border: hsl(220 5% 92%);
|
||||
--sidebar-ring: hsl(220 10% 15%);
|
||||
--tw-border-radius-xs: 0.125rem;
|
||||
--tw-border-radius-sm: 0.25rem;
|
||||
--tw-border-radius-md: 0.375rem;
|
||||
--tw-border-radius-lg: 0.5rem;
|
||||
--tw-border-radius-xl: 0.75rem;
|
||||
--tw-box-shadow-sm: none;
|
||||
--tw-box-shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||
--tw-box-shadow-lg: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
||||
--tw-box-shadow-xl: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.75rem;
|
||||
--tw-font-size-md: 0.8125rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.25rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 0.25rem;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-skin="iistwin"][data-theme="dark"] {
|
||||
--background: hsl(220 10% 8%);
|
||||
--foreground: hsl(220 5% 92%);
|
||||
--card: hsl(220 10% 10%);
|
||||
--card-foreground: hsl(220 5% 92%);
|
||||
--popover: hsl(220 10% 10%);
|
||||
--popover-foreground: hsl(220 5% 92%);
|
||||
--primary: hsl(0 0% 100%);
|
||||
--primary-foreground: hsl(220 10% 8%);
|
||||
--secondary: hsl(220 10% 14%);
|
||||
--secondary-foreground: hsl(220 5% 92%);
|
||||
--muted: hsl(220 10% 14%);
|
||||
--muted-foreground: hsl(220 5% 55%);
|
||||
--accent: hsl(220 10% 16%);
|
||||
--accent-foreground: hsl(220 5% 92%);
|
||||
--destructive: hsl(0 70% 55%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--border: hsl(220 10% 18%);
|
||||
--input: hsl(220 10% 12%);
|
||||
--ring: hsl(0 0% 100%);
|
||||
--tw-background-secondary: hsl(220 10% 10%);
|
||||
--tw-background-tertiary: hsl(220 10% 14%);
|
||||
--tw-background-transparent-primary: hsl(0 0% 100% / 0.04);
|
||||
--tw-background-transparent-secondary: hsl(0 0% 100% / 0.08);
|
||||
--tw-background-transparent-tertiary: hsl(0 0% 100% / 0.12);
|
||||
--tw-background-transparent-medium: hsl(0 0% 100% / 0.06);
|
||||
--tw-background-transparent-light: hsl(0 0% 100% / 0.03);
|
||||
--tw-background-transparent-lighter: hsl(0 0% 100% / 0.02);
|
||||
--tw-background-noisy: hsl(220 10% 8%);
|
||||
--tw-font-primary: hsl(220 5% 92%);
|
||||
--tw-font-secondary: hsl(220 5% 65%);
|
||||
--tw-font-tertiary: hsl(220 5% 55%);
|
||||
--tw-font-light: hsl(220 5% 45%);
|
||||
--tw-font-extra-light: hsl(220 5% 35%);
|
||||
--tw-font-inverted: hsl(220 10% 8%);
|
||||
--tw-border-light: hsl(220 10% 16%);
|
||||
--tw-border-medium: hsl(220 10% 20%);
|
||||
--tw-border-strong: hsl(220 10% 30%);
|
||||
--status-done: hsl(142 50% 55%);
|
||||
--status-progress: hsl(45 90% 55%);
|
||||
--status-overdue: hsl(0 70% 60%);
|
||||
--status-paused: hsl(220 5% 55%);
|
||||
--chart1: hsl(220 5% 70%);
|
||||
--chart2: hsl(142 50% 55%);
|
||||
--chart3: hsl(45 90% 55%);
|
||||
--chart4: hsl(200 70% 55%);
|
||||
--chart5: hsl(0 70% 60%);
|
||||
--sidebar: hsl(220 10% 8%);
|
||||
--sidebar-foreground: hsl(220 5% 92%);
|
||||
--sidebar-primary: hsl(0 0% 100%);
|
||||
--sidebar-primary-foreground: hsl(220 10% 8%);
|
||||
--sidebar-accent: hsl(220 10% 14%);
|
||||
--sidebar-accent-foreground: hsl(220 5% 92%);
|
||||
--sidebar-border: hsl(220 10% 16%);
|
||||
--sidebar-ring: hsl(0 0% 100%);
|
||||
--tw-border-radius-xs: 0.125rem;
|
||||
--tw-border-radius-sm: 0.25rem;
|
||||
--tw-border-radius-md: 0.375rem;
|
||||
--tw-border-radius-lg: 0.5rem;
|
||||
--tw-border-radius-xl: 0.75rem;
|
||||
--tw-box-shadow-sm: none;
|
||||
--tw-box-shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
|
||||
--tw-box-shadow-lg: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
|
||||
--tw-box-shadow-xl: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.75rem;
|
||||
--tw-font-size-md: 0.8125rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.25rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 0.25rem;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-skin="twenty"][data-theme="light"] {
|
||||
--background: #ffffff;
|
||||
--foreground: #333333;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #333333;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #333333;
|
||||
--primary: #3e63dd;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f4f4f5;
|
||||
--secondary-foreground: #333333;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #666666;
|
||||
--accent: #e0e7ff;
|
||||
--accent-foreground: #1e1b4b;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e4e4e7;
|
||||
--input: #ffffff;
|
||||
--ring: #3e63dd;
|
||||
--tw-background-secondary: #fcfcfc;
|
||||
--tw-background-tertiary: #f4f4f5;
|
||||
--tw-background-transparent-primary: rgba(0, 0, 0, 0.04);
|
||||
--tw-background-transparent-secondary: rgba(0, 0, 0, 0.08);
|
||||
--tw-background-transparent-tertiary: rgba(0, 0, 0, 0.12);
|
||||
--tw-background-transparent-medium: rgba(0, 0, 0, 0.06);
|
||||
--tw-background-transparent-light: rgba(0, 0, 0, 0.03);
|
||||
--tw-background-transparent-lighter: rgba(0, 0, 0, 0.02);
|
||||
--tw-background-noisy: #ffffff;
|
||||
--tw-font-primary: #333333;
|
||||
--tw-font-secondary: #666666;
|
||||
--tw-font-tertiary: #999999;
|
||||
--tw-font-light: #a1a1aa;
|
||||
--tw-font-extra-light: #c4c4c7;
|
||||
--tw-font-inverted: #ffffff;
|
||||
--tw-border-light: #f4f4f5;
|
||||
--tw-border-medium: #e4e4e7;
|
||||
--tw-border-strong: #d4d4d8;
|
||||
--status-done: #22c55e;
|
||||
--status-progress: #f59e0b;
|
||||
--status-overdue: #ef4444;
|
||||
--status-paused: #a1a1aa;
|
||||
--chart1: #333333;
|
||||
--chart2: #22c55e;
|
||||
--chart3: #f59e0b;
|
||||
--chart4: #3b82f6;
|
||||
--chart5: #ef4444;
|
||||
--sidebar: #fcfcfc;
|
||||
--sidebar-foreground: #333333;
|
||||
--sidebar-primary: #3e63dd;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #f4f4f5;
|
||||
--sidebar-accent-foreground: #333333;
|
||||
--sidebar-border: #e4e4e7;
|
||||
--sidebar-ring: #3e63dd;
|
||||
--tw-border-radius-xs: 2px;
|
||||
--tw-border-radius-sm: 4px;
|
||||
--tw-border-radius-md: 8px;
|
||||
--tw-border-radius-lg: 16px;
|
||||
--tw-border-radius-xl: 20px;
|
||||
--tw-box-shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, 0.04);
|
||||
--tw-box-shadow-md: 0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
|
||||
--tw-box-shadow-lg: 2px 4px 16px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.04);
|
||||
--tw-box-shadow-xl: 0px 0px 8px 0px rgba(0, 0, 0, 0.08), 0px 8px 32px -8px rgba(0, 0, 0, 0.12);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "DM Mono", "SF Mono", "Monaco", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.85rem;
|
||||
--tw-font-size-md: 0.92rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.23rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 4px;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
[data-skin="twenty"][data-theme="dark"] {
|
||||
--background: #171717;
|
||||
--foreground: #ededed;
|
||||
--card: #1f1f1f;
|
||||
--card-foreground: #ededed;
|
||||
--popover: #1f1f1f;
|
||||
--popover-foreground: #ededed;
|
||||
--primary: #5b7ee3;
|
||||
--primary-foreground: #0a0a0a;
|
||||
--secondary: #262626;
|
||||
--secondary-foreground: #ededed;
|
||||
--muted: #262626;
|
||||
--muted-foreground: #a1a1aa;
|
||||
--accent: #312e81;
|
||||
--accent-foreground: #e0e7ff;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #27272a;
|
||||
--input: #1f1f1f;
|
||||
--ring: #5b7ee3;
|
||||
--tw-background-secondary: #1f1f1f;
|
||||
--tw-background-tertiary: #262626;
|
||||
--tw-background-transparent-primary: rgba(255, 255, 255, 0.04);
|
||||
--tw-background-transparent-secondary: rgba(255, 255, 255, 0.08);
|
||||
--tw-background-transparent-tertiary: rgba(255, 255, 255, 0.12);
|
||||
--tw-background-transparent-medium: rgba(255, 255, 255, 0.06);
|
||||
--tw-background-transparent-light: rgba(255, 255, 255, 0.03);
|
||||
--tw-background-transparent-lighter: rgba(255, 255, 255, 0.02);
|
||||
--tw-background-noisy: #171717;
|
||||
--tw-font-primary: #ededed;
|
||||
--tw-font-secondary: #a1a1aa;
|
||||
--tw-font-tertiary: #71717a;
|
||||
--tw-font-light: #52525b;
|
||||
--tw-font-extra-light: #3f3f46;
|
||||
--tw-font-inverted: #171717;
|
||||
--tw-border-light: #262626;
|
||||
--tw-border-medium: #27272a;
|
||||
--tw-border-strong: #3f3f46;
|
||||
--status-done: #4ade80;
|
||||
--status-progress: #fbbf24;
|
||||
--status-overdue: #f87171;
|
||||
--status-paused: #a1a1aa;
|
||||
--chart1: #d4d4d8;
|
||||
--chart2: #4ade80;
|
||||
--chart3: #fbbf24;
|
||||
--chart4: #60a5fa;
|
||||
--chart5: #f87171;
|
||||
--sidebar: #171717;
|
||||
--sidebar-foreground: #ededed;
|
||||
--sidebar-primary: #5b7ee3;
|
||||
--sidebar-primary-foreground: #0a0a0a;
|
||||
--sidebar-accent: #262626;
|
||||
--sidebar-accent-foreground: #ededed;
|
||||
--sidebar-border: #27272a;
|
||||
--sidebar-ring: #5b7ee3;
|
||||
--tw-border-radius-xs: 2px;
|
||||
--tw-border-radius-sm: 4px;
|
||||
--tw-border-radius-md: 8px;
|
||||
--tw-border-radius-lg: 16px;
|
||||
--tw-border-radius-xl: 20px;
|
||||
--tw-box-shadow-sm: 0px 1px 2px 0px rgba(0, 0, 0, 0.2);
|
||||
--tw-box-shadow-md: 0px 2px 4px 0px rgba(0, 0, 0, 0.2), 0px 0px 4px 0px rgba(0, 0, 0, 0.3);
|
||||
--tw-box-shadow-lg: 2px 4px 16px 0px rgba(0, 0, 0, 0.35), 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
|
||||
--tw-box-shadow-xl: 0px 0px 8px 0px rgba(0, 0, 0, 0.3), 0px 8px 32px -8px rgba(0, 0, 0, 0.4);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "DM Mono", "SF Mono", "Monaco", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.85rem;
|
||||
--tw-font-size-md: 0.92rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.23rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 4px;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
[data-skin="omegaprom"][data-theme="light"] {
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(220 15% 18%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(220 15% 18%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(220 15% 18%);
|
||||
--primary: hsl(221 83% 53%);
|
||||
--primary-foreground: hsl(0 0% 100%);
|
||||
--secondary: hsl(217 91% 96%);
|
||||
--secondary-foreground: hsl(221 83% 40%);
|
||||
--muted: hsl(220 14% 96%);
|
||||
--muted-foreground: hsl(220 9% 46%);
|
||||
--accent: hsl(214 100% 92%);
|
||||
--accent-foreground: hsl(221 83% 40%);
|
||||
--destructive: hsl(0 72% 51%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--border: hsl(220 13% 90%);
|
||||
--input: hsl(0 0% 100%);
|
||||
--ring: hsl(221 83% 53%);
|
||||
--tw-background-secondary: hsl(220 20% 98%);
|
||||
--tw-background-tertiary: hsl(220 14% 96%);
|
||||
--tw-background-transparent-primary: hsl(221 83% 53% / 0.04);
|
||||
--tw-background-transparent-secondary: hsl(221 83% 53% / 0.08);
|
||||
--tw-background-transparent-tertiary: hsl(221 83% 53% / 0.12);
|
||||
--tw-background-transparent-medium: hsl(220 15% 18% / 0.06);
|
||||
--tw-background-transparent-light: hsl(220 15% 18% / 0.03);
|
||||
--tw-background-transparent-lighter: hsl(220 15% 18% / 0.02);
|
||||
--tw-background-noisy: hsl(0 0% 100%);
|
||||
--tw-font-primary: hsl(220 15% 18%);
|
||||
--tw-font-secondary: hsl(220 9% 46%);
|
||||
--tw-font-tertiary: hsl(220 9% 56%);
|
||||
--tw-font-light: hsl(220 9% 66%);
|
||||
--tw-font-extra-light: hsl(220 9% 76%);
|
||||
--tw-font-inverted: hsl(0 0% 100%);
|
||||
--tw-border-light: hsl(220 13% 94%);
|
||||
--tw-border-medium: hsl(220 13% 90%);
|
||||
--tw-border-strong: hsl(220 13% 80%);
|
||||
--status-done: hsl(142 55% 45%);
|
||||
--status-progress: hsl(45 90% 50%);
|
||||
--status-overdue: hsl(0 70% 55%);
|
||||
--status-paused: hsl(220 9% 60%);
|
||||
--chart1: hsl(221 83% 53%);
|
||||
--chart2: hsl(142 55% 45%);
|
||||
--chart3: hsl(45 90% 50%);
|
||||
--chart4: hsl(200 70% 50%);
|
||||
--chart5: hsl(0 70% 55%);
|
||||
--sidebar: hsl(220 20% 98%);
|
||||
--sidebar-foreground: hsl(220 15% 18%);
|
||||
--sidebar-primary: hsl(221 83% 53%);
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(220 14% 96%);
|
||||
--sidebar-accent-foreground: hsl(220 15% 18%);
|
||||
--sidebar-border: hsl(220 13% 90%);
|
||||
--sidebar-ring: hsl(221 83% 53%);
|
||||
--tw-border-radius-xs: 0.125rem;
|
||||
--tw-border-radius-sm: 0.25rem;
|
||||
--tw-border-radius-md: 0.5rem;
|
||||
--tw-border-radius-lg: 0.75rem;
|
||||
--tw-border-radius-xl: 1rem;
|
||||
--tw-box-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||
--tw-box-shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
--tw-box-shadow-lg: 0 4px 8px -2px rgba(0, 0, 0, 0.06);
|
||||
--tw-box-shadow-xl: 0 8px 16px -4px rgba(0, 0, 0, 0.08);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.75rem;
|
||||
--tw-font-size-md: 0.875rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.25rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 0.25rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
[data-skin="omegaprom"][data-theme="dark"] {
|
||||
--background: hsl(220 20% 8%);
|
||||
--foreground: hsl(220 10% 92%);
|
||||
--card: hsl(220 20% 10%);
|
||||
--card-foreground: hsl(220 10% 92%);
|
||||
--popover: hsl(220 20% 10%);
|
||||
--popover-foreground: hsl(220 10% 92%);
|
||||
--primary: hsl(217 91% 60%);
|
||||
--primary-foreground: hsl(220 20% 8%);
|
||||
--secondary: hsl(220 20% 16%);
|
||||
--secondary-foreground: hsl(220 10% 92%);
|
||||
--muted: hsl(220 20% 14%);
|
||||
--muted-foreground: hsl(220 10% 60%);
|
||||
--accent: hsl(220 20% 18%);
|
||||
--accent-foreground: hsl(220 10% 92%);
|
||||
--destructive: hsl(0 70% 55%);
|
||||
--destructive-foreground: hsl(0 0% 100%);
|
||||
--border: hsl(220 20% 18%);
|
||||
--input: hsl(220 20% 12%);
|
||||
--ring: hsl(217 91% 60%);
|
||||
--tw-background-secondary: hsl(220 20% 10%);
|
||||
--tw-background-tertiary: hsl(220 20% 14%);
|
||||
--tw-background-transparent-primary: hsl(217 91% 60% / 0.04);
|
||||
--tw-background-transparent-secondary: hsl(217 91% 60% / 0.08);
|
||||
--tw-background-transparent-tertiary: hsl(217 91% 60% / 0.12);
|
||||
--tw-background-transparent-medium: hsl(0 0% 100% / 0.06);
|
||||
--tw-background-transparent-light: hsl(0 0% 100% / 0.03);
|
||||
--tw-background-transparent-lighter: hsl(0 0% 100% / 0.02);
|
||||
--tw-background-noisy: hsl(220 20% 8%);
|
||||
--tw-font-primary: hsl(220 10% 92%);
|
||||
--tw-font-secondary: hsl(220 10% 65%);
|
||||
--tw-font-tertiary: hsl(220 10% 55%);
|
||||
--tw-font-light: hsl(220 10% 45%);
|
||||
--tw-font-extra-light: hsl(220 10% 35%);
|
||||
--tw-font-inverted: hsl(220 20% 8%);
|
||||
--tw-border-light: hsl(220 20% 16%);
|
||||
--tw-border-medium: hsl(220 20% 20%);
|
||||
--tw-border-strong: hsl(220 20% 30%);
|
||||
--status-done: hsl(142 55% 55%);
|
||||
--status-progress: hsl(45 90% 55%);
|
||||
--status-overdue: hsl(0 70% 60%);
|
||||
--status-paused: hsl(220 10% 55%);
|
||||
--chart1: hsl(217 91% 60%);
|
||||
--chart2: hsl(142 55% 55%);
|
||||
--chart3: hsl(45 90% 55%);
|
||||
--chart4: hsl(200 70% 55%);
|
||||
--chart5: hsl(0 70% 60%);
|
||||
--sidebar: hsl(220 20% 8%);
|
||||
--sidebar-foreground: hsl(220 10% 92%);
|
||||
--sidebar-primary: hsl(217 91% 60%);
|
||||
--sidebar-primary-foreground: hsl(220 20% 8%);
|
||||
--sidebar-accent: hsl(220 20% 14%);
|
||||
--sidebar-accent-foreground: hsl(220 10% 92%);
|
||||
--sidebar-border: hsl(220 20% 16%);
|
||||
--sidebar-ring: hsl(217 91% 60%);
|
||||
--tw-border-radius-xs: 0.125rem;
|
||||
--tw-border-radius-sm: 0.25rem;
|
||||
--tw-border-radius-md: 0.5rem;
|
||||
--tw-border-radius-lg: 0.75rem;
|
||||
--tw-border-radius-xl: 1rem;
|
||||
--tw-box-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
--tw-box-shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
|
||||
--tw-box-shadow-lg: 0 4px 8px -2px rgba(0, 0, 0, 0.3);
|
||||
--tw-box-shadow-xl: 0 8px 16px -4px rgba(0, 0, 0, 0.35);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
|
||||
--tw-font-size-xs: 0.625rem;
|
||||
--tw-font-size-sm: 0.75rem;
|
||||
--tw-font-size-md: 0.875rem;
|
||||
--tw-font-size-lg: 1rem;
|
||||
--tw-font-size-xl: 1.25rem;
|
||||
--tw-font-weight-regular: 400;
|
||||
--tw-font-weight-medium: 500;
|
||||
--tw-font-weight-semibold: 600;
|
||||
--tw-spacing-unit: 0.25rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
290
client/src/themes/tokens.ts
Normal file
290
client/src/themes/tokens.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Единый набор дизайн-токенов для skin-движка iistwin.
|
||||
* Каждый скин содержит tokens.light и tokens.dark.
|
||||
* Токены flatten-ятся в CSS-переменные.
|
||||
*/
|
||||
|
||||
export interface Skin {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
isEditable?: boolean;
|
||||
tokens: {
|
||||
light: SkinThemeTokens;
|
||||
dark: SkinThemeTokens;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SkinThemeTokens {
|
||||
colors: ColorTokens;
|
||||
radius: RadiusTokens;
|
||||
shadows: ShadowTokens;
|
||||
font: FontTokens;
|
||||
spacing: SpacingTokens;
|
||||
}
|
||||
|
||||
export interface ColorTokens {
|
||||
// shadcn/ui base tokens
|
||||
background: string;
|
||||
foreground: string;
|
||||
card: string;
|
||||
cardForeground: string;
|
||||
popover: string;
|
||||
popoverForeground: string;
|
||||
primary: string;
|
||||
primaryForeground: string;
|
||||
secondary: string;
|
||||
secondaryForeground: string;
|
||||
muted: string;
|
||||
mutedForeground: string;
|
||||
accent: string;
|
||||
accentForeground: string;
|
||||
destructive: string;
|
||||
destructiveForeground: string;
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
|
||||
// Twenty-style extended tokens
|
||||
backgroundSecondary: string;
|
||||
backgroundTertiary: string;
|
||||
backgroundTransparentPrimary: string;
|
||||
backgroundTransparentSecondary: string;
|
||||
backgroundTransparentTertiary: string;
|
||||
backgroundTransparentMedium: string;
|
||||
backgroundTransparentLight: string;
|
||||
backgroundTransparentLighter: string;
|
||||
backgroundNoisy: string;
|
||||
|
||||
fontPrimary: string;
|
||||
fontSecondary: string;
|
||||
fontTertiary: string;
|
||||
fontLight: string;
|
||||
fontExtraLight: string;
|
||||
fontInverted: string;
|
||||
|
||||
borderLight: string;
|
||||
borderMedium: string;
|
||||
borderStrong: string;
|
||||
|
||||
// Status colors
|
||||
statusDone: string;
|
||||
statusProgress: string;
|
||||
statusOverdue: string;
|
||||
statusPaused: string;
|
||||
|
||||
// Chart colors
|
||||
chart1: string;
|
||||
chart2: string;
|
||||
chart3: string;
|
||||
chart4: string;
|
||||
chart5: string;
|
||||
|
||||
// Sidebar colors
|
||||
sidebar: string;
|
||||
sidebarForeground: string;
|
||||
sidebarPrimary: string;
|
||||
sidebarPrimaryForeground: string;
|
||||
sidebarAccent: string;
|
||||
sidebarAccentForeground: string;
|
||||
sidebarBorder: string;
|
||||
sidebarRing: string;
|
||||
}
|
||||
|
||||
export interface RadiusTokens {
|
||||
xs: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
}
|
||||
|
||||
export interface ShadowTokens {
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
}
|
||||
|
||||
export interface FontTokens {
|
||||
sans: string;
|
||||
mono: string;
|
||||
size: {
|
||||
xs: string;
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
};
|
||||
weight: {
|
||||
regular: number;
|
||||
medium: number;
|
||||
semibold: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SpacingTokens {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
/** Список редактируемых цветовых токенов для UI-редактора. */
|
||||
export const EDITABLE_COLOR_TOKENS: Array<{ key: string; label: string; category: string }> = [
|
||||
{ key: 'colors.background', label: 'Фон', category: 'Base' },
|
||||
{ key: 'colors.foreground', label: 'Текст', category: 'Base' },
|
||||
{ key: 'colors.card', label: 'Фон карточки', category: 'Base' },
|
||||
{ key: 'colors.cardForeground', label: 'Текст карточки', category: 'Base' },
|
||||
{ key: 'colors.popover', label: 'Фон поповера', category: 'Base' },
|
||||
{ key: 'colors.popoverForeground', label: 'Текст поповера', category: 'Base' },
|
||||
{ key: 'colors.primary', label: 'Primary', category: 'Base' },
|
||||
{ key: 'colors.primaryForeground', label: 'Primary текст', category: 'Base' },
|
||||
{ key: 'colors.secondary', label: 'Secondary', category: 'Base' },
|
||||
{ key: 'colors.secondaryForeground', label: 'Secondary текст', category: 'Base' },
|
||||
{ key: 'colors.muted', label: 'Muted', category: 'Base' },
|
||||
{ key: 'colors.mutedForeground', label: 'Muted текст', category: 'Base' },
|
||||
{ key: 'colors.accent', label: 'Accent', category: 'Base' },
|
||||
{ key: 'colors.accentForeground', label: 'Accent текст', category: 'Base' },
|
||||
{ key: 'colors.destructive', label: 'Destructive', category: 'Base' },
|
||||
{ key: 'colors.destructiveForeground', label: 'Destructive текст', category: 'Base' },
|
||||
{ key: 'colors.border', label: 'Граница', category: 'Base' },
|
||||
{ key: 'colors.input', label: 'Фон input', category: 'Base' },
|
||||
{ key: 'colors.ring', label: 'Кольцо фокуса', category: 'Base' },
|
||||
|
||||
{ key: 'colors.backgroundSecondary', label: 'Secondary фон', category: 'Twenty' },
|
||||
{ key: 'colors.backgroundTertiary', label: 'Tertiary фон', category: 'Twenty' },
|
||||
{ key: 'colors.fontPrimary', label: 'Primary текст', category: 'Twenty' },
|
||||
{ key: 'colors.fontSecondary', label: 'Secondary текст', category: 'Twenty' },
|
||||
{ key: 'colors.fontTertiary', label: 'Tertiary текст', category: 'Twenty' },
|
||||
{ key: 'colors.borderLight', label: 'Light граница', category: 'Twenty' },
|
||||
{ key: 'colors.borderMedium', label: 'Medium граница', category: 'Twenty' },
|
||||
{ key: 'colors.borderStrong', label: 'Strong граница', category: 'Twenty' },
|
||||
|
||||
{ key: 'colors.statusDone', label: 'Готово', category: 'Status' },
|
||||
{ key: 'colors.statusProgress', label: 'В работе', category: 'Status' },
|
||||
{ key: 'colors.statusOverdue', label: 'Просрочено', category: 'Status' },
|
||||
{ key: 'colors.statusPaused', label: 'Приостановлено', category: 'Status' },
|
||||
|
||||
{ key: 'colors.sidebar', label: 'Фон сайдбара', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarForeground', label: 'Текст сайдбара', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarPrimary', label: 'Sidebar primary', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarPrimaryForeground', label: 'Sidebar primary текст', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarAccent', label: 'Sidebar accent', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarAccentForeground', label: 'Sidebar accent текст', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarBorder', label: 'Граница сайдбара', category: 'Sidebar' },
|
||||
{ key: 'colors.sidebarRing', label: 'Кольцо сайдбара', category: 'Sidebar' },
|
||||
];
|
||||
|
||||
/** Список редактируемых числовых токенов (радиусы). */
|
||||
export const EDITABLE_RADIUS_TOKENS: Array<{ key: string; label: string }> = [
|
||||
{ key: 'radius.xs', label: 'XS radius' },
|
||||
{ key: 'radius.sm', label: 'SM radius' },
|
||||
{ key: 'radius.md', label: 'MD radius' },
|
||||
{ key: 'radius.lg', label: 'LG radius' },
|
||||
{ key: 'radius.xl', label: 'XL radius' },
|
||||
];
|
||||
|
||||
/** Список редактируемых теней. */
|
||||
export const EDITABLE_SHADOW_TOKENS: Array<{ key: string; label: string }> = [
|
||||
{ key: 'shadows.sm', label: 'SM shadow' },
|
||||
{ key: 'shadows.md', label: 'MD shadow' },
|
||||
{ key: 'shadows.lg', label: 'LG shadow' },
|
||||
{ key: 'shadows.xl', label: 'XL shadow' },
|
||||
];
|
||||
|
||||
const SHADCN_COLOR_VARS = new Set([
|
||||
'background', 'foreground', 'card', 'cardForeground', 'popover', 'popoverForeground',
|
||||
'primary', 'primaryForeground', 'secondary', 'secondaryForeground',
|
||||
'muted', 'mutedForeground', 'accent', 'accentForeground',
|
||||
'destructive', 'destructiveForeground', 'border', 'input', 'ring',
|
||||
]);
|
||||
|
||||
const STATUS_VARS = new Set(['statusDone', 'statusProgress', 'statusOverdue', 'statusPaused']);
|
||||
const CHART_VARS = new Set(['chart1', 'chart2', 'chart3', 'chart4', 'chart5']);
|
||||
const SIDEBAR_VARS = new Set([
|
||||
'sidebar', 'sidebarForeground', 'sidebarPrimary', 'sidebarPrimaryForeground',
|
||||
'sidebarAccent', 'sidebarAccentForeground', 'sidebarBorder', 'sidebarRing',
|
||||
]);
|
||||
|
||||
const camelToKebab = (s: string) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
||||
|
||||
/** Преобразует путь токена в имя CSS-переменной.
|
||||
* shadcn/status/chart/sidebar цвета остаются без префикса;
|
||||
* Twenty-style tokens получают префикс --tw-*.
|
||||
*/
|
||||
export function tokenPathToCssVar(path: string[]): string {
|
||||
if (path[0] === 'colors') {
|
||||
const colorName = path[1];
|
||||
if (
|
||||
SHADCN_COLOR_VARS.has(colorName) ||
|
||||
STATUS_VARS.has(colorName) ||
|
||||
CHART_VARS.has(colorName) ||
|
||||
SIDEBAR_VARS.has(colorName)
|
||||
) {
|
||||
return '--' + camelToKebab(colorName);
|
||||
}
|
||||
return '--tw-' + camelToKebab(colorName);
|
||||
}
|
||||
|
||||
if (path[0] === 'radius') {
|
||||
return '--tw-border-radius-' + camelToKebab(path[1]);
|
||||
}
|
||||
|
||||
if (path[0] === 'shadows') {
|
||||
return '--tw-box-shadow-' + camelToKebab(path[1]);
|
||||
}
|
||||
|
||||
if (path[0] === 'font') {
|
||||
if (path[1] === 'sans') return '--font-sans';
|
||||
if (path[1] === 'mono') return '--font-mono';
|
||||
if (path[1] === 'size') return '--tw-font-size-' + camelToKebab(path[2]);
|
||||
if (path[1] === 'weight') return '--tw-font-weight-' + camelToKebab(path[2]);
|
||||
}
|
||||
|
||||
if (path[0] === 'spacing') {
|
||||
return '--tw-spacing-' + camelToKebab(path[1]);
|
||||
}
|
||||
|
||||
return '--' + path.map(camelToKebab).join('-');
|
||||
}
|
||||
|
||||
/** Flatten объекта токенов в плоскую карту CSS-переменных. */
|
||||
export function flattenTokens(
|
||||
tokens: object,
|
||||
prefix: string[] = [],
|
||||
): Record<string, string | number> {
|
||||
const result: Record<string, string | number> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(tokens)) {
|
||||
const path = [...prefix, key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
Object.assign(result, flattenTokens(value as Record<string, unknown>, path));
|
||||
} else {
|
||||
result[tokenPathToCssVar(path)] = value as string | number;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Генерирует CSS-блок для скина и темы. */
|
||||
export function generateSkinCss(skin: Skin, theme: 'light' | 'dark'): string {
|
||||
const themeTokens = skin.tokens[theme];
|
||||
const vars = flattenTokens(themeTokens);
|
||||
// shadcn/ui использует --radius как базовый радиус lg.
|
||||
vars['--radius'] = themeTokens.radius.lg;
|
||||
const lines = Object.entries(vars)
|
||||
.map(([name, value]) => ` ${name}: ${value};`)
|
||||
.join('\n');
|
||||
|
||||
return `[data-skin="${skin.id}"][data-theme="${theme}"] {\n${lines}\n}`;
|
||||
}
|
||||
|
||||
/** Генерирует CSS для всех скинов и тем. */
|
||||
export function generateAllSkinsCss(skins: Skin[]): string {
|
||||
return skins
|
||||
.flatMap((skin) => [
|
||||
generateSkinCss(skin, 'light'),
|
||||
generateSkinCss(skin, 'dark'),
|
||||
])
|
||||
.join('\n\n');
|
||||
}
|
||||
Reference in New Issue
Block a user