@medeo-plugin/plugin-screenshot 1.0.0 → 1.0.1
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.
- package/README.md +8 -5
- package/package.json +18 -5
- package/plugin.medeoplugin +0 -0
- package/src/README.md +28 -0
- package/src/README.zh-CN.md +20 -0
- package/src/about.json +7 -0
- package/src/icon.svg +1 -0
- package/src/locales/en.json +141 -0
- package/src/locales/zh-CN.json +141 -0
- package/src/locales/zh-TW.json +141 -0
- package/src/manifest.json +125 -0
- package/src/media/en/editor.png +0 -0
- package/src/media/en/settings.png +0 -0
- package/src/media/zh-CN/editor.png +0 -0
- package/src/media/zh-CN/settings.png +0 -0
- package/src/resources/helper.js +99 -0
- package/src/resources/trayTemplate.png +0 -0
- package/src/resources/trayTemplate.svg +1 -0
- package/src/src/annotations.ts +398 -0
- package/src/src/beautify.ts +41 -0
- package/src/src/geometry.ts +79 -0
- package/src/src/naming.ts +19 -0
- package/src/src/protocol.ts +53 -0
- package/src/src/settings.ts +48 -0
- package/src/src/shortcuts.ts +25 -0
- package/src/src/ui/overlay.ts +874 -0
- package/src/src/ui/settings.ts +140 -0
- package/src/tests/logic.test.mjs +101 -0
- package/src/ui/entry.ts +4 -0
- package/src/ui/icons/align-center.svg +1 -0
- package/src/ui/icons/align-left.svg +1 -0
- package/src/ui/icons/align-right.svg +1 -0
- package/src/ui/icons/arrow-double.svg +1 -0
- package/src/ui/icons/arrow-open.svg +1 -0
- package/src/ui/icons/arrow-single.svg +1 -0
- package/src/ui/icons/arrow-tail.svg +1 -0
- package/src/ui/icons/arrow-thick.svg +1 -0
- package/src/ui/icons/arrow.svg +1 -0
- package/src/ui/icons/beautify.svg +1 -0
- package/src/ui/icons/bold.svg +1 -0
- package/src/ui/icons/censor.svg +1 -0
- package/src/ui/icons/close.svg +1 -0
- package/src/ui/icons/copy-doc.svg +1 -0
- package/src/ui/icons/copy.svg +1 -0
- package/src/ui/icons/ellipse.svg +1 -0
- package/src/ui/icons/fill-fill.svg +1 -0
- package/src/ui/icons/fill-stroke.svg +1 -0
- package/src/ui/icons/fill-strokeFill.svg +1 -0
- package/src/ui/icons/highlight.svg +1 -0
- package/src/ui/icons/invert.svg +1 -0
- package/src/ui/icons/italic.svg +1 -0
- package/src/ui/icons/line-dashed.svg +1 -0
- package/src/ui/icons/line-dotted.svg +1 -0
- package/src/ui/icons/line-solid.svg +1 -0
- package/src/ui/icons/line.svg +1 -0
- package/src/ui/icons/marker.svg +1 -0
- package/src/ui/icons/measure.svg +1 -0
- package/src/ui/icons/number.svg +1 -0
- package/src/ui/icons/ocr.svg +1 -0
- package/src/ui/icons/open.svg +1 -0
- package/src/ui/icons/pencil.svg +1 -0
- package/src/ui/icons/rect.svg +1 -0
- package/src/ui/icons/redo.svg +1 -0
- package/src/ui/icons/sampler.svg +1 -0
- package/src/ui/icons/save.svg +1 -0
- package/src/ui/icons/select.svg +1 -0
- package/src/ui/icons/stamp.svg +1 -0
- package/src/ui/icons/text-bg.svg +1 -0
- package/src/ui/icons/text.svg +1 -0
- package/src/ui/icons/trash.svg +1 -0
- package/src/ui/icons/underline.svg +1 -0
- package/src/ui/icons/undo.svg +1 -0
- package/src/ui/index.html +12 -0
- package/src/ui/settings.html +5 -0
- package/src/ui/style.css +105 -0
- 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
|
+
});
|
package/src/ui/entry.ts
ADDED
|
@@ -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>
|
package/src/ui/style.css
ADDED
|
@@ -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; }
|