getobsrv 0.3.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/obsrv-mcp.js +19 -0
  4. package/bin/obsrv.js +73 -0
  5. package/out/cli/args.js +204 -0
  6. package/out/main/cli.js +600 -0
  7. package/out/main/index.js +539 -0
  8. package/out/main/targetSource-w_vWw7zd.js +351 -0
  9. package/out/mcp/lib.js +175 -0
  10. package/out/mcp/server.js +301 -0
  11. package/out/preload/app.js +87 -0
  12. package/out/preload/sync.js +41 -0
  13. package/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  14. package/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  15. package/out/renderer/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  16. package/out/renderer/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  17. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  18. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  19. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  20. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  21. package/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  22. package/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  23. package/out/renderer/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  24. package/out/renderer/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  25. package/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  26. package/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  27. package/out/renderer/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  28. package/out/renderer/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  29. package/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  30. package/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  31. package/out/renderer/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  32. package/out/renderer/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  33. package/out/renderer/assets/index-BAEO9_6W.js +14460 -0
  34. package/out/renderer/assets/index-VleEBVgL.css +446 -0
  35. package/out/renderer/index.html +13 -0
  36. package/out/shared/calibration.js +41 -0
  37. package/out/shared/presets.js +47 -0
  38. package/out/shared/types.js +2 -0
  39. package/package.json +62 -0
  40. package/skills/obsrv-screens/SKILL.md +80 -0
