omnieye-theme-studio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.github/workflows/ci.yml +88 -0
  3. package/.github/workflows/promote.yml +48 -0
  4. package/Dockerfile +20 -0
  5. package/README.md +52 -0
  6. package/dist/assets/index-B0UK7Lt_.js +200 -0
  7. package/dist/index.html +18 -0
  8. package/index.html +18 -0
  9. package/nginx.conf +15 -0
  10. package/package.json +26 -0
  11. package/src/App.tsx +79 -0
  12. package/src/chrome/Footer.tsx +21 -0
  13. package/src/chrome/IconRail.tsx +49 -0
  14. package/src/chrome/StudioSubHeader.tsx +14 -0
  15. package/src/chrome/TopBar.tsx +35 -0
  16. package/src/main.tsx +23 -0
  17. package/src/panels/CardsPanel.tsx +75 -0
  18. package/src/panels/ColourPanel.tsx +175 -0
  19. package/src/panels/LoginPanel.tsx +62 -0
  20. package/src/panels/NotificationsPanel.tsx +37 -0
  21. package/src/panels/SurfacePanel.tsx +66 -0
  22. package/src/panels/TablesPanel.tsx +57 -0
  23. package/src/panels/ThemeManagement.tsx +178 -0
  24. package/src/panels/TypographyPanel.tsx +154 -0
  25. package/src/preview/CamerasPreview.tsx +140 -0
  26. package/src/preview/ComponentsPreview.tsx +146 -0
  27. package/src/preview/DashboardPreview.tsx +138 -0
  28. package/src/preview/LoginPreview.tsx +111 -0
  29. package/src/preview/OperationsPreview.tsx +103 -0
  30. package/src/preview/OrdersPreview.tsx +108 -0
  31. package/src/preview/PreviewPane.tsx +111 -0
  32. package/src/preview/ReportsPreview.tsx +140 -0
  33. package/src/preview/ToastLayer.tsx +34 -0
  34. package/src/preview/useFitCount.ts +68 -0
  35. package/src/shared/ChipGroup.tsx +52 -0
  36. package/src/shared/CropModal.tsx +73 -0
  37. package/src/shared/InfoBadge.tsx +13 -0
  38. package/src/shared/SectionAccordion.tsx +89 -0
  39. package/src/shared/StudioSlider.tsx +103 -0
  40. package/src/shared/SwatchPicker.tsx +44 -0
  41. package/src/shared/TabStrip.tsx +51 -0
  42. package/src/store/useStudioUi.ts +47 -0
  43. package/src/store/useThemeStore.ts +103 -0
  44. package/src/studio/StudioRail.tsx +167 -0
  45. package/src/theme/buildMuiTheme.ts +53 -0
  46. package/src/theme/color.ts +68 -0
  47. package/src/theme/presets.ts +17 -0
  48. package/src/theme/settings.ts +158 -0
  49. package/src/theme/studioTokens.ts +24 -0
  50. package/src/theme/tokens.ts +243 -0
  51. package/src/theme/useTokens.ts +10 -0
  52. package/tsconfig.json +23 -0
  53. package/tsconfig.tsbuildinfo +1 -0
  54. package/vite.config.ts +7 -0
