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;
  };
})();
"""