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,140 @@
1
+ import { useTokens } from '../theme/useTokens';
2
+ import { useThemeStore } from '../store/useThemeStore';
3
+ import { useFitCount } from './useFitCount';
4
+
5
+ // Cameras screen — 1:1 port of the HTML app's camera surveillance screen.
6
+ const CAM_STATS = [
7
+ { label: 'Cameras Online', val: '5 / 6' },
8
+ { label: 'Recording', val: '4' },
9
+ { label: 'Motion Alerts', val: '2' },
10
+ { label: 'Storage Used', val: '82%' },
11
+ ];
12
+ const CAM_WALL = [
13
+ { name: 'CAM 01', loc: 'Main Lobby', rec: true, online: true, time: '14:32:08' },
14
+ { name: 'CAM 02', loc: 'North Gate', rec: true, online: true, time: '14:32:08' },
15
+ { name: 'CAM 03', loc: 'Loading Bay', rec: false, online: true, time: '14:32:07' },
16
+ { name: 'CAM 04', loc: 'Rooftop', rec: true, online: true, time: '14:32:08' },
17
+ { name: 'CAM 05', loc: 'Parking Deck', rec: false, online: false, time: '—' },
18
+ { name: 'CAM 06', loc: 'Server Room', rec: true, online: true, time: '14:32:08' },
19
+ ];
20
+ const DETS = [
21
+ { cam: 'CAM 04', label: 'Motion detected — Rooftop', time: 'just now', sev: 'error' },
22
+ { cam: 'CAM 02', label: 'Person identified — North Gate', time: '6m ago', sev: 'info' },
23
+ { cam: 'CAM 01', label: 'Loitering cleared — Main Lobby', time: '22m ago', sev: 'success' },
24
+ { cam: 'CAM 06', label: 'Door forced — Server Room', time: '34m ago', sev: 'error' },
25
+ { cam: 'CAM 03', label: 'Vehicle detected — Loading Bay', time: '48m ago', sev: 'info' },
26
+ { cam: 'CAM 05', label: 'Signal restored — Parking Deck', time: '1h ago', sev: 'success' },
27
+ { cam: 'CAM 02', label: 'Tailgating flagged — North Gate', time: '1h ago', sev: 'error' },
28
+ { cam: 'CAM 04', label: 'Crowd density normal — Rooftop', time: '2h ago', sev: 'success' },
29
+ { cam: 'CAM 01', label: 'Object left behind — Main Lobby', time: '2h ago', sev: 'info' },
30
+ { cam: 'CAM 03', label: 'Motion detected — Loading Bay', time: '3h ago', sev: 'error' },
31
+ { cam: 'CAM 06', label: 'Access granted — Server Room', time: '3h ago', sev: 'success' },
32
+ { cam: 'CAM 05', label: 'Camera offline — Parking Deck', time: '4h ago', sev: 'error' },
33
+ { cam: 'CAM 02', label: 'Face match — North Gate', time: '5h ago', sev: 'info' },
34
+ { cam: 'CAM 01', label: 'Queue length normal — Main Lobby', time: '6h ago', sev: 'success' },
35
+ { cam: 'CAM 04', label: 'Perimeter breach — Rooftop', time: '7h ago', sev: 'error' },
36
+ { cam: 'CAM 03', label: 'Package scanned — Loading Bay', time: '8h ago', sev: 'info' },
37
+ ];
38
+
39
+ export function CamerasPreview() {
40
+ const T = useTokens();
41
+ const s = useThemeStore((st) => st.settings);
42
+ const { t, rad, radSm, pad, padSm, gap, screenPad, tH1, tH2, tTitle, wH1: wH, headFam, headLH, accentBg, btnText, btnShadow, tBodySm, tCaption, tOverline, bwEmph, btnCase, bodyWeightEff, errSoft, errTxt, mediaBg, overlaySurface, overlayBorder, onOverlay, sevMap } = T;
43
+ const headFamCss = headFam + ',sans-serif';
44
+ const bodyFamCss = s.fontFamily + ',sans-serif';
45
+ const hc1 = T.hc1 || t.textPrimary, hc2 = T.hc2 || t.textPrimary, hc3 = T.hc3 || t.textPrimary;
46
+ const cardBorderCss = T.cardBorder === 'none' ? 'none' : T.cardBorder;
47
+
48
+ const cams = CAM_WALL.map((c) => ({ ...c, dotC: c.online ? s.successHex : t.textDisabled }));
49
+ const [detRef, detCount] = useFitCount(3, 3, 16);
50
+ const dets = DETS.slice(0, detCount).map((d) => ({ ...d, ...sevMap[d.sev] }));
51
+
52
+ const card: React.CSSProperties = { background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden' };
53
+ const cardHead: React.CSSProperties = { padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent, display: 'flex', alignItems: 'center', justifyContent: 'space-between' };
54
+ const secTitle: React.CSSProperties = { fontSize: tTitle, lineHeight: headLH, fontWeight: wH, color: hc3, fontFamily: headFamCss };
55
+
56
+ return (
57
+ <div style={{ display: 'flex', flexDirection: 'column', background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden', fontFamily: bodyFamCss, fontWeight: bodyWeightEff, color: t.textPrimary }}>
58
+ {/* App header */}
59
+ <div style={{ padding: pad, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 14 }}>
60
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
61
+ <div style={{ fontSize: tH1, lineHeight: headLH, fontWeight: wH, color: hc1, letterSpacing: '-.3px', fontFamily: headFamCss }}>Camera Surveillance</div>
62
+ <span title="6 feeds · live monitoring & recording" style={{ width: 16, height: 16, borderRadius: '50%', border: '1.2px solid ' + t.textSecondary, color: t.textSecondary, fontSize: 9.5, fontWeight: bwEmph, fontStyle: 'italic', fontFamily: 'Georgia,serif', display: 'grid', placeItems: 'center', flexShrink: 0, cursor: 'help' }}>i</span>
63
+ </div>
64
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
65
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: tCaption, fontWeight: bwEmph, color: errTxt, background: errSoft, padding: '6px 12px', borderRadius: 999 }}>
66
+ <span style={{ width: 7, height: 7, borderRadius: '50%', background: s.errorHex }} />LIVE
67
+ </span>
68
+ <button style={{ padding: '10px 18px', borderRadius: radSm, border: 'none', background: accentBg, color: btnText, fontSize: tBodySm, fontWeight: bwEmph, textTransform: btnCase as 'uppercase' | 'none', letterSpacing: '.4px', boxShadow: btnShadow, whiteSpace: 'nowrap', cursor: 'pointer' }}>+ Add Camera</button>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Body */}
73
+ <div style={{ padding: screenPad, background: t.bgDefault, display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
74
+ {/* Stats */}
75
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap, marginBottom: gap }}>
76
+ {CAM_STATS.map((k) => (
77
+ <div key={k.label} style={card}>
78
+ <div style={{ padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent }}>
79
+ <span style={{ fontSize: tOverline, color: t.textSecondary, textTransform: 'uppercase', letterSpacing: '.6px', fontWeight: bwEmph }}>{k.label}</span>
80
+ </div>
81
+ <div style={{ padding: `${padSm}px ${pad}px` }}>
82
+ <div style={{ fontSize: tH2, fontWeight: wH, color: hc2, letterSpacing: '-.8px', lineHeight: 1 }}>{k.val}</div>
83
+ </div>
84
+ </div>
85
+ ))}
86
+ </div>
87
+
88
+ {/* Wall + detections */}
89
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap, flex: 1, minHeight: 0 }}>
90
+ <div style={{ ...card, display: 'flex', flexDirection: 'column' }}>
91
+ <div style={cardHead}>
92
+ <span style={secTitle}>Camera Wall</span>
93
+ <span style={{ fontSize: tCaption, color: t.textSecondary, fontWeight: bwEmph }}>2 × 3 grid</span>
94
+ </div>
95
+ <div style={{ padding: pad, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gridAutoRows: '1fr', gap }}>
96
+ {cams.map((c) => (
97
+ <div key={c.name} style={{ position: 'relative', borderRadius: radSm, overflow: 'hidden', background: mediaBg, minHeight: 104, border: '1px solid ' + t.divider }}>
98
+ <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center' }}>
99
+ <span style={{ fontFamily: 'monospace', fontSize: tCaption, color: 'rgba(255,255,255,.3)', letterSpacing: 1 }}>{c.loc}</span>
100
+ </div>
101
+ <div style={{ position: 'absolute', top: 7, left: 7, display: 'flex', alignItems: 'center', gap: 5, background: overlaySurface, border: '1px solid ' + overlayBorder, borderRadius: 999, padding: '2px 8px' }}>
102
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: c.dotC }} />
103
+ <span style={{ color: onOverlay, fontSize: tCaption, fontWeight: 700, letterSpacing: '.4px' }}>{c.name}</span>
104
+ </div>
105
+ {c.rec && (
106
+ <div style={{ position: 'absolute', top: 7, right: 7, display: 'flex', alignItems: 'center', gap: 4, background: overlaySurface, borderRadius: 999, padding: '2px 7px' }}>
107
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: s.errorHex }} />
108
+ <span style={{ color: onOverlay, fontSize: 8, fontWeight: 800, letterSpacing: '.5px' }}>REC</span>
109
+ </div>
110
+ )}
111
+ {c.online && (
112
+ <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '5px 8px', background: 'linear-gradient(0deg,rgba(0,0,0,.7),rgba(0,0,0,0))', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
113
+ <span style={{ color: '#fff', fontSize: tCaption, fontWeight: 600, fontFamily: 'monospace' }}>{c.time}</span>
114
+ </div>
115
+ )}
116
+ </div>
117
+ ))}
118
+ </div>
119
+ </div>
120
+ <div style={{ ...card, display: 'flex', flexDirection: 'column' }}>
121
+ <div style={{ padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent }}>
122
+ <span style={secTitle}>Recent Detections</span>
123
+ </div>
124
+ <div ref={detRef} style={{ padding: padSm, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', gap: 2, flex: 1, minHeight: 0 }}>
125
+ {dets.map((d, i) => (
126
+ <div key={i} style={{ display: 'flex', gap: 11, alignItems: 'flex-start', padding: '9px 8px', borderRadius: radSm }}>
127
+ <div style={{ width: 28, height: 28, borderRadius: radSm, background: d.cb, color: d.cc, display: 'grid', placeItems: 'center', flexShrink: 0, fontSize: 11, fontWeight: 800, fontFamily: 'monospace' }}>●</div>
128
+ <div style={{ flex: 1, minWidth: 0 }}>
129
+ <div style={{ fontSize: tBodySm, color: t.textPrimary, fontWeight: bodyWeightEff, lineHeight: 1.35 }}>{d.label}</div>
130
+ <div style={{ fontSize: tCaption, color: t.textSecondary, marginTop: 2 }}>{d.cam} · {d.time}</div>
131
+ </div>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,146 @@
1
+ import { useTokens } from '../theme/useTokens';
2
+ import { useThemeStore } from '../store/useThemeStore';
3
+
4
+ // Components screen — 1:1 port of the HTML app's Component Library.
5
+ export function ComponentsPreview() {
6
+ const T = useTokens();
7
+ const s = useThemeStore((st) => st.settings);
8
+ const {
9
+ t, rad, radSm, pad, padSm, gap, screenPad, tH1, tH2, tTitle, tH4, tH5, tH6, tBody, tBodySm, tCaption,
10
+ wH1: wH, headFam, headLH, accentBg, btnText, btnShadow, bwEmph, btnCase, bodyWeightEff, bodyLH, pMain,
11
+ errSoft, errTxt, warnSoft, warnTxt, infoSoft, infoTxt, sucSoft, sucTxt,
12
+ } = T;
13
+ const headFamCss = headFam + ',sans-serif';
14
+ const bodyFamCss = s.fontFamily + ',sans-serif';
15
+ const highlight = T.t.highlight, highlightSoft = T.t.highlightSoft, highlightBd = T.t.highlightBd;
16
+ const pContainer = t.pContainer, onPContainer = t.onPContainer, pContrast = t.pContrast;
17
+ const hc1 = T.hc1 || t.textPrimary, hc2 = T.hc2 || t.textPrimary, hc3 = T.hc3 || t.textPrimary, hc4 = T.hc4 || t.textPrimary, hc5 = T.hc5 || t.textPrimary, hc6 = T.hc6 || t.textPrimary;
18
+ const cardBorderCss = T.cardBorder === 'none' ? 'none' : T.cardBorder;
19
+
20
+ const card: React.CSSProperties = { display: 'flex', flexDirection: 'column', background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden', height: '100%' };
21
+ const cardHead: React.CSSProperties = { padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent };
22
+ const secTitle: React.CSSProperties = { fontSize: tTitle, lineHeight: headLH, fontWeight: wH, color: hc3, fontFamily: headFamCss };
23
+
24
+ const typeRows = [
25
+ { label: 'Dashboard Overview', size: tH1, w: wH, color: hc1, lh: 1.1, ls: '-.5px', name: 'H1', border: true },
26
+ { label: 'Revenue Report', size: tH2, w: wH, color: hc2, lh: 1.1, ls: '-.4px', name: 'H2', border: true },
27
+ { label: 'Performance Overview', size: tTitle, w: wH, color: hc3, lh: 1.15, name: 'H3', border: true },
28
+ { label: 'Active Users', size: tH4, w: wH, color: hc4, lh: 1.2, name: 'H4', border: true },
29
+ { label: 'Last updated 2m ago', size: tH5, w: wH, color: hc5, lh: 1.25, name: 'H5', border: true },
30
+ { label: 'Status · Live', size: tH6, w: wH, color: hc6, lh: 1.3, name: 'H6', border: true },
31
+ ];
32
+
33
+ const selRow = (selected: boolean) => (
34
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 12px', borderBottom: '1px solid ' + t.divider, background: selected ? highlightSoft : 'transparent', boxShadow: selected ? 'inset 3px 0 0 ' + highlight : 'none' }}>
35
+ <div style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid ' + (selected ? highlight : t.outline), display: 'grid', placeItems: 'center', flexShrink: 0 }}>{selected && <div style={{ width: 8, height: 8, borderRadius: '50%', background: highlight }} />}</div>
36
+ <span style={{ flex: 1, fontSize: tBodySm, fontFamily: bodyFamCss, fontWeight: selected ? bwEmph : bodyWeightEff, color: t.textPrimary }}>{selected ? 'Selected item' : 'Unselected item'}</span>
37
+ {selected && <span style={{ fontSize: tCaption, fontFamily: bodyFamCss, fontWeight: bwEmph, color: t.textSecondary }}>Active</span>}
38
+ </div>
39
+ );
40
+ const inputBox = (focused: boolean, label: string, value: string) => (
41
+ <div style={{ position: 'relative', border: focused ? '2px solid ' + pMain : '1.5px solid ' + t.outline, borderRadius: radSm, padding: '12px 13px' }}>
42
+ <span style={{ position: 'absolute', top: -8, left: 10, background: t.paper, padding: '0 5px', fontSize: tCaption, color: focused ? pMain : t.textSecondary, fontWeight: focused ? bwEmph : undefined }}>{label}</span>
43
+ <span style={{ fontSize: tBody, color: focused ? t.textPrimary : t.textSecondary }}>{value}</span>
44
+ </div>
45
+ );
46
+ const chip = (bg: string, c: string, label: string) => <span style={{ padding: '5px 13px', borderRadius: 999, background: bg, color: c, fontSize: tCaption, fontWeight: bwEmph }}>{label}</span>;
47
+ const btn = (extra: React.CSSProperties): React.CSSProperties => ({ padding: '9px 20px', borderRadius: radSm, fontSize: tBodySm, fontWeight: bwEmph, textTransform: btnCase as 'uppercase' | 'none', letterSpacing: '.4px', cursor: 'pointer', ...extra });
48
+
49
+ return (
50
+ <div style={{ display: 'flex', flexDirection: 'column', background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden', fontFamily: bodyFamCss, fontWeight: bodyWeightEff, color: t.textPrimary }}>
51
+ {/* App header */}
52
+ <div style={{ padding: pad, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent }}>
53
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
54
+ <div style={{ fontSize: tH1, lineHeight: headLH, fontWeight: wH, color: hc1, letterSpacing: '-.3px', fontFamily: headFamCss }}>Component Library</div>
55
+ <span title="Every Material UI component reflects your live theme" style={{ width: 16, height: 16, borderRadius: '50%', border: '1.2px solid ' + t.textSecondary, color: t.textSecondary, fontSize: 9.5, fontWeight: bwEmph, fontStyle: 'italic', fontFamily: 'Georgia,serif', display: 'grid', placeItems: 'center', flexShrink: 0, cursor: 'help' }}>i</span>
56
+ </div>
57
+ </div>
58
+
59
+ {/* Body */}
60
+ <div style={{ padding: screenPad, background: t.bgDefault, display: 'flex', flexDirection: 'column', gap, flex: 1, minHeight: 0 }}>
61
+ {/* Row 1: Type scale + Selection states */}
62
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap, alignItems: 'stretch' }}>
63
+ <div style={card}>
64
+ <div style={cardHead}><span style={secTitle}>Typography Scale</span></div>
65
+ <div style={{ padding: pad, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', flex: 1, minHeight: 0, gap: 6 }}>
66
+ {typeRows.map((r) => (
67
+ <div key={r.name} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, borderBottom: '1px solid ' + t.divider, paddingBottom: 14 }}>
68
+ <span style={{ fontSize: r.size, lineHeight: r.lh, fontWeight: r.w, color: r.color, fontFamily: headFamCss, letterSpacing: r.ls }}>{r.label}</span>
69
+ <span style={{ fontSize: r.size, color: t.textSecondary, fontWeight: r.w, whiteSpace: 'nowrap', flexShrink: 0, fontFamily: headFamCss }}>{r.name} · {r.size}</span>
70
+ </div>
71
+ ))}
72
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
73
+ <span style={{ fontSize: tBody, lineHeight: bodyLH, color: t.textPrimary, fontFamily: bodyFamCss, fontWeight: bodyWeightEff }}>Body text — the quick brown fox jumps over the lazy dog.</span>
74
+ <span style={{ fontSize: tBody, color: t.textSecondary, fontWeight: bodyWeightEff, whiteSpace: 'nowrap', flexShrink: 0, fontFamily: bodyFamCss }}>Body · {tBody}</span>
75
+ </div>
76
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
77
+ <span style={{ fontSize: tCaption, lineHeight: bodyLH, color: t.textSecondary, fontFamily: bodyFamCss, fontWeight: bodyWeightEff }}>Caption &amp; helper text style</span>
78
+ <span style={{ fontSize: tCaption, color: t.textSecondary, fontWeight: bodyWeightEff, whiteSpace: 'nowrap', flexShrink: 0, fontFamily: bodyFamCss }}>Caption · {tCaption}</span>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <div style={card}>
83
+ <div style={cardHead}><span style={secTitle}>Selection States</span></div>
84
+ <div style={{ padding: pad, display: 'flex', flexDirection: 'column', gap: 14 }}>
85
+ <div style={{ display: 'flex', gap: 2, borderBottom: '1.5px solid ' + t.divider }}>
86
+ <div style={{ flex: 1, textAlign: 'center', padding: '8px 4px', fontSize: tBodySm, fontWeight: bwEmph, color: t.textPrimary, background: highlightSoft, borderBottom: '2.5px solid ' + pMain, marginBottom: -1.5, borderRadius: '6px 6px 0 0' }}>Overview</div>
87
+ <div style={{ flex: 1, textAlign: 'center', padding: '8px 4px', fontSize: tBodySm, fontWeight: bwEmph, color: t.textSecondary }}>Activity</div>
88
+ <div style={{ flex: 1, textAlign: 'center', padding: '8px 4px', fontSize: tBodySm, fontWeight: bwEmph, color: t.textSecondary }}>Settings</div>
89
+ </div>
90
+ <div style={{ display: 'flex', flexDirection: 'column', border: '1px solid ' + t.divider, borderRadius: radSm, overflow: 'hidden' }}>
91
+ {selRow(true)}{selRow(false)}{selRow(false)}{selRow(false)}{selRow(true)}{selRow(false)}
92
+ </div>
93
+ <div style={{ display: 'inline-flex', alignSelf: 'flex-start', padding: '6px 12px', borderRadius: radSm, background: highlightSoft, border: '1px solid ' + highlightBd }}>
94
+ <span style={{ fontSize: tCaption, fontWeight: bwEmph, color: highlight }}>2 selected items</span>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ {/* Inputs & Selection */}
101
+ <div style={{ ...card, height: 'auto' }}>
102
+ <div style={cardHead}><span style={secTitle}>Inputs &amp; Selection</span></div>
103
+ <div style={{ padding: pad }}>
104
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap, marginBottom: gap }}>
105
+ {inputBox(false, 'Outlined', 'Placeholder…')}
106
+ {inputBox(true, 'Focused', 'Active value')}
107
+ </div>
108
+ <div style={{ display: 'flex', gap: 22, alignItems: 'center', flexWrap: 'wrap', paddingTop: padSm, borderTop: '1px solid ' + t.divider }}>
109
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><div style={{ width: 18, height: 18, borderRadius: 4, background: pMain, display: 'grid', placeItems: 'center', color: pContrast, fontSize: 11, fontWeight: bwEmph }}>✓</div><span style={{ fontSize: tBodySm, color: t.textPrimary }}>Checkbox</span></div>
110
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><div style={{ width: 18, height: 18, borderRadius: '50%', border: '2px solid ' + pMain, display: 'grid', placeItems: 'center' }}><div style={{ width: 9, height: 9, borderRadius: '50%', background: pMain }} /></div><span style={{ fontSize: tBodySm, color: t.textPrimary }}>Radio</span></div>
111
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><div style={{ width: 38, height: 22, borderRadius: 999, background: pMain, position: 'relative' }}><div style={{ position: 'absolute', top: 3, right: 3, width: 16, height: 16, borderRadius: '50%', background: '#fff' }} /></div><span style={{ fontSize: tBodySm, color: t.textPrimary }}>Switch</span></div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ {/* Buttons + Chips/Alerts/Progress */}
117
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap, alignItems: 'stretch' }}>
118
+ <div style={card}>
119
+ <div style={cardHead}><span style={secTitle}>Buttons</span></div>
120
+ <div style={{ padding: pad, display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
121
+ <button style={btn({ border: 'none', background: accentBg, color: btnText, boxShadow: btnShadow })}>Contained</button>
122
+ <button style={btn({ border: '1.5px solid ' + pMain, background: 'transparent', color: pMain })}>Outlined</button>
123
+ <button style={btn({ border: 'none', background: 'transparent', color: pMain })}>Text</button>
124
+ <button style={btn({ border: 'none', background: pContainer, color: onPContainer })}>Tonal</button>
125
+ <button style={btn({ border: 'none', background: accentBg, color: btnText, boxShadow: btnShadow, borderRadius: 999 })}>Rounded</button>
126
+ <button style={btn({ border: 'none', background: errSoft, color: errTxt })}>Error</button>
127
+ </div>
128
+ </div>
129
+ <div style={card}>
130
+ <div style={cardHead}><span style={secTitle}>Chips, Alerts &amp; Progress</span></div>
131
+ <div style={{ padding: pad }}>
132
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: gap }}>
133
+ {chip(pContainer, onPContainer, 'Primary')}{chip(sucSoft, sucTxt, 'Success')}{chip(warnSoft, warnTxt, 'Warning')}{chip(infoSoft, infoTxt, 'Info')}{chip(errSoft, errTxt, 'Error')}
134
+ </div>
135
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 7 }}>
136
+ <span style={{ fontSize: tBodySm, color: t.textPrimary, fontWeight: bwEmph }}>Revenue goal</span>
137
+ <span style={{ fontSize: tBodySm, color: pMain, fontWeight: bwEmph }}>74%</span>
138
+ </div>
139
+ <div style={{ height: 8, background: t.paper2, border: '1px solid ' + t.divider, borderRadius: 999, overflow: 'hidden' }}><div style={{ height: '100%', width: '74%', background: pMain, borderRadius: 999 }} /></div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,138 @@
1
+ import { useTokens } from '../theme/useTokens';
2
+ import { useThemeStore } from '../store/useThemeStore';
3
+ import { useFitCount } from './useFitCount';
4
+
5
+ // Dashboard screen — 1:1 port of the HTML app's dashboard, rendered from the exact token engine.
6
+
7
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
8
+ const H1 = [44, 60, 50, 74, 88, 96];
9
+ const H2 = [24, 32, 30, 42, 52, 60];
10
+
11
+ export function DashboardPreview() {
12
+ const T = useTokens();
13
+ const s = useThemeStore((st) => st.settings);
14
+ const { t, rad, radSm, pad, padSm, gap, screenPad, tH1, tH2, tTitle, tH4, wH1: wH, headFam, headLH, accentBg, btnText, tBodySm, bwEmph, btnCase, bodyWeightEff, bodyLH, tCaption, pMain, sMain } = T;
15
+ const headFamCss = headFam + ',sans-serif';
16
+ const bodyFamCss = s.fontFamily + ',sans-serif';
17
+ const hc1 = T.hc1 || t.textPrimary, hc2 = T.hc2 || t.textPrimary, hc3 = T.hc3 || t.textPrimary;
18
+
19
+ const kpis = [
20
+ { label: 'Revenue', val: '$2.41M', chg: '+12.5%', up: true },
21
+ { label: 'Open Orders', val: '1,284', chg: '+3.2%', up: true },
22
+ { label: 'Inventory', val: '8,540', chg: '-1.8%', up: false },
23
+ { label: 'Active Users', val: '342', chg: '+5', up: true },
24
+ ].map((k) => ({ ...k, cc: k.up ? T.sucTxt : T.errTxt, cb: k.up ? T.sucSoft : T.errSoft, arrow: k.up ? '▲' : '▼' }));
25
+
26
+ // Activity pool + dynamic count — the list grows to fill the card height, 1:1 with the HTML's
27
+ // _actPool + actFit. Icon → colour mapping matches the original (✓ success, ! warning, + info, $/▤ primary).
28
+ const actClr = (ic: string) =>
29
+ ic === '✓' ? { cb: T.sucSoft, cc: T.sucTxt }
30
+ : ic === '!' ? { cb: T.warnSoft, cc: T.warnTxt }
31
+ : ic === '+' ? { cb: T.infoSoft, cc: T.infoTxt }
32
+ : { cb: t.pContainer, cc: T.isDark ? t.pLight : t.pDark };
33
+ const actPool = [
34
+ { title: 'Order ORD-1042 delivered to Acme Corp', time: '2m ago', ic: '✓' },
35
+ { title: 'Invoice #4821 paid — $12,400', time: '18m ago', ic: '$' },
36
+ { title: 'Low stock alert: Widget Pro (12 left)', time: '1h ago', ic: '!' },
37
+ { title: 'New customer onboarded: Globex Ltd', time: '3h ago', ic: '+' },
38
+ { title: 'Report "Q2 Summary" exported to PDF', time: '5h ago', ic: '▤' },
39
+ { title: 'Shipment SHP-2093 dispatched — Initech', time: '6h ago', ic: '✓' },
40
+ { title: 'Refund processed — $840 to Umbrella Co', time: '7h ago', ic: '$' },
41
+ { title: 'Payment overdue: Waystar Royco', time: '8h ago', ic: '!' },
42
+ { title: 'New support ticket — Stark Industries', time: '9h ago', ic: '+' },
43
+ { title: 'Backup completed — 12,480 records', time: '11h ago', ic: '✓' },
44
+ { title: 'Purchase order PO-771 approved', time: '13h ago', ic: '$' },
45
+ { title: 'Inventory reconciled — Warehouse 3', time: '15h ago', ic: '▤' },
46
+ { title: 'SLA breach warning — Hooli Inc', time: '17h ago', ic: '!' },
47
+ { title: 'New user invited — M. Fernandez', time: '19h ago', ic: '+' },
48
+ { title: 'Quarterly report scheduled', time: '21h ago', ic: '▤' },
49
+ { title: 'Order ORD-1039 delivered to Oscorp', time: '23h ago', ic: '✓' },
50
+ ].map((a) => ({ ...a, ...actClr(a.ic) }));
51
+ const [actRef, actCount] = useFitCount(5, 4, actPool.length);
52
+ const activity = actPool.slice(0, actCount);
53
+
54
+ const cardBorderCss = T.cardBorder === 'none' ? 'none' : T.cardBorder;
55
+ const cardBase: React.CSSProperties = { background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden' };
56
+ const cardHead: React.CSSProperties = { padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent, display: 'flex', alignItems: 'center', justifyContent: 'space-between' };
57
+ const secTitle: React.CSSProperties = { fontSize: tTitle, lineHeight: headLH, fontWeight: wH, color: hc3, fontFamily: headFamCss };
58
+
59
+ return (
60
+ <div style={{ display: 'flex', flexDirection: 'column', background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, overflow: 'hidden', fontFamily: bodyFamCss, fontWeight: bodyWeightEff, color: t.textPrimary }}>
61
+ {/* App header */}
62
+ <div style={{ padding: pad, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14 }}>
63
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
64
+ <div style={{ fontSize: tH1, lineHeight: 1.15, fontWeight: wH, color: hc1, letterSpacing: '-.3px', fontFamily: headFamCss }}>Dashboard</div>
65
+ <span title="Good morning, Admin · Jun 21, 2026" style={{ width: 16, height: 16, borderRadius: '50%', border: '1.2px solid ' + t.textSecondary, color: t.textSecondary, fontSize: 9.5, fontWeight: bwEmph, fontStyle: 'italic', fontFamily: 'Georgia,serif', display: 'grid', placeItems: 'center', flexShrink: 0, cursor: 'help' }}>i</span>
66
+ </div>
67
+ <button style={{ padding: '10px 20px', borderRadius: radSm, border: 'none', background: accentBg, color: btnText, fontSize: tBodySm, fontWeight: bwEmph, textTransform: btnCase as 'uppercase' | 'none', letterSpacing: '.4px', boxShadow: T.btnShadow, whiteSpace: 'nowrap', cursor: 'pointer' }}>+ New</button>
68
+ </div>
69
+
70
+ {/* Body */}
71
+ <div style={{ padding: screenPad, background: t.bgDefault, display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
72
+ {/* Overview card */}
73
+ <div style={{ ...cardBase, marginBottom: gap }}>
74
+ <div style={cardHead}>
75
+ <span style={{ ...secTitle }}>Performance Overview</span>
76
+ <span style={{ fontSize: tCaption, color: t.textSecondary, fontWeight: bwEmph }}>This month</span>
77
+ </div>
78
+ <div style={{ padding: pad, display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap }}>
79
+ {kpis.map((k) => (
80
+ <div key={k.label} style={{ background: t.paper2, border: T.nestBorder, borderRadius: radSm, boxShadow: T.nestShadow, padding: padSm }}>
81
+ <div style={{ fontSize: tH4, color: t.textSecondary, fontWeight: wH, fontFamily: headFamCss, textTransform: 'uppercase', letterSpacing: '.6px', marginBottom: 8 }}>{k.label}</div>
82
+ <div style={{ fontSize: tH2, fontWeight: wH, color: hc2, letterSpacing: '-.8px', lineHeight: 1, marginBottom: 6 }}>{k.val}</div>
83
+ <span style={{ fontSize: tCaption, fontWeight: bwEmph, color: k.cc, background: k.cb, padding: '3px 9px', borderRadius: 999 }}>{k.arrow} {k.chg}</span>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+
89
+ {/* Two-up row — stretches to fill the body (matches the HTML's flex:1 row). */}
90
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap, flex: 1, minHeight: 0 }}>
91
+ {/* Revenue Trend */}
92
+ <div style={{ ...cardBase, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
93
+ <div style={cardHead}>
94
+ <span style={secTitle}>Revenue Trend</span>
95
+ <div style={{ display: 'flex', gap: 12 }}>
96
+ {[['Revenue', pMain], ['Profit', sMain]].map(([lab, col]) => (
97
+ <div key={lab} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
98
+ <div style={{ width: 10, height: 10, borderRadius: 2, background: col }} />
99
+ <span style={{ fontSize: tCaption, color: t.textSecondary }}>{lab}</span>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ <div style={{ padding: `${padSm}px ${pad}px`, display: 'flex', alignItems: 'flex-end', gap: 12, flex: 1, minHeight: 200 }}>
105
+ {MONTHS.map((m, i) => (
106
+ <div key={m} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, height: '100%' }}>
107
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 3, flex: 1 }}>
108
+ <div style={{ width: '100%', height: H2[i] + '%', background: sMain, opacity: 0.55, borderRadius: `${radSm} ${radSm} 0 0` }} />
109
+ <div style={{ width: '100%', height: H1[i] + '%', background: pMain, borderRadius: `${radSm} ${radSm} 0 0` }} />
110
+ </div>
111
+ <span style={{ fontSize: tCaption, color: t.textSecondary }}>{m}</span>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </div>
116
+
117
+ {/* Recent Activity */}
118
+ <div style={{ ...cardBase, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
119
+ <div style={{ padding: `${padSm}px ${pad}px`, background: T.cardHdrBg, borderBottom: T.cardHdrBorder, boxShadow: T.cardHdrAccent }}>
120
+ <span style={secTitle}>Recent Activity</span>
121
+ </div>
122
+ <div ref={actRef} style={{ padding: padSm, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', gap: 4, flex: 1, minHeight: 0 }}>
123
+ {activity.map((ac, i) => (
124
+ <div key={i} style={{ display: 'flex', gap: 11, alignItems: 'flex-start', padding: 8, borderRadius: radSm }}>
125
+ <div style={{ width: 28, height: 28, borderRadius: radSm, background: ac.cb, color: ac.cc, display: 'grid', placeItems: 'center', flexShrink: 0, fontSize: 13, fontWeight: bwEmph }}>{ac.ic}</div>
126
+ <div style={{ flex: 1, minWidth: 0 }}>
127
+ <div style={{ fontSize: tBodySm, color: t.textPrimary, fontFamily: bodyFamCss, fontWeight: bodyWeightEff, lineHeight: bodyLH, marginBottom: 2 }}>{ac.title}</div>
128
+ <div style={{ fontSize: tCaption, color: t.textSecondary, fontFamily: bodyFamCss }}>{ac.time}</div>
129
+ </div>
130
+ </div>
131
+ ))}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,111 @@
1
+ import { useTokens } from '../theme/useTokens';
2
+ import { useThemeStore } from '../store/useThemeStore';
3
+
4
+ const ImgIcon = ({ size }: { size: number }) => (
5
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" /></svg>
6
+ );
7
+
8
+ // Login screen — 1:1 port of the HTML app's full-frame login (split + centered layouts).
9
+ export function LoginPreview() {
10
+ const T = useTokens();
11
+ const s = useThemeStore((st) => st.settings);
12
+ const {
13
+ t, rad, radSm, pad, tH1, tBody, tBodySm, tCaption, wH1: wH, headLH, accentBg, btnText, btnShadow,
14
+ bwEmph, pMain, iconInitials, hasLoginImage, loginCenteredGrad, loginScrimGradC,
15
+ } = T;
16
+ const bodyFamCss = s.fontFamily + ',sans-serif';
17
+ const hc1 = T.hc1 || t.textPrimary;
18
+ const pContrast = t.pContrast;
19
+ const cardBorderCss = T.cardBorder === 'none' ? 'none' : T.cardBorder;
20
+ const hasLogo = s.logoMode === 'image' && !!s.logoUrl;
21
+ const loginBrandBg = T.isDark ? t.paper2 : '#ffffff';
22
+ const loginFormBg = s.loginPanelColor || t.bgDefault;
23
+ const imgOpacity = s.loginImgOpacity > 0 ? s.loginImgOpacity : 1;
24
+
25
+ const bgImg = hasLoginImage ? <img src={s.loginImage} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', zIndex: 0, opacity: imgOpacity }} /> : null;
26
+
27
+ const card: React.CSSProperties = { background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: T.cardShadow, padding: pad, display: 'flex', flexDirection: 'column', gap: 20 };
28
+ const inputWrap = (focused: boolean): React.CSSProperties => ({ position: 'relative', background: t.paper2, border: '1.5px solid ' + (focused ? pMain : t.outline), borderRadius: radSm });
29
+
30
+
31
+ if (s.loginLayout === 'centered') {
32
+ return (
33
+ <div style={{ minHeight: 560, background: loginCenteredGrad, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40, position: 'relative', overflow: 'hidden', fontFamily: bodyFamCss, borderRadius: rad }}>
34
+ {bgImg}
35
+ {hasLoginImage && <div style={{ position: 'absolute', inset: 0, background: loginScrimGradC, zIndex: 0 }} />}
36
+ <div style={{ position: 'relative', zIndex: 1, width: '100%', maxWidth: 380, background: t.paper, border: cardBorderCss, borderRadius: rad, boxShadow: '0 24px 60px rgba(0,0,0,.35)', padding: pad, display: 'flex', flexDirection: 'column', gap: 20, alignItems: 'center' }}>
37
+ <div style={{ width: 72, height: 72, borderRadius: 20, background: s.iconColor, display: 'grid', placeItems: 'center', boxShadow: '0 6px 18px ' + s.iconColor + '55' }}>
38
+ <span style={{ color: '#fff', fontSize: 24, fontWeight: bwEmph, letterSpacing: '-1px' }}>{iconInitials}</span>
39
+ </div>
40
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 14 }}>
41
+ <div style={{ position: 'relative', border: '1.5px solid ' + t.outline, borderRadius: radSm, padding: '13px 14px' }}>
42
+ <span style={{ position: 'absolute', top: -8, left: 11, background: t.paper, padding: '0 5px', fontSize: 11, color: t.textSecondary }}>Email</span>
43
+ <span style={{ fontSize: tBody, color: t.textPrimary }}>admin@company.com</span>
44
+ </div>
45
+ <div style={{ position: 'relative', border: '1.5px solid ' + t.outline, borderRadius: radSm, padding: '13px 14px' }}>
46
+ <span style={{ position: 'absolute', top: -8, left: 11, background: t.paper, padding: '0 5px', fontSize: 11, color: t.textSecondary }}>Password</span>
47
+ <span style={{ fontSize: tBody, color: t.textPrimary, letterSpacing: 3 }}>••••••••</span>
48
+ </div>
49
+ <button style={{ padding: 13, borderRadius: radSm, border: 'none', background: accentBg, color: btnText, fontSize: tBody, fontWeight: bwEmph, textTransform: 'uppercase', letterSpacing: '.5px', boxShadow: btnShadow, cursor: 'pointer' }}>Sign in</button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ // split
57
+ return (
58
+ <div style={{ display: 'grid', gridTemplateColumns: '3fr 1fr', minHeight: 560, fontFamily: bodyFamCss, borderRadius: rad, overflow: 'hidden' }}>
59
+ {/* Brand panel */}
60
+ <div style={{ background: loginBrandBg, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '48px 40px', gap: 22, position: 'relative', overflow: 'hidden' }}>
61
+ {bgImg}
62
+ {!hasLoginImage && (
63
+ <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, color: t.textSecondary, border: '1.5px dashed ' + t.outline, padding: 24, boxSizing: 'border-box' }}>
64
+ <ImgIcon size={46} />
65
+ <div style={{ fontSize: 14, fontWeight: bwEmph, color: t.textPrimary }}>No brand image</div>
66
+ <div style={{ fontSize: 12, color: t.textSecondary, textAlign: 'center', maxWidth: 220 }}>Upload an image in the Login Screen settings to fill this panel.</div>
67
+ </div>
68
+ )}
69
+ </div>
70
+
71
+ {/* Form panel */}
72
+ <div style={{ background: loginFormBg, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 22, padding: '48px 40px' }}>
73
+ {hasLogo
74
+ ? <img src={s.logoUrl} alt={s.appName} style={{ maxHeight: 64, maxWidth: 200, objectFit: 'contain' }} />
75
+ : <div style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, maxWidth: 200, maxHeight: 64, border: '1.5px dashed ' + t.outline, borderRadius: radSm, boxSizing: 'border-box', color: t.textSecondary, padding: '8px 16px' }}><ImgIcon size={20} /><span style={{ fontSize: tCaption, fontWeight: bwEmph, color: t.textSecondary, whiteSpace: 'nowrap' }}>No logo uploaded</span></div>}
76
+ <div style={{ width: '100%', maxWidth: 340, ...card }}>
77
+ <div style={{ textAlign: 'center' }}>
78
+ <div style={{ fontSize: tH1, lineHeight: headLH, fontWeight: wH, color: hc1, letterSpacing: '-.4px', marginBottom: 5 }}>Welcome back</div>
79
+ <div style={{ fontSize: tBodySm, color: t.textSecondary }}>Sign in to continue</div>
80
+ </div>
81
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
82
+ <div>
83
+ <div style={{ fontSize: tCaption, fontWeight: bwEmph, color: t.textPrimary, marginBottom: 7 }}>Email Address</div>
84
+ <div style={{ ...inputWrap(false), padding: '12px 14px 12px 40px' }}>
85
+ <span style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: t.textSecondary, fontSize: 14 }}>✉</span>
86
+ <span style={{ fontSize: tBody, color: t.textPrimary }}>admin@company.com</span>
87
+ </div>
88
+ </div>
89
+ <div>
90
+ <div style={{ fontSize: tCaption, fontWeight: bwEmph, color: t.textPrimary, marginBottom: 7 }}>Password</div>
91
+ <div style={{ ...inputWrap(true), padding: '12px 40px 12px 40px' }}>
92
+ <span style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: t.textSecondary, fontSize: 14 }}>🔒</span>
93
+ <span style={{ fontSize: tBody, color: t.textPrimary, letterSpacing: 3 }}>••••••••</span>
94
+ <span style={{ position: 'absolute', right: 13, top: '50%', transform: 'translateY(-50%)', color: t.textSecondary, fontSize: 14 }}>👁</span>
95
+ </div>
96
+ </div>
97
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
98
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
99
+ <div style={{ width: 17, height: 17, borderRadius: 4, background: pMain, display: 'grid', placeItems: 'center', color: pContrast, fontSize: 11, fontWeight: bwEmph }}>✓</div>
100
+ <span style={{ fontSize: tBodySm, color: t.textPrimary }}>Remember me</span>
101
+ </div>
102
+ <span style={{ fontSize: tBodySm, color: pMain, fontWeight: bwEmph }}>Forgot password?</span>
103
+ </div>
104
+ <button style={{ padding: 13, borderRadius: radSm, border: 'none', background: accentBg, color: btnText, fontSize: tBody, fontWeight: bwEmph, letterSpacing: '.3px', boxShadow: btnShadow, marginTop: 2, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, cursor: 'pointer' }}>Login <span style={{ fontSize: 15 }}>→</span></button>
105
+ </div>
106
+ <div style={{ textAlign: 'center', fontSize: tCaption, color: t.textSecondary }}>Powered by <span style={{ color: pMain, fontWeight: bwEmph }}>{s.appName}</span> © 2026</div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ );
111
+ }