Insight_UX_1.0 / theme.py
Aryaman25's picture
Update Insight_UX_1.0 with latest changes: add auth, participants, tests, and dev requirements
f9609df
Raw
History Blame Contribute Delete
14.6 kB
"""
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;
};
})();
"""