pursr 0.6.0 → 0.7.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.
- package/LICENSE +20 -20
- package/README.md +9 -9
- package/assets/icon.svg +20 -20
- package/assets/logo.svg +28 -28
- package/assets/social-preview.svg +76 -76
- package/bin/pursr-mcp.mjs +10 -9
- package/bin/pursr.mjs +11 -11
- package/package.json +4 -4
- package/plans/m5.4-polish.json +21 -21
- package/plugins/plugin-audit.js +57 -57
- package/plugins/plugin-demo.js +63 -63
- package/src/ai-diff.js +7 -6
- package/src/auth.js +92 -91
- package/src/baseline.js +126 -125
- package/src/ci-output.js +156 -156
- package/src/dom-snapshot.js +192 -192
- package/src/eval.js +17 -17
- package/src/every-viewport.js +51 -51
- package/src/frames.js +33 -33
- package/src/har.js +158 -158
- package/src/hover.js +25 -25
- package/src/index.js +6 -6
- package/src/interact.js +137 -137
- package/src/mcp-resources.js +111 -110
- package/src/mcp.js +436 -435
- package/src/overlays.js +169 -169
- package/src/plugin-audit.js +260 -260
- package/src/plugin.js +120 -120
- package/src/probe.js +19 -19
- package/src/report.js +175 -175
- package/src/runway.js +65 -65
- package/src/selector-heal.js +85 -85
- package/src/selector.js +38 -38
- package/src/shoot.js +73 -73
- package/src/shot.js +17 -17
- package/src/snap.js +128 -128
- package/src/sweep-schema.js +69 -69
- package/src/sweep.js +1 -1
- package/src/util.js +204 -188
- package/src/viewport.js +38 -38
- package/src/watch.js +134 -134
package/src/util.js
CHANGED
|
@@ -1,188 +1,204 @@
|
|
|
1
|
-
// Tiny utility module: arg reading, flag parsing, output path picking,
|
|
2
|
-
// sidecar writing.
|
|
3
|
-
|
|
4
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
5
|
-
import { join, basename } from "node:path";
|
|
6
|
-
import { homedir, tmpdir } from "node:os";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export function
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
export function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
1
|
+
// Tiny utility module: arg reading, flag parsing, output path picking,
|
|
2
|
+
// sidecar writing.
|
|
3
|
+
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, basename } from "node:path";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// Env-var helper. Primary: PURSR_X. Legacy: PURSOR_X (one-time deprecation warn).
|
|
10
|
+
const __PURSR_WARNED = new Set();
|
|
11
|
+
export function __PURSR_GET(name) {
|
|
12
|
+
const v = process.env[name];
|
|
13
|
+
if (v !== undefined) return v;
|
|
14
|
+
// legacy alias
|
|
15
|
+
const legacy = name.replace(/^PURSR_/, "PURSOR_");
|
|
16
|
+
const lv = process.env[legacy];
|
|
17
|
+
if (lv !== undefined && !__PURSR_WARNED.has(legacy)) {
|
|
18
|
+
__PURSR_WARNED.add(legacy);
|
|
19
|
+
console.error("[pursr] " + legacy + " is deprecated, use " + name + " instead");
|
|
20
|
+
}
|
|
21
|
+
return lv;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function outDir() {
|
|
25
|
+
// Use $XDG_PICTURES_DIR, ~/Pictures, or fallback to system tmp
|
|
26
|
+
const base = process.env.XDG_PICTURES_DIR || join(homedir(), "Pictures");
|
|
27
|
+
const dir = process.platform === "win32" ? join(base, "gen") : join(base, "gen");
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
return dir;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const fallback = join(tmpdir(), "pursr");
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(fallback, { recursive: true });
|
|
35
|
+
} catch {}
|
|
36
|
+
// Surface a single warning so silent fallback is debuggable.
|
|
37
|
+
if (__PURSR_GET("PURSR_DEBUG")) console.error("[pursr] outDir fallback to", fallback, "(", e?.message, ")");
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Validate required arg — throws with caller-friendly message. Returns value. */
|
|
43
|
+
export function requireArg(name, value, kind) {
|
|
44
|
+
if (value === undefined || value === null) throw new Error(`missing required argument: ${name}`);
|
|
45
|
+
if (kind === "string" && typeof value !== "string") throw new Error(`${name}: expected string, got ${typeof value}`);
|
|
46
|
+
if (kind === "number" && (typeof value !== "number" || !Number.isFinite(value))) throw new Error(`${name}: expected finite number, got ${typeof value}`);
|
|
47
|
+
if (kind === "string" && value.trim() === "") throw new Error(`${name}: must not be empty`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function makeOut(name) {
|
|
52
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
53
|
+
return join(outDir(), `pursr-${ts}-${name}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function nowIso() { return new Date().toISOString(); }
|
|
57
|
+
|
|
58
|
+
export function shortHash(buf) {
|
|
59
|
+
if (!buf || !Buffer.isBuffer(buf)) return "".padStart(10, "0");
|
|
60
|
+
return createHash("sha1").update(buf).digest("hex").slice(0, 10);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readArg(arg) {
|
|
64
|
+
if (arg === undefined || arg === null) return undefined;
|
|
65
|
+
if (typeof arg !== "string" || !arg.startsWith("@")) return arg;
|
|
66
|
+
const path = arg.slice(1);
|
|
67
|
+
if (!existsSync(path)) throw new Error(`@file not found: ${path}`);
|
|
68
|
+
return readFileSync(path, "utf8").replace(/\r?\n$/, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseFlags(argv) {
|
|
72
|
+
const flags = {};
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i];
|
|
75
|
+
if (!a || !a.startsWith("--")) continue;
|
|
76
|
+
const eq = a.indexOf("=");
|
|
77
|
+
let key, val;
|
|
78
|
+
if (eq >= 0) { key = a.slice(2, eq); val = a.slice(eq + 1); }
|
|
79
|
+
else {
|
|
80
|
+
key = a.slice(2);
|
|
81
|
+
const next = i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
82
|
+
val = (next !== undefined && !next.startsWith("--")) ? argv[++i] : true;
|
|
83
|
+
}
|
|
84
|
+
flags[key] = val;
|
|
85
|
+
}
|
|
86
|
+
return flags;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function asNum(v, dflt) {
|
|
90
|
+
if (v === undefined || v === null) return dflt;
|
|
91
|
+
const n = Number(v);
|
|
92
|
+
return Number.isFinite(n) ? n : dflt;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function asBool(v, dflt) {
|
|
96
|
+
if (v === true) return true;
|
|
97
|
+
if (v === false || v === undefined || v === null) return dflt;
|
|
98
|
+
const s = String(v).toLowerCase();
|
|
99
|
+
if (s === "1" || s === "true" || s === "yes" || s === "on") return true;
|
|
100
|
+
if (s === "0" || s === "false" || s === "no" || s === "off") return false;
|
|
101
|
+
return dflt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Pick a positional output path from argv, skipping --flags and their values.
|
|
105
|
+
// Returns the path or undefined.
|
|
106
|
+
export function pickOutPath(argv) {
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const a = argv[i];
|
|
109
|
+
if (!a) continue;
|
|
110
|
+
if (a.startsWith("--")) { if (!a.includes("=")) i++; continue; }
|
|
111
|
+
if (a.startsWith("@")) continue;
|
|
112
|
+
if (/[\\\/]/.test(a) || a.endsWith(".png")) return a.endsWith(".png") ? a : a + ".png";
|
|
113
|
+
return undefined; // first positional non-path token is not an out path
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function writeSidecar(meta) {
|
|
119
|
+
if (!meta?.out) return null;
|
|
120
|
+
try {
|
|
121
|
+
if (existsSync(meta.out)) {
|
|
122
|
+
const buf = readFileSync(meta.out);
|
|
123
|
+
meta.size = buf.length;
|
|
124
|
+
meta.hash = shortHash(buf);
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
const sidecar = meta.out.replace(/\.png$/i, ".json");
|
|
128
|
+
writeFileSync(sidecar, JSON.stringify(meta, null, 2));
|
|
129
|
+
return sidecar;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function findStepPng(dir, stepName) {
|
|
133
|
+
const target = String(stepName || "").replace(/.png$/i, "").trim();
|
|
134
|
+
const files = readdirSyncFiles(dir);
|
|
135
|
+
if (!files.length) return null;
|
|
136
|
+
if (!target) return null;
|
|
137
|
+
// exact basename match first (handles refs like "baseline" or "00-baseline")
|
|
138
|
+
for (const f of files) {
|
|
139
|
+
const base = basename(f, ".png");
|
|
140
|
+
if (base === target) return join(dir, f);
|
|
141
|
+
}
|
|
142
|
+
// match the "NN-" prefix-stripped basename (e.g. "03-baseline" referenced as "baseline")
|
|
143
|
+
for (const f of files) {
|
|
144
|
+
const base = basename(f, ".png");
|
|
145
|
+
const m = base.match(/^\d+-(.+)$/);
|
|
146
|
+
if (m && m[1] === target) return join(dir, f);
|
|
147
|
+
}
|
|
148
|
+
// loose suffix match
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
const base = basename(f, ".png");
|
|
151
|
+
if (base.endsWith("-" + target)) return join(dir, f);
|
|
152
|
+
}
|
|
153
|
+
// substring match last resort
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
const base = basename(f, ".png");
|
|
156
|
+
if (base.includes(target)) return join(dir, f);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readdirSyncFiles(dir) {
|
|
162
|
+
try { return readdirSync(dir).filter(f => f.endsWith(".png")); } catch { return []; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ESCAPE_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
166
|
+
export function escapeHtml(s) {
|
|
167
|
+
return String(s ?? "").replace(/[&<>"']/g, c => ESCAPE_MAP[c]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function stripLarge(obj) {
|
|
171
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
174
|
+
if (k === "viewport" || k === "flags") continue;
|
|
175
|
+
if (typeof v === "string" && v.length > 400) { out[k] = v.slice(0, 400) + "…"; continue; }
|
|
176
|
+
out[k] = v;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function renderSweepHtml(summary) {
|
|
182
|
+
if (!summary?.steps?.length) return `<!doctype html><html><meta charset="utf-8"><title>pursr sweep — empty</title><body><p>No steps.</p></body></html>`;
|
|
183
|
+
const rows = summary.steps.map(s => {
|
|
184
|
+
const png = s.meta && s.meta.out ? basename(s.meta.out) : null;
|
|
185
|
+
const errCell = s.ok ? "" : `<div class="err">${escapeHtml(s.error || "")}</div>`;
|
|
186
|
+
const meta = s.meta ? `<pre>${escapeHtml(JSON.stringify(stripLarge(s.meta), null, 2))}</pre>` : "";
|
|
187
|
+
return `<article class="step ${s.ok ? "ok" : "fail"}"><header><span class="i">#${s.i}</span><span class="name">${escapeHtml(s.name)}</span><span class="op">${escapeHtml(s.op || "")}</span><span class="ms">${s.ms}ms</span><span class="status">${s.ok ? "OK" : "FAIL"}</span></header>${png ? `<img src="${png}" loading="lazy" alt="${escapeHtml(s.name)}" />` : ""}${errCell}${meta}</article>`;
|
|
188
|
+
}).join("\n");
|
|
189
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>pursr sweep — ${escapeHtml(summary.name || "")}</title>
|
|
190
|
+
<style>:root { color-scheme: light dark; } body { font: 14px/1.4 -apple-system, system-ui, sans-serif; margin: 0; background:#0b0b0b; color:#eee; } header.bar { padding: 12px 20px; background:#181818; border-bottom: 1px solid #2a2a2a; position: sticky; top:0; } header.bar h1 { font-size: 16px; margin: 0; } header.bar .meta { font-size: 12px; opacity: .7; } main { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 12px; padding: 12px; } article.step { background:#161616; border:1px solid #2a2a2a; border-radius: 8px; overflow: hidden; } article.step.fail { border-color: #b04; } article.step header { display: flex; gap: 6px; padding: 8px 10px; font-size: 12px; background: #1c1c1c; align-items: center; } article.step header .i { color:#888; } article.step header .name { font-weight: 600; } article.step header .op { color:#9ad; font-family: monospace; } article.step header .ms { color:#888; margin-left: auto; } article.step header .status { padding: 1px 6px; border-radius: 4px; background:#234; color:#adf; font-size: 11px; } article.step.fail header .status { background:#421; color:#fbb; } article.step img { display: block; width: 100%; height: auto; background:#000; } article.step pre { margin: 0; padding: 8px 10px; font-size: 11px; max-height: 180px; overflow: auto; background:#111; color:#aaa; border-top: 1px solid #222; } article.step .err { padding: 8px 10px; background: #2a0e0e; color: #fbb; font-size: 12px; }</style></head>
|
|
191
|
+
<body><header class="bar"><h1>pursr sweep: ${escapeHtml(summary.name || "(unnamed)")}</h1><div class="meta">${summary.steps.length} steps · ${escapeHtml(summary.outDir)} · ${escapeHtml(summary.ts)}</div></header><main>${rows}</main></body></html>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function renderEveryViewportHtml(summary) {
|
|
195
|
+
if (!summary?.captures?.length) return '<!doctype html><html><meta charset="utf-8"><title>every-viewport — empty</title><body><p>No captures.</p></body></html>';
|
|
196
|
+
const rows = summary.captures.map(c => {
|
|
197
|
+
const png = c.meta?.out ? basename(c.meta.out) : null;
|
|
198
|
+
const err = c.error ? '<div class="err">' + escapeHtml(c.error) + '</div>' : '';
|
|
199
|
+
const img = png ? '<img src="' + png + '" loading="lazy" alt="' + escapeHtml(c.name) + '" />' : '';
|
|
200
|
+
return '<article class="step ' + (c.ok ? "ok" : "fail") + '"><header><span class="name">' + escapeHtml(c.name) + '</span><span class="ms">' + c.ms + 'ms</span><span class="status">' + (c.ok ? "OK" : "FAIL") + '</span></header>' + img + err + '</article>';
|
|
201
|
+
}).join("\n");
|
|
202
|
+
return '<!doctype html><html><head><meta charset="utf-8"><title>every-viewport — ' + escapeHtml(summary.url || "") + '</title><style>:root{color-scheme:light dark}body{font:14px/1.4 -apple-system,system-ui,sans-serif;margin:0;background:#0b0b0b;color:#eee}header.bar{padding:12px 20px;background:#181818;border-bottom:1px solid #2a2a2a;position:sticky;top:0}header.bar h1{font-size:16px;margin:0}header.bar .meta{font-size:12px;opacity:.7}main{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:12px;padding:12px}article.step{background:#161616;border:1px solid #2a2a2a;border-radius:8px;overflow:hidden}article.step.fail{border-color:#b04}article.step header{display:flex;gap:6px;padding:8px 10px;font-size:12px;background:#1c1c1c;align-items:center}article.step header .name{font-weight:600}article.step header .ms{color:#888;margin-left:auto}article.step header .status{padding:1px 6px;border-radius:4px;background:#234;color:#adf;font-size:11px}article.step.fail header .status{background:#421;color:#fbb}article.step img{display:block;width:100%;height:auto;background:#000}article.step .err{padding:8px 10px;background:#2a0e0e;color:#fbb;font-size:12px}</style></head><body><header class="bar"><h1>every-viewport: ' + escapeHtml(summary.url || "") + '</h1><div class="meta">' + summary.captures.length + ' viewports · ' + escapeHtml(summary.outDir) + ' · ' + escapeHtml(summary.ts) + '</div></header><main>' + rows + '</main></body></html>';
|
|
203
|
+
}
|
|
204
|
+
|
package/src/viewport.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
// viewport presets + flag -> viewport resolution.
|
|
2
|
-
|
|
3
|
-
export const VIEWPORTS = {
|
|
4
|
-
"desktop-1280": { width: 1280, height: 800, dpr: 1, label: "Desktop 1280x800" },
|
|
5
|
-
"desktop-1440": { width: 1440, height: 900, dpr: 1, label: "Desktop 1440x900" },
|
|
6
|
-
"desktop-1920": { width: 1920, height: 1080, dpr: 1, label: "Desktop 1920x1080" },
|
|
7
|
-
"desktop-2560": { width: 2560, height: 1440, dpr: 1, label: "QHD 2560x1440" },
|
|
8
|
-
"ultrawide-3440":{ width: 3440, height: 1440, dpr: 1, label: "Ultrawide 3440x1440" },
|
|
9
|
-
"tablet-768": { width: 768, height: 1024, dpr: 2, label: "Tablet portrait 768x1024 @2x" },
|
|
10
|
-
"tablet-1024": { width: 1024, height: 768, dpr: 2, label: "Tablet landscape 1024x768 @2x" },
|
|
11
|
-
"mobile-375": { width: 375, height: 812, dpr: 3, label: "iPhone X 375x812 @3x" },
|
|
12
|
-
"mobile-414": { width: 414, height: 896, dpr: 3, label: "iPhone XR 414x896 @3x" },
|
|
13
|
-
"mobile-360": { width: 360, height: 800, dpr: 2, label: "Android 360x800 @2x" },
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_VIEWPORT = VIEWPORTS["desktop-1280"];
|
|
17
|
-
|
|
18
|
-
import { listViewportPresets } from "./plugin.js";
|
|
19
|
-
import { asNum } from "./util.js";
|
|
20
|
-
|
|
21
|
-
export function listViewports() {
|
|
22
|
-
return Object.entries({ ...VIEWPORTS, ...listViewportPresets() }).map(([k, v]) => ({ name: k, ...v }));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function resolveViewport(flags = {}) {
|
|
26
|
-
const all = { ...VIEWPORTS, ...listViewportPresets() };
|
|
27
|
-
const name = flags.preset || flags.viewport;
|
|
28
|
-
if (name && all[name]) return { ...all[name], name };
|
|
29
|
-
if (flags.width || flags.height) {
|
|
30
|
-
return {
|
|
31
|
-
name: "custom",
|
|
32
|
-
label: `Custom ${flags.width}x${flags.height}`,
|
|
33
|
-
width: asNum(flags.width, DEFAULT_VIEWPORT.width),
|
|
34
|
-
height: asNum(flags.height, DEFAULT_VIEWPORT.height),
|
|
35
|
-
dpr: asNum(flags.dpr, 1),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
return { ...DEFAULT_VIEWPORT, name: "desktop-1280" };
|
|
1
|
+
// viewport presets + flag -> viewport resolution.
|
|
2
|
+
|
|
3
|
+
export const VIEWPORTS = {
|
|
4
|
+
"desktop-1280": { width: 1280, height: 800, dpr: 1, label: "Desktop 1280x800" },
|
|
5
|
+
"desktop-1440": { width: 1440, height: 900, dpr: 1, label: "Desktop 1440x900" },
|
|
6
|
+
"desktop-1920": { width: 1920, height: 1080, dpr: 1, label: "Desktop 1920x1080" },
|
|
7
|
+
"desktop-2560": { width: 2560, height: 1440, dpr: 1, label: "QHD 2560x1440" },
|
|
8
|
+
"ultrawide-3440":{ width: 3440, height: 1440, dpr: 1, label: "Ultrawide 3440x1440" },
|
|
9
|
+
"tablet-768": { width: 768, height: 1024, dpr: 2, label: "Tablet portrait 768x1024 @2x" },
|
|
10
|
+
"tablet-1024": { width: 1024, height: 768, dpr: 2, label: "Tablet landscape 1024x768 @2x" },
|
|
11
|
+
"mobile-375": { width: 375, height: 812, dpr: 3, label: "iPhone X 375x812 @3x" },
|
|
12
|
+
"mobile-414": { width: 414, height: 896, dpr: 3, label: "iPhone XR 414x896 @3x" },
|
|
13
|
+
"mobile-360": { width: 360, height: 800, dpr: 2, label: "Android 360x800 @2x" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_VIEWPORT = VIEWPORTS["desktop-1280"];
|
|
17
|
+
|
|
18
|
+
import { listViewportPresets } from "./plugin.js";
|
|
19
|
+
import { asNum } from "./util.js";
|
|
20
|
+
|
|
21
|
+
export function listViewports() {
|
|
22
|
+
return Object.entries({ ...VIEWPORTS, ...listViewportPresets() }).map(([k, v]) => ({ name: k, ...v }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveViewport(flags = {}) {
|
|
26
|
+
const all = { ...VIEWPORTS, ...listViewportPresets() };
|
|
27
|
+
const name = flags.preset || flags.viewport;
|
|
28
|
+
if (name && all[name]) return { ...all[name], name };
|
|
29
|
+
if (flags.width || flags.height) {
|
|
30
|
+
return {
|
|
31
|
+
name: "custom",
|
|
32
|
+
label: `Custom ${flags.width}x${flags.height}`,
|
|
33
|
+
width: asNum(flags.width, DEFAULT_VIEWPORT.width),
|
|
34
|
+
height: asNum(flags.height, DEFAULT_VIEWPORT.height),
|
|
35
|
+
dpr: asNum(flags.dpr, 1),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { ...DEFAULT_VIEWPORT, name: "desktop-1280" };
|
|
39
39
|
}
|