@medeo-plugin/plugin-screenshot 1.0.0 → 1.0.2

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 (76) hide show
  1. package/README.md +8 -5
  2. package/package.json +18 -5
  3. package/plugin.medeoplugin +0 -0
  4. package/src/README.md +28 -0
  5. package/src/README.zh-CN.md +20 -0
  6. package/src/about.json +7 -0
  7. package/src/icon.svg +1 -0
  8. package/src/locales/en.json +141 -0
  9. package/src/locales/zh-CN.json +141 -0
  10. package/src/locales/zh-TW.json +141 -0
  11. package/src/manifest.json +125 -0
  12. package/src/media/en/editor.png +0 -0
  13. package/src/media/en/settings.png +0 -0
  14. package/src/media/zh-CN/editor.png +0 -0
  15. package/src/media/zh-CN/settings.png +0 -0
  16. package/src/resources/helper.js +99 -0
  17. package/src/resources/trayTemplate.png +0 -0
  18. package/src/resources/trayTemplate.svg +1 -0
  19. package/src/src/annotations.ts +398 -0
  20. package/src/src/beautify.ts +41 -0
  21. package/src/src/geometry.ts +79 -0
  22. package/src/src/naming.ts +19 -0
  23. package/src/src/protocol.ts +53 -0
  24. package/src/src/settings.ts +48 -0
  25. package/src/src/shortcuts.ts +25 -0
  26. package/src/src/ui/overlay.ts +874 -0
  27. package/src/src/ui/settings.ts +140 -0
  28. package/src/tests/logic.test.mjs +101 -0
  29. package/src/ui/entry.ts +4 -0
  30. package/src/ui/icons/align-center.svg +1 -0
  31. package/src/ui/icons/align-left.svg +1 -0
  32. package/src/ui/icons/align-right.svg +1 -0
  33. package/src/ui/icons/arrow-double.svg +1 -0
  34. package/src/ui/icons/arrow-open.svg +1 -0
  35. package/src/ui/icons/arrow-single.svg +1 -0
  36. package/src/ui/icons/arrow-tail.svg +1 -0
  37. package/src/ui/icons/arrow-thick.svg +1 -0
  38. package/src/ui/icons/arrow.svg +1 -0
  39. package/src/ui/icons/beautify.svg +1 -0
  40. package/src/ui/icons/bold.svg +1 -0
  41. package/src/ui/icons/censor.svg +1 -0
  42. package/src/ui/icons/close.svg +1 -0
  43. package/src/ui/icons/copy-doc.svg +1 -0
  44. package/src/ui/icons/copy.svg +1 -0
  45. package/src/ui/icons/ellipse.svg +1 -0
  46. package/src/ui/icons/fill-fill.svg +1 -0
  47. package/src/ui/icons/fill-stroke.svg +1 -0
  48. package/src/ui/icons/fill-strokeFill.svg +1 -0
  49. package/src/ui/icons/highlight.svg +1 -0
  50. package/src/ui/icons/invert.svg +1 -0
  51. package/src/ui/icons/italic.svg +1 -0
  52. package/src/ui/icons/line-dashed.svg +1 -0
  53. package/src/ui/icons/line-dotted.svg +1 -0
  54. package/src/ui/icons/line-solid.svg +1 -0
  55. package/src/ui/icons/line.svg +1 -0
  56. package/src/ui/icons/marker.svg +1 -0
  57. package/src/ui/icons/measure.svg +1 -0
  58. package/src/ui/icons/number.svg +1 -0
  59. package/src/ui/icons/ocr.svg +1 -0
  60. package/src/ui/icons/open.svg +1 -0
  61. package/src/ui/icons/pencil.svg +1 -0
  62. package/src/ui/icons/rect.svg +1 -0
  63. package/src/ui/icons/redo.svg +1 -0
  64. package/src/ui/icons/sampler.svg +1 -0
  65. package/src/ui/icons/save.svg +1 -0
  66. package/src/ui/icons/select.svg +1 -0
  67. package/src/ui/icons/stamp.svg +1 -0
  68. package/src/ui/icons/text-bg.svg +1 -0
  69. package/src/ui/icons/text.svg +1 -0
  70. package/src/ui/icons/trash.svg +1 -0
  71. package/src/ui/icons/underline.svg +1 -0
  72. package/src/ui/icons/undo.svg +1 -0
  73. package/src/ui/index.html +12 -0
  74. package/src/ui/settings.html +5 -0
  75. package/src/ui/style.css +105 -0
  76. package/src/worker.ts +285 -0