@@ -0,0 +1,446 @@
1
+ /* ibm-plex-mono-cyrillic-ext-400-normal */
2
+ @font-face {
3
+ font-family: 'IBM Plex Mono';
4
+ font-style: normal;
5
+ font-display: swap;
6
+ font-weight: 400;
7
+ src: url(./ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2) format('woff2'), url(./ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff) format('woff');
8
+ unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
9
+ }
10
+
11
+ /* ibm-plex-mono-cyrillic-400-normal */
12
+ @font-face {
13
+ font-family: 'IBM Plex Mono';
14
+ font-style: normal;
15
+ font-display: swap;
16
+ font-weight: 400;
17
+ src: url(./ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2) format('woff2'), url(./ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff) format('woff');
18
+ unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
19
+ }
20
+
21
+ /* ibm-plex-mono-vietnamese-400-normal */
22
+ @font-face {
23
+ font-family: 'IBM Plex Mono';
24
+ font-style: normal;
25
+ font-display: swap;
26
+ font-weight: 400;
27
+ src: url(./ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2) format('woff2'), url(./ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff) format('woff');
28
+ unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
29
+ }
30
+
31
+ /* ibm-plex-mono-latin-ext-400-normal */
32
+ @font-face {
33
+ font-family: 'IBM Plex Mono';
34
+ font-style: normal;
35
+ font-display: swap;
36
+ font-weight: 400;
37
+ src: url(./ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2) format('woff2'), url(./ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff) format('woff');
38
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
39
+ }
40
+
41
+ /* ibm-plex-mono-latin-400-normal */
42
+ @font-face {
43
+ font-family: 'IBM Plex Mono';
44
+ font-style: normal;
45
+ font-display: swap;
46
+ font-weight: 400;
47
+ src: url(./ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format('woff2'), url(./ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format('woff');
48
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
49
+ }/* ibm-plex-mono-cyrillic-ext-600-normal */
50
+ @font-face {
51
+ font-family: 'IBM Plex Mono';
52
+ font-style: normal;
53
+ font-display: swap;
54
+ font-weight: 600;
55
+ src: url(./ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2) format('woff2'), url(./ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff) format('woff');
56
+ unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
57
+ }
58
+
59
+ /* ibm-plex-mono-cyrillic-600-normal */
60
+ @font-face {
61
+ font-family: 'IBM Plex Mono';
62
+ font-style: normal;
63
+ font-display: swap;
64
+ font-weight: 600;
65
+ src: url(./ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2) format('woff2'), url(./ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff) format('woff');
66
+ unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
67
+ }
68
+
69
+ /* ibm-plex-mono-vietnamese-600-normal */
70
+ @font-face {
71
+ font-family: 'IBM Plex Mono';
72
+ font-style: normal;
73
+ font-display: swap;
74
+ font-weight: 600;
75
+ src: url(./ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2) format('woff2'), url(./ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff) format('woff');
76
+ unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
77
+ }
78
+
79
+ /* ibm-plex-mono-latin-ext-600-normal */
80
+ @font-face {
81
+ font-family: 'IBM Plex Mono';
82
+ font-style: normal;
83
+ font-display: swap;
84
+ font-weight: 600;
85
+ src: url(./ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2) format('woff2'), url(./ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff) format('woff');
86
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
87
+ }
88
+
89
+ /* ibm-plex-mono-latin-600-normal */
90
+ @font-face {
91
+ font-family: 'IBM Plex Mono';
92
+ font-style: normal;
93
+ font-display: swap;
94
+ font-weight: 600;
95
+ src: url(./ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2) format('woff2'), url(./ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff) format('woff');
96
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
97
+ }:root {
98
+ color-scheme: dark;
99
+ font: 13px/1.4 system-ui, -apple-system, sans-serif;
100
+
101
+ /* Every grey is exactly neutral (R = G = B). A blue- or warm-tinted chrome
102
+ shifts the perceived hue of the render sitting next to it, which is the
103
+ one thing this app must never do. See docs/superpowers/specs/
104
+ 2026-08-23-obsrv-ui-style.md. */
105
+ --void: #000000;
106
+ --chrome-0: #141414;
107
+ --chrome-1: #1c1c1c;
108
+ --chrome-2: #262626;
109
+ --line: #333333;
110
+ --text-0: #ededed;
111
+ --text-1: #8a8a8a;
112
+ /* Text fields sit a step below the chrome so their contents read as content. */
113
+ --field: #0d0d0d;
114
+
115
+ /* The only chromatic values in the whole chrome. Colour means attention,
116
+ never decoration — there is deliberately no brand accent. */
117
+ --warn: #d8a33a;
118
+ --error: #e5484d;
119
+
120
+ /* Overwritten by the surround control; the pane field only. */
121
+ --surround: #2a2a2a;
122
+
123
+ --mono: 'IBM Plex Mono', ui-monospace, monospace;
124
+ }
125
+
126
+ :root[data-surround='black'] { --surround: #000000; }
127
+ :root[data-surround='graphite'] { --surround: #2a2a2a; }
128
+ :root[data-surround='grey50'] { --surround: #808080; }
129
+
130
+ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color: var(--text-0); }
131
+ .app { height: 100%; display: flex; flex-direction: column; }
132
+ .muted { color: var(--text-1); }
133
+ .warn { color: var(--warn); }
134
+ .toolbar .warn { white-space: nowrap; }
135
+
136
+ /* 44px matches TOOLBAR_H in src/main/ipc.ts, used until NativeSlot reports. */
137
+ .toolbar {
138
+ flex: 0 0 44px;
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 8px;
142
+ padding: 0 10px;
143
+ background: var(--chrome-1);
144
+ border-bottom: 1px solid var(--line);
145
+ }
146
+ .toolbar button {
147
+ background: var(--chrome-2);
148
+ color: var(--text-0);
149
+ border: 1px solid var(--line);
150
+ border-radius: 4px;
151
+ width: 26px;
152
+ height: 24px;
153
+ cursor: pointer;
154
+ }
155
+ .toolbar select {
156
+ background: var(--chrome-2);
157
+ color: var(--text-0);
158
+ border: 1px solid var(--line);
159
+ border-radius: 4px;
160
+ height: 24px;
161
+ }
162
+ .url-form { flex: 1 1 auto; min-width: 120px; }
163
+ .url-form input {
164
+ width: 100%;
165
+ height: 24px;
166
+ box-sizing: border-box;
167
+ padding: 0 8px;
168
+ background: var(--field);
169
+ color: var(--text-0);
170
+ border: 1px solid var(--line);
171
+ border-radius: 4px;
172
+ }
173
+ .url-form input[readonly] { color: var(--text-1); }
174
+ .badge-error {
175
+ color: var(--error);
176
+ border: 1px solid var(--error);
177
+ border-radius: 4px;
178
+ padding: 1px 6px;
179
+ font-variant-numeric: tabular-nums;
180
+ }
181
+ .pixel-exact { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
182
+ /* The checked state would otherwise take the OS accent colour. */
183
+ .toolbar input[type='checkbox'] { accent-color: var(--text-0); }
184
+
185
+ /* Focus is a 1px inset neutral ring (the style spec's "weight and a 1px
186
+ inset ring"), never Chromium's accent-coloured outline. Pointer focus on
187
+ buttons and selects shows nothing; keyboard focus (and any focus in a
188
+ text field) shows the ring. */
189
+ .toolbar :is(button, select, input):focus { outline: none; }
190
+ .toolbar :is(button, select, input):focus-visible {
191
+ outline: 1px solid var(--text-1);
192
+ outline-offset: -1px;
193
+ }
194
+
195
+ .panes { flex: 1 1 auto; display: flex; min-height: 0; }
196
+ .pane {
197
+ flex: 1 1 50%;
198
+ min-width: 0;
199
+ display: flex;
200
+ flex-direction: column;
201
+ background: var(--surround);
202
+ }
203
+ .target-pane { border-left: 1px solid var(--line); }
204
+ /* Square corners, no shadow, no blur: a rounded or blurred pane edge would
205
+ destroy the very corner pixels the user opened this app to inspect. */
206
+ .pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
207
+ .native-slot { width: 100%; height: 100%; }
208
+ /* The backing store is already at device resolution; this only guards against
209
+ a fractional CSS size introducing a resample. */
210
+ .target-canvas { display: block; image-rendering: pixelated; }
211
+ /* The canvas itself never carries a ring: an outline over its edge would sit
212
+ on the very pixels under inspection. Keyboard focus in the target pane is
213
+ shown on the pane's own hairlines instead, which touch no page pixel. */
214
+ .target-canvas:focus { outline: none; }
215
+ .target-pane:has(.target-canvas:focus-visible) { border-left-color: var(--text-1); }
216
+ .target-pane:has(.target-canvas:focus-visible) .pane-footer { border-top-color: var(--text-1); }
217
+
218
+ /* Signature: each pane always states what it is showing, so a screenshot of
219
+ Obsrv is self-documenting. */
220
+ .pane-footer {
221
+ flex: 0 0 auto;
222
+ display: flex;
223
+ gap: 10px;
224
+ padding: 5px 10px;
225
+ font: 400 11px/1.4 var(--mono);
226
+ font-variant-numeric: tabular-nums;
227
+ color: var(--text-1);
228
+ background: var(--chrome-1);
229
+ border-top: 1px solid var(--line);
230
+ white-space: nowrap;
231
+ overflow: hidden;
232
+ }
233
+ .pane-footer .role { color: var(--text-0); letter-spacing: 0.08em; }
234
+
235
+ /* Segmented surround control. Active state is weight and a border, not hue. */
236
+ .surround-control { display: flex; border: 1px solid var(--line); border-radius: 4px; }
237
+ .surround-control button {
238
+ width: 22px;
239
+ height: 22px;
240
+ border: 0;
241
+ border-right: 1px solid var(--line);
242
+ border-radius: 0;
243
+ background: var(--chrome-2);
244
+ cursor: pointer;
245
+ }
246
+ .surround-control button:last-child { border-right: 0; }
247
+ .surround-control button[aria-pressed='true'] { box-shadow: inset 0 0 0 2px var(--text-0); }
248
+ .surround-swatch { display: block; width: 12px; height: 12px; margin: 0 auto; }
249
+
250
+ /* Numbers never jitter as a slider moves. */
251
+ .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
252
+
253
+ /* An open drawer's button: weight and a 1px border, not hue. */
254
+ .toolbar :is(.toggle-panel, .toggle-settings)[aria-pressed='true'] {
255
+ border-color: var(--text-0);
256
+ background: var(--chrome-1);
257
+ }
258
+
259
+ /* Drawers sit beside the panes, never over them: the native WebContentsView is
260
+ an OS-level overlay and would cover anything painted on top of it. */
261
+ .body { flex: 1 1 auto; display: flex; min-height: 0; }
262
+ .panes { min-width: 0; }
263
+ .drawer {
264
+ flex: 0 0 280px;
265
+ overflow: auto;
266
+ padding: 12px 14px;
267
+ background: var(--chrome-1);
268
+ border-left: 1px solid var(--line);
269
+ }
270
+
271
+ /* Dense inspector: ~11px labels, the value pinned right in the mono face. */
272
+ .controls h2 {
273
+ margin: 18px 0 8px;
274
+ font-size: 11px;
275
+ font-weight: 600;
276
+ letter-spacing: 0.08em;
277
+ text-transform: uppercase;
278
+ color: var(--text-1);
279
+ }
280
+ .controls h2:first-child { margin-top: 0; }
281
+ .controls p { margin: 6px 0; font-size: 11px; }
282
+ .control { display: block; margin: 10px 0; }
283
+ .control-row {
284
+ display: flex;
285
+ justify-content: space-between;
286
+ align-items: baseline;
287
+ gap: 8px;
288
+ margin-bottom: 4px;
289
+ font-size: 11px;
290
+ color: var(--text-1);
291
+ }
292
+ .control-row .num { color: var(--text-0); }
293
+ .control.inline { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-1); }
294
+ .control.inline > span { margin: 0; }
295
+ /* accent-color pins the thumb and filled track to a neutral; unset, Chromium
296
+ paints them in the OS accent — the one hue this chrome may never carry. */
297
+ .control input[type='range'] { display: block; width: 100%; margin: 0; accent-color: var(--text-0); }
298
+ .control input[type='checkbox'] { margin: 0; accent-color: var(--text-0); }
299
+ .control input[type='number'], .control select {
300
+ width: 100%;
301
+ height: 24px;
302
+ box-sizing: border-box;
303
+ padding: 0 6px;
304
+ background: var(--field);
305
+ color: var(--text-0);
306
+ border: 1px solid var(--line);
307
+ border-radius: 4px;
308
+ }
309
+ /* A numeric field reads like the readouts: digits flush right. */
310
+ .control input.num { text-align: right; }
311
+ .readout {
312
+ font: 400 11px/1.6 var(--mono);
313
+ font-variant-numeric: tabular-nums;
314
+ color: var(--text-1);
315
+ }
316
+ .field-error { color: var(--error); }
317
+
318
+ /* Same 1px inset neutral ring as the toolbar; a range input's ring sits just
319
+ outside its track instead, where an inset one would cross the thumb. */
320
+ .drawer :is(button, select, input):focus { outline: none; }
321
+ .drawer :is(button, select, input):focus-visible {
322
+ outline: 1px solid var(--text-1);
323
+ outline-offset: -1px;
324
+ }
325
+ .drawer input[type='range']:focus-visible { outline-offset: 2px; }
326
+
327
+ .fatal { padding: 48px; max-width: 44em; }
328
+ .fatal h1 { font-size: 20px; margin: 0 0 12px; }
329
+
330
+ /* Image mode. In normal flow, above the panes: an overlay here would sit
331
+ behind the native WebContentsView. */
332
+ .strip {
333
+ flex: 0 0 auto;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 8px;
337
+ padding: 8px 10px;
338
+ background: var(--chrome-1);
339
+ border-bottom: 1px solid var(--line);
340
+ }
341
+ .strip button {
342
+ background: var(--chrome-2);
343
+ color: var(--text-0);
344
+ border: 1px solid var(--line);
345
+ border-radius: 4px;
346
+ height: 24px;
347
+ padding: 0 10px;
348
+ cursor: pointer;
349
+ }
350
+ .strip button:focus { outline: none; }
351
+ .strip button:focus-visible { outline: 1px solid var(--text-1); outline-offset: 1px; }
352
+ .toolbar .close-image { width: auto; padding: 0 8px; }
353
+ /* `safe`: a file wider than the pane scrolls from its left edge instead of
354
+ losing it off-screen. */
355
+ .image-pane .pane-body { display: flex; align-items: flex-start; justify-content: safe center; }
356
+ .image-pane img { display: block; image-rendering: pixelated; }
357
+
358
+ /* Pinned to the right so they land over the target pane, never over the native
359
+ view on the left. The drag hint is fixed rather than a strip: a strip would
360
+ shift the panes under the pointer and flicker dragleave/dragenter. */
361
+ .toast,
362
+ .drop-hint {
363
+ position: fixed;
364
+ right: 16px;
365
+ bottom: 16px;
366
+ padding: 8px 12px;
367
+ background: var(--chrome-2);
368
+ color: var(--text-0);
369
+ border: 1px solid var(--line);
370
+ border-radius: 6px;
371
+ pointer-events: none;
372
+ }
373
+ .strip button[aria-pressed='true'] { border-color: var(--text-1); font-weight: 600; }
374
+
375
+ /* Spec §9 stall notice. Inside the target pane's scroll box, never over the
376
+ native view on the left. Sticky rather than absolute so it stays visible
377
+ while the pane is scrolled; a direct block child of `.pane-body` (a sibling
378
+ before `.target-wrap`, not inside it), so it is the pane's width and its
379
+ `left: 0` holds while a wide canvas is scrolled horizontally. The fill is a
380
+ neutral grey: only the rule under it is red, so the one chromatic thing on
381
+ screen is the error itself. */
382
+ .target-wrap { min-width: min-content; }
383
+ .stall {
384
+ position: sticky;
385
+ top: 0;
386
+ left: 0;
387
+ z-index: 1;
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 10px;
391
+ padding: 8px 12px;
392
+ background: var(--chrome-1);
393
+ border-bottom: 1px solid var(--error);
394
+ color: var(--text-0);
395
+ }
396
+ .stall button {
397
+ background: var(--chrome-2);
398
+ color: var(--text-0);
399
+ border: 1px solid var(--line);
400
+ border-radius: 4px;
401
+ height: 22px;
402
+ padding: 0 10px;
403
+ cursor: pointer;
404
+ }
405
+ .stall button:focus { outline: none; }
406
+ .stall button:focus-visible { outline: 1px solid var(--text-1); outline-offset: 1px; }
407
+
408
+ /* v1.1 fit & pan. The 1:1/Fit segmented control follows the surround
409
+ control's grammar: active state is weight and an inset ring, never hue. */
410
+ .view-control { display: flex; border: 1px solid var(--line); border-radius: 4px; }
411
+ .view-control button {
412
+ width: auto;
413
+ height: 22px;
414
+ padding: 0 8px;
415
+ border: 0;
416
+ border-right: 1px solid var(--line);
417
+ border-radius: 0;
418
+ background: var(--chrome-2);
419
+ font-variant-numeric: tabular-nums;
420
+ cursor: pointer;
421
+ }
422
+ .view-control button:last-child { border-right: 0; }
423
+ .view-control button[aria-pressed='true'] {
424
+ box-shadow: inset 0 0 0 1px var(--text-0);
425
+ font-weight: 600;
426
+ }
427
+
428
+ /* The overview invites the jump-to-1:1 click; the pan chords (middle drag,
429
+ Option+drag) show the grab affordances. Cursors only — no overlay may sit
430
+ on the pixels under inspection. */
431
+ .target-canvas.fit { cursor: zoom-in; }
432
+ .target-canvas.pan-ready { cursor: grab; }
433
+ .target-canvas.panning { cursor: grabbing; }
434
+
435
+ /* Notice shown when the renderer loads outside Electron (plain browser tab). */
436
+ .browser-notice {
437
+ max-width: 32rem;
438
+ margin: 20vh auto 0;
439
+ padding: 1.5rem;
440
+ background: var(--chrome-1);
441
+ border: 1px solid var(--chrome-2);
442
+ color: var(--text-0);
443
+ line-height: 1.5;
444
+ }
445
+ .browser-notice p { margin: 0.75rem 0 0; color: var(--text-1); }
446
+ .browser-notice code { font-family: var(--mono); }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:" />
6
+ <title>Obsrv</title>
7
+ <script type="module" crossorigin src="./assets/index-BAEO9_6W.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-VleEBVgL.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ppi = ppi;
4
+ exports.computeScale = computeScale;
5
+ exports.maxCssViewport = maxCssViewport;
6
+ exports.clampViewport = clampViewport;
7
+ const presets_1 = require("./presets");
8
+ function ppi(width, height, diagonalInches) {
9
+ if (!(diagonalInches > 0))
10
+ throw new RangeError('diagonalInches must be > 0');
11
+ return Math.hypot(width, height) / diagonalInches;
12
+ }
13
+ /**
14
+ * Physical host pixels per target *device* pixel. The target's PPI is a
15
+ * device-pixel density: a 393x852 CSS phone at 3x packs 1179x2556 pixels into
16
+ * its 6.1" diagonal, so each of them gets ~0.3 host pixels on a desktop
17
+ * monitor — physically smaller than any 1x screen's pixel, which is the point.
18
+ */
19
+ function computeScale(host, target, pixelExact) {
20
+ if (pixelExact)
21
+ return host.scaleFactor;
22
+ const dsf = target.deviceScaleFactor ?? 1;
23
+ return (ppi(host.physicalWidth, host.physicalHeight, host.diagonalInches) /
24
+ ppi(target.width * dsf, target.height * dsf, target.diagonalInches));
25
+ }
26
+ /**
27
+ * The CSS-pixel budget for a surface rasterising at `deviceScaleFactor`:
28
+ * `MAX_VIEWPORT` limits *device* pixels, so the CSS clamp shrinks with the
29
+ * factor (393x852 at 3x is 1179x2556 device pixels and fits). A non-finite or
30
+ * sub-1 factor counts as 1.
31
+ */
32
+ function maxCssViewport(deviceScaleFactor) {
33
+ const dsf = Number.isFinite(deviceScaleFactor) && deviceScaleFactor > 1 ? deviceScaleFactor : 1;
34
+ return Math.max(1, Math.floor(presets_1.MAX_VIEWPORT / dsf));
35
+ }
36
+ function clampViewport(width, height, max = presets_1.MAX_VIEWPORT) {
37
+ const finite = (v) => (Number.isFinite(v) ? v : 1);
38
+ const w = Math.min(max, Math.max(1, Math.floor(finite(width))));
39
+ const h = Math.min(max, Math.max(1, Math.floor(finite(height))));
40
+ return { width: w, height: h, clamped: w !== width || h !== height };
41
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exports.MAX_VIEWPORT = void 0;
4
+ exports.findPreset = findPreset;
5
+ exports.findProfile = findProfile;
6
+ exports.MAX_VIEWPORT = 4096;
7
+ exports.DEFAULT_SETTINGS = { hostDiagonalInches: 27, hostNits: 500 };
8
+ exports.SCREEN_PRESETS = [
9
+ // Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
10
+ { id: 'laptop-768', label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: 'laptop' },
11
+ { id: 'laptop-768-14', label: '1366×768 14"', width: 1366, height: 768, diagonalInches: 14, deviceScaleFactor: 1, group: 'laptop' },
12
+ { id: 'laptop-768-11', label: '1366×768 11.6" (Chromebook)', width: 1366, height: 768, diagonalInches: 11.6, deviceScaleFactor: 1, group: 'laptop' },
13
+ { id: 'laptop-800-11', label: '1280×800 11.6" (Chromebook)', width: 1280, height: 800, diagonalInches: 11.6, deviceScaleFactor: 1, group: 'laptop' },
14
+ { id: 'laptop-900-17', label: '1600×900 17.3"', width: 1600, height: 900, diagonalInches: 17.3, deviceScaleFactor: 1, group: 'laptop' },
15
+ { id: 'laptop-1080-15', label: '1080p 15.6"', width: 1920, height: 1080, diagonalInches: 15.6, deviceScaleFactor: 1, group: 'laptop' },
16
+ // Desktops.
17
+ { id: '1080p-24', label: '1080p 24"', width: 1920, height: 1080, diagonalInches: 24, deviceScaleFactor: 1, group: 'desktop' },
18
+ { id: '1080p-27', label: '1080p 27"', width: 1920, height: 1080, diagonalInches: 27, deviceScaleFactor: 1, group: 'desktop' },
19
+ { id: '1440p-27', label: '1440p 27"', width: 2560, height: 1440, diagonalInches: 27, deviceScaleFactor: 1, group: 'desktop' },
20
+ { id: 'sxga-19', label: '1280×1024 19" (5:4)', width: 1280, height: 1024, diagonalInches: 19, deviceScaleFactor: 1, group: 'desktop' },
21
+ { id: '1440x900-19', label: '1440×900 19"', width: 1440, height: 900, diagonalInches: 19, deviceScaleFactor: 1, group: 'desktop' },
22
+ // Mobiles — CSS viewport x the device's real scale factor. A phone preset
23
+ // rendered at 1x would look worse than any real phone; these rasterise at
24
+ // 2x/3x and are shown at true physical size (usually minified on a desktop).
25
+ { id: 'android-65', label: 'Budget Android 6.5" @2x', width: 360, height: 800, diagonalInches: 6.5, deviceScaleFactor: 2, group: 'mobile' },
26
+ { id: 'iphone-se', label: 'iPhone SE 4.7" @2x', width: 375, height: 667, diagonalInches: 4.7, deviceScaleFactor: 2, group: 'mobile' },
27
+ { id: 'iphone-61', label: 'iPhone 6.1" @3x', width: 393, height: 852, diagonalInches: 6.1, deviceScaleFactor: 3, group: 'mobile' },
28
+ { id: 'ipad-109', label: 'iPad 10.9" @2x', width: 820, height: 1180, diagonalInches: 10.9, deviceScaleFactor: 2, group: 'mobile' },
29
+ ];
30
+ exports.PANEL_PROFILES = [
31
+ { id: 'reference', label: 'Reference (off)', contrastRatio: null, gamutCoverage: 1, bits: 8, frc: false, nits: null },
32
+ { id: 'office-ips', label: 'Office IPS', contrastRatio: 1000, gamutCoverage: 1, bits: 8, frc: false, nits: 300 },
33
+ { id: 'budget-tn', label: 'Budget TN', contrastRatio: 700, gamutCoverage: 0.72, bits: 6, frc: true, nits: 250 },
34
+ { id: 'old-laptop', label: 'Old laptop', contrastRatio: 600, gamutCoverage: 0.6, bits: 6, frc: false, nits: 220 },
35
+ ];
36
+ function findPreset(id) {
37
+ const p = exports.SCREEN_PRESETS.find(x => x.id === id);
38
+ if (!p)
39
+ throw new Error(`unknown preset: ${id}`);
40
+ return p;
41
+ }
42
+ function findProfile(id) {
43
+ const p = exports.PANEL_PROFILES.find(x => x.id === id);
44
+ if (!p)
45
+ throw new Error(`unknown profile: ${id}`);
46
+ return p;
47
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "getobsrv",
3
+ "version": "0.3.0",
4
+ "description": "See your site the way 1x screens see it",
5
+ "main": "./out/main/index.js",
6
+ "bin": {
7
+ "obsrv": "./bin/obsrv.js",
8
+ "obsrv-mcp": "./bin/obsrv-mcp.js"
9
+ },
10
+ "author": "Opeyemi Ajagbe <auto-report@hydrogenpay.com>",
11
+ "homepage": "https://github.com/vibesyemmy/obsrv",
12
+ "scripts": {
13
+ "dev": "electron-vite dev",
14
+ "build": "electron-vite build && npm run build:mcp",
15
+ "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json && tsc --noEmit -p tsconfig.mcp.json",
16
+ "test": "vitest run --project unit",
17
+ "test:browser": "vitest run --project browser",
18
+ "test:e2e": "npm run build && playwright test",
19
+ "dist": "npm run build && electron-builder --mac",
20
+ "build:mcp": "tsc -p tsconfig.mcp.json",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "dependencies": {
24
+ "@fontsource/ibm-plex-mono": "^5.3.0",
25
+ "@modelcontextprotocol/sdk": "^1.30.0",
26
+ "react": "^19.2.8",
27
+ "react-dom": "^19.2.8",
28
+ "zod": "^4.4.3",
29
+ "zustand": "^5.0.15",
30
+ "electron": "^43.4.1"
31
+ },
32
+ "devDependencies": {
33
+ "@playwright/test": "^1.62.1",
34
+ "@types/node": "^26.2.0",
35
+ "@types/react": "^19.2.18",
36
+ "@types/react-dom": "^19.2.4",
37
+ "@vitejs/plugin-react": "^5.2.0",
38
+ "@vitest/browser": "^3.2.7",
39
+ "electron-builder": "^26.15.3",
40
+ "electron-vite": "^3.1.0",
41
+ "playwright": "^1.62.1",
42
+ "typescript": "^5.9.3",
43
+ "vite": "^6.4.3",
44
+ "vitest": "^3.2.7"
45
+ },
46
+ "productName": "Obsrv",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/vibesyemmy/obsrv.git"
50
+ },
51
+ "license": "MIT",
52
+ "files": [
53
+ "bin",
54
+ "out",
55
+ "skills",
56
+ "README.md",
57
+ "LICENSE"
58
+ ],
59
+ "engines": {
60
+ "node": ">=20"
61
+ }
62
+ }