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:
2026-07-09 20:25:49 +03:00
parent 7ed89f7f97
commit 95279b8fcc
24 changed files with 2562 additions and 223 deletions

View File

@@ -1,12 +1,17 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap');
@import './themes/styles/generated-skins.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
* Fallback переменные для случая, когда data-skin/data-theme ещё не применены
* (например, во время гидратации). Основные значения берутся из generated-skins.css.
*/
:root {
/* Light Theme - Ultra Minimal */
--background: hsl(0 0% 100%);
--foreground: hsl(220 10% 15%);
--card: hsl(0 0% 100%);
@@ -26,21 +31,18 @@
--border: hsl(220 5% 90%);
--input: hsl(0 0% 100%);
--ring: hsl(220 10% 15%);
/* Status colors - muted */
--status-done: hsl(142 50% 45%);
--status-progress: hsl(45 90% 50%);
--status-overdue: hsl(0 70% 55%);
--status-paused: hsl(220 5% 60%);
/* Chart colors */
--chart-1: hsl(220 10% 25%);
--chart-2: hsl(142 50% 45%);
--chart-3: hsl(45 90% 50%);
--chart-4: hsl(200 70% 50%);
--chart-5: hsl(0 70% 55%);
/* Sidebar - clean */
--sidebar: hsl(0 0% 100%);
--sidebar-foreground: hsl(220 10% 15%);
--sidebar-primary: hsl(220 10% 15%);
@@ -49,15 +51,12 @@
--sidebar-accent-foreground: hsl(220 10% 15%);
--sidebar-border: hsl(220 5% 92%);
--sidebar-ring: hsl(220 10% 15%);
/* Typography */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
/* Minimal radius */
--font-mono: "DM Mono", "SF Mono", "Monaco", "Inconsolata", monospace;
--radius: 0.25rem;
/* No shadows */
--shadow-2xs: none;
--shadow-xs: none;
--shadow-sm: none;
@@ -66,19 +65,17 @@
--shadow-lg: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
--shadow-xl: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
--shadow-2xl: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
--tracking-normal: -0.01em;
--spacing: 0.25rem;
/* Finance elevation system */
--elevate-1: rgba(0,0,0,.03);
--elevate-2: rgba(0,0,0,.08);
--elevate-1: rgba(0, 0, 0, 0.03);
--elevate-2: rgba(0, 0, 0, 0.08);
}
.dark {
--elevate-1: rgba(255,255,255,.03);
--elevate-2: rgba(255,255,255,.08);
/* Dark Theme - Ultra Minimal */
--elevate-1: rgba(255, 255, 255, 0.03);
--elevate-2: rgba(255, 255, 255, 0.08);
--background: hsl(220 10% 8%);
--foreground: hsl(220 5% 92%);
--card: hsl(220 10% 10%);
@@ -98,21 +95,18 @@
--border: hsl(220 10% 18%);
--input: hsl(220 10% 12%);
--ring: hsl(0 0% 100%);
/* Status colors - brighter for dark mode */
--status-done: hsl(142 50% 55%);
--status-progress: hsl(45 90% 55%);
--status-overdue: hsl(0 70% 60%);
--status-paused: hsl(220 5% 55%);
/* Chart colors - brighter */
--chart-1: hsl(220 5% 70%);
--chart-2: hsl(142 50% 55%);
--chart-3: hsl(45 90% 55%);
--chart-4: hsl(200 70% 55%);
--chart-5: hsl(0 70% 60%);
/* Sidebar */
--sidebar: hsl(220 10% 8%);
--sidebar-foreground: hsl(220 5% 92%);
--sidebar-primary: hsl(0 0% 100%);
@@ -121,26 +115,13 @@
--sidebar-accent-foreground: hsl(220 5% 92%);
--sidebar-border: hsl(220 10% 16%);
--sidebar-ring: hsl(0 0% 100%);
/* Typography */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "SF Mono", "Monaco", "Inconsolata", monospace;
/* Minimal radius */
--radius: 0.25rem;
/* Subtle shadows for dark mode */
--shadow-2xs: none;
--shadow-xs: none;
--shadow-sm: none;
--shadow: none;
--shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
--shadow-lg: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
--shadow-xl: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
--shadow-2xl: 0 8px 10px -2px rgba(0, 0, 0, 0.25);
--spacing: 0.25rem;
--tracking-normal: -0.01em;
}
@layer base {
@@ -249,15 +230,18 @@
}
.status-badge-done {
@apply bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400;
background-color: color-mix(in srgb, var(--status-done) 12%, transparent);
color: var(--status-done);
}
.status-badge-progress {
@apply bg-yellow-50 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400;
background-color: color-mix(in srgb, var(--status-progress) 12%, transparent);
color: var(--status-progress);
}
.status-badge-overdue {
@apply bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400;
background-color: color-mix(in srgb, var(--status-overdue) 12%, transparent);
color: var(--status-overdue);
}
/* Nav item */
@@ -392,13 +376,8 @@
/* jspreadsheet formula cell highlight */
td.formula-highlight {
background-color: rgba(34, 34, 34, 0.1) !important;
border: 1px solid hsl(220 10% 40%) !important;
}
.dark td.formula-highlight {
background-color: rgba(255, 255, 255, 0.1) !important;
border: 1px solid hsl(220 5% 60%) !important;
background-color: var(--tw-background-transparent-secondary) !important;
border: 1px solid var(--tw-border-strong) !important;
}
/* Mobile bottom navigation safe area */
@@ -423,20 +402,12 @@ td.formula-highlight {
}
::-webkit-scrollbar-thumb {
background: hsl(220 5% 80%);
background: var(--tw-border-strong);
border-radius: 3px;
}
.dark ::-webkit-scrollbar-thumb {
background: hsl(220 10% 25%);
}
::-webkit-scrollbar-thumb:hover {
background: hsl(220 5% 70%);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: hsl(220 10% 35%);
background: var(--tw-font-extra-light);
}
/* Focus visible — removed per user request */
@@ -451,9 +422,5 @@ td.formula-highlight {
/* Selection */
::selection {
background-color: hsla(220, 10%, 15%, 0.15);
}
.dark ::selection {
background-color: hsla(0, 0%, 100%, 0.15);
background-color: var(--tw-background-transparent-secondary);
}