@@ -0,0 +1,140 @@
1
+ // Settings and setup: the macOS screen-recording grant (with a button that asks for it),
2
+ // shortcuts, where screenshots are saved. The worker owns the values; this page only edits.
3
+ import { capture, channel, i18n } from '@medeo-plugin/sdk';
4
+ import type { FromSettings, SettingsState, ToSettings } from '../protocol.ts';
5
+ import { SHORTCUTS, type Settings, type ShortcutKey } from '../settings.ts';
6
+ import { fromKey, symbols, valid } from '../shortcuts.ts';
7
+
8
+ const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
9
+ const post = (m: FromSettings) => channel.post(m);
10
+ let t: Record<string, string> = {};
11
+ const tr = (key: string, params: Record<string, string | number> = {}) => (t[key] ?? key).replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? ''));
12
+ let state: SettingsState | null = null;
13
+ let recording: ShortcutKey | null = null;
14
+ let screen = 'not_checked';
15
+
16
+ const el = <K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string, text?: string) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
17
+
18
+ function row(label: string, note: string | null, ...controls: HTMLElement[]) {
19
+ const r = el('div', 'line'); const left = el('div', 'label');
20
+ left.append(el('span', '', label)); if (note) left.append(el('small', '', note));
21
+ const right = el('div', 'controls'); right.append(...controls);
22
+ r.append(left, right); return r;
23
+ }
24
+
25
+ function button(text: string, testid: string, onClick: () => void, cls = 'pill') {
26
+ const b = el('button', cls, text); b.dataset.testid = testid; b.addEventListener('click', onClick); return b;
27
+ }
28
+
29
+ function shortcutControl(key: ShortcutKey) {
30
+ const value = state!.settings[key];
31
+ const box = el('div', 'shortcut');
32
+ const rec = el('button', `keys${recording === key ? ' recording' : ''}`, recording === key ? tr('shortcut.press') : value ? symbols(value) : tr('shortcut.none'));
33
+ rec.dataset.testid = `screenshot.settings.${key}`;
34
+ rec.addEventListener('click', () => { recording = recording === key ? null : key; render(); });
35
+ box.append(rec);
36
+ if (value) box.append(button(tr('shortcut.clear'), `screenshot.settings.${key}.clear`, () => { recording = null; void post({ type: 'set', patch: { [key]: null } }); }, 'link'));
37
+ const error = state!.errors[key];
38
+ if (error) box.append(el('small', 'error', tr(`shortcut.error.${error}`)));
39
+ return box;
40
+ }
41
+
42
+ function render() {
43
+ const root = $('settings'); root.replaceChildren();
44
+ if (!state) return;
45
+ // Permission card first: nothing works without it.
46
+ const perm = el('section', `card perm ${screen === 'granted' ? 'ok' : 'warn'}`); perm.dataset.testid = 'screenshot.settings.permission';
47
+ const head = el('div', 'perm-head');
48
+ head.append(el('b', '', tr('perm.title')), el('span', `badge ${screen === 'granted' ? 'ok' : 'warn'}`, tr(`perm.state.${screen === 'granted' ? 'granted' : screen === 'not_checked' || screen === 'unknown' ? 'unknown' : 'missing'}`)));
49
+ perm.append(head, el('p', '', tr(screen === 'granted' ? 'perm.ok' : 'perm.body')));
50
+ if (screen !== 'granted') {
51
+ const actions = el('div', 'actions');
52
+ actions.append(button(tr('perm.request'), 'screenshot.settings.permission.request', () => void requestPermission(), 'cta'),
53
+ button(tr('perm.open'), 'screenshot.settings.permission.open', () => void post({ type: 'open-privacy' })));
54
+ perm.append(actions, el('small', 'muted', tr('perm.restart')));
55
+ }
56
+ root.append(perm);
57
+
58
+ const keys = el('section', 'card');
59
+ keys.append(el('h3', '', tr('section.shortcuts')), ...SHORTCUTS.map(k => row(tr(`shortcut.${k}`), tr(`shortcut.${k}.note`), shortcutControl(k))));
60
+ root.append(keys);
61
+
62
+ const output = el('section', 'card');
63
+ output.append(el('h3', '', tr('section.output')),
64
+ row(tr('output.enter'), tr('output.enter.note'), choice('screenshot.settings.enterAction', state.settings.enterAction, [['copy', tr('output.copy')], ['save', tr('output.save')]], v => ({ enterAction: v }))),
65
+ row(tr('output.format'), null, choice('screenshot.settings.format', state.settings.format, [['png', 'PNG'], ['jpeg', 'JPEG']], v => ({ format: v }))),
66
+ row(tr('output.downscale'), tr('output.downscale.note'), check('screenshot.settings.downscale', state.settings.downscale, v => ({ downscale: v }))));
67
+ root.append(output);
68
+
69
+ const save = el('section', 'card');
70
+ const path = el('code', 'path', state.saveDir); path.title = state.saveDir; path.dataset.testid = 'screenshot.settings.folder';
71
+ const toggle = check('screenshot.settings.openAfterSave', state.settings.openAfterSave, v => ({ openAfterSave: v }));
72
+ save.append(el('h3', '', tr('section.save')),
73
+ row(tr('save.folder'), null, path, button(tr('save.change'), 'screenshot.settings.folder.change', () => void post({ type: 'choose-folder' })), button(tr('save.reveal'), 'screenshot.settings.folder.reveal', () => void post({ type: 'reveal-folder' }))),
74
+ row(tr('save.open'), tr('save.open.note'), toggle));
75
+ root.append(save);
76
+
77
+ const foot = el('div', 'foot');
78
+ foot.append(el('small', 'muted', tr('foot.tips')), button(tr('action.capture'), 'screenshot.settings.capture', () => void post({ type: 'capture' }), 'cta'));
79
+ root.append(foot);
80
+ }
81
+
82
+ function check(testid: string, value: boolean, patch: (v: boolean) => Partial<Settings>) {
83
+ const box = el('input') as HTMLInputElement; box.type = 'checkbox'; box.className = 'switch'; box.checked = value; box.dataset.testid = testid;
84
+ box.addEventListener('change', () => void post({ type: 'set', patch: patch(box.checked) }));
85
+ return box;
86
+ }
87
+ function choice<T extends string>(testid: string, value: T, items: [T, string][], patch: (v: T) => Partial<Settings>) {
88
+ const g = el('div', 'seg'); g.dataset.testid = testid;
89
+ for (const [v, label] of items) {
90
+ const b = el('button', `seg-item${v === value ? ' on' : ''}`, label); b.dataset.value = v;
91
+ b.addEventListener('click', () => void post({ type: 'set', patch: patch(v) }));
92
+ g.append(b);
93
+ }
94
+ return g;
95
+ }
96
+
97
+ async function refreshPermission(refresh = true) {
98
+ // The status call answers "loading" until the native helper has looked.
99
+ let s = await capture.status(refresh).catch(() => null);
100
+ for (let i = 0; i < 20 && s?.status === 'loading'; i++) { await new Promise(r => setTimeout(r, 250)); s = await capture.status(false).catch(() => null); }
101
+ screen = s?.os?.screen_capture ?? 'unknown';
102
+ render();
103
+ }
104
+
105
+ async function requestPermission() {
106
+ // Asks macOS for Medeo's screen-recording grant (the system dialog the first time; after a
107
+ // refusal macOS only lets the user turn it on in System Settings).
108
+ let s = await capture.sources(true, true).catch(() => null);
109
+ for (let i = 0; i < 40 && s?.status === 'loading'; i++) { await new Promise(r => setTimeout(r, 250)); s = await capture.sources(false).catch(() => null); }
110
+ await refreshPermission(true);
111
+ if (screen !== 'granted') void post({ type: 'open-privacy' });
112
+ }
113
+
114
+ function toast(tone: 'success' | 'error', key: string) {
115
+ const box = $('toast'); box.textContent = tr(key); box.className = `toast ${tone}`; box.hidden = false;
116
+ clearTimeout((toast as any).timer); (toast as any).timer = setTimeout(() => { box.hidden = true; }, 2200);
117
+ }
118
+
119
+ export async function startSettings() {
120
+ t = await i18n.catalog().catch(() => ({}));
121
+ document.title = tr('settings.title');
122
+ i18n.onDidChangeLocale(async () => { t = await i18n.catalog().catch(() => t); document.title = tr('settings.title'); render(); });
123
+ channel.onMessage<ToSettings>(m => {
124
+ if (m?.type === 'settings') { state = m.state; render(); }
125
+ else if (m?.type === 'toast') toast(m.tone, m.key);
126
+ });
127
+ addEventListener('keydown', e => {
128
+ if (!recording) return;
129
+ e.preventDefault();
130
+ if (e.key === 'Escape') { recording = null; render(); return; }
131
+ const acc = fromKey(e);
132
+ if (!acc) return;
133
+ const key = recording; recording = null;
134
+ if (!valid(acc)) { toast('error', 'shortcut.error.invalid'); render(); return; }
135
+ void post({ type: 'set', patch: { [key]: acc } });
136
+ });
137
+ addEventListener('focus', () => void refreshPermission(true));
138
+ await post({ type: 'settings-ready' });
139
+ await refreshPermission(true);
140
+ }
@@ -0,0 +1,101 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { fromPoints, resize, move, hitHandle, windowAt, placeToolbar, round, intersect } from '../src/geometry.ts';
4
+ import { fileName, stamp, expandHome } from '../src/naming.ts';
5
+ import { normalize, DEFAULTS } from '../src/settings.ts';
6
+ import { valid, fromKey, symbols } from '../src/shortcuts.ts';
7
+ import { split, CHUNK, joinLines } from '../src/protocol.ts';
8
+ import { formatNumber, constrain } from '../src/annotations.ts';
9
+
10
+ const D = { x: 0, y: 0, w: 1000, h: 800 };
11
+
12
+ test('drag rectangle is normalized and clamped to the display', () => {
13
+ assert.deepEqual(fromPoints({ x: 300, y: 200 }, { x: 100, y: 50 }, D), { x: 100, y: 50, w: 200, h: 150 });
14
+ assert.deepEqual(fromPoints({ x: 900, y: 700 }, { x: 1200, y: 900 }, D), { x: 900, y: 700, w: 100, h: 100 });
15
+ });
16
+
17
+ test('shift square follows the longer side but stays on the display', () => {
18
+ assert.deepEqual(fromPoints({ x: 100, y: 100 }, { x: 300, y: 150 }, D, true), { x: 100, y: 100, w: 200, h: 200 });
19
+ assert.deepEqual(fromPoints({ x: 100, y: 100 }, { x: 0, y: 90 }, D, true), { x: 0, y: 0, w: 100, h: 100 });
20
+ const r = fromPoints({ x: 900, y: 100 }, { x: 1000, y: 600 }, D, true);
21
+ assert.equal(r.w, r.h); assert.ok(r.x + r.w <= 1000);
22
+ });
23
+
24
+ test('resize flips past the opposite edge, move stops at the edges', () => {
25
+ const s = { x: 100, y: 100, w: 100, h: 100 };
26
+ assert.deepEqual(resize(s, 'e', { x: 50, y: 0 }, D), { x: 50, y: 100, w: 50, h: 100 });
27
+ assert.deepEqual(resize(s, 'se', { x: 400, y: 300 }, D), { x: 100, y: 100, w: 300, h: 200 });
28
+ assert.deepEqual(move(s, -500, 2000, D), { x: 0, y: 700, w: 100, h: 100 });
29
+ });
30
+
31
+ test('handles prefer corners', () => {
32
+ const s = { x: 100, y: 100, w: 8, h: 8 };
33
+ assert.equal(hitHandle(s, { x: 101, y: 101 }), 'nw');
34
+ assert.equal(hitHandle({ x: 0, y: 0, w: 100, h: 100 }, { x: 50, y: 98 }), 's');
35
+ assert.equal(hitHandle({ x: 0, y: 0, w: 100, h: 100 }, { x: 50, y: 50 }), null);
36
+ });
37
+
38
+ test('front window wins, else the display', () => {
39
+ const front = { x: 10, y: 10, w: 100, h: 100 }, back = { x: 0, y: 0, w: 500, h: 500 };
40
+ assert.equal(windowAt([front, back], { x: 50, y: 50 }, D), front);
41
+ assert.equal(windowAt([front, back], { x: 300, y: 300 }, D), back);
42
+ assert.equal(windowAt([front, back], { x: 900, y: 700 }, D), D);
43
+ });
44
+
45
+ test('toolbar goes below, then above, then inside', () => {
46
+ const size = { w: 400, h: 40 };
47
+ assert.deepEqual(placeToolbar({ x: 500, y: 100, w: 300, h: 200 }, size, D), { x: 400, y: 308, above: false });
48
+ assert.equal(placeToolbar({ x: 0, y: 500, w: 300, h: 290 }, size, D).above, true);
49
+ const full = placeToolbar(D, size, D);
50
+ assert.ok(full.y + 40 <= 800 && full.x >= 4);
51
+ });
52
+
53
+ test('round and intersect', () => {
54
+ assert.deepEqual(round({ x: 1.4, y: 1.6, w: 10.2, h: 10.2 }), { x: 1, y: 2, w: 11, h: 10 });
55
+ assert.equal(intersect({ x: 0, y: 0, w: 10, h: 10 }, { x: 20, y: 0, w: 5, h: 5 }), null);
56
+ });
57
+
58
+ test('file names never overwrite', () => {
59
+ const d = new Date(2026, 10, 9, 14, 5, 1);
60
+ assert.equal(stamp(d), '20261109_140501');
61
+ const taken = new Set(['Medeo_20261109_140501.png', 'Medeo_20261109_140501_1.png']);
62
+ assert.equal(fileName('Medeo', d, n => taken.has(n)), 'Medeo_20261109_140501_2.png');
63
+ assert.equal(expandHome('~/Pictures/x', '/Users/a', '/f'), '/Users/a/Pictures/x');
64
+ assert.equal(expandHome('relative', '/Users/a', '/f'), '/f');
65
+ });
66
+
67
+ test('settings fall back to defaults and never bind one shortcut twice', () => {
68
+ assert.deepEqual(normalize(null), DEFAULTS);
69
+ assert.deepEqual(normalize({ shortcutArea: 'A', saveDir: 3, format: 'gif' }), DEFAULTS);
70
+ assert.equal(normalize({ shortcutArea: null }).shortcutArea, null);
71
+ assert.equal(normalize({ shortcutArea: 'Alt+A', shortcutQuick: 'alt+a' }).shortcutQuick, null);
72
+ assert.equal(normalize({ shortcutOcr: 'Command+Shift+T' }).shortcutOcr, 'Command+Shift+T');
73
+ assert.equal(normalize({ shortcutCapture: 'Alt+A' }).shortcutArea, 'Alt+A');
74
+ assert.equal(normalize({ enterAction: 'save', format: 'jpeg', downscale: true }).format, 'jpeg');
75
+ });
76
+
77
+ test('number formats and shift constraint', () => {
78
+ assert.deepEqual([1, 4, 9, 14, 2026].map(n => formatNumber(n, 'roman')), ['I', 'IV', 'IX', 'XIV', 'MMXXVI']);
79
+ assert.deepEqual([1, 26, 27, 52].map(n => formatNumber(n, 'alpha')), ['A', 'Z', 'AA', 'AZ']);
80
+ assert.equal(formatNumber(3, 'alphaLower'), 'c');
81
+ const p = constrain({ x: 0, y: 0 }, { x: 100, y: 10 }, 'line');
82
+ assert.ok(Math.abs(p.y) < 1e-9 && Math.abs(p.x - Math.hypot(100, 10)) < 1e-9);
83
+ assert.deepEqual(constrain({ x: 0, y: 0 }, { x: 30, y: -50 }, 'rect'), { x: 50, y: -50 });
84
+ });
85
+
86
+ test('OCR lines come back in reading order', () => {
87
+ const lines = [{ t: 'world', x: 0.5, y: 0.8, h: 0.1 }, { t: 'second', x: 0.1, y: 0.5, h: 0.1 }, { t: 'hello', x: 0.1, y: 0.81, h: 0.1 }];
88
+ assert.equal(joinLines(lines), 'hello world\nsecond');
89
+ });
90
+
91
+ test('shortcuts', () => {
92
+ assert.ok(valid('Alt+A')); assert.ok(valid('F1')); assert.ok(!valid('Shift+A')); assert.ok(!valid('A')); assert.ok(!valid('Alt+Shift'));
93
+ assert.equal(fromKey({ code: 'KeyA', altKey: true, ctrlKey: false, shiftKey: true, metaKey: false }), 'Alt+Shift+A');
94
+ assert.equal(fromKey({ code: 'AltLeft', altKey: true, ctrlKey: false, shiftKey: false, metaKey: false }), null);
95
+ assert.equal(symbols('Shift+Alt+A'), '⌥⇧A');
96
+ });
97
+
98
+ test('chunks reassemble', () => {
99
+ const s = 'x'.repeat(CHUNK * 2 + 5);
100
+ assert.equal(split(s).length, 3); assert.equal(split(s).join(''), s); assert.deepEqual(split(''), ['']);
101
+ });
@@ -0,0 +1,4 @@
1
+ // One bundle for both pages; the page says which one it is.
2
+ import { startOverlay } from '../src/ui/overlay.ts';
3
+ import { startSettings } from '../src/ui/settings.ts';
4
+ if (document.body.dataset.page === 'settings') void startSettings(); else void startOverlay();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M7 10h10M4 14h16M7 18h10"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 10h10M4 14h16M4 18h10"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M10 10h10M4 14h16M10 18h10"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M15 8l4 4-4 4M9 8l-4 4 4 4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h15"/><path d="M13 6l6 6-6 6" stroke-width="2.2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h15"/><path d="M14 7l5 5-5 5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h13"/><path d="M14 7l5 5-5 5"/><circle cx="5" cy="12" r="2.2" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5h11V6.5l7 5.5-7 5.5v-4H3z" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19L18 6"/><path d="M10 6h8v8"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z" fill="#000" stroke="none"/><path d="M18.5 15l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4.5h6a3.75 3.75 0 010 7.5H7zM7 12h7a3.75 3.75 0 010 7.5H7z" stroke-width="2.2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="1.5"/><rect x="9.3" y="4" width="5.3" height="5.3" fill="#000" stroke="none"/><rect x="4" y="9.3" width="5.3" height="5.3" fill="#000" stroke="none"/><rect x="14.6" y="9.3" width="5.4" height="5.3" fill="#000" stroke="none"/><rect x="9.3" y="14.6" width="5.3" height="5.4" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5.5A1.5 1.5 0 0014.5 4h-9A1.5 1.5 0 004 5.5v9A1.5 1.5 0 005.5 16H8"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5.5A1.5 1.5 0 0014.5 4h-9A1.5 1.5 0 004 5.5v9A1.5 1.5 0 005.5 16H8"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="12" rx="8.5" ry="7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><rect x="4" y="5" width="16" height="14" rx="2" fill="#000" opacity=".35" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h18v18H3z" fill="#000" opacity=".35" stroke="none"/><rect x="7" y="7" width="10" height="10" rx="1.5" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 010 17z" fill="#000" stroke="none"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4.5h8M6 19.5h8M14 4.5l-4 15"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4M10 12h4M17 12h4" stroke-width="2.4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h.01M8.5 12h.01M13 12h.01M17.5 12h.01M21 12h.01" stroke-width="3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18" stroke-width="2.4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19L19 5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 15l-3 3h5l1.5-1.5"/><path d="M8 13l7-9 5 4-8 8z"/><path d="M4 21h16" stroke-width="2.4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 15.5L15.5 3.5l5 5-12 12z"/><path d="M7 12l2 2M10 9l1.5 1.5M13 6l2 2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M10.5 9l2-1.5V16.5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8V5.5A1.5 1.5 0 015.5 4H8M16 4h2.5A1.5 1.5 0 0120 5.5V8M20 16v2.5a1.5 1.5 0 01-1.5 1.5H16M8 20H5.5A1.5 1.5 0 014 18.5V16"/><path d="M8 9h8M8 12h8M8 15h5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9"/><path d="M18 14v4.5a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 014 18.5v-11A1.5 1.5 0 015.5 6H10"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20l1-4L16 5l3 3L8 19l-4 1z"/><path d="M14 7l3 3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="1.5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14l5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 000 11H13"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5l5.5 5.5"/><path d="M16.5 7.5L8 16l-3.5 1 1-3.5 8.5-8.5"/><path d="M13 3.5l7.5 7.5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 19.5h14"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5l12 7.2-5.4 1.3 3.2 6-2.4 1.3-3.2-6L6 17z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M8.5 14.2c.9 1.3 2.1 2 3.5 2s2.6-.7 3.5-2"/><path d="M9 9.5h.01M15 9.5h.01" stroke-width="2.6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3" fill="#000" opacity=".3" stroke="none"/><path d="M8 9h8M12 9v7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6V4.5h14V6"/><path d="M12 4.5V19.5"/><path d="M9 19.5h6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 7h15M9.5 7V4.5h5V7M6.5 7l1 13h9l1-13"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4v7a5 5 0 0010 0V4"/><path d="M5 20h14"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 010 11H11"/></svg>
@@ -0,0 +1,12 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="color-scheme" content="light dark"><link rel="stylesheet" href="style.css"><title>Screenshot</title></head>
2
+ <body data-page="overlay" class="overlay">
3
+ <canvas id="base"></canvas><canvas id="ann"></canvas><canvas id="ui" data-testid="screenshot.canvas"></canvas>
4
+ <div id="tools" class="bar tools" data-testid="screenshot.toolbar" hidden></div>
5
+ <div id="options" class="bar options" data-testid="screenshot.options" hidden></div>
6
+ <div id="actions" class="bar actions" data-testid="screenshot.actions" hidden></div>
7
+ <div id="popover" class="bar popover" data-testid="screenshot.popover" hidden></div>
8
+ <div id="ocr" class="bar ocr" data-testid="screenshot.ocr" hidden></div>
9
+ <div id="hint" class="hint" data-testid="screenshot.hint" hidden></div>
10
+ <div id="toast" class="otoast" data-testid="screenshot.toast" hidden></div>
11
+ <textarea id="text" class="text-editor" data-testid="screenshot.text" spellcheck="false" hidden></textarea>
12
+ <script src="index.js" type="module"></script></body></html>
@@ -0,0 +1,5 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="color-scheme" content="light dark"><link rel="stylesheet" href="/_medeo/theme.css"><link rel="stylesheet" href="style.css"><title>Screenshot</title></head>
2
+ <body data-page="settings" class="settings-page">
3
+ <main id="settings" data-testid="screenshot.settings"></main>
4
+ <div id="toast" class="toast" role="status" data-testid="screenshot.toast" hidden></div>
5
+ <script src="index.js" type="module"></script></body></html>
@@ -0,0 +1,105 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Helvetica Neue", sans-serif;
4
+ --bar: rgba(255,255,255,.96); --bar-text: rgba(0,0,0,.82); --bar-hover: rgba(0,0,0,.06); --bar-on: rgba(59,130,246,.14); --bar-sep: rgba(0,0,0,.1);
5
+ --accent: #007AFF; --shadow: 0 8px 28px rgba(0,0,0,.28);
6
+ }
7
+ @media (prefers-color-scheme: dark) {
8
+ :root { --bar: rgba(38,38,40,.96); --bar-text: rgba(255,255,255,.88); --bar-hover: rgba(255,255,255,.08); --bar-on: rgba(59,130,246,.3); --bar-sep: rgba(255,255,255,.12); }
9
+ }
10
+ * { box-sizing: border-box; margin: 0; padding: 0; }
11
+ [hidden] { display: none !important; }
12
+ button, input, textarea { font: inherit; color: inherit; }
13
+ button { background: none; border: 0; cursor: default; }
14
+ .icon { display: inline-block; flex: none; width: 18px; height: 18px; background: currentColor; -webkit-mask: var(--src) center / contain no-repeat; mask: var(--src) center / contain no-repeat; }
15
+
16
+ /* ---- overlay ---- */
17
+ body.overlay { width: 100vw; height: 100vh; overflow: hidden; background: transparent; font: 12px/1.4 var(--font); user-select: none; -webkit-user-select: none; }
18
+ .overlay canvas { position: fixed; inset: 0; display: block; }
19
+ #base, #ann { pointer-events: none; }
20
+ #ui { cursor: crosshair; }
21
+ .bar { position: fixed; z-index: 10; display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: 10px; background: var(--bar); color: var(--bar-text); box-shadow: var(--shadow); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); }
22
+ .actions { flex-direction: column; }
23
+ .actions .sep { width: 20px; height: 1px; margin: 3px 0; }
24
+ .options { gap: 10px; padding: 5px 10px; min-height: 38px; }
25
+ .tb { width: 30px; height: 30px; border-radius: 7px; display: grid; place-items: center; flex: none; }
26
+ .tb .icon { width: 17px; height: 17px; }
27
+ .tb.small { width: 26px; height: 26px; }
28
+ .tb.small .icon { width: 15px; height: 15px; }
29
+ .tb:hover { background: var(--bar-hover); }
30
+ .tb.on { background: var(--bar-on); color: var(--accent); }
31
+ .tb.lit { color: #F5B400; }
32
+ .tb:disabled { opacity: .35; }
33
+ .tb.primary { background: var(--accent); color: #fff; }
34
+ .tb.primary:hover { filter: brightness(1.08); }
35
+ .tb.color .swatch-dot { width: 16px; height: 16px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0,0,0,.25), 0 0 0 2px var(--bar), 0 0 0 3px var(--bar-sep); }
36
+ .sep { width: 1px; height: 20px; margin: 0 4px; background: var(--bar-sep); flex: none; }
37
+ .seg { display: flex; gap: 1px; padding: 2px; border-radius: 7px; background: var(--bar-hover); }
38
+ .seg-item { min-width: 26px; height: 24px; padding: 0 6px; border-radius: 5px; display: grid; place-items: center; font-weight: 600; font-size: 12px; }
39
+ .seg-item .icon { width: 15px; height: 15px; }
40
+ .seg-item.solo { background: var(--bar-hover); }
41
+ .seg-item.on { background: var(--bar); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,.15); }
42
+ .slider { display: flex; align-items: center; gap: 6px; }
43
+ .slider-name { opacity: .7; white-space: nowrap; }
44
+ .slider input { width: 90px; accent-color: var(--accent); }
45
+ .slider-value { width: 34px; text-align: right; font-variant-numeric: tabular-nums; opacity: .8; }
46
+ .note { opacity: .65; white-space: nowrap; }
47
+ .emojis { display: flex; flex-wrap: wrap; gap: 1px; max-width: 360px; }
48
+ .emoji { width: 26px; height: 26px; border-radius: 6px; font-size: 17px; line-height: 26px; }
49
+ .emoji:hover { background: var(--bar-hover); }
50
+ .emoji.on { background: var(--bar-on); }
51
+ .popover { flex-direction: column; align-items: stretch; gap: 10px; padding: 10px 12px; z-index: 12; }
52
+ .palette { display: grid; grid-template-columns: repeat(6, 22px); gap: 8px; }
53
+ .swatch { width: 22px; height: 22px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0,0,0,.2); }
54
+ .swatch.on { box-shadow: 0 0 0 2px var(--bar), 0 0 0 4px var(--accent); }
55
+ .custom { width: 100%; height: 24px; border: 0; padding: 0; background: none; }
56
+ .gradients { display: grid; grid-template-columns: repeat(4, 34px); gap: 8px; }
57
+ .gradient { width: 34px; height: 24px; border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); }
58
+ .gradient.on { box-shadow: 0 0 0 2px var(--bar), 0 0 0 4px var(--accent); }
59
+ .row-line { display: flex; align-items: center; gap: 6px; }
60
+ .row-line input { accent-color: var(--accent); }
61
+ .ocr { flex-direction: column; align-items: stretch; gap: 8px; width: 340px; padding: 10px 12px; z-index: 11; }
62
+ .ocr-head { display: flex; align-items: center; justify-content: space-between; }
63
+ .ocr-text { width: 100%; height: 200px; padding: 8px; border-radius: 7px; border: 1px solid var(--bar-sep); background: var(--bar-hover); font: 12.5px/1.5 var(--font); resize: vertical; outline: none; user-select: text; -webkit-user-select: text; }
64
+ .ocr .cta { align-self: flex-end; padding: 5px 14px; border-radius: 6px; background: var(--accent); color: #fff; }
65
+ .code { display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 7px; background: var(--bar-hover); }
66
+ .code-label { font-weight: 600; flex: none; }
67
+ .code-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; user-select: text; }
68
+ .muted { opacity: .65; }
69
+ .hint { position: fixed; left: 16px; bottom: 16px; z-index: 5; padding: 8px 12px; border-radius: 8px; background: rgba(28,28,30,.72); color: rgba(255,255,255,.9); font-size: 12px; line-height: 1.7; pointer-events: none; }
70
+ .hint.top { bottom: auto; top: 16px; }
71
+ .otoast { position: fixed; left: 50%; top: 24px; transform: translateX(-50%); z-index: 20; padding: 7px 14px; border-radius: 8px; background: rgba(28,28,30,.9); color: #fff; font-size: 12.5px; pointer-events: none; }
72
+ .text-editor { position: fixed; z-index: 9; min-width: 40px; padding: 3px; border: 1px dashed var(--accent); border-radius: 3px; background: transparent; outline: none; resize: none; overflow: hidden; white-space: pre; line-height: 1.25; caret-color: var(--accent); }
73
+
74
+ /* ---- settings ---- */
75
+ body.settings-page { font: 13px/1.5 var(--font); background: var(--medeo-bg, Canvas); color: var(--medeo-text, CanvasText); padding: 20px; overflow-y: auto; }
76
+ #settings { display: flex; flex-direction: column; gap: 14px; }
77
+ .card { border-radius: 10px; padding: 14px 16px; background: rgba(127,127,127,.08); display: flex; flex-direction: column; gap: 10px; }
78
+ .card h3 { font-size: 13px; font-weight: 600; }
79
+ .perm.warn { background: rgba(245,158,11,.12); }
80
+ .perm.ok { background: rgba(34,197,94,.1); }
81
+ .perm-head { display: flex; align-items: center; justify-content: space-between; }
82
+ .badge { font-size: 11px; padding: 1px 8px; border-radius: 10px; }
83
+ .badge.ok { background: rgba(34,197,94,.18); color: #15803d; }
84
+ .badge.warn { background: rgba(245,158,11,.2); color: #b45309; }
85
+ .actions { display: flex; gap: 8px; }
86
+ .line { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
87
+ .label { display: flex; flex-direction: column; min-width: 0; }
88
+ .label small, .muted { color: rgba(127,127,127,1); font-size: 11.5px; }
89
+ .controls { display: flex; align-items: center; gap: 8px; min-width: 0; }
90
+ .path { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 12px ui-monospace, Menlo, monospace; opacity: .8; }
91
+ .pill { padding: 4px 12px; border-radius: 6px; background: rgba(127,127,127,.16); white-space: nowrap; }
92
+ .pill:hover { background: rgba(127,127,127,.24); }
93
+ .cta { padding: 5px 14px; border-radius: 6px; background: var(--accent); color: #fff; white-space: nowrap; }
94
+ .link { color: var(--accent); white-space: nowrap; }
95
+ .shortcut { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
96
+ .keys { min-width: 92px; padding: 4px 10px; border-radius: 6px; background: rgba(127,127,127,.16); font-variant-numeric: tabular-nums; letter-spacing: .5px; }
97
+ .keys.recording { box-shadow: 0 0 0 2px var(--accent); }
98
+ .error { color: #dc2626; font-size: 11.5px; width: 100%; text-align: right; }
99
+ .switch { width: 16px; height: 16px; accent-color: var(--accent); }
100
+ .foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 4px; }
101
+ .toast { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); padding: 7px 14px; border-radius: 8px; background: rgba(17,24,39,.9); color: #fff; font-size: 12px; }
102
+ .toast.error { background: #dc2626; }
103
+ .settings-page .seg { display: flex; gap: 1px; padding: 2px; border-radius: 7px; background: rgba(127,127,127,.16); }
104
+ .settings-page .seg-item { padding: 3px 12px; border-radius: 5px; }
105
+ .settings-page .seg-item.on { background: var(--accent); color: #fff; }