@@ -0,0 +1,167 @@
1
+ import { Box, Typography, IconButton, Tooltip } from '@mui/material';
2
+ import type { ReactNode } from 'react';
3
+ import RestartAltRoundedIcon from '@mui/icons-material/RestartAltRounded';
4
+ import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined';
5
+ import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
6
+ import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
7
+ import { SectionAccordion } from '../shared/SectionAccordion';
8
+ import { ThemeManagement } from '../panels/ThemeManagement';
9
+ import { ColourPanel } from '../panels/ColourPanel';
10
+ import { TypographyPanel } from '../panels/TypographyPanel';
11
+ import { CardsPanel } from '../panels/CardsPanel';
12
+ import { TablesPanel } from '../panels/TablesPanel';
13
+ import { SurfacePanel } from '../panels/SurfacePanel';
14
+ import { NotificationsPanel } from '../panels/NotificationsPanel';
15
+ import { LoginPanel } from '../panels/LoginPanel';
16
+ import { useStudioUi, type ScreenKey, PREVIEW_TABS } from '../store/useStudioUi';
17
+ import { useThemeStore } from '../store/useThemeStore';
18
+ import { PRESETS } from '../theme/presets';
19
+ import { studioTokens } from '../theme/studioTokens';
20
+ import { useRef } from 'react';
21
+
22
+ interface Sec { id: string; title: string; info: string; icon: ReactNode; screen: ScreenKey; body: ReactNode; }
23
+
24
+ // Studio rail: active-theme header + all sections in HTML order (one open at a time).
25
+ export function StudioRail({ onClose }: { onClose?: () => void }) {
26
+ const openId = useStudioUi((s) => s.openSection);
27
+ const setOpen = useStudioUi((s) => s.setOpenSection);
28
+ const setScreen = useStudioUi((s) => s.setPreviewScreen);
29
+ const curScreen = useStudioUi((s) => s.previewScreen);
30
+ const previewMenuOpen = useStudioUi((s) => s.previewMenuOpen);
31
+ const setPreviewMenuOpen = useStudioUi((s) => s.setPreviewMenuOpen);
32
+ const settings = useThemeStore((s) => s.settings);
33
+ const saved = useThemeStore((s) => s.savedThemes);
34
+ const set = useThemeStore((s) => s.set);
35
+ const reset = useThemeStore((s) => s.reset);
36
+ const sk = studioTokens(settings.mode);
37
+ const lastAuto = useRef<string>('theme');
38
+
39
+ // Opening a section auto-switches the preview (guarded so re-opening the same section won't yank you back).
40
+ const toggle = (id: string, screen: ScreenKey) => {
41
+ const willOpen = openId !== id;
42
+ setOpen(willOpen ? id : '');
43
+ setPreviewMenuOpen(false);
44
+ if (willOpen && lastAuto.current !== id) { setScreen(screen); lastAuto.current = id; }
45
+ };
46
+
47
+ const activeName =
48
+ PRESETS.find((p) => p.id === settings.presetId)?.name ||
49
+ saved.find((z) => z.id === settings.presetId)?.name ||
50
+ 'Custom Theme';
51
+
52
+ // Section icons — inline SVGs ported 1:1 from the HTML (18×18, stroke = studio body colour).
53
+ const strokeProps = { fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' } as const;
54
+ const Svg = ({ children }: { children: ReactNode }) => <svg width="18" height="18" viewBox="0 0 24 24" {...strokeProps}>{children}</svg>;
55
+
56
+ const icoTheme = <Svg><circle cx="13.5" cy="6.5" r="2.5" /><circle cx="17.5" cy="10.5" r="2.5" /><circle cx="8.5" cy="7.5" r="2.5" /><circle cx="6.5" cy="12.5" r="2.5" /><path d="M12 2a10 10 0 0 0 0 20 3 3 0 0 0 0-6 2 2 0 0 1 0-4 4 4 0 0 0 4-4 6 6 0 0 0-4-6z" /></Svg>;
57
+ const icoColor = (
58
+ <Box sx={{ display: 'flex', gap: '2px', width: 14, height: 14 }}>
59
+ <Box sx={{ flex: 1, background: settings.secondaryHex, borderRadius: '2px' }} />
60
+ <Box sx={{ flex: 1, background: settings.errorHex, borderRadius: '2px' }} />
61
+ <Box sx={{ flex: 1, background: settings.successHex, borderRadius: '2px' }} />
62
+ </Box>
63
+ );
64
+ const icoType = <Svg><path d="M4 20 9 5l5 15M6 14h6" /><path d="M15 20l3-9 3 9M16.2 17h3.6" /></Svg>;
65
+ const icoCards = <Svg><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M3 9h18" /></Svg>;
66
+ const icoTables = <Svg><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M3 9h18M3 15h18M9 3v18" /></Svg>;
67
+ const icoShape = <Svg><rect x="4" y="4" width="16" height="16" rx="4" /></Svg>;
68
+ const icoNotif = (
69
+ <>
70
+ <Svg><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10 21a2 2 0 0 0 4 0" /></Svg>
71
+ <Box sx={{ position: 'absolute', top: '6px', right: '6px', width: 7, height: 7, borderRadius: '50%', background: settings.primaryHex, border: `1.5px solid ${sk.iconTile}` }} />
72
+ </>
73
+ );
74
+ const icoLogin = <Svg><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" /><path d="M10 17l5-5-5-5" /><path d="M15 12H3" /></Svg>;
75
+
76
+ // Dynamic header summaries — shown as the info-dot tooltip, 1:1 with the HTML *Summary values.
77
+ const isDark = settings.mode === 'dark';
78
+ const cap = (x: string) => x.charAt(0).toUpperCase() + x.slice(1);
79
+ const SIZE_L: Record<string, string> = { sm: 'Small', df: 'Default', lg: 'Large' };
80
+ const RAD_L: Record<string, string> = { sharp: 'Sharp', rounded: 'Rounded', round: 'Round' };
81
+ const DENS_L: Record<string, string> = { compact: 'Compact', default: 'Default', comfortable: 'Comfortable' };
82
+ const CARD_L: Record<string, string> = { flat: 'Flat', outlined: 'Outlined', soft: 'Soft', raised: 'Raised', elevated: 'Elevated', floating: 'Floating' };
83
+ const LOGIN_L: Record<string, string> = { split: 'Split Brand', centered: 'Centered Card' };
84
+ const NPOS_L: Record<string, string> = { 'top-center': 'Top Center', 'top-right': 'Top Right', 'bottom-left': 'Bottom Left', 'bottom-right': 'Bottom Right' };
85
+ const summary: Record<string, string> = {
86
+ theme: `${activeName} · ${isDark ? 'Dark' : 'Light'}`,
87
+ color: 'Secondary & status colours',
88
+ type: `${settings.fontFamily} · ${SIZE_L[settings.textSize] ?? 'Default'}`,
89
+ cards: 'Card style, header, nesting & surface tint',
90
+ tables: 'Table row density, dividers, striping & header style — previewed on the Orders screen',
91
+ shape: `${RAD_L[settings.radiusPreset] ?? 'Custom'} · ${DENS_L[settings.density] ?? 'Default'} · ${CARD_L[settings.cardStyle] ?? ''}`,
92
+ notif: `${NPOS_L[settings.notifPosition] ?? ''} · ${cap(settings.notifStyle)}`,
93
+ login: `${LOGIN_L[settings.loginLayout] ?? ''}${settings.loginImage ? ' · image' : ''}`,
94
+ };
95
+
96
+ const sections: Sec[] = [
97
+ { id: 'theme', title: 'Theme Settings', info: 'Presets, saved themes & light/dark mode.', icon: icoTheme, screen: 'dashboard', body: <ThemeManagement /> },
98
+ { id: 'color', title: 'Colour Settings', info: 'Brand, text & status colours.', icon: icoColor, screen: 'components', body: <ColourPanel /> },
99
+ { id: 'type', title: 'Typography', info: 'Fonts, sizes, weight & line-height.', icon: icoType, screen: 'components', body: <TypographyPanel /> },
100
+ { id: 'cards', title: 'Cards', info: 'Card style, header, nesting & tint.', icon: icoCards, screen: 'dashboard', body: <CardsPanel /> },
101
+ { id: 'tables', title: 'Tables', info: 'Row/column density, dividers & alignment.', icon: icoTables, screen: 'orders', body: <TablesPanel /> },
102
+ { id: 'shape', title: 'Surface & Shape', info: 'Radius, density, elevation & spacing.', icon: icoShape, screen: 'dashboard', body: <SurfacePanel /> },
103
+ { id: 'notif', title: 'Notifications', info: 'Toast placement & behaviour.', icon: icoNotif, screen: 'operations', body: <NotificationsPanel /> },
104
+ { id: 'login', title: 'Login Screen', info: 'Brand image, logo & panel colour.', icon: icoLogin, screen: 'login', body: <LoginPanel /> },
105
+ ];
106
+
107
+ return (
108
+ <Box sx={{ display: 'flex', flexDirection: 'column', bgcolor: sk.card }}>
109
+ {/* Active-theme header */}
110
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1.5, py: 1.1, borderBottom: `1px solid ${sk.divider}`, bgcolor: sk.card }}>
111
+ <Box sx={{ display: 'flex', gap: 0.5, flexShrink: 0 }}>
112
+ <Box sx={{ width: 13, height: 13, borderRadius: '3px', bgcolor: settings.primaryHex }} />
113
+ <Box sx={{ width: 13, height: 13, borderRadius: '3px', bgcolor: settings.secondaryHex }} />
114
+ </Box>
115
+ <Typography variant="caption" sx={{ flex: 1, minWidth: 0, fontWeight: 800, color: sk.text, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{activeName}</Typography>
116
+ <Tooltip title="Reset to defaults" arrow>
117
+ <IconButton size="small" onClick={reset} sx={{ bgcolor: 'action.selected' }}><RestartAltRoundedIcon sx={{ fontSize: 16 }} /></IconButton>
118
+ </Tooltip>
119
+ <Tooltip title={settings.mode === 'dark' ? 'Switch to light' : 'Switch to dark'} arrow>
120
+ <IconButton size="small" onClick={() => set({ mode: settings.mode === 'dark' ? 'light' : 'dark' })} sx={{ bgcolor: 'action.selected' }}>
121
+ {settings.mode === 'dark' ? <LightModeOutlinedIcon sx={{ fontSize: 16 }} /> : <DarkModeOutlinedIcon sx={{ fontSize: 16 }} />}
122
+ </IconButton>
123
+ </Tooltip>
124
+ {onClose && (
125
+ <Tooltip title="Close studio" arrow>
126
+ <IconButton size="small" onClick={onClose} sx={{ bgcolor: 'action.selected' }}><CloseRoundedIcon sx={{ fontSize: 16 }} /></IconButton>
127
+ </Tooltip>
128
+ )}
129
+ </Box>
130
+
131
+ {/* Change-preview-screen popover (toggled by the monitor icon on any section header) */}
132
+ {previewMenuOpen && (
133
+ <Box sx={{ borderBottom: '1px solid', borderColor: 'divider', borderLeft: '3px solid #eab308', bgcolor: (t) => t.palette.mode === 'dark' ? 'rgba(234,179,8,0.14)' : '#fef9c3', px: 1.6, py: 1.5 }}>
134
+ <Typography sx={{ fontSize: 10, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '.6px', color: (t) => t.palette.mode === 'dark' ? '#f2cd5c' : '#854d0e', mb: 1 }}>Previewing Screen</Typography>
135
+ <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 0.75 }}>
136
+ {PREVIEW_TABS.map((sc) => {
137
+ const on = curScreen === sc.key;
138
+ return (
139
+ <Box key={sc.key} component="button"
140
+ onClick={() => { if (curScreen !== sc.key) setScreen(sc.key); setPreviewMenuOpen(false); }}
141
+ sx={{ px: 0.75, py: 1, borderRadius: 2, cursor: 'pointer', fontSize: 11, fontWeight: on ? 800 : 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
142
+ border: '1.5px solid', borderColor: on ? '#d4a915' : '#e6dca8', bgcolor: on ? '#fde68a' : 'background.paper', color: on ? '#713f12' : 'text.secondary' }}>
143
+ {sc.label}
144
+ </Box>
145
+ );
146
+ })}
147
+ </Box>
148
+ </Box>
149
+ )}
150
+
151
+ {sections.map((sec) => (
152
+ <SectionAccordion
153
+ key={sec.id}
154
+ icon={sec.icon}
155
+ title={sec.title}
156
+ info={summary[sec.id] ?? sec.info}
157
+ open={openId === sec.id}
158
+ dimmed={openId !== '' && openId !== sec.id}
159
+ onToggle={() => toggle(sec.id, sec.screen)}
160
+ onPreviewMenu={() => setPreviewMenuOpen(!previewMenuOpen)}
161
+ >
162
+ {sec.body}
163
+ </SectionAccordion>
164
+ ))}
165
+ </Box>
166
+ );
167
+ }
@@ -0,0 +1,53 @@
1
+ import { createTheme, type Theme } from '@mui/material/styles';
2
+ import { buildTokens } from './tokens';
3
+ import type { ThemeSettings } from './settings';
4
+
5
+ // MUI theme for the studio chrome + MUI-based controls. Derived from the SAME token
6
+ // engine the preview screens use, so chrome and preview stay in lockstep.
7
+ export function buildMuiTheme(s: ThemeSettings): Theme {
8
+ const T = buildTokens(s);
9
+ const t = T.t;
10
+ const headFamCss = `${T.headFam},sans-serif`;
11
+ const bodyFamCss = `${T.bodyFam},sans-serif`;
12
+ const heading = (px: number) => ({ fontFamily: headFamCss, fontSize: px, fontWeight: T.wH1, lineHeight: T.headLH });
13
+
14
+ return createTheme({
15
+ cssVariables: true,
16
+ palette: {
17
+ mode: s.mode,
18
+ primary: { main: s.primaryHex, dark: T.pal[700], light: T.pal[300], contrastText: t.pContrast },
19
+ secondary: { main: s.secondaryHex, dark: T.spal[700], light: T.spal[300], contrastText: t.sContrast },
20
+ success: { main: s.successHex }, warning: { main: s.warningHex },
21
+ info: { main: s.infoHex }, error: { main: s.errorHex },
22
+ background: { default: t.bgDefault, paper: t.paper },
23
+ text: { primary: t.textPrimary, secondary: t.textSecondary, disabled: t.textDisabled },
24
+ divider: t.divider,
25
+ },
26
+ shape: { borderRadius: T.r },
27
+ typography: {
28
+ fontFamily: bodyFamCss,
29
+ fontSize: T.base,
30
+ h1: heading(T.tH1), h2: heading(T.tH2), h3: heading(T.tTitle),
31
+ h4: heading(T.tH4), h5: heading(T.tH5), h6: heading(T.tH6),
32
+ body1: { fontFamily: bodyFamCss, fontSize: T.tBody, lineHeight: T.bodyLH, fontWeight: T.bodyWeightEff },
33
+ body2: { fontFamily: bodyFamCss, fontSize: T.tBodySm, lineHeight: T.bodyLH, fontWeight: T.bodyWeightEff },
34
+ caption: { fontFamily: bodyFamCss, fontSize: T.tCaption, lineHeight: T.bodyLH },
35
+ button: { textTransform: T.btnCase as 'uppercase' | 'none', fontWeight: T.bwEmph },
36
+ },
37
+ components: {
38
+ // Match the HTML app's global `button{font-family:inherit}` so raw preview buttons
39
+ // inherit the body typeface instead of the browser default (Arial). MUI's own class-based
40
+ // font rules out-specify this element selector, so themed chrome buttons are unaffected.
41
+ MuiCssBaseline: {
42
+ styleOverrides: { 'button, input, textarea, select': { fontFamily: 'inherit' } },
43
+ },
44
+ MuiCard: {
45
+ styleOverrides: {
46
+ root: { borderRadius: T.r, border: T.cardBorder === 'none' ? 'none' : T.cardBorder, boxShadow: T.cardShadow, backgroundImage: 'none' },
47
+ },
48
+ },
49
+ MuiButton: { styleOverrides: { root: { borderRadius: T.r, boxShadow: 'none' } }, defaultProps: { disableElevation: true } },
50
+ MuiChip: { styleOverrides: { root: { borderRadius: 999 } } },
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,68 @@
1
+ // Colour math — ported VERBATIM from Theme Manager.dc.html. Correctness-critical for parity.
2
+
3
+ export function hexToHSL(h: string): [number, number, number] {
4
+ if (!h || h.length < 7) return [210, 60, 45];
5
+ const r = parseInt(h.slice(1, 3), 16) / 255, g = parseInt(h.slice(3, 5), 16) / 255, b = parseInt(h.slice(5, 7), 16) / 255;
6
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
7
+ let hh = 0, s = 0; const l = (max + min) / 2;
8
+ if (d) {
9
+ s = d / (1 - Math.abs(2 * l - 1));
10
+ if (max === r) hh = ((g - b) / d) % 6;
11
+ else if (max === g) hh = (b - r) / d + 2;
12
+ else hh = (r - g) / d + 4;
13
+ hh = Math.round(hh * 60); if (hh < 0) hh += 360;
14
+ }
15
+ return [hh, Math.round(s * 100), Math.round(l * 100)];
16
+ }
17
+
18
+ export function hslToHex(h: number, s: number, l: number): string {
19
+ s /= 100; l /= 100;
20
+ const f = (n: number) => {
21
+ const k = (n + h / 30) % 12, a = s * Math.min(l, 1 - l);
22
+ return Math.round((l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))) * 255).toString(16).padStart(2, '0');
23
+ };
24
+ return '#' + f(0) + f(8) + f(4);
25
+ }
26
+
27
+ export type Palette = Record<number, string>;
28
+
29
+ export function palette(hex: string): Palette {
30
+ try {
31
+ const [h, s] = hexToHSL(hex), sat = Math.max(s, 18);
32
+ return {
33
+ 50: hslToHex(h, Math.max(Math.round(sat * 0.3), 10), 96),
34
+ 100: hslToHex(h, Math.max(Math.round(sat * 0.42), 12), 91),
35
+ 200: hslToHex(h, Math.max(Math.round(sat * 0.55), 16), 82),
36
+ 300: hslToHex(h, Math.max(Math.round(sat * 0.7), 20), 70),
37
+ 400: hslToHex(h, sat, 60),
38
+ 500: hex,
39
+ 600: hslToHex(h, Math.min(sat + 4, 100), 44),
40
+ 700: hslToHex(h, Math.min(sat + 8, 100), 36),
41
+ 800: hslToHex(h, Math.max(Math.round(sat * 0.92), 16), 26),
42
+ 900: hslToHex(h, Math.max(Math.round(sat * 0.8), 14), 16),
43
+ };
44
+ } catch {
45
+ return { 50: '#eef', 100: '#dde', 200: '#bbd', 300: '#99c', 400: '#77b', 500: hex || '#1565c0', 600: '#1257a8', 700: '#0e4488', 800: '#0a3266', 900: '#062044' };
46
+ }
47
+ }
48
+
49
+ // WCAG relative-luminance contrast ratio.
50
+ export function contrast(h1: string, h2: string): number {
51
+ try {
52
+ const lum = (h: string) => {
53
+ const rgb = [1, 3, 5].map((i) => {
54
+ const c = parseInt(h.slice(i, i + 2), 16) / 255;
55
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
56
+ });
57
+ return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
58
+ };
59
+ const a = lum(h1), b = lum(h2), hi = Math.max(a, b), lo = Math.min(a, b);
60
+ return (hi + 0.05) / (lo + 0.05);
61
+ } catch { return 1; }
62
+ }
63
+
64
+ export function rgbaOf(hex: string, a: number): string {
65
+ if (!hex || hex[0] !== '#' || hex.length < 7) return 'rgba(20,30,48,' + a + ')';
66
+ const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
67
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
68
+ }
@@ -0,0 +1,17 @@
1
+ import type { ThemeSettings } from './settings';
2
+
3
+ export interface Preset {
4
+ id: string;
5
+ name: string;
6
+ snap: Partial<ThemeSettings>;
7
+ }
8
+
9
+ // Colour presets — 1:1 with the HTML app's THEMES list (applyPreset sets primary/secondary/iconColor).
10
+ export const PRESETS: Preset[] = [
11
+ { id: 'corporate', name: 'Corporate Blue', snap: { primaryHex: '#1565c0', secondaryHex: '#00897b', iconColor: '#1565c0' } },
12
+ { id: 'emerald', name: 'Emerald', snap: { primaryHex: '#2e7d32', secondaryHex: '#00897b', iconColor: '#2e7d32' } },
13
+ { id: 'indigo', name: 'Indigo Violet', snap: { primaryHex: '#3949ab', secondaryHex: '#7c4dff', iconColor: '#3949ab' } },
14
+ { id: 'slate', name: 'Graphite', snap: { primaryHex: '#455a64', secondaryHex: '#78909c', iconColor: '#455a64' } },
15
+ { id: 'teal', name: 'Ocean Teal', snap: { primaryHex: '#00838f', secondaryHex: '#0277bd', iconColor: '#00838f' } },
16
+ { id: 'amber', name: 'Amber', snap: { primaryHex: '#e65100', secondaryHex: '#d84315', iconColor: '#e65100' } },
17
+ ];
@@ -0,0 +1,158 @@
1
+ // Full theme-setting state — a 1:1 port of the HTML app's design state (this.state design keys).
2
+ // Transient UI keys (open/menu/zoom/fit counts) live in component state, not here.
3
+
4
+ export type Mode = 'light' | 'dark';
5
+
6
+ export interface ThemeSettings {
7
+ // Identity / brand
8
+ appName: string;
9
+ appTagline: string;
10
+ logoMode: 'icon' | 'image';
11
+ logoUrl: string;
12
+ iconColor: string;
13
+ presetId: string;
14
+ mode: Mode;
15
+
16
+ // Brand / status colour
17
+ primaryHex: string;
18
+ secondaryHex: string;
19
+ highlightHex: string;
20
+ errorHex: string;
21
+ warningHex: string;
22
+ infoHex: string;
23
+ successHex: string;
24
+
25
+ // Surface / text overrides ('' = derive)
26
+ cardBgHex: string;
27
+ cardHdrHex: string;
28
+ pageBgHex: string;
29
+ notifCardHex: string;
30
+ textPrimaryHex: string;
31
+ textSecondaryHex: string;
32
+ dividerHex: string;
33
+ btnColorHex: string;
34
+ btnTextHex: string;
35
+
36
+ // Per-heading colour overrides ('' = derive)
37
+ h1Color: string; h2Color: string; h3Color: string; h4Color: string; h5Color: string; h6Color: string;
38
+
39
+ // Typography
40
+ fontFamily: string;
41
+ headingFont: string;
42
+ customFont: string;
43
+ customHeadFont: string;
44
+ textSize: 'sm' | 'df' | 'lg';
45
+ baseFontPx: number; // 0 = use textSize
46
+ bodyLineHeight: number; // 0 = auto (1.5)
47
+ bodyWeight: number; // 0 = auto (400)
48
+ letterSpace: number;
49
+ scaleRatio: 'compact' | 'default' | 'airy';
50
+ scaleRatioCustom: number;
51
+ h1Size: number; // 0 = 20
52
+ h2Size: number; h3Size: number; h4Size: number; h5Size: number; h6Size: number;
53
+ h1Weight: number; h2Weight: number; h3Weight: number; h4Weight: number; h5Weight: number; h6Weight: number;
54
+ headingWeight: number;
55
+ headingLH: number; // 0 = auto (1.2)
56
+
57
+ // Shape / density
58
+ radiusPreset: 'sharp' | 'rounded' | 'round';
59
+ radiusVal: number;
60
+ density: 'compact' | 'default' | 'comfortable';
61
+ densityScale: number; // 0 = use density preset
62
+ cardShadowPx: number; // 0 = use cardStyle preset
63
+ contrast: 'soft' | 'medium' | 'high';
64
+ contrastMode: 'auto' | 'light' | 'dark';
65
+ containerPadPx: number;
66
+ edgePadPx: number;
67
+ cardGapPx: number;
68
+
69
+ // Cards / surfaces
70
+ cardStyle: 'outlined' | 'soft' | 'floating';
71
+ cardHeader: 'plain' | 'tinted' | 'accent';
72
+ surfaceTint: 'snow' | 'neutral' | 'warm' | 'cool' | 'brand' | 'slate';
73
+ nestedStyle: 'recessed' | 'lighter' | 'bordered';
74
+ nestedDepth: number;
75
+ headerStrength: number;
76
+ tintIntensity: number;
77
+
78
+ // Global toggles
79
+ btnCase: 'none' | 'upper';
80
+ accentGradient: boolean;
81
+ statusBold: boolean;
82
+ mapGlass: 'light' | 'dark';
83
+ chartScheme: 'brand' | 'categorical' | 'sequential' | 'cbsafe' | 'warm' | 'cool';
84
+
85
+ // Tables
86
+ tableDensity: 'compact' | 'default' | 'comfortable';
87
+ tableColDensity: 'compact' | 'default' | 'comfortable';
88
+ tableDividers: boolean;
89
+ tableColDividers: boolean;
90
+ tableRowDivW: number;
91
+ tableColDivW: number;
92
+ tableStripe: boolean;
93
+ tableHeaderStyle: 'plain' | 'tinted' | 'accent';
94
+ tableCellAlignH: 'left' | 'center' | 'right';
95
+ tableCellAlignV: 'top' | 'middle' | 'bottom';
96
+
97
+ // Notifications
98
+ notifPosition: 'top-right' | 'top-center' | 'bottom-right' | 'bottom-left';
99
+ notifStyle: 'toast' | 'banner' | 'inline';
100
+ notifAccent: 'severity' | 'primary' | 'mono';
101
+ notifBadge: boolean;
102
+ notifAutoDismiss: boolean;
103
+ autoDismissSec: number;
104
+ toastW: number;
105
+
106
+ // Nav / chrome
107
+ navPattern: 'sidebar' | 'rail' | 'topbar';
108
+ sidebarWidth: number;
109
+ appBarH: number;
110
+
111
+ // Login
112
+ loginLayout: 'split' | 'centered';
113
+ loginImage: string;
114
+ loginShowTagline: boolean;
115
+ loginPanelColor: string;
116
+ loginImgOpacity: number;
117
+ loginCardW: number;
118
+ }
119
+
120
+ export const DEFAULT_SETTINGS: ThemeSettings = {
121
+ appName: 'Crystal Ball', appTagline: 'Run your whole business in one place',
122
+ logoMode: 'icon', logoUrl: '', iconColor: '#1565c0', presetId: 'corporate', mode: 'light',
123
+
124
+ primaryHex: '#1565c0', secondaryHex: '#00897b', highlightHex: '',
125
+ errorHex: '#d32f2f', warningHex: '#ed6c02', infoHex: '#0288d1', successHex: '#2e7d32',
126
+
127
+ cardBgHex: '', cardHdrHex: '', pageBgHex: '', notifCardHex: '',
128
+ textPrimaryHex: '', textSecondaryHex: '', dividerHex: '', btnColorHex: '', btnTextHex: '',
129
+ h1Color: '', h2Color: '', h3Color: '', h4Color: '', h5Color: '', h6Color: '',
130
+
131
+ fontFamily: 'Roboto', headingFont: 'Figtree', customFont: '', customHeadFont: '',
132
+ textSize: 'df', baseFontPx: 0, bodyLineHeight: 0, bodyWeight: 0, letterSpace: 0,
133
+ scaleRatio: 'default', scaleRatioCustom: 0,
134
+ h1Size: 0, h2Size: 0, h3Size: 0, h4Size: 0, h5Size: 0, h6Size: 0,
135
+ h1Weight: 0, h2Weight: 0, h3Weight: 0, h4Weight: 0, h5Weight: 0, h6Weight: 0,
136
+ headingWeight: 600, headingLH: 0,
137
+
138
+ radiusPreset: 'rounded', radiusVal: 9,
139
+ density: 'default', densityScale: 0, cardShadowPx: 0,
140
+ contrast: 'medium', contrastMode: 'auto', containerPadPx: 0, edgePadPx: 0, cardGapPx: 0,
141
+
142
+ cardStyle: 'soft', cardHeader: 'plain', surfaceTint: 'snow',
143
+ nestedStyle: 'recessed', nestedDepth: 0, headerStrength: 0, tintIntensity: 0,
144
+
145
+ btnCase: 'upper', accentGradient: false, statusBold: false, mapGlass: 'light', chartScheme: 'brand',
146
+
147
+ tableDensity: 'default', tableColDensity: 'default', tableDividers: false, tableColDividers: false,
148
+ tableRowDivW: 1, tableColDivW: 1, tableStripe: false, tableHeaderStyle: 'tinted',
149
+ tableCellAlignH: 'left', tableCellAlignV: 'middle',
150
+
151
+ notifPosition: 'bottom-right', notifStyle: 'toast', notifAccent: 'severity',
152
+ notifBadge: true, notifAutoDismiss: false, autoDismissSec: 0, toastW: 0,
153
+
154
+ navPattern: 'sidebar', sidebarWidth: 220, appBarH: 0,
155
+
156
+ loginLayout: 'split', loginImage: '', loginShowTagline: true, loginPanelColor: '',
157
+ loginImgOpacity: 0, loginCardW: 0,
158
+ };
@@ -0,0 +1,24 @@
1
+ // Fixed neutral tokens for the Theme Studio CHROME — theme-independent (only mode-dependent),
2
+ // ported verbatim from Theme Manager.dc.html (sLabel2/sBody/sInsetBd/sCard/sMuted/sChevron…).
3
+ // The studio panel must NOT recolour itself with the theme being edited.
4
+ export interface StudioTokens {
5
+ studioBg: string; studioBd: string;
6
+ card: string; cardBd: string;
7
+ inset: string; insetBd: string;
8
+ divider: string; iconTile: string;
9
+ text: string; label: string; label2: string;
10
+ body: string; muted: string; chevron: string;
11
+ }
12
+
13
+ export function studioTokens(mode: 'light' | 'dark'): StudioTokens {
14
+ const d = mode === 'dark';
15
+ return {
16
+ studioBg: d ? '#15181d' : '#f5f7fa', studioBd: d ? '#2a2f38' : '#d8dee6',
17
+ card: d ? '#1e222a' : '#ffffff', cardBd: d ? '#2b313b' : '#e6eaf0',
18
+ inset: d ? '#262b34' : '#f7f9fb', insetBd: d ? '#363d48' : '#e9edf2',
19
+ divider: d ? '#262b34' : '#f0f3f7', iconTile: d ? '#2b313b' : '#eef1f6',
20
+ text: d ? 'rgba(255,255,255,0.92)' : '#1c2127', label: d ? 'rgba(255,255,255,0.5)' : '#9aa4b0',
21
+ label2: d ? 'rgba(255,255,255,0.6)' : '#7a8593', body: d ? 'rgba(255,255,255,0.78)' : '#5a6573',
22
+ muted: d ? 'rgba(255,255,255,0.42)' : '#aeb6c0', chevron: d ? 'rgba(255,255,255,0.42)' : '#b3bcc7',
23
+ };
24
+ }