File size: 14,644 Bytes
b85a07c baebe6b 0cdf466 f9609df b85a07c f9609df b85a07c f9609df b85a07c f9609df b85a07c f9609df b85a07c f9609df b85a07c f9609df b85a07c f9609df b85a07c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | """
theme.py
Single source of truth for InsightUX's visual design system: CSS custom
properties (dark + light), a handful of shared utility classes, an inline
icon set, and the theme-toggle/persistence script.
Every InsightUX UI surface is a separate HTML document or an independently
injected JS bundle (landing page, toolbar/sidebar overlay, session report,
mouse-tracker panel) — none of them share a stylesheet at runtime. This
module exists so all four still look like one product: each template
splices THEME_CSS + THEME_TOGGLE_JS into its own <style>/<script>, instead
of every surface hand-rolling its own palette.
No heavy imports (no cv2/mediapipe/pyautogui) so analysis.py can import
this standalone, the same way it already runs outside the pywebview app.
"""
import json
# =============================================================================
# ICONS — one inline SVG path set, reused from both Python (string-built
# templates) and JS (client-side rendering in the report). Kept intentionally
# small: only the glyphs actually used across the four surfaces.
# =============================================================================
ICON_PATHS = {
"search": '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
"x": '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
"sun": '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>',
"moon": '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
"eye": '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>',
"cursor": '<path d="M12.586 12.586 19 19"/><path d="M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z"/>',
"target": '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
"play": '<polygon points="6 3 20 12 6 21 6 3"/>',
"stop": '<rect width="16" height="16" x="4" y="4" rx="3"/>',
"home": '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
"arrow-left": '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
"arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
"refresh": '<path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/>',
"lock": '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
"globe": '<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z"/>',
"image": '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',
"type": '<path d="M17 6.1H3"/><path d="M21 12.1H3"/><path d="M15.1 18H3"/>',
"video": '<path d="m16 13 5.2 3.5a.5.5 0 0 0 .8-.4V7.9a.5.5 0 0 0-.8-.4L16 10.5"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
"click": '<path d="m9 9 5 12 1.8-5.2L21 14Z"/><path d="M7.2 2.2 8 5.1"/><path d="m5.1 8-2.9-.8"/><path d="M14 4.1 12 6"/><path d="m6 12-1.9 2"/>',
"download": '<path d="M12 15V3"/><path d="M3 15v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4"/><path d="m7 10 5 5 5-5"/>',
"maximize": '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
"sliders": '<line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="1" x2="7" y1="14" y2="14"/><line x1="9" x2="15" y1="8" y2="8"/><line x1="17" x2="23" y1="16" y2="16"/>',
"clock": '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
"chevron-left": '<path d="m15 18-6-6 6-6"/>',
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
"settings": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/>',
"layers": '<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 12.32-8.58 3.9a2 2 0 0 1-1.66 0L2 12.32"/><path d="m22 17.32-8.58 3.9a2 2 0 0 1-1.66 0L2 17.32"/>',
"trash": '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
"info": '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
"check": '<path d="M20 6 9 17l-5-5"/>',
"camera": '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
"menu": '<line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="18" y2="18"/>',
"bar-chart": '<line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/>',
"trending-up": '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>',
"user": '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
"users": '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
"log-out": '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>',
"more-vertical": '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
"plus": '<path d="M5 12h14"/><path d="M12 5v14"/>',
}
def icon(name, size=18, css_class=""):
"""Python-side inline <svg> for a template string."""
body = ICON_PATHS.get(name, "")
cls = ("iux-icon " + css_class).strip()
return (
f'<svg class="{cls}" width="{size}" height="{size}" viewBox="0 0 24 24" '
f'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" '
f'stroke-linejoin="round">{body}</svg>'
)
ICONS_JS = (
"const IUX_ICONS = " + json.dumps(ICON_PATHS) + ";\n"
"function iuxIcon(name, size){\n"
" size = size || 18;\n"
" return '<svg class=\"iux-icon\" width=\"' + size + '\" height=\"' + size + '\" "
"viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" "
"stroke-linecap=\"round\" stroke-linejoin=\"round\">' + (IUX_ICONS[name] || '') + '</svg>';\n"
"}\n"
)
# =============================================================================
# CSS — shared custom properties + a small utility layer. Dark is the
# default (:root); :root[data-theme="light"] overrides. Deep charcoal, not
# pitch black, for dark; warm off-white, not pure white, for light.
# =============================================================================
THEME_CSS = r"""
:root {
--iux-bg: #1B1918;
--iux-bg-alt: #221F1D;
--iux-surface: #292522;
--iux-surface-hi: #332E2A;
--iux-border: rgba(198, 188, 178, 0.16);
--iux-text: #F9F8F3;
--iux-text-dim: #C6BCB2;
--iux-text-faint: #8E8E92;
--iux-primary: #FFE9A8;
--iux-primary-light: #FFE9A8;
--iux-indigo: #FFE9A8;
--iux-accent-grad: #FFE9A8;
--iux-cyan: #7FE0C8;
--iux-lavender: #FFE9A8;
--iux-on-accent: #312F2E;
--iux-success: #34D399;
--iux-warning: #F59E0B;
--iux-danger: #FB7185;
--iux-info: #38BDF8;
--iux-radius-sm: 8px;
--iux-radius: 14px;
--iux-radius-lg: 20px;
--iux-shadow-sm: 0 2px 10px rgba(0,0,0,0.32);
--iux-shadow: 0 10px 34px rgba(10,8,4,0.5);
--iux-shadow-glow: 0 0 0 1px rgba(198,188,178,0.22), 0 8px 22px rgba(0,0,0,0.35);
--iux-blur: blur(18px);
--iux-ease: cubic-bezier(0.22, 1, 0.36, 1);
--iux-font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Manrope', Arial, sans-serif;
--iux-page-bg:
radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--iux-primary-light) 16%, transparent), transparent 42%),
radial-gradient(circle at 82% 82%, color-mix(in srgb, var(--iux-indigo) 18%, transparent), transparent 45%),
var(--iux-bg);
}
:root[data-theme="light"] {
--iux-bg: #FBF7E7;
--iux-bg-alt: #F3EBC6;
--iux-surface: #F9F8F3;
--iux-surface-hi: #E6E7E4;
--iux-border: rgba(49, 47, 46, 0.10);
--iux-text: #312F2E;
--iux-text-dim: #747579;
--iux-text-faint: #8E8E92;
--iux-primary: #D9A400;
--iux-primary-light: #D9A400;
--iux-indigo: #D9A400;
--iux-accent-grad: #D9A400;
--iux-lavender: #B8860B;
--iux-shadow-sm: 0 2px 10px rgba(49,47,46,0.08);
--iux-shadow: 0 14px 34px rgba(49,47,46,0.12);
--iux-shadow-glow: 0 0 0 1px rgba(49,47,46,0.14), 0 8px 18px rgba(49,47,46,0.10);
--iux-page-bg:
linear-gradient(120deg, #E6E7E4 0%, #F9F8F3 45%, #FBF7E7 75%),
radial-gradient(circle at 88% 18%, color-mix(in srgb, #F3EBC6 90%, transparent), transparent 58%);
}
.iux-icon { display: inline-block; vertical-align: middle; flex-shrink: 0; }
.iux-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
border-radius: var(--iux-radius-sm); border: 1px solid var(--iux-border);
background: var(--iux-surface); color: var(--iux-text); cursor: pointer;
font: 500 12.5px var(--iux-font);
transition: transform 0.15s var(--iux-ease), background 0.15s var(--iux-ease), box-shadow 0.15s var(--iux-ease);
}
.iux-btn:hover { background: var(--iux-surface-hi); transform: translateY(-1px); }
.iux-btn:active { transform: translateY(0) scale(0.97); }
.iux-btn.primary { background: var(--iux-accent-grad); border-color: transparent; color: #fff; }
.iux-btn.primary:hover { box-shadow: var(--iux-shadow-glow); }
.iux-card {
background: var(--iux-surface); border: 1px solid var(--iux-border);
border-radius: var(--iux-radius); box-shadow: var(--iux-shadow-sm);
transition: transform 0.18s var(--iux-ease), box-shadow 0.18s var(--iux-ease);
}
.iux-card.hoverable:hover { transform: translateY(-2px); box-shadow: var(--iux-shadow); }
.iux-glass {
background: color-mix(in srgb, var(--iux-surface) 82%, transparent);
backdrop-filter: var(--iux-blur); -webkit-backdrop-filter: var(--iux-blur);
border: 1px solid var(--iux-border);
}
.iux-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 12px; border-radius: 999px; font: 600 11px var(--iux-font);
border: 1px solid var(--iux-border); background: var(--iux-surface-hi); color: var(--iux-text-dim);
white-space: nowrap;
}
.iux-pill .dot {
width: 7px; height: 7px; border-radius: 50%; background: var(--iux-text-faint); flex-shrink: 0;
}
.iux-pill.on { color: var(--iux-success); border-color: rgba(52,211,153,0.35); background: rgba(52,211,153,0.1); }
.iux-pill.on .dot { background: var(--iux-success); animation: iux-pulse 1.6s ease-in-out infinite; }
@keyframes iux-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(52,211,153,0.55); }
70% { box-shadow: 0 0 0 6px rgba(52,211,153,0); }
}
.iux-toast {
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
border-radius: var(--iux-radius-sm); background: var(--iux-surface); color: var(--iux-text);
border: 1px solid var(--iux-border); box-shadow: var(--iux-shadow);
font: 500 12.5px var(--iux-font);
animation: iux-toast-in 0.25s var(--iux-ease);
}
@keyframes iux-toast-in {
from { opacity: 0; transform: translateY(-6px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.iux-fade-in { animation: iux-fade 0.35s var(--iux-ease); }
@keyframes iux-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--iux-border); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: var(--iux-text-faint); }
"""
# =============================================================================
# THEME PERSISTENCE — pure frontend, no Api/IPC calls.
#
# CHROME_JS and MOUSE_JS are re-injected into whatever real website is
# currently loaded (google.com, wikipedia.org, ...), each a different
# origin, so localStorage (per-origin) can't carry a preference between
# them. window.name is the standard trick here: it survives navigation
# across origins within the same tab/window and is only cleared when the
# window itself closes. localStorage is kept as a secondary mirror so the
# landing page and the generated report (both file:// — same origin) stay
# in sync with each other even in a fresh window.
# =============================================================================
THEME_TOGGLE_JS = r"""
(function(){
function readPrefs(){
try {
if (window.name && window.name.indexOf('__iuxprefs__:') === 0) {
return JSON.parse(window.name.slice(13));
}
} catch (e) {}
try {
const raw = localStorage.getItem('__iux_prefs__');
if (raw) return JSON.parse(raw);
} catch (e) {}
return {};
}
function writePrefs(prefs){
try { window.name = '__iuxprefs__:' + JSON.stringify(prefs); } catch (e) {}
try { localStorage.setItem('__iux_prefs__', JSON.stringify(prefs)); } catch (e) {}
}
const prefs = readPrefs();
const theme = prefs.theme === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
window.insightuxGetPrefs = readPrefs;
window.insightuxSetPref = function(key, value){
const p = readPrefs();
p[key] = value;
writePrefs(p);
};
window.insightuxGetTheme = function(){ return readPrefs().theme === 'dark' ? 'dark' : 'light'; };
window.insightuxToggleTheme = function(){
const next = window.insightuxGetTheme() === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
window.insightuxSetPref('theme', next);
return next;
};
})();
"""
|