@seybel/breakhound 1.0.8 → 1.0.10
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/dist/cli.js +225 -42
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/inject.js +187 -6
- package/dist/schema.json +20 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -77,6 +77,7 @@ function findingKey(f) {
|
|
|
77
77
|
}
|
|
78
78
|
function severityScore(f) {
|
|
79
79
|
const e = f.evidence;
|
|
80
|
+
if (typeof e.dayWidthPx === "number") return -e.dayWidthPx;
|
|
80
81
|
if (typeof e.overflowPx === "number") return e.overflowPx;
|
|
81
82
|
if (typeof e.overlapWidth === "number" && typeof e.overlapHeight === "number") {
|
|
82
83
|
return e.overlapWidth * e.overlapHeight;
|
|
@@ -173,6 +174,9 @@ function buildIssues(route, widths, results) {
|
|
|
173
174
|
evidence = { ...evidence, fixedWidthPx: onlyWidth };
|
|
174
175
|
suggestion = `It stays ${onlyWidth}px wide no matter the screen size. Replace the fixed width with max-width: 100% (or a fluid width) so it can shrink.`;
|
|
175
176
|
}
|
|
177
|
+
if (f.detector === "calendar") {
|
|
178
|
+
suggestion = calendarSuggestion(ranges, widths[widths.length - 1] ?? 0, f.evidence);
|
|
179
|
+
}
|
|
176
180
|
const issue = {
|
|
177
181
|
id: issueId(route, f),
|
|
178
182
|
fingerprint: findingKey(f),
|
|
@@ -195,6 +199,18 @@ function buildIssues(route, widths, results) {
|
|
|
195
199
|
(a, b) => order[a.severity] - order[b.severity] || (a.ranges[0]?.from ?? 0) - (b.ranges[0]?.from ?? 0)
|
|
196
200
|
);
|
|
197
201
|
}
|
|
202
|
+
function calendarSuggestion(ranges, widest, evidence) {
|
|
203
|
+
const last = ranges[ranges.length - 1]?.to ?? widest;
|
|
204
|
+
const list = (where) => `show the days as a list ${where} (an agenda view, one day per row), the usual choice for calendars on phones`;
|
|
205
|
+
if (last >= widest) {
|
|
206
|
+
return `Its 7 days don't fit side by side at any width up to ${widest}px. Give the calendar more room or show less in each day, and ${list("on small screens")}.`;
|
|
207
|
+
}
|
|
208
|
+
const below = `below ${last + 1}px`;
|
|
209
|
+
if (evidence.scrollsSideways) {
|
|
210
|
+
return `It already scrolls sideways, but its days are still too narrow ${below}. Give the grid a min-width so each day keeps its room as it scrolls, or ${list(below)}. Narrower days, or fewer than 7 columns, won't fix it.`;
|
|
211
|
+
}
|
|
212
|
+
return `A calendar needs its 7 days side by side, and ${below} they don't fit. Narrower days, or fewer than 7 columns, won't fix it. Either let it scroll sideways inside its own box ${below} (a wrapper with overflow-x: auto, and a min-width on the grid), which keeps one layout; or ${list(below)}.`;
|
|
213
|
+
}
|
|
198
214
|
function joinRanges(ranges) {
|
|
199
215
|
const sorted = [...ranges].sort((a, b) => a.from - b.from);
|
|
200
216
|
const out = [];
|
|
@@ -918,6 +934,7 @@ var DETECTOR_IDS = [
|
|
|
918
934
|
"unexpected-wrap",
|
|
919
935
|
"clipped-text",
|
|
920
936
|
"squished-text",
|
|
937
|
+
"calendar",
|
|
921
938
|
"distorted-media",
|
|
922
939
|
"small-tap-target",
|
|
923
940
|
"tiny-text",
|
|
@@ -1272,6 +1289,7 @@ var DETECTOR_LABELS = {
|
|
|
1272
1289
|
"unexpected-wrap": "Unexpected wrap",
|
|
1273
1290
|
"clipped-text": "Clipped text",
|
|
1274
1291
|
"squished-text": "Squished text",
|
|
1292
|
+
calendar: "Calendar too narrow",
|
|
1275
1293
|
"distorted-media": "Distorted image",
|
|
1276
1294
|
"small-tap-target": "Small tap target",
|
|
1277
1295
|
"tiny-text": "Tiny text",
|
|
@@ -5317,12 +5335,34 @@ function enabledDetectors(config) {
|
|
|
5317
5335
|
if (!overrides) return void 0;
|
|
5318
5336
|
return DETECTOR_IDS.filter((id) => overrides[id] !== false);
|
|
5319
5337
|
}
|
|
5338
|
+
var SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
|
|
5339
|
+
function calendarChecks(issue, overrides) {
|
|
5340
|
+
return String(issue.evidence.checksInside ?? "").split(" ").filter(Boolean).map((part) => {
|
|
5341
|
+
const [id = "", severity = "error"] = part.split(":");
|
|
5342
|
+
const o = overrides[id];
|
|
5343
|
+
return { id, severity: typeof o === "string" ? o : severity };
|
|
5344
|
+
}).filter((c) => overrides[c.id] !== false);
|
|
5345
|
+
}
|
|
5346
|
+
function keepIssue(issue, overrides) {
|
|
5347
|
+
if (overrides[issue.detector] === false) return false;
|
|
5348
|
+
if (issue.detector !== "calendar" || !issue.evidence.checksInside) return true;
|
|
5349
|
+
return calendarChecks(issue, overrides).length > 0;
|
|
5350
|
+
}
|
|
5320
5351
|
function applySeverity(routes, config) {
|
|
5321
5352
|
const overrides = config.detectors ?? {};
|
|
5322
5353
|
for (const route of routes) {
|
|
5323
5354
|
for (const issue of route.issues) {
|
|
5324
5355
|
const o = overrides[issue.detector];
|
|
5325
5356
|
if (o === "error" || o === "warning" || o === "info") issue.severity = o;
|
|
5357
|
+
else if (issue.detector === "calendar") {
|
|
5358
|
+
const checks = calendarChecks(issue, overrides);
|
|
5359
|
+
if (checks.length) {
|
|
5360
|
+
issue.severity = checks.reduce(
|
|
5361
|
+
(worst, c) => SEVERITY_RANK[c.severity] < SEVERITY_RANK[worst] ? c.severity : worst,
|
|
5362
|
+
"info"
|
|
5363
|
+
);
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5326
5366
|
}
|
|
5327
5367
|
}
|
|
5328
5368
|
}
|
|
@@ -5600,7 +5640,7 @@ async function saveExtensionScan(sent, config, cwd) {
|
|
|
5600
5640
|
const off = config.detectors ?? {};
|
|
5601
5641
|
const routes = sent.routes.map((r) => ({
|
|
5602
5642
|
...r,
|
|
5603
|
-
issues: r.issues.filter((i) =>
|
|
5643
|
+
issues: r.issues.filter((i) => keepIssue(i, off))
|
|
5604
5644
|
}));
|
|
5605
5645
|
applySeverity(routes, config);
|
|
5606
5646
|
relativeSources(routes, project?.root ?? cwd, project?.basePath ?? "", project?.workspace);
|
|
@@ -5772,7 +5812,7 @@ async function runVerify(req) {
|
|
|
5772
5812
|
root: req.cwd
|
|
5773
5813
|
});
|
|
5774
5814
|
const off = req.config.detectors ?? {};
|
|
5775
|
-
for (const r of now.routes) r.issues = r.issues.filter((i) =>
|
|
5815
|
+
for (const r of now.routes) r.issues = r.issues.filter((i) => keepIssue(i, off));
|
|
5776
5816
|
return finishVerify(req, baseline, now);
|
|
5777
5817
|
}
|
|
5778
5818
|
const { settings } = baseline;
|
|
@@ -6294,6 +6334,12 @@ function checkRoutes(value) {
|
|
|
6294
6334
|
}
|
|
6295
6335
|
for (const i of r.issues) {
|
|
6296
6336
|
const element = isObject(i) ? i.element : null;
|
|
6337
|
+
if (isObject(i) && typeof i.detector === "string" && !DETECTOR_IDS.includes(i.detector)) {
|
|
6338
|
+
throw new HttpError(
|
|
6339
|
+
400,
|
|
6340
|
+
`This project's breakhound doesn't know the check "${i.detector.slice(0, 40)}" yet. Restart your agent with the latest breakhound to send it.`
|
|
6341
|
+
);
|
|
6342
|
+
}
|
|
6297
6343
|
if (!isObject(i) || typeof i.id !== "string" || typeof i.title !== "string" || !DETECTOR_IDS.includes(i.detector) || !SEVERITIES.has(String(i.severity)) || !Array.isArray(i.ranges) || !isObject(element) || typeof element.selector !== "string") {
|
|
6298
6344
|
throw new HttpError(400, "Bad issue");
|
|
6299
6345
|
}
|
|
@@ -6349,7 +6395,9 @@ async function startBridge(opts) {
|
|
|
6349
6395
|
name: "breakhound",
|
|
6350
6396
|
version: opts.version,
|
|
6351
6397
|
root: opts.root,
|
|
6352
|
-
project: projectName(opts.root)
|
|
6398
|
+
project: projectName(opts.root),
|
|
6399
|
+
// So a newer extension sends only checks this version knows.
|
|
6400
|
+
detectors: DETECTOR_IDS
|
|
6353
6401
|
});
|
|
6354
6402
|
case "/breakhound/match": {
|
|
6355
6403
|
if (typeof body.url !== "string") throw new HttpError(400, "No url");
|
|
@@ -6420,7 +6468,7 @@ async function startBridge(opts) {
|
|
|
6420
6468
|
case "/breakhound/checks/progress": {
|
|
6421
6469
|
const check = checks.get(String(body.id));
|
|
6422
6470
|
if (check && typeof body.done === "number" && typeof body.message === "string") {
|
|
6423
|
-
check.onProgress?.(body.done, body.message.slice(0,
|
|
6471
|
+
check.onProgress?.(body.done, body.message.slice(0, 400));
|
|
6424
6472
|
}
|
|
6425
6473
|
return reply({});
|
|
6426
6474
|
}
|
|
@@ -6430,7 +6478,14 @@ async function startBridge(opts) {
|
|
|
6430
6478
|
checks.delete(check.id);
|
|
6431
6479
|
clearTimeout(check.timer);
|
|
6432
6480
|
if (typeof body.error === "string") check.reject(new Error(body.error.slice(0, 500)));
|
|
6433
|
-
else
|
|
6481
|
+
else {
|
|
6482
|
+
try {
|
|
6483
|
+
check.resolve(checkRoutes(body.routes));
|
|
6484
|
+
} catch (error) {
|
|
6485
|
+
check.reject(error instanceof Error ? error : new Error(String(error)));
|
|
6486
|
+
throw error;
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6434
6489
|
return reply({});
|
|
6435
6490
|
}
|
|
6436
6491
|
default:
|
|
@@ -6526,6 +6581,9 @@ async function startBridge(opts) {
|
|
|
6526
6581
|
};
|
|
6527
6582
|
}
|
|
6528
6583
|
|
|
6584
|
+
// src/icon.ts
|
|
6585
|
+
var ICON_PNG = "data:image/png;base64,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";
|
|
6586
|
+
|
|
6529
6587
|
// src/mcp.ts
|
|
6530
6588
|
var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
6531
6589
|
var INSTRUCTIONS = `breakhound finds responsive layout bugs by resizing pages in a real browser, and tells you the exact widths where they happen.
|
|
@@ -6538,12 +6596,15 @@ How to fix issues with it:
|
|
|
6538
6596
|
5. When verify is clean for a route, checkpoint it. That commits the fix on its own, so it can be undone later with undo.
|
|
6539
6597
|
Before finishing, run verify with no routes to check every page.
|
|
6540
6598
|
|
|
6541
|
-
|
|
6599
|
+
scan and verify answer within about 45 seconds. When one says it's still running, it carries on: call it again with the same arguments to get its result, rather than something else. It says what it's waiting for; if that's the developer's browser tab, ask them to bring it into view.
|
|
6600
|
+
|
|
6601
|
+
Issues sent from the extension are checked again in the developer's own browser: verify asks the open breakhound panel to scan the pages, so their login and browser carry over. If no panel is open, verify says so; ask the developer to open it rather than calling scan, which replaces their issues with a headless scan. Chrome doesn't draw a tab that's out of view, so before you first verify them, tell the developer in one line that breakhound is about to check your fixes in their Chrome tab, and that the tab needs to be in view (beside their editor is fine) until it's done.`;
|
|
6542
6602
|
var FIX_PROMPT = `Fix the responsive layout issues breakhound found in this project.
|
|
6543
6603
|
|
|
6544
6604
|
Start with list_issues: if I sent them from the breakhound extension they are already there, so don't scan again. Only scan when there are none.
|
|
6545
6605
|
Work through the errors first, then the warnings. For each one: get_issue for the details, fix the cause in the code (not overflow: hidden), verify the routes you touched, and checkpoint each route once verify is clean for it.
|
|
6546
6606
|
Before you finish, verify with no routes to check every page, and tell me what you fixed and anything you couldn't.`;
|
|
6607
|
+
var REPLY_WITHIN_MS = 45e3;
|
|
6547
6608
|
var text = (t, isError = false) => ({
|
|
6548
6609
|
content: [{ type: "text", text: t }],
|
|
6549
6610
|
...isError ? { isError } : {}
|
|
@@ -6602,6 +6663,7 @@ function createTools() {
|
|
|
6602
6663
|
return [
|
|
6603
6664
|
{
|
|
6604
6665
|
name: "scan",
|
|
6666
|
+
kind: "long",
|
|
6605
6667
|
description: "Find the app (starting its dev server if needed), discover its routes and scan each one at every width from 320 to 1920px in a headless browser. Returns every issue with its id. The result is the baseline that verify compares with, replacing any issues the developer sent from the browser extension, so check list_issues first. Takes from seconds to a few minutes on large apps.",
|
|
6606
6668
|
inputSchema: {
|
|
6607
6669
|
type: "object",
|
|
@@ -6629,6 +6691,7 @@ function createTools() {
|
|
|
6629
6691
|
},
|
|
6630
6692
|
{
|
|
6631
6693
|
name: "list_routes",
|
|
6694
|
+
kind: "read",
|
|
6632
6695
|
description: "Routes from the last scan, with their status and number of issues.",
|
|
6633
6696
|
inputSchema: { type: "object", properties: {} },
|
|
6634
6697
|
async run(_args, ctx) {
|
|
@@ -6638,6 +6701,7 @@ function createTools() {
|
|
|
6638
6701
|
},
|
|
6639
6702
|
{
|
|
6640
6703
|
name: "list_issues",
|
|
6704
|
+
kind: "read",
|
|
6641
6705
|
description: "Issues from the last scan or verify, each listed once even when it shows up on several routes. Filter by route or severity.",
|
|
6642
6706
|
inputSchema: {
|
|
6643
6707
|
type: "object",
|
|
@@ -6657,6 +6721,7 @@ function createTools() {
|
|
|
6657
6721
|
},
|
|
6658
6722
|
{
|
|
6659
6723
|
name: "get_issue",
|
|
6724
|
+
kind: "read",
|
|
6660
6725
|
description: "Everything about one issue: the widths, the element and where it is in the code (when known), measurements, computed styles, a likely fix, every page it is on, and a screenshot.",
|
|
6661
6726
|
inputSchema: {
|
|
6662
6727
|
type: "object",
|
|
@@ -6687,7 +6752,8 @@ function createTools() {
|
|
|
6687
6752
|
},
|
|
6688
6753
|
{
|
|
6689
6754
|
name: "verify",
|
|
6690
|
-
|
|
6755
|
+
kind: "long",
|
|
6756
|
+
description: "Scan routes again and compare with the baseline from scan: which issues are fixed, which are still there, and which are new (a change broke something, maybe at another width or on another route). Pass the routes you changed for a quick check, and nothing to check every route before you finish. Issues sent from the browser extension are checked in the developer's Chrome tab, which must be in view: tell them before you first call it.",
|
|
6691
6757
|
inputSchema: {
|
|
6692
6758
|
type: "object",
|
|
6693
6759
|
properties: {
|
|
@@ -6861,6 +6927,8 @@ async function runMcpServer(version, io = { input: process.stdin, output: proces
|
|
|
6861
6927
|
let clientCanListRoots = false;
|
|
6862
6928
|
let root = process.env.BREAKHOUND_ROOT ?? null;
|
|
6863
6929
|
let queue = Promise.resolve();
|
|
6930
|
+
let job = null;
|
|
6931
|
+
const replyWithinMs = Number(process.env.BREAKHOUND_REPLY_WITHIN_MS) || REPLY_WITHIN_MS;
|
|
6864
6932
|
const send = (message) => io.output.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}
|
|
6865
6933
|
`);
|
|
6866
6934
|
const request = (method, params = {}) => new Promise((resolve8, reject) => {
|
|
@@ -6890,13 +6958,17 @@ async function runMcpServer(version, io = { input: process.stdin, output: proces
|
|
|
6890
6958
|
root ??= cwd;
|
|
6891
6959
|
return root;
|
|
6892
6960
|
}
|
|
6893
|
-
|
|
6894
|
-
const tool = tools.find((t) => t.name === params.name);
|
|
6895
|
-
if (!tool) {
|
|
6896
|
-
send({ id, error: { code: -32602, message: `Unknown tool: ${String(params.name)}` } });
|
|
6897
|
-
return;
|
|
6898
|
-
}
|
|
6961
|
+
const progressFor = (params) => {
|
|
6899
6962
|
const token = params._meta?.progressToken;
|
|
6963
|
+
return (done, message) => {
|
|
6964
|
+
if (token !== void 0)
|
|
6965
|
+
send({
|
|
6966
|
+
method: "notifications/progress",
|
|
6967
|
+
params: { progressToken: token, progress: done, message }
|
|
6968
|
+
});
|
|
6969
|
+
};
|
|
6970
|
+
};
|
|
6971
|
+
async function runTool(tool, params, progress) {
|
|
6900
6972
|
try {
|
|
6901
6973
|
const dir = await projectRoot();
|
|
6902
6974
|
const { config, warnings } = await loadConfig(dir);
|
|
@@ -6907,22 +6979,93 @@ async function runMcpServer(version, io = { input: process.stdin, output: proces
|
|
|
6907
6979
|
root: dir,
|
|
6908
6980
|
config,
|
|
6909
6981
|
bridge,
|
|
6910
|
-
progress
|
|
6911
|
-
if (token !== void 0)
|
|
6912
|
-
send({
|
|
6913
|
-
method: "notifications/progress",
|
|
6914
|
-
params: { progressToken: token, progress: done, message }
|
|
6915
|
-
});
|
|
6916
|
-
}
|
|
6982
|
+
progress
|
|
6917
6983
|
})
|
|
6918
6984
|
);
|
|
6919
6985
|
if (warnings.length)
|
|
6920
6986
|
result.content.push({ type: "text", text: `Config warnings: ${warnings.join(" ")}` });
|
|
6921
|
-
|
|
6987
|
+
return result;
|
|
6922
6988
|
} catch (error) {
|
|
6923
6989
|
if (error instanceof MemoryError) server.current = null;
|
|
6924
|
-
|
|
6990
|
+
return text(error instanceof Error ? error.message : String(error), true);
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
const running = (j) => `${j.label} has been running for ${Math.round((Date.now() - j.startedAt) / 1e3)}s${j.latest ? ` (${j.latest})` : ""}.`;
|
|
6994
|
+
async function collect(j, params) {
|
|
6995
|
+
const late = new Promise((r) => setTimeout(() => r(null), replyWithinMs).unref());
|
|
6996
|
+
j.waiting = progressFor(params);
|
|
6997
|
+
const result = await Promise.race([j.done, late]);
|
|
6998
|
+
j.waiting = null;
|
|
6999
|
+
if (result) {
|
|
7000
|
+
if (job === j) job = null;
|
|
7001
|
+
return result;
|
|
7002
|
+
}
|
|
7003
|
+
return text(
|
|
7004
|
+
`Still running: ${running(j)} It carries on by itself. Call ${j.name} again with the same arguments to get its result; list_issues and get_issue answer meanwhile.`
|
|
7005
|
+
);
|
|
7006
|
+
}
|
|
7007
|
+
async function callTool(id, params) {
|
|
7008
|
+
const tool = tools.find((t) => t.name === params.name);
|
|
7009
|
+
if (!tool) {
|
|
7010
|
+
send({ id, error: { code: -32602, message: `Unknown tool: ${String(params.name)}` } });
|
|
7011
|
+
return;
|
|
6925
7012
|
}
|
|
7013
|
+
const key = `${tool.name} ${JSON.stringify(params.arguments ?? {})}`;
|
|
7014
|
+
const busy = job && !job.finished ? job : null;
|
|
7015
|
+
if (tool.kind === "read") {
|
|
7016
|
+
const result = await runTool(tool, params, progressFor(params));
|
|
7017
|
+
if (busy)
|
|
7018
|
+
result.content.push({
|
|
7019
|
+
type: "text",
|
|
7020
|
+
text: `Note: ${running(busy)} Call ${busy.name} again with the same arguments for its result.`
|
|
7021
|
+
});
|
|
7022
|
+
send({ id, result });
|
|
7023
|
+
return;
|
|
7024
|
+
}
|
|
7025
|
+
if (job?.key === key) {
|
|
7026
|
+
send({ id, result: await collect(job, params) });
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
if (busy) {
|
|
7030
|
+
send({
|
|
7031
|
+
id,
|
|
7032
|
+
result: text(
|
|
7033
|
+
`Not yet: ${running(busy)} Call ${busy.name} again with the same arguments to get its result first.`,
|
|
7034
|
+
true
|
|
7035
|
+
)
|
|
7036
|
+
});
|
|
7037
|
+
return;
|
|
7038
|
+
}
|
|
7039
|
+
if (tool.kind !== "long") {
|
|
7040
|
+
job = null;
|
|
7041
|
+
queue = queue.then(() => runTool(tool, params, progressFor(params)));
|
|
7042
|
+
send({ id, result: await queue });
|
|
7043
|
+
return;
|
|
7044
|
+
}
|
|
7045
|
+
const args = params.arguments ?? {};
|
|
7046
|
+
const routes = strings(args.routes);
|
|
7047
|
+
const j = {
|
|
7048
|
+
key,
|
|
7049
|
+
name: tool.name,
|
|
7050
|
+
label: `${tool.name}${routes.length ? ` of ${routes.join(", ")}` : ""}`,
|
|
7051
|
+
startedAt: Date.now(),
|
|
7052
|
+
latest: "",
|
|
7053
|
+
finished: false,
|
|
7054
|
+
waiting: null,
|
|
7055
|
+
done: Promise.resolve(text(""))
|
|
7056
|
+
};
|
|
7057
|
+
j.done = queue.then(
|
|
7058
|
+
() => runTool(tool, params, (done, message) => {
|
|
7059
|
+
j.latest = message;
|
|
7060
|
+
j.waiting?.(done, message);
|
|
7061
|
+
})
|
|
7062
|
+
);
|
|
7063
|
+
j.done.then(() => {
|
|
7064
|
+
j.finished = true;
|
|
7065
|
+
});
|
|
7066
|
+
queue = j.done;
|
|
7067
|
+
job = j;
|
|
7068
|
+
send({ id, result: await collect(j, params) });
|
|
6926
7069
|
}
|
|
6927
7070
|
function handle(message) {
|
|
6928
7071
|
const { id, method } = message;
|
|
@@ -6948,7 +7091,13 @@ async function runMcpServer(version, io = { input: process.stdin, output: proces
|
|
|
6948
7091
|
result: {
|
|
6949
7092
|
protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
6950
7093
|
capabilities: { tools: { listChanged: false }, prompts: { listChanged: false } },
|
|
6951
|
-
serverInfo: {
|
|
7094
|
+
serverInfo: {
|
|
7095
|
+
name: "breakhound",
|
|
7096
|
+
version: VERSION,
|
|
7097
|
+
// Shown beside the server by clients that show icons. No sizes: some clients' older
|
|
7098
|
+
// schemas expect another shape for it, and an icon isn't worth a failed connection.
|
|
7099
|
+
icons: [{ src: ICON_PNG, mimeType: "image/png" }]
|
|
7100
|
+
},
|
|
6952
7101
|
instructions: INSTRUCTIONS
|
|
6953
7102
|
}
|
|
6954
7103
|
});
|
|
@@ -6996,7 +7145,7 @@ async function runMcpServer(version, io = { input: process.stdin, output: proces
|
|
|
6996
7145
|
});
|
|
6997
7146
|
return;
|
|
6998
7147
|
case "tools/call":
|
|
6999
|
-
|
|
7148
|
+
void callTool(id, params);
|
|
7000
7149
|
return;
|
|
7001
7150
|
default:
|
|
7002
7151
|
send({ id, error: { code: -32601, message: `Method not found: ${String(method)}` } });
|
|
@@ -7030,11 +7179,16 @@ import { createInterface as createInterface3 } from "readline/promises";
|
|
|
7030
7179
|
import { styleText as styleText8 } from "util";
|
|
7031
7180
|
var SERVER_NAME = "breakhound";
|
|
7032
7181
|
var PACKAGE = "@seybel/breakhound";
|
|
7182
|
+
var LATEST = `${PACKAGE}@latest`;
|
|
7033
7183
|
function startsOtherPackage(args) {
|
|
7034
|
-
return Array.isArray(args) && args.includes("breakhound") && !args.
|
|
7184
|
+
return Array.isArray(args) && args.includes("breakhound") && !args.some((a) => typeof a === "string" && a.startsWith(PACKAGE));
|
|
7185
|
+
}
|
|
7186
|
+
function wroteUntagged(args) {
|
|
7187
|
+
return Array.isArray(args) && args.includes(PACKAGE) && args.includes("mcp");
|
|
7035
7188
|
}
|
|
7189
|
+
var toLatest = (args) => args.map((a) => a === PACKAGE ? LATEST : a);
|
|
7036
7190
|
function serverCommand(platform = process.platform) {
|
|
7037
|
-
const args = ["-y",
|
|
7191
|
+
const args = ["-y", LATEST, "mcp"];
|
|
7038
7192
|
return platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
|
|
7039
7193
|
}
|
|
7040
7194
|
function jsonClient(id, name, file, key, entry) {
|
|
@@ -7055,8 +7209,11 @@ ${JSON.stringify({ [SERVER_NAME]: entry }, null, 2)}`
|
|
|
7055
7209
|
}
|
|
7056
7210
|
}
|
|
7057
7211
|
const existing = data[key]?.[SERVER_NAME];
|
|
7058
|
-
if (existing && !startsOtherPackage(existing.args))
|
|
7059
|
-
|
|
7212
|
+
if (existing && !startsOtherPackage(existing.args) && !wroteUntagged(existing.args)) {
|
|
7213
|
+
return null;
|
|
7214
|
+
}
|
|
7215
|
+
const next = existing && wroteUntagged(existing.args) ? { ...existing, args: toLatest(existing.args) } : entry;
|
|
7216
|
+
data[key] = { ...data[key], [SERVER_NAME]: next };
|
|
7060
7217
|
return `${JSON.stringify(data, null, 2)}
|
|
7061
7218
|
`;
|
|
7062
7219
|
}
|
|
@@ -7076,6 +7233,9 @@ args = ${JSON.stringify(args).replaceAll(",", ", ")}
|
|
|
7076
7233
|
const table = /^\[mcp_servers\.breakhound\]\n(?:(?!\[)[^\n]*\n?)*/m;
|
|
7077
7234
|
const found = current?.match(table)?.[0];
|
|
7078
7235
|
if (found) {
|
|
7236
|
+
if (found.includes(`"${PACKAGE}"`)) {
|
|
7237
|
+
return current.replace(table, found.replace(`"${PACKAGE}"`, `"${LATEST}"`));
|
|
7238
|
+
}
|
|
7079
7239
|
if (found.includes(PACKAGE) || !/["']breakhound["']/.test(found)) return null;
|
|
7080
7240
|
return current.replace(table, `${block}${found.endsWith("\n\n") ? "\n" : ""}`);
|
|
7081
7241
|
}
|
|
@@ -7140,6 +7300,7 @@ function show(path, cwd) {
|
|
|
7140
7300
|
const rel = relative7(cwd, path);
|
|
7141
7301
|
return rel.startsWith("..") ? path.replace(homedir(), "~") : rel;
|
|
7142
7302
|
}
|
|
7303
|
+
var NOT_SHOWING_UP = "Not showing up? Your agent runs breakhound only while this folder is open in it, and with breakhound switched on in its MCP settings (Cursor and VS Code may add it switched off), so check both. How, for each agent: https://seybel.github.io/breakhound/guide/agents#not-connected";
|
|
7143
7304
|
async function installMcp(cwd, options = {}) {
|
|
7144
7305
|
const home = options.home ?? homedir();
|
|
7145
7306
|
const clients = options.clients?.length ? allClients(cwd, home).filter((c) => options.clients?.includes(c.id)) : detectClients(cwd, home);
|
|
@@ -7151,24 +7312,43 @@ async function installMcp(cwd, options = {}) {
|
|
|
7151
7312
|
return 1;
|
|
7152
7313
|
}
|
|
7153
7314
|
const changes = [];
|
|
7315
|
+
let alreadySetUp = false;
|
|
7154
7316
|
for (const client of clients) {
|
|
7155
7317
|
const current = existsSync15(client.file) ? readFileSync18(client.file, "utf8") : null;
|
|
7156
7318
|
try {
|
|
7157
7319
|
const next = client.update(current);
|
|
7158
|
-
if (next === null)
|
|
7320
|
+
if (next === null) {
|
|
7321
|
+
alreadySetUp = true;
|
|
7159
7322
|
console.log(
|
|
7160
7323
|
`${styleText8("green", "\u2714")} ${client.name}: already set up in ${show(client.file, cwd)}`
|
|
7161
7324
|
);
|
|
7162
|
-
else
|
|
7325
|
+
} else {
|
|
7326
|
+
const updating2 = !!current && /["']@?(?:seybel\/)?breakhound["']/.test(current);
|
|
7327
|
+
changes.push({ client, next, updating: updating2 });
|
|
7328
|
+
}
|
|
7163
7329
|
} catch (error) {
|
|
7164
7330
|
console.log(`${styleText8("yellow", "!")} ${client.name}: ${error.message}`);
|
|
7165
7331
|
}
|
|
7166
7332
|
}
|
|
7167
|
-
if (!changes.length)
|
|
7168
|
-
|
|
7333
|
+
if (!changes.length) {
|
|
7334
|
+
if (alreadySetUp) console.log(`
|
|
7335
|
+
${NOT_SHOWING_UP}`);
|
|
7336
|
+
return 0;
|
|
7337
|
+
}
|
|
7169
7338
|
const { command, args } = serverCommand();
|
|
7170
|
-
|
|
7171
|
-
|
|
7339
|
+
const adding = changes.filter((c) => !c.updating);
|
|
7340
|
+
const updating = changes.filter((c) => c.updating);
|
|
7341
|
+
if (adding.length) {
|
|
7342
|
+
console.log("\nbreakhound will add its MCP server to:\n");
|
|
7343
|
+
for (const { client } of adding)
|
|
7344
|
+
console.log(` ${client.name.padEnd(12)} ${show(client.file, cwd)}`);
|
|
7345
|
+
}
|
|
7346
|
+
if (updating.length) {
|
|
7347
|
+
console.log(
|
|
7348
|
+
"\nbreakhound will update its MCP server, so your agent always starts the latest, in:\n"
|
|
7349
|
+
);
|
|
7350
|
+
for (const { client } of updating)
|
|
7351
|
+
console.log(` ${client.name.padEnd(12)} ${show(client.file, cwd)}`);
|
|
7172
7352
|
}
|
|
7173
7353
|
console.log(styleText8("dim", `
|
|
7174
7354
|
The server runs: ${command} ${args.join(" ")}
|
|
@@ -7179,22 +7359,25 @@ async function installMcp(cwd, options = {}) {
|
|
|
7179
7359
|
return 1;
|
|
7180
7360
|
}
|
|
7181
7361
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7182
|
-
const answer = (await rl.question("Add it? [Y/n] ")).trim().toLowerCase();
|
|
7362
|
+
const answer = (await rl.question(adding.length ? "Add it? [Y/n] " : "Update it? [Y/n] ")).trim().toLowerCase();
|
|
7183
7363
|
rl.close();
|
|
7184
7364
|
if (answer && answer !== "y" && answer !== "yes") {
|
|
7185
7365
|
console.log("Nothing changed.");
|
|
7186
7366
|
return 0;
|
|
7187
7367
|
}
|
|
7188
7368
|
}
|
|
7189
|
-
for (const { client, next } of changes) {
|
|
7369
|
+
for (const { client, next, updating: updating2 } of changes) {
|
|
7190
7370
|
mkdirSync3(dirname5(client.file), { recursive: true });
|
|
7191
7371
|
writeFileSync8(client.file, next);
|
|
7192
|
-
console.log(
|
|
7372
|
+
console.log(
|
|
7373
|
+
`${styleText8("green", "\u2714")} ${client.name}: ${updating2 ? "updated in" : "added to"} ${show(client.file, cwd)}`
|
|
7374
|
+
);
|
|
7193
7375
|
}
|
|
7194
7376
|
console.log(
|
|
7195
7377
|
`
|
|
7196
7378
|
Restart your agent (or reload its MCP servers), then ask it to "fix the responsive issues breakhound finds".`
|
|
7197
7379
|
);
|
|
7380
|
+
console.log(NOT_SHOWING_UP);
|
|
7198
7381
|
return 0;
|
|
7199
7382
|
}
|
|
7200
7383
|
|
|
@@ -7299,7 +7482,7 @@ function normalizeBaseUrl(url) {
|
|
|
7299
7482
|
throw new Error(`--url must be a URL like http://localhost:3000 (got "${url}")`);
|
|
7300
7483
|
}
|
|
7301
7484
|
}
|
|
7302
|
-
var
|
|
7485
|
+
var SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
|
|
7303
7486
|
function positiveInt(name, value) {
|
|
7304
7487
|
if (value === void 0) return void 0;
|
|
7305
7488
|
const n = Number(value);
|
|
@@ -7356,7 +7539,7 @@ async function main(argv) {
|
|
|
7356
7539
|
const cwd = process.cwd();
|
|
7357
7540
|
const [command, ...rest2] = positionals;
|
|
7358
7541
|
const askedFailOn = values["fail-on"];
|
|
7359
|
-
if (askedFailOn !== void 0 && askedFailOn !== "none" && !Object.hasOwn(
|
|
7542
|
+
if (askedFailOn !== void 0 && askedFailOn !== "none" && !Object.hasOwn(SEVERITY_RANK2, askedFailOn)) {
|
|
7360
7543
|
throw new Error("--fail-on must be one of: error, warning, info, none");
|
|
7361
7544
|
}
|
|
7362
7545
|
if (command === "init") {
|
|
@@ -7508,7 +7691,7 @@ ${formatVerify(v, severities.length ? severities : severitiesAtOrAbove("info"))}
|
|
|
7508
7691
|
const concurrency = positiveInt("concurrency", values.concurrency);
|
|
7509
7692
|
const maxRoutes = positiveInt("max-routes", values["max-routes"]);
|
|
7510
7693
|
const failOn = values["fail-on"] ?? fileConfig.failOn ?? "error";
|
|
7511
|
-
if (failOn !== "none" && !Object.hasOwn(
|
|
7694
|
+
if (failOn !== "none" && !Object.hasOwn(SEVERITY_RANK2, failOn)) {
|
|
7512
7695
|
throw new Error("--fail-on must be one of: error, warning, info, none");
|
|
7513
7696
|
}
|
|
7514
7697
|
const browsers = values.browsers?.split(",").map((b) => b.trim().toLowerCase()).filter(Boolean);
|
|
@@ -7546,9 +7729,9 @@ ${formatVerify(v, severities.length ? severities : severitiesAtOrAbove("info"))}
|
|
|
7546
7729
|
}
|
|
7547
7730
|
if (result.routes.some((r) => FAILED_STATUSES.includes(r.status))) return 2;
|
|
7548
7731
|
if (failOn === "none") return 0;
|
|
7549
|
-
const threshold =
|
|
7732
|
+
const threshold = SEVERITY_RANK2[failOn];
|
|
7550
7733
|
const failing = result.routes.some(
|
|
7551
|
-
(r) => r.issues.some((i) =>
|
|
7734
|
+
(r) => r.issues.some((i) => SEVERITY_RANK2[i.severity] <= threshold)
|
|
7552
7735
|
);
|
|
7553
7736
|
return failing ? 1 : 0;
|
|
7554
7737
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type Severity = "error" | "warning" | "info";
|
|
2
|
-
export type DetectorId = "page-overflow" | "content-overflow" | "media-overflow" | "overlap" | "unexpected-wrap" | "clipped-text" | "squished-text"
|
|
2
|
+
export type DetectorId = "page-overflow" | "content-overflow" | "media-overflow" | "overlap" | "unexpected-wrap" | "clipped-text" | "squished-text"
|
|
3
|
+
/** What breaks inside a month or week calendar, reported once as the calendar. */
|
|
4
|
+
| "calendar" | "distorted-media" | "small-tap-target" | "tiny-text" | "oversized-text" | "edge-gutter" | "viewport-meta"
|
|
3
5
|
/** Opt-in: what a vision model noticed in screenshots. Never run in the page. */
|
|
4
6
|
| "vision";
|
|
5
7
|
/** Where an element comes from in the project's code. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type Severity = "error" | "warning" | "info";
|
|
2
|
-
export type DetectorId = "page-overflow" | "content-overflow" | "media-overflow" | "overlap" | "unexpected-wrap" | "clipped-text" | "squished-text"
|
|
2
|
+
export type DetectorId = "page-overflow" | "content-overflow" | "media-overflow" | "overlap" | "unexpected-wrap" | "clipped-text" | "squished-text"
|
|
3
|
+
/** What breaks inside a month or week calendar, reported once as the calendar. */
|
|
4
|
+
| "calendar" | "distorted-media" | "small-tap-target" | "tiny-text" | "oversized-text" | "edge-gutter" | "viewport-meta"
|
|
3
5
|
/** Opt-in: what a vision model noticed in screenshots. Never run in the page. */
|
|
4
6
|
| "vision";
|
|
5
7
|
/** Where an element comes from in the project's code. */
|
package/dist/inject.js
CHANGED
|
@@ -559,6 +559,169 @@
|
|
|
559
559
|
return info;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
// ../analyzers/src/detectors/calendar.ts
|
|
563
|
+
var FOLDED = /* @__PURE__ */ new Set([
|
|
564
|
+
"page-overflow",
|
|
565
|
+
"content-overflow",
|
|
566
|
+
"media-overflow",
|
|
567
|
+
"overlap",
|
|
568
|
+
"clipped-text",
|
|
569
|
+
"squished-text"
|
|
570
|
+
]);
|
|
571
|
+
var EVENT_CELL_HEIGHT = 56;
|
|
572
|
+
var ALIGN_PX = 2;
|
|
573
|
+
var MAX_LEVELS = 10;
|
|
574
|
+
var ALIASES = { weds: "wednesday" };
|
|
575
|
+
var normalize = (text) => text.replace(/\s+/g, " ").trim().toLowerCase().replace(/[.,:]$/, "");
|
|
576
|
+
var names = null;
|
|
577
|
+
function weekNames() {
|
|
578
|
+
const langs = [document.documentElement.lang, navigator.language, "en"].filter(Boolean);
|
|
579
|
+
const key = langs.join("|");
|
|
580
|
+
if (names?.key === key) return names.weeks;
|
|
581
|
+
const weeks = [];
|
|
582
|
+
for (const lang of new Set(langs)) {
|
|
583
|
+
try {
|
|
584
|
+
const format = (weekday) => new Intl.DateTimeFormat(lang, { weekday, timeZone: "UTC" });
|
|
585
|
+
const days = Array.from({ length: 7 }, (_, i) => new Date(Date.UTC(2024, 0, 1 + i)));
|
|
586
|
+
const long = days.map((d) => normalize(format("long").format(d)));
|
|
587
|
+
const exact = days.map(
|
|
588
|
+
(d, i) => /* @__PURE__ */ new Set([
|
|
589
|
+
long[i],
|
|
590
|
+
normalize(format("short").format(d)),
|
|
591
|
+
normalize(format("narrow").format(d))
|
|
592
|
+
])
|
|
593
|
+
);
|
|
594
|
+
weeks.push({ long, exact });
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
names = { key, weeks };
|
|
599
|
+
return weeks;
|
|
600
|
+
}
|
|
601
|
+
function isDay(text, week, day) {
|
|
602
|
+
if (week.exact[day]?.has(text) || ALIASES[text] === week.long[day]) return true;
|
|
603
|
+
return text.length >= 2 && (week.long[day]?.startsWith(text) ?? false);
|
|
604
|
+
}
|
|
605
|
+
function isWeek(texts) {
|
|
606
|
+
if (texts.length !== 7 || texts.some((t) => !t)) return false;
|
|
607
|
+
return weekNames().some(
|
|
608
|
+
(week) => Array.from({ length: 7 }, (_, start) => start).some(
|
|
609
|
+
(start) => texts.every((t, i) => isDay(t, week, (start + i) % 7))
|
|
610
|
+
)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
var lined = (a, b) => Math.abs(a.rect.left - b.rect.left) <= ALIGN_PX && Math.abs(a.rect.width - b.rect.width) <= ALIGN_PX;
|
|
614
|
+
function dayCells(snap, scope, weekdays) {
|
|
615
|
+
const header = new Set(weekdays.map((w) => w.el));
|
|
616
|
+
const out = [];
|
|
617
|
+
for (const el of scope.querySelectorAll("*")) {
|
|
618
|
+
if (header.has(el) || weekdays.some((w) => w.el.contains(el))) continue;
|
|
619
|
+
const rec = snap.byElement.get(el);
|
|
620
|
+
if (!rec || rec.rect.height < 1) continue;
|
|
621
|
+
if (out.some((o) => o.el.contains(el))) continue;
|
|
622
|
+
if (weekdays.some((w) => lined(w, rec)) && rec.rect.top >= (weekdays[0]?.rect.bottom ?? 0) - 1)
|
|
623
|
+
out.push(rec);
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
function holdsEvents(days) {
|
|
628
|
+
const heights = days.map((d) => d.rect.height).sort((a, b) => a - b);
|
|
629
|
+
return (heights[Math.floor(heights.length / 2)] ?? 0) >= EVENT_CELL_HEIGHT;
|
|
630
|
+
}
|
|
631
|
+
function shownText(el) {
|
|
632
|
+
const text = el instanceof HTMLElement ? el.innerText : el.textContent;
|
|
633
|
+
return normalize((text ?? "").replace(/\d+/g, " "));
|
|
634
|
+
}
|
|
635
|
+
function weekRow(snap, cells) {
|
|
636
|
+
for (let start = 0; start <= 1 && start + 7 <= cells.length; start++) {
|
|
637
|
+
const row = cells.slice(start, start + 7).map((c) => snap.byElement.get(c)).filter((r) => !!r && r.rect.width > 0);
|
|
638
|
+
if (row.length !== 7 || !isWeek(row.map((r) => shownText(r.el)))) continue;
|
|
639
|
+
const tops = row.map((r) => Math.round(r.rect.top));
|
|
640
|
+
if (Math.max(...tops) - Math.min(...tops) <= ALIGN_PX) return row;
|
|
641
|
+
}
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
function scrollsSideways(snap, el) {
|
|
645
|
+
for (let p = el; p && p !== document.body; p = composedParent(p)) {
|
|
646
|
+
const overflow = snap.byElement.get(p)?.style.overflowX;
|
|
647
|
+
if (overflow === "auto" || overflow === "scroll") return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
function findCalendars(snap) {
|
|
652
|
+
const found = [];
|
|
653
|
+
for (const rec of snap.records) {
|
|
654
|
+
const children = renderedChildren(rec.el);
|
|
655
|
+
if (children.length < 7) continue;
|
|
656
|
+
if (found.some((c) => c.root.contains(rec.el))) continue;
|
|
657
|
+
const weekdays = weekRow(snap, children);
|
|
658
|
+
if (!weekdays) continue;
|
|
659
|
+
let scope = rec.el;
|
|
660
|
+
for (let level = 0; scope && level < MAX_LEVELS; level++, scope = composedParent(scope)) {
|
|
661
|
+
if (scope === document.body) break;
|
|
662
|
+
const days = dayCells(snap, scope, weekdays);
|
|
663
|
+
const columns = new Set(days.map((d) => weekdays.findIndex((w) => lined(w, d))));
|
|
664
|
+
if (days.length < 7 || columns.size < 7) continue;
|
|
665
|
+
if (holdsEvents(days)) {
|
|
666
|
+
found.push({ root: scope, weekdays, days, scrolls: scrollsSideways(snap, scope) });
|
|
667
|
+
}
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return found;
|
|
672
|
+
}
|
|
673
|
+
var RANK = { error: 0, warning: 1, info: 2 };
|
|
674
|
+
function inside(root, el) {
|
|
675
|
+
for (let p = el ?? null; p; p = composedParent(p)) if (p === root) return true;
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
function foldCalendars(snap, findings, keep) {
|
|
679
|
+
const calendars = findCalendars(snap).filter((c) => keep(c.root));
|
|
680
|
+
if (!calendars.length) return findings;
|
|
681
|
+
const folded = /* @__PURE__ */ new Map();
|
|
682
|
+
const rest = [];
|
|
683
|
+
for (const f of findings) {
|
|
684
|
+
const calendar = FOLDED.has(f.detector) ? calendars.find(
|
|
685
|
+
(c) => inside(c.root, elementOf(f.element)) && (!f.related || inside(c.root, elementOf(f.related)))
|
|
686
|
+
) : void 0;
|
|
687
|
+
if (!calendar) {
|
|
688
|
+
rest.push(f);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
folded.set(calendar, [...folded.get(calendar) ?? [], f]);
|
|
692
|
+
}
|
|
693
|
+
for (const [calendar, list] of folded) {
|
|
694
|
+
const severity = list.reduce(
|
|
695
|
+
(worst, f) => RANK[f.severity] < RANK[worst] ? f.severity : worst,
|
|
696
|
+
"info"
|
|
697
|
+
);
|
|
698
|
+
const dayWidth = Math.round(Math.min(...calendar.weekdays.map((w) => w.rect.width)) * 10) / 10;
|
|
699
|
+
const kinds = [...new Map(list.map((f) => [f.detector, f])).values()];
|
|
700
|
+
const checks = /* @__PURE__ */ new Map();
|
|
701
|
+
for (const f of list) {
|
|
702
|
+
const was = checks.get(f.detector);
|
|
703
|
+
if (!was || RANK[f.severity] < RANK[was]) checks.set(f.detector, f.severity);
|
|
704
|
+
}
|
|
705
|
+
rest.push({
|
|
706
|
+
detector: "calendar",
|
|
707
|
+
severity,
|
|
708
|
+
title: "Calendar's 7 days don't fit side by side",
|
|
709
|
+
element: describe(calendar.root),
|
|
710
|
+
evidence: {
|
|
711
|
+
dayWidthPx: dayWidth,
|
|
712
|
+
elementsInside: new Set(list.map((f) => f.element.selector)).size,
|
|
713
|
+
found: kinds.map((f) => `${f.title} at ${f.element.selector}`).join("; "),
|
|
714
|
+
checksInside: [...checks].map(([id, s]) => `${id}:${s}`).join(" "),
|
|
715
|
+
...calendar.scrolls ? { scrollsSideways: true } : {}
|
|
716
|
+
},
|
|
717
|
+
styles: {},
|
|
718
|
+
suggestion: "A calendar needs its 7 days side by side, so narrower days or fewer columns won't fix it. Let it scroll sideways inside its own box, or show the days as a list (an agenda view) on small screens.",
|
|
719
|
+
lineage: lineage(calendar.root)
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return rest;
|
|
723
|
+
}
|
|
724
|
+
|
|
562
725
|
// ../analyzers/src/detectors/page-overflow.ts
|
|
563
726
|
var TOLERANCE = 0.5;
|
|
564
727
|
var SIDEWAYS_CONTENT = /* @__PURE__ */ new Set(["TABLE", "PRE", "CODE"]);
|
|
@@ -1117,6 +1280,13 @@
|
|
|
1117
1280
|
function name(rec) {
|
|
1118
1281
|
return `\`${describe(rec.el).selector}\``;
|
|
1119
1282
|
}
|
|
1283
|
+
function itemsFit(grid) {
|
|
1284
|
+
const { left, right } = grid.rect;
|
|
1285
|
+
return Array.from(grid.el.children).every((child) => {
|
|
1286
|
+
const r = child.getBoundingClientRect();
|
|
1287
|
+
return r.width === 0 || r.left >= left - 1 && r.right <= right + 1;
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1120
1290
|
function suggestionFor(g) {
|
|
1121
1291
|
if (g.spill?.direction === "down") {
|
|
1122
1292
|
return `The content of ${name(g.spill.box)} is taller than its box at this width and runs down over what comes next. Its height is fixed (height or max-height) and the content no longer fits once it wraps; use min-height instead, or let it grow with its content.`;
|
|
@@ -1130,7 +1300,10 @@
|
|
|
1130
1300
|
return `They sit side by side in the flex row ${name(lca)}, which runs out of room at this width. Let the row wrap (flex-wrap: wrap), let the item that should give way shrink (min-width: 0), or stack them below this width with a media query.`;
|
|
1131
1301
|
}
|
|
1132
1302
|
if (lca && display.includes("grid")) {
|
|
1133
|
-
|
|
1303
|
+
if (!itemsFit(lca)) {
|
|
1304
|
+
return `They share the grid ${name(lca)}, whose columns are too narrow here for what is in them. Use minmax(0, 1fr) columns so items can shrink, or switch to fewer columns below this width.`;
|
|
1305
|
+
}
|
|
1306
|
+
return `They share the grid ${name(lca)}, whose columns have already shrunk as far as they go and are too narrow here for what is in them. Switch to fewer columns below this width.`;
|
|
1134
1307
|
}
|
|
1135
1308
|
return "These two areas stop making room for each other at this width. Stack them earlier with a media query, or let the narrower one shrink (min-width: 0).";
|
|
1136
1309
|
}
|
|
@@ -1735,10 +1908,10 @@
|
|
|
1735
1908
|
for (const rec of snap.records) {
|
|
1736
1909
|
const s = rec.style;
|
|
1737
1910
|
if (!s.containerType || s.containerType === "normal") continue;
|
|
1738
|
-
const
|
|
1911
|
+
const names2 = s.containerName.split(/\s+/);
|
|
1739
1912
|
const size = rec.rect.width - (Number.parseFloat(s.paddingLeft) || 0) - (Number.parseFloat(s.paddingRight) || 0) - (Number.parseFloat(s.borderLeftWidth) || 0) - (Number.parseFloat(s.borderRightWidth) || 0);
|
|
1740
1913
|
for (const q of queries) {
|
|
1741
|
-
if (q.name && !
|
|
1914
|
+
if (q.name && !names2.includes(q.name)) continue;
|
|
1742
1915
|
const unit = q.unit === "px" ? 1 : q.unit === "em" ? Number.parseFloat(s.fontSize) || 16 : rootFont;
|
|
1743
1916
|
const threshold = q.value * unit;
|
|
1744
1917
|
bits += (q.after ? size > threshold : size >= threshold) ? "1" : "0";
|
|
@@ -1854,12 +2027,12 @@
|
|
|
1854
2027
|
}
|
|
1855
2028
|
}
|
|
1856
2029
|
function looksLikeMenu(el) {
|
|
1857
|
-
const
|
|
2030
|
+
const names2 = [
|
|
1858
2031
|
typeof el.className === "string" ? el.className : "",
|
|
1859
2032
|
el.id,
|
|
1860
2033
|
el.getAttribute("data-testid") ?? ""
|
|
1861
2034
|
].join(" ");
|
|
1862
|
-
return MENU_CLASS.test(
|
|
2035
|
+
return MENU_CLASS.test(names2) || MENU_LABEL.test(nameOf(el));
|
|
1863
2036
|
}
|
|
1864
2037
|
function kindOf(el) {
|
|
1865
2038
|
if (el.matches("details > summary")) {
|
|
@@ -2061,6 +2234,7 @@
|
|
|
2061
2234
|
"unexpected-wrap",
|
|
2062
2235
|
"clipped-text",
|
|
2063
2236
|
"squished-text",
|
|
2237
|
+
"calendar",
|
|
2064
2238
|
"distorted-media",
|
|
2065
2239
|
"small-tap-target",
|
|
2066
2240
|
"tiny-text",
|
|
@@ -2196,7 +2370,7 @@
|
|
|
2196
2370
|
const enabled = new Set(options.detectors ?? DETECTOR_IDS);
|
|
2197
2371
|
const rules = options.ignore ?? [];
|
|
2198
2372
|
const snap = snapshot();
|
|
2199
|
-
|
|
2373
|
+
let findings = [];
|
|
2200
2374
|
for (const [id, run] of Object.entries(DETECTORS)) {
|
|
2201
2375
|
if (!enabled.has(id)) continue;
|
|
2202
2376
|
for (const f of run(snap)) {
|
|
@@ -2204,6 +2378,13 @@
|
|
|
2204
2378
|
findings.push(f);
|
|
2205
2379
|
}
|
|
2206
2380
|
}
|
|
2381
|
+
if (enabled.has("calendar")) {
|
|
2382
|
+
findings = foldCalendars(
|
|
2383
|
+
snap,
|
|
2384
|
+
findings,
|
|
2385
|
+
(root) => !isIgnored(describe(root), "calendar", rules)
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2207
2388
|
const wraps = enabled.has("unexpected-wrap") ? wrapStats(snap).filter((w) => !isIgnored(w.element, "unexpected-wrap", rules)) : [];
|
|
2208
2389
|
const containers = containerSignature(snap);
|
|
2209
2390
|
return {
|
package/dist/schema.json
CHANGED
|
@@ -223,6 +223,25 @@
|
|
|
223
223
|
}
|
|
224
224
|
]
|
|
225
225
|
},
|
|
226
|
+
"calendar": {
|
|
227
|
+
"anyOf": [
|
|
228
|
+
{
|
|
229
|
+
"type": "boolean"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"type": "string",
|
|
233
|
+
"const": "error"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"type": "string",
|
|
237
|
+
"const": "warning"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"type": "string",
|
|
241
|
+
"const": "info"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
},
|
|
226
245
|
"distorted-media": {
|
|
227
246
|
"anyOf": [
|
|
228
247
|
{
|
|
@@ -386,6 +405,7 @@
|
|
|
386
405
|
"unexpected-wrap",
|
|
387
406
|
"clipped-text",
|
|
388
407
|
"squished-text",
|
|
408
|
+
"calendar",
|
|
389
409
|
"distorted-media",
|
|
390
410
|
"small-tap-target",
|
|
391
411
|
"tiny-text",
|