@svelte-vitals/core 0.24.0 → 0.25.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/README.md +2 -2
- package/dist/index.d.ts +52 -11
- package/dist/index.js +669 -173
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@svelte-vitals/core)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer,
|
|
6
|
+
Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer, and reporters, plus the full rule set across five categories — SEO, Performance, Correctness, Security, Architecture.
|
|
7
7
|
|
|
8
8
|
This package is **mode-independent** and contains no I/O — it operates on a normalized `ResolvedHead[]` intermediate representation, so the same rules run unchanged whether heads come from static source analysis (the `svelte-vitals` CLI) or from prerendered HTML (the `@svelte-vitals/vite` plugin). It has zero runtime dependencies and no `node:` imports.
|
|
9
9
|
|
|
10
10
|
> Most users don't depend on this directly — install [`svelte-vitals`](https://www.npmjs.com/package/svelte-vitals) instead. This package is for building tools on top of the shared engine.
|
|
11
11
|
|
|
12
|
-
> **ESM-only** (Node
|
|
12
|
+
> **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
|
|
13
13
|
|
|
14
14
|
## License
|
|
15
15
|
|
package/dist/index.d.ts
CHANGED
|
@@ -740,6 +740,57 @@ declare function formatMarkdownReport(results: Result[], config: Config, meta: {
|
|
|
740
740
|
version: string;
|
|
741
741
|
}): string;
|
|
742
742
|
|
|
743
|
+
/** Provenance of a route's findings: real rendered page vs. source-only analysis. */
|
|
744
|
+
type RouteBadge = 'measured' | 'static';
|
|
745
|
+
interface AppSnapshot {
|
|
746
|
+
report: JsonReport;
|
|
747
|
+
badges: Record<string, RouteBadge>;
|
|
748
|
+
analyzing: boolean;
|
|
749
|
+
/** Monotonically increasing; lets the client discard an out-of-order /data.json response. */
|
|
750
|
+
sequence: number;
|
|
751
|
+
/** Whether a dev server is behind this page (SSE updates, /data.json refetch, connection dot). */
|
|
752
|
+
live: boolean;
|
|
753
|
+
meta: {
|
|
754
|
+
version: string;
|
|
755
|
+
coreVersion?: string;
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Hand-authored CSS for the master/detail shell. Reuses the same design-token
|
|
760
|
+
* names/values as the rest of the project, and adds a dark theme via
|
|
761
|
+
* `:root[data-theme="dark"]` plus a `prefers-color-scheme` fallback for a
|
|
762
|
+
* first-ever visit with no stored preference.
|
|
763
|
+
*/
|
|
764
|
+
declare const APP_STYLE: string;
|
|
765
|
+
/**
|
|
766
|
+
* Hand-authored client script for the shell — no bundler, no framework. Parses the
|
|
767
|
+
* AppSnapshot embedded by renderAppShell, then owns all rendering: sidebar
|
|
768
|
+
* (search/sort/route list) and detail pane (Overview or a selected route). When the
|
|
769
|
+
* snapshot says `live`, it additionally re-fetches /data.json on every SSE `update`
|
|
770
|
+
* and on the EventSource's `open` event (covers the initial connection and every
|
|
771
|
+
* auto-reconnect, since EventSource replays no missed events) — discarding any
|
|
772
|
+
* response whose `sequence` isn't newer than what's already rendered.
|
|
773
|
+
*/
|
|
774
|
+
declare const APP_SCRIPT: string;
|
|
775
|
+
/** The shell HTML: empty sidebar/detail/topbar containers, the stylesheet, the
|
|
776
|
+
* client script, and the snapshot embedded as JSON for the client's first paint. */
|
|
777
|
+
declare function renderAppShell(snapshot: AppSnapshot): string;
|
|
778
|
+
/**
|
|
779
|
+
* Static (non-live) document over a prebuilt JsonReport — kept as the public name the
|
|
780
|
+
* html reporter has always exported. `routeBadges` preserves the old opts shape.
|
|
781
|
+
*/
|
|
782
|
+
declare function buildHtmlDocument(report: JsonReport, meta: {
|
|
783
|
+
version: string;
|
|
784
|
+
coreVersion?: string;
|
|
785
|
+
}, opts?: {
|
|
786
|
+
routeBadges?: Record<string, RouteBadge>;
|
|
787
|
+
}): string;
|
|
788
|
+
/** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
|
|
789
|
+
declare function formatHtmlReport(results: Result[], config: Config, meta: {
|
|
790
|
+
version: string;
|
|
791
|
+
coreVersion?: string;
|
|
792
|
+
}): string;
|
|
793
|
+
|
|
743
794
|
type Band = 'good' | 'warn' | 'poor';
|
|
744
795
|
declare const BAND_COLOR: Record<Band, string>;
|
|
745
796
|
declare function scoreBand(score: number): Band;
|
|
@@ -754,20 +805,10 @@ declare function escapeHtml(s: string): string;
|
|
|
754
805
|
* keeping core runtime-agnostic and lib-minimal.
|
|
755
806
|
*/
|
|
756
807
|
declare function safeHref(url: string): string | null;
|
|
757
|
-
declare function buildHtmlDocument(report: JsonReport, meta: {
|
|
758
|
-
version: string;
|
|
759
|
-
coreVersion?: string;
|
|
760
|
-
}, opts?: {
|
|
761
|
-
routeBadges?: Record<string, 'measured' | 'static'>;
|
|
762
|
-
}): string;
|
|
763
|
-
declare function formatHtmlReport(results: Result[], config: Config, meta: {
|
|
764
|
-
version: string;
|
|
765
|
-
coreVersion?: string;
|
|
766
|
-
}): string;
|
|
767
808
|
|
|
768
809
|
/** Drop rules disabled via config (design §6). */
|
|
769
810
|
declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
770
811
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
771
812
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
772
813
|
|
|
773
|
-
export { BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
|
814
|
+
export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, renderAppShell, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
package/dist/index.js
CHANGED
|
@@ -2793,200 +2793,693 @@ function formatMarkdownReport(results, config, meta) {
|
|
|
2793
2793
|
return lines.join("\n");
|
|
2794
2794
|
}
|
|
2795
2795
|
|
|
2796
|
-
// src/reporter/
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
warn: "#E8A317",
|
|
2800
|
-
poor: "#E5484D"
|
|
2801
|
-
};
|
|
2802
|
-
function scoreBand(score) {
|
|
2803
|
-
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
2796
|
+
// src/reporter/app-shell.ts
|
|
2797
|
+
function embedJson(value) {
|
|
2798
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2804
2799
|
}
|
|
2805
|
-
function
|
|
2806
|
-
return
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
);
|
|
2800
|
+
function sanitizeDocsUrl(issue) {
|
|
2801
|
+
if (issue.docsUrl === void 0) return issue;
|
|
2802
|
+
if (safeHref(issue.docsUrl) !== null) return issue;
|
|
2803
|
+
return { ...issue, docsUrl: void 0 };
|
|
2810
2804
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
|
|
2805
|
+
function sanitizeReport(report) {
|
|
2806
|
+
return {
|
|
2807
|
+
...report,
|
|
2808
|
+
routes: report.routes.map((route) => ({ ...route, issues: route.issues.map(sanitizeDocsUrl) })),
|
|
2809
|
+
siteIssues: report.siteIssues.map(sanitizeDocsUrl)
|
|
2810
|
+
};
|
|
2814
2811
|
}
|
|
2815
|
-
var
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
const sev = SEVERITIES.includes(issue.severity) ? issue.severity : "info";
|
|
2820
|
-
const dyn = issue.detection.value === "dynamic" ? ' <span class="dyn" title="set dynamically (verified at runtime)">\u21AF</span>' : "";
|
|
2821
|
-
const line = issue.line !== void 0 ? `:${issue.line}` : "";
|
|
2822
|
-
const fix = issue.fix?.snippet !== void 0 ? `<div class="fix"><div class="label">fix</div><pre><code>${escapeHtml(issue.fix.snippet)}</code></pre></div>` : "";
|
|
2823
|
-
const href = issue.docsUrl ? safeHref(issue.docsUrl) : null;
|
|
2824
|
-
const docs = href ? `<a class="f-link" href="${escapeHtml(href)}">Learn more</a>` : "";
|
|
2825
|
-
return `<article class="finding sev-${sev}" data-severity="${sev}" data-category="${escapeHtml(issue.category)}"><div class="f-head"><span class="ruleid">${escapeHtml(issue.id)}</span><span class="f-title">${escapeHtml(issue.title)}</span><span class="sev-tag ${sev}">${sev}</span></div>` + (issue.location ? `<p class="f-loc">${escapeHtml(issue.location)}${line}${dyn}</p>` : "") + (issue.recommendation ? `<p class="f-rec">${escapeHtml(issue.recommendation)}</p>` : "") + fix + docs + `</article>`;
|
|
2826
|
-
}
|
|
2827
|
-
function renderTopbar(report, meta) {
|
|
2828
|
-
const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
|
|
2829
|
-
const core = meta.coreVersion ? `<span title="@svelte-vitals/core version">core v${escapeHtml(meta.coreVersion)}</span>` : "";
|
|
2830
|
-
return `<header class="topbar"><div class="brand"><span class="bolt">\u21AF</span>svelte-<span class="v">vitals</span></div><div class="meta"><span>v${escapeHtml(meta.version)}</span>` + core + `<span>${report.routes.length} routes</span><span>${findings} findings</span></div></header>`;
|
|
2831
|
-
}
|
|
2832
|
-
function renderHero(report) {
|
|
2833
|
-
const C = 2 * Math.PI * 58;
|
|
2834
|
-
const offset = (C * (1 - report.score / 100)).toFixed(1);
|
|
2835
|
-
const hb = scoreBand(report.score);
|
|
2836
|
-
const s = report.summary;
|
|
2837
|
-
const dynNote = s.dynamic > 0 ? `<span class="tally"><span class="dot dyn-dot">\u21AF</span>Dynamic <span class="n">${s.dynamic}</span></span>` : "";
|
|
2838
|
-
const cats = Object.entries(report.categories).map(([cat, { score }]) => {
|
|
2839
|
-
const b = scoreBand(score);
|
|
2840
|
-
const weight = report.weights[cat];
|
|
2841
|
-
const w = weight !== void 0 ? `<span class="w">weight ${weight}</span>` : "";
|
|
2842
|
-
const name = categoryLabel(cat);
|
|
2843
|
-
return `<div class="cat"><div class="top"><span class="name">${escapeHtml(name)} ${w}</span><span class="sc" style="color:${BAND_COLOR[b]}">${score}</span></div><div class="bar"><i style="width:${score}%;background:${BAND_COLOR[b]}"></i></div></div>`;
|
|
2844
|
-
}).join("");
|
|
2845
|
-
return `<section class="hero"><div class="gauge"><svg width="132" height="132" viewBox="0 0 132 132" aria-hidden="true"><circle cx="66" cy="66" r="58" fill="none" stroke="#e4e7ec" stroke-width="11"></circle><circle id="arc" cx="66" cy="66" r="58" fill="none" stroke="${BAND_COLOR[hb]}" stroke-width="11" stroke-linecap="round" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${offset}"></circle></svg><div class="num"><strong id="hnum">${report.score}</strong><span>Health</span></div></div><div class="readout"><div class="eyebrow">SvelteKit \xB7 SEO & Performance</div><div class="tallies"><span class="tally"><span class="dot crit"></span>Critical <span class="n">${s.critical}</span></span><span class="tally"><span class="dot warn"></span>Warning <span class="n">${s.warning}</span></span><span class="tally"><span class="dot info"></span>Info <span class="n">${s.info}</span></span><span class="tally"><span class="dot pass"></span>Passed <span class="n">${s.passed}</span></span>` + dynNote + `</div><div class="cats">${cats}</div></div></section>`;
|
|
2846
|
-
}
|
|
2847
|
-
var ROUTE_BADGES = ["measured", "static"];
|
|
2848
|
-
function renderRoutes(report, routeBadges) {
|
|
2849
|
-
if (report.routes.length === 0) return "";
|
|
2850
|
-
const rows = report.routes.map((r) => {
|
|
2851
|
-
const b = scoreBand(r.score);
|
|
2852
|
-
const crit = r.issues.filter((i) => i.severity === "critical").length;
|
|
2853
|
-
const warn = r.issues.filter((i) => i.severity === "warning").length;
|
|
2854
|
-
const info = r.issues.filter((i) => i.severity === "info").length;
|
|
2855
|
-
const parts = [];
|
|
2856
|
-
if (crit) parts.push(`${crit} critical`);
|
|
2857
|
-
if (warn) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
|
|
2858
|
-
if (info) parts.push(`${info} info`);
|
|
2859
|
-
const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
|
|
2860
|
-
const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
|
|
2861
|
-
const rawBadge = routeBadges?.[r.route];
|
|
2862
|
-
const badge = rawBadge && ROUTE_BADGES.includes(rawBadge) ? rawBadge : void 0;
|
|
2863
|
-
const badgeHtml = badge ? ` <span class="badge badge-${badge}">${escapeHtml(badge)}</span>` : "";
|
|
2864
|
-
return `<details class="route" id="${slug(r.route)}" data-score="${r.score}"${r.issues.length ? " open" : ""}><summary><span class="route-name"><span class="path">${escapeHtml(r.route)}</span>${badgeHtml}</span><span class="issue-sum">${sum}</span><span class="score-chip"><span class="ring" style="background:${BAND_COLOR[b]}"></span>${r.score}</span><span class="chev">\u203A</span></summary><div class="route-body">${body}</div></details>`;
|
|
2865
|
-
}).join("");
|
|
2866
|
-
return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
|
|
2867
|
-
}
|
|
2868
|
-
function renderSiteChecks(report) {
|
|
2869
|
-
if (report.siteIssues.length === 0) return "";
|
|
2870
|
-
const cards = report.siteIssues.map(renderFinding).join("");
|
|
2871
|
-
return `<section class="section"><h2>Site checks</h2>${cards}</section>`;
|
|
2872
|
-
}
|
|
2873
|
-
function renderFilters(report) {
|
|
2874
|
-
const chip = (filter, label, pressed = false) => `<button class="chip" type="button" aria-pressed="${pressed}" data-filter="${escapeHtml(filter)}">${escapeHtml(label)}</button>`;
|
|
2875
|
-
const catChips = Object.keys(report.categories).map((cat) => chip(cat, categoryLabel(cat))).join("");
|
|
2876
|
-
return `<div class="filters" role="group" aria-label="Filter findings">` + chip("all", "All", true) + chip("critical", "Critical") + chip("warning", "Warning") + chip("info", "Info") + catChips + `</div>`;
|
|
2877
|
-
}
|
|
2878
|
-
var STYLE = `
|
|
2879
|
-
:root{--ground: #f6f7f9;--panel: #fff;--ink: #0c1322;--muted: #5a6472;--faint: #8c95a3;--line: #e4e7ec;--line-strong: #d3d8e0;--accent: #ff3e00;--good: #2fa968;--warn: #e8a317;--poor: #e5484d;--code-bg: #0e1525;--code-ink: #e7ecf4;--radius: 12px;--mono: ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
2812
|
+
var APP_STYLE = `
|
|
2813
|
+
:root{--ground:#f6f7f9;--panel:#fff;--ink:#0c1322;--muted:#5a6472;--faint:#8c95a3;--line:#e4e7ec;--line-strong:#d3d8e0;--accent:#ff3e00;--good:#2fa968;--warn:#e8a317;--poor:#e5484d;--code-bg:#0e1525;--code-ink:#e7ecf4;--active-bg:#0c1322;--active-ink:#fff;--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
2814
|
+
:root[data-theme="dark"]{--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}
|
|
2815
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}}
|
|
2880
2816
|
*{box-sizing:border-box}
|
|
2881
|
-
body{margin:0;
|
|
2882
|
-
|
|
2883
|
-
.
|
|
2884
|
-
.
|
|
2885
|
-
.
|
|
2886
|
-
.
|
|
2887
|
-
.
|
|
2888
|
-
.
|
|
2889
|
-
.
|
|
2890
|
-
.
|
|
2891
|
-
.
|
|
2892
|
-
.
|
|
2893
|
-
.
|
|
2894
|
-
.
|
|
2895
|
-
.
|
|
2896
|
-
.
|
|
2897
|
-
.
|
|
2898
|
-
.
|
|
2899
|
-
.
|
|
2900
|
-
.
|
|
2901
|
-
.
|
|
2902
|
-
.
|
|
2903
|
-
.
|
|
2904
|
-
.
|
|
2905
|
-
.
|
|
2906
|
-
.
|
|
2907
|
-
.
|
|
2908
|
-
.
|
|
2909
|
-
.
|
|
2910
|
-
.
|
|
2911
|
-
.
|
|
2912
|
-
.
|
|
2913
|
-
.
|
|
2914
|
-
.
|
|
2915
|
-
.
|
|
2916
|
-
.
|
|
2917
|
-
.
|
|
2918
|
-
.
|
|
2919
|
-
.
|
|
2920
|
-
.
|
|
2921
|
-
.
|
|
2922
|
-
.
|
|
2923
|
-
.
|
|
2924
|
-
.
|
|
2925
|
-
.
|
|
2926
|
-
.
|
|
2927
|
-
.
|
|
2928
|
-
.
|
|
2929
|
-
.
|
|
2930
|
-
.
|
|
2931
|
-
.
|
|
2932
|
-
.
|
|
2933
|
-
.
|
|
2934
|
-
.
|
|
2935
|
-
.
|
|
2936
|
-
.
|
|
2937
|
-
.
|
|
2938
|
-
.
|
|
2939
|
-
.
|
|
2940
|
-
.
|
|
2941
|
-
.
|
|
2942
|
-
.
|
|
2943
|
-
.
|
|
2944
|
-
.
|
|
2945
|
-
|
|
2817
|
+
html,body{margin:0;height:100%}
|
|
2818
|
+
body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
2819
|
+
.dv-app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:280px 1fr;grid-template-areas:"top top" "side main";height:100vh}
|
|
2820
|
+
.dv-topbar{grid-area:top;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
2821
|
+
.dv-topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:12px 20px}
|
|
2822
|
+
.dv-brand{display:flex;align-items:center;background:none;border:none;padding:0;cursor:pointer;border-radius:6px}
|
|
2823
|
+
.dv-brand svg{height:28px;width:auto;display:block}
|
|
2824
|
+
.dv-brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
|
|
2825
|
+
.dv-meta{font-family:var(--mono);font-size:12px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
|
|
2826
|
+
.dv-status{display:flex;align-items:center;gap:10px}
|
|
2827
|
+
.dv-analyzing{font-size:12px;color:var(--accent);font-weight:600}
|
|
2828
|
+
.dv-conn{width:8px;height:8px;border-radius:50%;background:var(--faint);display:inline-block}
|
|
2829
|
+
.dv-conn-connected{background:var(--good)}
|
|
2830
|
+
.dv-conn-reconnecting{background:var(--warn)}
|
|
2831
|
+
.dv-menu-toggle{display:none;border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:8px;width:28px;height:28px;cursor:pointer}
|
|
2832
|
+
.dv-theme-toggle{border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:999px;width:28px;height:28px;cursor:pointer}
|
|
2833
|
+
.dv-theme-toggle:focus-visible,.dv-menu-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2834
|
+
.dv-sidebar{grid-area:side;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto}
|
|
2835
|
+
.dv-sidebar-inner{display:flex;flex-direction:column;gap:10px;padding:14px}
|
|
2836
|
+
.dv-search{font:inherit;font-size:13px;padding:7px 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
2837
|
+
.dv-sort{font:inherit;font-size:12.5px;padding:6px 8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
2838
|
+
.dv-nav{display:flex;flex-direction:column;gap:2px}
|
|
2839
|
+
.dv-nav-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border-radius:8px;cursor:pointer}
|
|
2840
|
+
.dv-nav-item:hover{background:var(--ground)}
|
|
2841
|
+
.dv-nav-item.active{background:var(--active-bg);color:var(--active-ink)}
|
|
2842
|
+
.dv-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
2843
|
+
.dv-nav-label{font-family:var(--mono);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2844
|
+
.dv-nav-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
|
|
2845
|
+
.dv-nav-item.active .dv-nav-meta{color:inherit}
|
|
2846
|
+
.dv-nav-score{font-family:var(--mono);font-weight:700}
|
|
2847
|
+
.dv-badge{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px}
|
|
2848
|
+
.dv-badge-measured{background:rgba(47,169,104,.16);color:var(--good)}
|
|
2849
|
+
.dv-badge-static{background:rgba(140,149,163,.2);color:var(--muted)}
|
|
2850
|
+
.dv-detail{grid-area:main;overflow-y:auto;padding:24px 28px 80px}
|
|
2851
|
+
.dv-gauge{position:relative;width:132px;height:132px;margin-bottom:20px}
|
|
2852
|
+
.dv-gauge svg{position:absolute;inset:0;transform:rotate(-90deg)}
|
|
2853
|
+
.dv-gauge-track{stroke:var(--line)}
|
|
2854
|
+
.dv-gauge-num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
2855
|
+
.dv-gauge-num strong{font-family:var(--mono);font-size:36px;font-weight:600}
|
|
2856
|
+
.dv-gauge-num span{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted)}
|
|
2857
|
+
.dv-cats{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:20px}
|
|
2858
|
+
.dv-cat{min-width:180px;flex:1}
|
|
2859
|
+
.dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
|
|
2860
|
+
.dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
|
|
2861
|
+
.dv-bar>i{display:block;height:100%;border-radius:999px}
|
|
2862
|
+
.dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
|
|
2863
|
+
.dv-chip{font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--muted);padding:5px 12px;border-radius:999px}
|
|
2864
|
+
.dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
|
|
2865
|
+
.dv-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2866
|
+
.dv-section h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:24px 0 12px}
|
|
2867
|
+
.dv-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
|
|
2868
|
+
.dv-route-path{font-family:var(--mono);font-size:16px;font-weight:600}
|
|
2869
|
+
.dv-score-chip{font-family:var(--mono);font-weight:700}
|
|
2870
|
+
.dv-finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
|
|
2871
|
+
.dv-finding-critical{border-left-color:var(--poor)}
|
|
2872
|
+
.dv-finding-warning{border-left-color:var(--warn)}
|
|
2873
|
+
.dv-finding-info{border-left-color:var(--faint)}
|
|
2874
|
+
.dv-f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
2875
|
+
.dv-ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:var(--ground);padding:2px 8px;border-radius:6px}
|
|
2876
|
+
.dv-f-title{font-weight:650;font-size:15px}
|
|
2877
|
+
.dv-sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
2878
|
+
.dv-sev-critical{color:var(--poor)}
|
|
2879
|
+
.dv-sev-warning{color:var(--warn)}
|
|
2880
|
+
.dv-sev-info{color:var(--faint)}
|
|
2881
|
+
.dv-f-route{display:block;font:inherit;font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--accent);background:none;border:none;padding:0;margin:8px 0 0;cursor:pointer;text-align:left}
|
|
2882
|
+
.dv-f-route:hover{text-decoration:underline}
|
|
2883
|
+
.dv-f-route:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2884
|
+
.dv-f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}
|
|
2885
|
+
.dv-f-rec{font-size:14px;margin:10px 0 0}
|
|
2886
|
+
.dv-fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
|
|
2887
|
+
.dv-fix-label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
|
|
2888
|
+
.dv-fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
|
|
2889
|
+
.dv-fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
|
|
2890
|
+
.tok-kw{color:#ff7ab8}
|
|
2891
|
+
.tok-str{color:#9ece6a}
|
|
2892
|
+
.tok-num{color:#ff9e64}
|
|
2893
|
+
.tok-cm{color:#6b7280;font-style:italic}
|
|
2894
|
+
.tok-id{color:var(--code-ink)}
|
|
2895
|
+
.tok-pn{color:#8da0bd}
|
|
2896
|
+
.dv-f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
|
|
2897
|
+
.dv-f-link:hover{text-decoration:underline}
|
|
2898
|
+
.dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
2899
|
+
.dv-ai-prompt>summary{cursor:pointer;list-style:none;padding:8px 12px;font-size:12px;font-weight:600;color:var(--muted);display:flex;align-items:center;gap:6px;user-select:none}
|
|
2900
|
+
.dv-ai-prompt>summary::-webkit-details-marker{display:none}
|
|
2901
|
+
.dv-ai-prompt>summary::before{content:"\u25B8";display:inline-block;transition:transform .15s ease}
|
|
2902
|
+
.dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
|
|
2903
|
+
.dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
|
|
2904
|
+
.dv-ai-prompt-pre{margin:0;padding:10px 12px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;font-family:var(--mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto}
|
|
2905
|
+
.dv-ai-copy-btn{align-self:flex-start;font:inherit;font-size:12px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);padding:5px 12px;border-radius:999px}
|
|
2906
|
+
.dv-ai-copy-btn:hover{border-color:var(--faint)}
|
|
2907
|
+
.dv-ai-copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2908
|
+
.dv-empty{color:var(--muted);font-size:13px}
|
|
2909
|
+
@media (max-width:640px){.dv-app{grid-template-columns:1fr;grid-template-areas:"top" "main"}.dv-menu-toggle{display:inline-flex}.dv-sidebar{position:fixed;inset:0 20% 0 0;transform:translateX(-100%);transition:transform .2s ease;z-index:10}.dv-sidebar.open{transform:translateX(0)}}
|
|
2946
2910
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
2947
2911
|
`;
|
|
2948
|
-
var
|
|
2912
|
+
var APP_SCRIPT = `
|
|
2949
2913
|
(function(){
|
|
2950
|
-
var
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2914
|
+
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
2915
|
+
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
2916
|
+
|
|
2917
|
+
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
|
|
2918
|
+
// copy, not an <img src>, since the dashboard is a single self-contained HTML response
|
|
2919
|
+
// with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
|
|
2920
|
+
// properties), matching the docs usage: the wordmark reads the same in both themes.
|
|
2921
|
+
var WORDMARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 56" role="img" aria-labelledby="dv-wordmark-title"><title id="dv-wordmark-title">svelte-vitals</title><defs><clipPath id="dv-wordmark-clip"><rect x="2" y="2" width="52" height="52" rx="14"/></clipPath></defs><rect x="2" y="2" width="52" height="52" rx="14" fill="#FF3E00"/><polyline clip-path="url(#dv-wordmark-clip)" points="4,28 15,28 17.5,23.5 20,28 23,28 26,7 29,49 32,28 35,28 37,24.5 39.5,28 52,28" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><text x="70" y="38" font-family="ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, sans-serif" font-size="30" font-weight="700" fill="#FF3E00">svelte-vitals</text></svg>';
|
|
2922
|
+
|
|
2923
|
+
function h(tag, attrs, kids) {
|
|
2924
|
+
var n = document.createElement(tag);
|
|
2925
|
+
if (attrs) {
|
|
2926
|
+
for (var k in attrs) {
|
|
2927
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
2928
|
+
var v = attrs[k];
|
|
2929
|
+
if (v === undefined || v === null || v === false) continue;
|
|
2930
|
+
if (k === 'class') n.className = v;
|
|
2931
|
+
else if (k === 'text') n.textContent = v;
|
|
2932
|
+
else if (k.indexOf('on') === 0 && typeof v === 'function') n.addEventListener(k.slice(2), v);
|
|
2933
|
+
else n.setAttribute(k, v === true ? '' : String(v));
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
(kids || []).forEach(function (c) {
|
|
2937
|
+
if (c === undefined || c === null || c === false) return;
|
|
2938
|
+
n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
2939
|
+
});
|
|
2940
|
+
return n;
|
|
2941
|
+
}
|
|
2942
|
+
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
|
|
2943
|
+
function mount(id, node) { var el = document.getElementById(id); clear(el); el.appendChild(node); }
|
|
2944
|
+
|
|
2945
|
+
var HL_KEYWORDS = ['import','export','from','const','let','var','function','return','if','else','for','while','class','new','await','async','default','type','interface','extends','implements','this','typeof','instanceof','of','in','true','false','null','undefined'];
|
|
2946
|
+
var HL_LANGS = { js: 1, javascript: 1, ts: 1, typescript: 1, svelte: 1, html: 1, css: 1 };
|
|
2947
|
+
|
|
2948
|
+
function highlightTokens(code) {
|
|
2949
|
+
var tokens = [];
|
|
2950
|
+
var i = 0;
|
|
2951
|
+
var n = code.length;
|
|
2952
|
+
var reIdent = /[A-Za-z_$][A-Za-z0-9_$]*/y;
|
|
2953
|
+
var reNum = /\\d+(\\.\\d+)?/y;
|
|
2954
|
+
while (i < n) {
|
|
2955
|
+
var ch = code[i];
|
|
2956
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
2957
|
+
var end = code.indexOf('\\n', i);
|
|
2958
|
+
if (end === -1) end = n;
|
|
2959
|
+
tokens.push({ text: code.slice(i, end), cls: 'cm' });
|
|
2960
|
+
i = end;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
2964
|
+
var end2 = code.indexOf('*/', i + 2);
|
|
2965
|
+
end2 = end2 === -1 ? n : end2 + 2;
|
|
2966
|
+
tokens.push({ text: code.slice(i, end2), cls: 'cm' });
|
|
2967
|
+
i = end2;
|
|
2968
|
+
continue;
|
|
2969
|
+
}
|
|
2970
|
+
if (ch === '"' || ch === "'" || ch === '\`') {
|
|
2971
|
+
var quote = ch;
|
|
2972
|
+
var j = i + 1;
|
|
2973
|
+
while (j < n && code[j] !== quote) {
|
|
2974
|
+
if (code[j] === '\\\\') j++;
|
|
2975
|
+
j++;
|
|
2976
|
+
}
|
|
2977
|
+
j = Math.min(j + 1, n);
|
|
2978
|
+
tokens.push({ text: code.slice(i, j), cls: 'str' });
|
|
2979
|
+
i = j;
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
reIdent.lastIndex = i;
|
|
2983
|
+
var mIdent = reIdent.exec(code);
|
|
2984
|
+
if (mIdent && mIdent.index === i) {
|
|
2985
|
+
var word = mIdent[0];
|
|
2986
|
+
tokens.push({ text: word, cls: HL_KEYWORDS.indexOf(word) !== -1 ? 'kw' : 'id' });
|
|
2987
|
+
i += word.length;
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
reNum.lastIndex = i;
|
|
2991
|
+
var mNum = reNum.exec(code);
|
|
2992
|
+
if (mNum && mNum.index === i) {
|
|
2993
|
+
tokens.push({ text: mNum[0], cls: 'num' });
|
|
2994
|
+
i += mNum[0].length;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
tokens.push({ text: ch, cls: 'pn' });
|
|
2998
|
+
i += 1;
|
|
2999
|
+
}
|
|
3000
|
+
return tokens;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function renderFixSnippet(fix) {
|
|
3004
|
+
var pre = h('pre', null, []);
|
|
3005
|
+
var code = h('code', null, []);
|
|
3006
|
+
var lang = (fix.lang || 'svelte').toLowerCase();
|
|
3007
|
+
if (HL_LANGS[lang]) {
|
|
3008
|
+
highlightTokens(fix.snippet).forEach(function (t) {
|
|
3009
|
+
code.appendChild(h('span', { class: 'tok-' + t.cls, text: t.text }, []));
|
|
2963
3010
|
});
|
|
2964
|
-
|
|
3011
|
+
} else {
|
|
3012
|
+
code.textContent = fix.snippet;
|
|
3013
|
+
}
|
|
3014
|
+
pre.appendChild(code);
|
|
3015
|
+
return pre;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// Plain-text, copy-pasteable prompt for a single finding \u2014 same ingredients as the
|
|
3019
|
+
// agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
|
|
3020
|
+
// reshaped for a standalone request rather than a whole-project remediation doc.
|
|
3021
|
+
function buildAiPrompt(issue, route) {
|
|
3022
|
+
var lines = ['Fix this svelte-vitals finding:', ''];
|
|
3023
|
+
lines.push('- Rule: ' + issue.id + ' \u2014 ' + issue.title + ' (' + issue.severity + ')');
|
|
3024
|
+
if (route) lines.push('- Route: ' + route);
|
|
3025
|
+
if (issue.location) {
|
|
3026
|
+
lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
|
|
3027
|
+
}
|
|
3028
|
+
if (issue.recommendation) lines.push('- Recommendation: ' + issue.recommendation);
|
|
3029
|
+
if (issue.fix) {
|
|
3030
|
+
lines.push('- Fix: ' + issue.fix.description);
|
|
3031
|
+
if (issue.fix.snippet) {
|
|
3032
|
+
lines.push('', '\`\`\`' + (issue.fix.lang || 'svelte'), issue.fix.snippet, '\`\`\`');
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
if (issue.docsUrl) lines.push('- Docs: ' + issue.docsUrl);
|
|
3036
|
+
lines.push(
|
|
3037
|
+
'',
|
|
3038
|
+
'After fixing, re-run \`svelte-vitals --diff\` (or revisit this route) to confirm ' +
|
|
3039
|
+
issue.id +
|
|
3040
|
+
' passes' +
|
|
3041
|
+
(route ? ' for ' + route : '') +
|
|
3042
|
+
'.'
|
|
3043
|
+
);
|
|
3044
|
+
return lines.join('\\n');
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function copyToClipboard(text, btn) {
|
|
3048
|
+
var original = 'Copy';
|
|
3049
|
+
function reset(label) {
|
|
3050
|
+
btn.textContent = label;
|
|
3051
|
+
setTimeout(function () { btn.textContent = original; }, 1500);
|
|
3052
|
+
}
|
|
3053
|
+
function done() { reset('Copied!'); }
|
|
3054
|
+
function fail() { reset('Copy failed'); }
|
|
3055
|
+
function fallbackCopy() {
|
|
3056
|
+
var ta = document.createElement('textarea');
|
|
3057
|
+
ta.value = text;
|
|
3058
|
+
ta.setAttribute('readonly', '');
|
|
3059
|
+
ta.style.position = 'fixed';
|
|
3060
|
+
ta.style.opacity = '0';
|
|
3061
|
+
document.body.appendChild(ta);
|
|
3062
|
+
ta.select();
|
|
3063
|
+
var ok = false;
|
|
3064
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
3065
|
+
document.body.removeChild(ta);
|
|
3066
|
+
return ok;
|
|
3067
|
+
}
|
|
3068
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3069
|
+
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy() ? done() : fail(); });
|
|
3070
|
+
} else {
|
|
3071
|
+
fallbackCopy() ? done() : fail();
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function renderAiPrompt(issue, route) {
|
|
3076
|
+
var text = buildAiPrompt(issue, route);
|
|
3077
|
+
var btn = h('button', { type: 'button', class: 'dv-ai-copy-btn', text: 'Copy' }, []);
|
|
3078
|
+
btn.addEventListener('click', function () { copyToClipboard(text, btn); });
|
|
3079
|
+
return h('details', { class: 'dv-ai-prompt' }, [
|
|
3080
|
+
h('summary', { text: 'AI Prompt' }, []),
|
|
3081
|
+
h('div', { class: 'dv-ai-prompt-body' }, [
|
|
3082
|
+
h('pre', { class: 'dv-ai-prompt-pre', text: text }, []),
|
|
3083
|
+
btn
|
|
3084
|
+
])
|
|
3085
|
+
]);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
var state = {
|
|
3089
|
+
snapshot: null,
|
|
3090
|
+
selected: 'overview',
|
|
3091
|
+
search: '',
|
|
3092
|
+
sort: 'score-asc',
|
|
3093
|
+
filter: 'all',
|
|
3094
|
+
theme: initialTheme(),
|
|
3095
|
+
connection: 'connecting',
|
|
3096
|
+
routeBySlug: {}
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
function initialTheme() {
|
|
3100
|
+
try {
|
|
3101
|
+
var stored = localStorage.getItem('svelte-vitals-theme');
|
|
3102
|
+
if (stored === 'dark' || stored === 'light') return stored;
|
|
3103
|
+
} catch (e) {}
|
|
3104
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
3105
|
+
}
|
|
3106
|
+
function applyTheme() { document.documentElement.setAttribute('data-theme', state.theme); }
|
|
3107
|
+
function toggleTheme() {
|
|
3108
|
+
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
|
3109
|
+
try { localStorage.setItem('svelte-vitals-theme', state.theme); } catch (e) {}
|
|
3110
|
+
applyTheme();
|
|
3111
|
+
renderTopbar();
|
|
3112
|
+
}
|
|
3113
|
+
function toggleSidebar() {
|
|
3114
|
+
var sb = document.getElementById('dv-sidebar');
|
|
3115
|
+
if (sb) sb.classList.toggle('open');
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function brandEl() {
|
|
3119
|
+
var el = h('button', { type: 'button', class: 'dv-brand', 'aria-label': 'Go to Overview', onclick: function () { selectItem('overview'); } }, []);
|
|
3120
|
+
el.innerHTML = WORDMARK_SVG;
|
|
3121
|
+
return el;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function renderTopbar() {
|
|
3125
|
+
var s = state.snapshot;
|
|
3126
|
+
var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
3127
|
+
var kids = [
|
|
3128
|
+
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '\u2261' }, []),
|
|
3129
|
+
brandEl(),
|
|
3130
|
+
h('div', { class: 'dv-meta' }, [
|
|
3131
|
+
h('span', { text: 'v' + s.meta.version }, []),
|
|
3132
|
+
s.meta.coreVersion ? h('span', { title: '@svelte-vitals/core version', text: 'core v' + s.meta.coreVersion }, []) : null,
|
|
3133
|
+
h('span', { text: s.report.routes.length + ' routes' }, []),
|
|
3134
|
+
h('span', { text: findings + ' findings' }, [])
|
|
3135
|
+
].filter(Boolean)),
|
|
3136
|
+
h('div', { class: 'dv-status' }, [
|
|
3137
|
+
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing\u2026' }, []) : null,
|
|
3138
|
+
s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
|
|
3139
|
+
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '\u2600' : '\u263E' }, [])
|
|
3140
|
+
].filter(Boolean))
|
|
3141
|
+
];
|
|
3142
|
+
mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function slugify(route) {
|
|
3146
|
+
return 'route-' + route.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
function matchesSearch(route, q) {
|
|
3150
|
+
if (!q) return true;
|
|
3151
|
+
q = q.toLowerCase();
|
|
3152
|
+
if (route.route.toLowerCase().indexOf(q) !== -1) return true;
|
|
3153
|
+
return route.issues.some(function (iss) {
|
|
3154
|
+
return (iss.id + ' ' + iss.title + ' ' + (iss.location || '')).toLowerCase().indexOf(q) !== -1;
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
function sortedRoutes() {
|
|
3159
|
+
var s = state.snapshot;
|
|
3160
|
+
var q = state.search.trim();
|
|
3161
|
+
var list = s.report.routes.filter(function (r) { return matchesSearch(r, q); }).slice();
|
|
3162
|
+
var sort = state.sort;
|
|
3163
|
+
if (sort === 'score-asc') list.sort(function (a, b) { return a.score - b.score; });
|
|
3164
|
+
else if (sort === 'score-desc') list.sort(function (a, b) { return b.score - a.score; });
|
|
3165
|
+
else if (sort === 'alpha') list.sort(function (a, b) { return a.route.localeCompare(b.route); });
|
|
3166
|
+
else if (sort === 'most-findings') list.sort(function (a, b) { return b.issues.length - a.issues.length; });
|
|
3167
|
+
return list;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function renderNavItem(label, key, route, active) {
|
|
3171
|
+
var kids = [h('span', { class: 'dv-nav-label', text: label }, [])];
|
|
3172
|
+
if (route) {
|
|
3173
|
+
var band = scoreBand(route.score);
|
|
3174
|
+
var crit = route.issues.filter(function (i) { return i.severity === 'critical'; }).length;
|
|
3175
|
+
var warn = route.issues.filter(function (i) { return i.severity === 'warning'; }).length;
|
|
3176
|
+
var info = route.issues.filter(function (i) { return i.severity === 'info'; }).length;
|
|
3177
|
+
var summary = [];
|
|
3178
|
+
if (crit) summary.push(crit + ' critical');
|
|
3179
|
+
if (warn) summary.push(warn + ' warning' + (warn > 1 ? 's' : ''));
|
|
3180
|
+
if (info) summary.push(info + ' info');
|
|
3181
|
+
var badge = state.snapshot.badges[route.route];
|
|
3182
|
+
kids.push(h('span', { class: 'dv-nav-meta' }, [
|
|
3183
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
3184
|
+
h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
|
|
3185
|
+
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' \xB7 ') : 'no issues' }, [])
|
|
3186
|
+
].filter(Boolean)));
|
|
3187
|
+
}
|
|
3188
|
+
return h('div', {
|
|
3189
|
+
class: 'dv-nav-item' + (active ? ' active' : ''),
|
|
3190
|
+
role: 'option',
|
|
3191
|
+
'aria-selected': active ? 'true' : 'false',
|
|
3192
|
+
tabindex: '0',
|
|
3193
|
+
onclick: function () { selectItem(key); },
|
|
3194
|
+
onkeydown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem(key); } }
|
|
3195
|
+
}, kids);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function selectItem(key) {
|
|
3199
|
+
state.selected = key;
|
|
3200
|
+
location.hash = key === 'overview' ? 'overview' : 'route/' + slugify(key);
|
|
3201
|
+
var sb = document.getElementById('dv-sidebar');
|
|
3202
|
+
if (sb) sb.classList.remove('open');
|
|
3203
|
+
renderSidebar();
|
|
3204
|
+
renderDetail();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function renderSidebar() {
|
|
3208
|
+
var s = state.snapshot;
|
|
3209
|
+
state.routeBySlug = {};
|
|
3210
|
+
|
|
3211
|
+
// mount() clears and rebuilds the whole sidebar, including the <input> itself, so
|
|
3212
|
+
// a naive re-render on every keystroke drops focus and cursor position \u2014 capture
|
|
3213
|
+
// them before rebuilding and restore them on the freshly-created input afterward.
|
|
3214
|
+
var prevSearch = document.querySelector('.dv-search');
|
|
3215
|
+
var hadFocus = !!prevSearch && document.activeElement === prevSearch;
|
|
3216
|
+
var selStart = hadFocus ? prevSearch.selectionStart : null;
|
|
3217
|
+
var selEnd = hadFocus ? prevSearch.selectionEnd : null;
|
|
3218
|
+
|
|
3219
|
+
var searchInput = h('input', {
|
|
3220
|
+
type: 'search',
|
|
3221
|
+
class: 'dv-search',
|
|
3222
|
+
placeholder: 'Search routes or rules\u2026',
|
|
3223
|
+
value: state.search,
|
|
3224
|
+
oninput: function (e) { state.search = e.target.value; renderSidebar(); }
|
|
3225
|
+
}, []);
|
|
3226
|
+
|
|
3227
|
+
var sortSelect = h('select', { class: 'dv-sort', 'aria-label': 'Sort routes', onchange: function (e) { state.sort = e.target.value; renderSidebar(); } }, [
|
|
3228
|
+
h('option', { value: 'score-asc', selected: state.sort === 'score-asc' || undefined, text: 'Score (worst first)' }, []),
|
|
3229
|
+
h('option', { value: 'score-desc', selected: state.sort === 'score-desc' || undefined, text: 'Score (best first)' }, []),
|
|
3230
|
+
h('option', { value: 'alpha', selected: state.sort === 'alpha' || undefined, text: 'Alphabetical' }, []),
|
|
3231
|
+
h('option', { value: 'most-findings', selected: state.sort === 'most-findings' || undefined, text: 'Most findings' }, [])
|
|
3232
|
+
]);
|
|
3233
|
+
|
|
3234
|
+
var items = [renderNavItem('Overview', 'overview', null, state.selected === 'overview')];
|
|
3235
|
+
sortedRoutes().forEach(function (r) {
|
|
3236
|
+
var slug = slugify(r.route);
|
|
3237
|
+
state.routeBySlug[slug] = r.route;
|
|
3238
|
+
items.push(renderNavItem(r.route, r.route, r, state.selected === r.route));
|
|
3239
|
+
});
|
|
3240
|
+
|
|
3241
|
+
var nav = h('div', { class: 'dv-nav', role: 'listbox', 'aria-label': 'Routes' }, items);
|
|
3242
|
+
mount('dv-sidebar', h('div', { class: 'dv-sidebar-inner' }, [searchInput, sortSelect, nav]));
|
|
3243
|
+
|
|
3244
|
+
if (hadFocus) {
|
|
3245
|
+
searchInput.focus();
|
|
3246
|
+
if (selStart !== null && searchInput.setSelectionRange) {
|
|
3247
|
+
searchInput.setSelectionRange(selStart, selEnd);
|
|
3248
|
+
}
|
|
2965
3249
|
}
|
|
2966
3250
|
}
|
|
2967
|
-
|
|
2968
|
-
function
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3251
|
+
|
|
3252
|
+
function renderFilterChips(categories) {
|
|
3253
|
+
var chip = function (filter, label) {
|
|
3254
|
+
return h('button', {
|
|
3255
|
+
type: 'button', class: 'dv-chip', 'aria-pressed': state.filter === filter ? 'true' : 'false',
|
|
3256
|
+
onclick: function () { state.filter = filter; renderDetail(); },
|
|
3257
|
+
text: label
|
|
3258
|
+
}, []);
|
|
3259
|
+
};
|
|
3260
|
+
var catChips = Object.keys(categories).map(function (cat) {
|
|
3261
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
3262
|
+
return chip(cat, name);
|
|
2972
3263
|
});
|
|
3264
|
+
return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
|
|
3265
|
+
[chip('all', 'All'), chip('critical', 'Critical'), chip('warning', 'Warning'), chip('info', 'Info')].concat(catChips));
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
function passesFilter(issue) {
|
|
3269
|
+
var f = state.filter;
|
|
3270
|
+
return f === 'all' || issue.severity === f || issue.category === f;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3274
|
+
|
|
3275
|
+
function renderFinding(issue, route, promptRoute) {
|
|
3276
|
+
var kids = [
|
|
3277
|
+
h('div', { class: 'dv-f-head' }, [
|
|
3278
|
+
h('span', { class: 'dv-ruleid', text: issue.id }, []),
|
|
3279
|
+
h('span', { class: 'dv-f-title', text: issue.title }, []),
|
|
3280
|
+
h('span', { class: 'dv-sev-tag dv-sev-' + issue.severity, text: issue.severity }, [])
|
|
3281
|
+
])
|
|
3282
|
+
];
|
|
3283
|
+
if (route) {
|
|
3284
|
+
kids.push(h('button', { type: 'button', class: 'dv-f-route', onclick: function () { selectItem(route); }, text: route }, []));
|
|
3285
|
+
}
|
|
3286
|
+
if (issue.location) {
|
|
3287
|
+
kids.push(h('p', { class: 'dv-f-loc', text: issue.location + (issue.line !== undefined ? ':' + issue.line : '') }, []));
|
|
3288
|
+
}
|
|
3289
|
+
if (issue.recommendation) {
|
|
3290
|
+
kids.push(h('p', { class: 'dv-f-rec', text: issue.recommendation }, []));
|
|
3291
|
+
}
|
|
3292
|
+
if (issue.fix && issue.fix.snippet) {
|
|
3293
|
+
kids.push(h('div', { class: 'dv-fix' }, [h('div', { class: 'dv-fix-label', text: 'fix' }, []), renderFixSnippet(issue.fix)]));
|
|
3294
|
+
}
|
|
3295
|
+
if (issue.docsUrl) {
|
|
3296
|
+
kids.push(h('a', { class: 'dv-f-link', href: issue.docsUrl, text: 'Learn more' }, []));
|
|
3297
|
+
}
|
|
3298
|
+
kids.push(renderAiPrompt(issue, promptRoute !== undefined ? promptRoute : route));
|
|
3299
|
+
return h('article', { class: 'dv-finding dv-finding-' + issue.severity }, kids);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
function renderGauge(score) {
|
|
3303
|
+
var band = scoreBand(score);
|
|
3304
|
+
var svgNs = 'http://www.w3.org/2000/svg';
|
|
3305
|
+
var C = 2 * Math.PI * 58;
|
|
3306
|
+
var offset = (C * (1 - score / 100)).toFixed(1);
|
|
3307
|
+
var svg = document.createElementNS(svgNs, 'svg');
|
|
3308
|
+
svg.setAttribute('width', '132');
|
|
3309
|
+
svg.setAttribute('height', '132');
|
|
3310
|
+
svg.setAttribute('viewBox', '0 0 132 132');
|
|
3311
|
+
var bg = document.createElementNS(svgNs, 'circle');
|
|
3312
|
+
bg.setAttribute('cx', '66'); bg.setAttribute('cy', '66'); bg.setAttribute('r', '58');
|
|
3313
|
+
bg.setAttribute('fill', 'none'); bg.setAttribute('class', 'dv-gauge-track'); bg.setAttribute('stroke-width', '11');
|
|
3314
|
+
var arc = document.createElementNS(svgNs, 'circle');
|
|
3315
|
+
arc.setAttribute('cx', '66'); arc.setAttribute('cy', '66'); arc.setAttribute('r', '58');
|
|
3316
|
+
arc.setAttribute('fill', 'none'); arc.setAttribute('stroke', BAND_COLOR[band]); arc.setAttribute('stroke-width', '11');
|
|
3317
|
+
arc.setAttribute('stroke-linecap', 'round');
|
|
3318
|
+
arc.setAttribute('stroke-dasharray', C.toFixed(1));
|
|
3319
|
+
arc.setAttribute('stroke-dashoffset', offset);
|
|
3320
|
+
svg.appendChild(bg);
|
|
3321
|
+
svg.appendChild(arc);
|
|
3322
|
+
var wrap = h('div', { class: 'dv-gauge' }, [h('div', { class: 'dv-gauge-num' }, [h('strong', { text: String(score) }, []), h('span', { text: 'Health' }, [])])]);
|
|
3323
|
+
wrap.insertBefore(svg, wrap.firstChild);
|
|
3324
|
+
return wrap;
|
|
2973
3325
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3326
|
+
|
|
3327
|
+
function renderOverview(s) {
|
|
3328
|
+
var gauge = renderGauge(s.report.score);
|
|
3329
|
+
var cats = Object.keys(s.report.categories).map(function (cat) {
|
|
3330
|
+
var c = s.report.categories[cat];
|
|
3331
|
+
var band = scoreBand(c.score);
|
|
3332
|
+
var weight = s.report.weights[cat];
|
|
3333
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
3334
|
+
return h('div', { class: 'dv-cat' }, [
|
|
3335
|
+
h('div', { class: 'dv-cat-top' }, [
|
|
3336
|
+
h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
|
|
3337
|
+
h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
|
|
3338
|
+
]),
|
|
3339
|
+
h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])])
|
|
3340
|
+
]);
|
|
2979
3341
|
});
|
|
2980
|
-
|
|
3342
|
+
var chips = renderFilterChips(s.report.categories);
|
|
3343
|
+
var totalCount = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
3344
|
+
var entries = [];
|
|
3345
|
+
s.report.routes.forEach(function (r) {
|
|
3346
|
+
r.issues.forEach(function (issue) { entries.push({ issue: issue, route: r.route }); });
|
|
3347
|
+
});
|
|
3348
|
+
s.report.siteIssues.forEach(function (issue) { entries.push({ issue: issue, route: null }); });
|
|
3349
|
+
entries = entries.filter(function (e) { return passesFilter(e.issue); });
|
|
3350
|
+
entries.sort(function (a, b) {
|
|
3351
|
+
var sd = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
|
|
3352
|
+
if (sd !== 0) return sd;
|
|
3353
|
+
return (a.route || '').localeCompare(b.route || '');
|
|
3354
|
+
});
|
|
3355
|
+
var body = entries.length
|
|
3356
|
+
? entries.map(function (e) { return renderFinding(e.issue, e.route); })
|
|
3357
|
+
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found \u2014 nice work!' }, [])];
|
|
3358
|
+
var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
|
|
3359
|
+
return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
function renderRouteDetail(route) {
|
|
3363
|
+
var badge = state.snapshot.badges[route.route];
|
|
3364
|
+
var band = scoreBand(route.score);
|
|
3365
|
+
var header = h('div', { class: 'dv-detail-header' }, [
|
|
3366
|
+
h('span', { class: 'dv-route-path', text: route.route }, []),
|
|
3367
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
3368
|
+
h('span', { class: 'dv-score-chip', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, [])
|
|
3369
|
+
].filter(Boolean));
|
|
3370
|
+
var chips = renderFilterChips(state.snapshot.report.categories);
|
|
3371
|
+
var findings = route.issues.filter(passesFilter);
|
|
3372
|
+
var body = findings.length
|
|
3373
|
+
? findings.map(function (issue) { return renderFinding(issue, undefined, route.route); })
|
|
3374
|
+
: [h('p', { class: 'dv-empty', text: 'No issues match the current filter.' }, [])];
|
|
3375
|
+
return h('div', { class: 'dv-route-detail' }, [header, chips].concat(body));
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
function renderDetail() {
|
|
3379
|
+
var s = state.snapshot;
|
|
3380
|
+
if (state.selected === 'overview') {
|
|
3381
|
+
mount('dv-detail', renderOverview(s));
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
var route = s.report.routes.filter(function (r) { return r.route === state.selected; })[0];
|
|
3385
|
+
if (!route) {
|
|
3386
|
+
state.selected = 'overview';
|
|
3387
|
+
mount('dv-detail', renderOverview(s));
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
mount('dv-detail', renderRouteDetail(route));
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function renderAll() {
|
|
3394
|
+
renderTopbar();
|
|
3395
|
+
renderSidebar();
|
|
3396
|
+
renderDetail();
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function restoreSelectionFromHash() {
|
|
3400
|
+
var raw = location.hash.replace(/^#/, '');
|
|
3401
|
+
if (!raw || raw === 'overview') { state.selected = 'overview'; return; }
|
|
3402
|
+
var m = /^route\\/(.+)$/.exec(raw);
|
|
3403
|
+
if (m && state.routeBySlug[m[1]]) state.selected = state.routeBySlug[m[1]];
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
function fetchSnapshot() {
|
|
3407
|
+
fetch('/__svelte-vitals/data.json').then(function (r) { return r.json(); }).then(function (data) {
|
|
3408
|
+
if (state.snapshot && data.sequence <= state.snapshot.sequence) return;
|
|
3409
|
+
state.snapshot = data;
|
|
3410
|
+
renderAll();
|
|
3411
|
+
}).catch(function () {});
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function boot() {
|
|
3415
|
+
var raw = document.getElementById('svelte-vitals-data');
|
|
3416
|
+
state.snapshot = JSON.parse(raw.textContent);
|
|
3417
|
+
applyTheme();
|
|
3418
|
+
renderSidebar(); // populates routeBySlug before the hash can be trusted
|
|
3419
|
+
restoreSelectionFromHash();
|
|
3420
|
+
renderAll();
|
|
3421
|
+
|
|
3422
|
+
window.addEventListener('hashchange', function () {
|
|
3423
|
+
restoreSelectionFromHash();
|
|
3424
|
+
renderSidebar();
|
|
3425
|
+
renderDetail();
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
// Static export (the CLI's --reporter html): no dev server behind the page, so no
|
|
3429
|
+
// SSE connection, no /data.json refetch, no connection indicator.
|
|
3430
|
+
if (state.snapshot.live && typeof EventSource !== 'undefined') {
|
|
3431
|
+
var es = new EventSource('/__svelte-vitals/events');
|
|
3432
|
+
es.addEventListener('open', function () { state.connection = 'connected'; renderTopbar(); fetchSnapshot(); });
|
|
3433
|
+
es.addEventListener('update', fetchSnapshot);
|
|
3434
|
+
es.addEventListener('error', function () { state.connection = 'reconnecting'; renderTopbar(); });
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
boot();
|
|
2981
3439
|
})();
|
|
2982
3440
|
`;
|
|
3441
|
+
function renderAppShell(snapshot) {
|
|
3442
|
+
const safe = { ...snapshot, report: sanitizeReport(snapshot.report) };
|
|
3443
|
+
const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
|
|
3444
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}</script><script>${APP_SCRIPT}</script></body></html>`;
|
|
3445
|
+
}
|
|
2983
3446
|
function buildHtmlDocument(report, meta, opts) {
|
|
2984
|
-
|
|
3447
|
+
const badges = Object.fromEntries(
|
|
3448
|
+
Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
|
|
3449
|
+
);
|
|
3450
|
+
return renderAppShell({
|
|
3451
|
+
report,
|
|
3452
|
+
badges,
|
|
3453
|
+
analyzing: false,
|
|
3454
|
+
sequence: 0,
|
|
3455
|
+
live: false,
|
|
3456
|
+
meta
|
|
3457
|
+
});
|
|
2985
3458
|
}
|
|
2986
3459
|
function formatHtmlReport(results, config, meta) {
|
|
2987
3460
|
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
2988
3461
|
}
|
|
2989
3462
|
|
|
3463
|
+
// src/reporter/html.ts
|
|
3464
|
+
var BAND_COLOR = {
|
|
3465
|
+
good: "#2FA968",
|
|
3466
|
+
warn: "#E8A317",
|
|
3467
|
+
poor: "#E5484D"
|
|
3468
|
+
};
|
|
3469
|
+
function scoreBand(score) {
|
|
3470
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
3471
|
+
}
|
|
3472
|
+
function escapeHtml(s) {
|
|
3473
|
+
return s.replace(
|
|
3474
|
+
/[&<>"']/g,
|
|
3475
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
function safeHref(url) {
|
|
3479
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
3480
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
2990
3483
|
// src/config-apply.ts
|
|
2991
3484
|
function selectRules(rules, config) {
|
|
2992
3485
|
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
@@ -2998,6 +3491,8 @@ function applyRuleSeverities(results, config) {
|
|
|
2998
3491
|
});
|
|
2999
3492
|
}
|
|
3000
3493
|
export {
|
|
3494
|
+
APP_SCRIPT,
|
|
3495
|
+
APP_STYLE,
|
|
3001
3496
|
BAND_COLOR,
|
|
3002
3497
|
CHILD_NODE_KEYS,
|
|
3003
3498
|
ROBOTS_SOURCE_PATHS,
|
|
@@ -3055,6 +3550,7 @@ export {
|
|
|
3055
3550
|
perf008Preconnect,
|
|
3056
3551
|
perf009HeavyImport,
|
|
3057
3552
|
perf010NamespaceImport,
|
|
3553
|
+
renderAppShell,
|
|
3058
3554
|
runRules,
|
|
3059
3555
|
safeHref,
|
|
3060
3556
|
scoreBand,
|