@ogygia/devtools 0.0.0-stage → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/dist/client.d.ts +3 -0
- package/dist/client.js +24 -0
- package/dist/devtools/BoundaryOverlay.svelte +396 -0
- package/dist/devtools/BoundaryOverlay.svelte.d.ts +19 -0
- package/dist/devtools/Devtools.svelte +663 -0
- package/dist/devtools/Devtools.svelte.d.ts +15 -0
- package/dist/devtools/HubTab.svelte +206 -0
- package/dist/devtools/HubTab.svelte.d.ts +11 -0
- package/dist/devtools/HydrationTab.svelte +237 -0
- package/dist/devtools/HydrationTab.svelte.d.ts +13 -0
- package/dist/devtools/IslandDetail.svelte +615 -0
- package/dist/devtools/IslandDetail.svelte.d.ts +15 -0
- package/dist/devtools/LedgerTab.svelte +287 -0
- package/dist/devtools/LedgerTab.svelte.d.ts +11 -0
- package/dist/devtools/LensTab.svelte +290 -0
- package/dist/devtools/LensTab.svelte.d.ts +19 -0
- package/dist/devtools/NavTab.svelte +229 -0
- package/dist/devtools/NavTab.svelte.d.ts +11 -0
- package/dist/devtools/PageTab.svelte +1122 -0
- package/dist/devtools/PageTab.svelte.d.ts +15 -0
- package/dist/devtools/ProfilerTab.svelte +755 -0
- package/dist/devtools/ProfilerTab.svelte.d.ts +15 -0
- package/dist/devtools/PropsTree.svelte +155 -0
- package/dist/devtools/PropsTree.svelte.d.ts +15 -0
- package/dist/devtools/SessionTab.svelte +796 -0
- package/dist/devtools/SessionTab.svelte.d.ts +11 -0
- package/dist/devtools/TimelineTab.svelte +267 -0
- package/dist/devtools/TimelineTab.svelte.d.ts +11 -0
- package/dist/devtools/WireTab.svelte +149 -0
- package/dist/devtools/WireTab.svelte.d.ts +11 -0
- package/dist/devtools/bytes.d.ts +70 -0
- package/dist/devtools/bytes.js +82 -0
- package/dist/devtools/cache-headers.d.ts +20 -0
- package/dist/devtools/cache-headers.js +81 -0
- package/dist/devtools/highlight.d.ts +7 -0
- package/dist/devtools/highlight.js +83 -0
- package/dist/devtools/hydration.d.ts +48 -0
- package/dist/devtools/hydration.js +93 -0
- package/dist/devtools/index.d.ts +3 -0
- package/dist/devtools/index.js +3 -0
- package/dist/devtools/ledger-dom.d.ts +7 -0
- package/dist/devtools/ledger-dom.js +151 -0
- package/dist/devtools/leftovers.d.ts +77 -0
- package/dist/devtools/leftovers.js +319 -0
- package/dist/devtools/names.d.ts +1 -0
- package/dist/devtools/names.js +34 -0
- package/dist/devtools/page.d.ts +96 -0
- package/dist/devtools/page.js +957 -0
- package/dist/devtools/profile-anchors.d.ts +22 -0
- package/dist/devtools/profile-anchors.js +135 -0
- package/dist/devtools/profile-store.d.ts +175 -0
- package/dist/devtools/profile-store.js +239 -0
- package/dist/devtools/regions.d.ts +77 -0
- package/dist/devtools/regions.js +132 -0
- package/dist/devtools/session.d.ts +31 -0
- package/dist/devtools/session.js +455 -0
- package/dist/devtools/since-load.d.ts +50 -0
- package/dist/devtools/since-load.js +89 -0
- package/dist/devtools/source-lines.d.ts +24 -0
- package/dist/devtools/source-lines.js +106 -0
- package/dist/devtools/styles.d.ts +52 -0
- package/dist/devtools/styles.js +248 -0
- package/dist/devtools/testing-engine.d.ts +83 -0
- package/dist/devtools/testing-engine.js +220 -0
- package/dist/devtools/ui-app.d.ts +5 -0
- package/dist/devtools/ui-app.js +69 -0
- package/dist/devtools/ui.d.ts +50 -0
- package/dist/devtools/ui.js +175 -0
- package/dist/profiler/alloc.d.ts +75 -0
- package/dist/profiler/alloc.js +76 -0
- package/dist/profiler/analyze.d.ts +437 -0
- package/dist/profiler/analyze.js +1941 -0
- package/dist/profiler/app-path.d.ts +7 -0
- package/dist/profiler/app-path.js +26 -0
- package/dist/profiler/async-io.d.ts +58 -0
- package/dist/profiler/async-io.js +187 -0
- package/dist/profiler/beacon-standalone.d.ts +4 -0
- package/dist/profiler/beacon-standalone.js +409 -0
- package/dist/profiler/browser-findings.d.ts +25 -0
- package/dist/profiler/browser-findings.js +260 -0
- package/dist/profiler/byte-strip.d.ts +58 -0
- package/dist/profiler/byte-strip.js +228 -0
- package/dist/profiler/client-files.d.ts +16 -0
- package/dist/profiler/client-files.js +60 -0
- package/dist/profiler/client-windows.d.ts +49 -0
- package/dist/profiler/client-windows.js +95 -0
- package/dist/profiler/client.d.ts +40 -0
- package/dist/profiler/client.js +82 -0
- package/dist/profiler/compare.d.ts +308 -0
- package/dist/profiler/compare.js +733 -0
- package/dist/profiler/compress.d.ts +10 -0
- package/dist/profiler/compress.js +36 -0
- package/dist/profiler/contention.d.ts +60 -0
- package/dist/profiler/contention.js +86 -0
- package/dist/profiler/crypto.d.ts +19 -0
- package/dist/profiler/crypto.js +101 -0
- package/dist/profiler/doc-diff.d.ts +47 -0
- package/dist/profiler/doc-diff.js +214 -0
- package/dist/profiler/dom-diff.d.ts +43 -0
- package/dist/profiler/dom-diff.js +121 -0
- package/dist/profiler/drill.d.ts +89 -0
- package/dist/profiler/drill.js +553 -0
- package/dist/profiler/embedded-maps.d.ts +37 -0
- package/dist/profiler/embedded-maps.js +135 -0
- package/dist/profiler/env.d.ts +6 -0
- package/dist/profiler/env.js +10 -0
- package/dist/profiler/forecast.d.ts +76 -0
- package/dist/profiler/forecast.js +225 -0
- package/dist/profiler/frames.d.ts +11 -0
- package/dist/profiler/frames.js +42 -0
- package/dist/profiler/gc.d.ts +144 -0
- package/dist/profiler/gc.js +271 -0
- package/dist/profiler/hole-slots.d.ts +31 -0
- package/dist/profiler/hole-slots.js +90 -0
- package/dist/profiler/index.d.ts +288 -0
- package/dist/profiler/index.js +4921 -0
- package/dist/profiler/insights.d.ts +96 -0
- package/dist/profiler/insights.js +239 -0
- package/dist/profiler/ledger.d.ts +121 -0
- package/dist/profiler/ledger.js +440 -0
- package/dist/profiler/lineage.d.ts +81 -0
- package/dist/profiler/lineage.js +359 -0
- package/dist/profiler/module-map.d.ts +12 -0
- package/dist/profiler/module-map.js +94 -0
- package/dist/profiler/net.d.ts +169 -0
- package/dist/profiler/net.js +586 -0
- package/dist/profiler/on-demand.d.ts +11 -0
- package/dist/profiler/on-demand.js +15 -0
- package/dist/profiler/own-requests.d.ts +9 -0
- package/dist/profiler/own-requests.js +17 -0
- package/dist/profiler/patterns.d.ts +476 -0
- package/dist/profiler/patterns.js +2203 -0
- package/dist/profiler/profiler-router.d.ts +245 -0
- package/dist/profiler/profiler-router.js +113 -0
- package/dist/profiler/refresh-stub.d.ts +2 -0
- package/dist/profiler/refresh-stub.js +37 -0
- package/dist/profiler/report.d.ts +1404 -0
- package/dist/profiler/report.js +2638 -0
- package/dist/profiler/river.d.ts +65 -0
- package/dist/profiler/river.js +298 -0
- package/dist/profiler/route-files.d.ts +22 -0
- package/dist/profiler/route-files.js +55 -0
- package/dist/profiler/route-imports.d.ts +14 -0
- package/dist/profiler/route-imports.js +83 -0
- package/dist/profiler/saving.d.ts +22 -0
- package/dist/profiler/saving.js +42 -0
- package/dist/profiler/score.d.ts +109 -0
- package/dist/profiler/score.js +261 -0
- package/dist/profiler/session-cookie.d.ts +28 -0
- package/dist/profiler/session-cookie.js +45 -0
- package/dist/profiler/sink.d.ts +81 -0
- package/dist/profiler/sink.js +124 -0
- package/dist/profiler/site-fixes.d.ts +25 -0
- package/dist/profiler/site-fixes.js +62 -0
- package/dist/profiler/source-scan.d.ts +210 -0
- package/dist/profiler/source-scan.js +1724 -0
- package/dist/profiler/span.d.ts +90 -0
- package/dist/profiler/span.js +109 -0
- package/dist/profiler/stacks.d.ts +43 -0
- package/dist/profiler/stacks.js +107 -0
- package/dist/profiler/standalone.d.ts +27 -0
- package/dist/profiler/standalone.js +98 -0
- package/dist/profiler/steps.d.ts +39 -0
- package/dist/profiler/steps.js +74 -0
- package/dist/profiler/storage/index.d.ts +87 -0
- package/dist/profiler/storage/index.js +76 -0
- package/dist/profiler/storage/postgres.d.ts +12 -0
- package/dist/profiler/storage/postgres.js +94 -0
- package/dist/profiler/storage/redis.d.ts +19 -0
- package/dist/profiler/storage/redis.js +148 -0
- package/dist/profiler/storage/sqlite.d.ts +2 -0
- package/dist/profiler/storage/sqlite.js +85 -0
- package/dist/profiler/storage/visits.d.ts +4 -0
- package/dist/profiler/storage/visits.js +6 -0
- package/dist/profiler/stream-tail.d.ts +26 -0
- package/dist/profiler/stream-tail.js +74 -0
- package/dist/profiler/timeline.d.ts +279 -0
- package/dist/profiler/timeline.js +690 -0
- package/dist/profiler/ui/AllocStrip.svelte +146 -0
- package/dist/profiler/ui/AllocStrip.svelte.d.ts +17 -0
- package/dist/profiler/ui/Awaits.svelte +363 -0
- package/dist/profiler/ui/Awaits.svelte.d.ts +16 -0
- package/dist/profiler/ui/ByteStrip.svelte +182 -0
- package/dist/profiler/ui/ByteStrip.svelte.d.ts +18 -0
- package/dist/profiler/ui/Compare.svelte +24 -0
- package/dist/profiler/ui/Compare.svelte.d.ts +16 -0
- package/dist/profiler/ui/CompareBody.svelte +429 -0
- package/dist/profiler/ui/CompareBody.svelte.d.ts +8 -0
- package/dist/profiler/ui/ComponentsTable.svelte +265 -0
- package/dist/profiler/ui/ComponentsTable.svelte.d.ts +20 -0
- package/dist/profiler/ui/Dashboard.svelte +492 -0
- package/dist/profiler/ui/Dashboard.svelte.d.ts +31 -0
- package/dist/profiler/ui/DataRiver.svelte +206 -0
- package/dist/profiler/ui/DataRiver.svelte.d.ts +13 -0
- package/dist/profiler/ui/DomTravel.svelte +207 -0
- package/dist/profiler/ui/DomTravel.svelte.d.ts +9 -0
- package/dist/profiler/ui/DownloadHtml.svelte +52 -0
- package/dist/profiler/ui/DownloadHtml.svelte.d.ts +10 -0
- package/dist/profiler/ui/Drill.svelte +276 -0
- package/dist/profiler/ui/Drill.svelte.d.ts +12 -0
- package/dist/profiler/ui/ExportButton.svelte +59 -0
- package/dist/profiler/ui/ExportButton.svelte.d.ts +8 -0
- package/dist/profiler/ui/Flame.svelte +222 -0
- package/dist/profiler/ui/Flame.svelte.d.ts +12 -0
- package/dist/profiler/ui/FrameDetails.svelte +308 -0
- package/dist/profiler/ui/FrameDetails.svelte.d.ts +16 -0
- package/dist/profiler/ui/FunctionsTable.svelte +156 -0
- package/dist/profiler/ui/FunctionsTable.svelte.d.ts +17 -0
- package/dist/profiler/ui/IslandHeatmap.svelte +151 -0
- package/dist/profiler/ui/IslandHeatmap.svelte.d.ts +16 -0
- package/dist/profiler/ui/IslandsTable.svelte +448 -0
- package/dist/profiler/ui/IslandsTable.svelte.d.ts +16 -0
- package/dist/profiler/ui/KeepLocal.svelte +46 -0
- package/dist/profiler/ui/KeepLocal.svelte.d.ts +8 -0
- package/dist/profiler/ui/Ledger.svelte +196 -0
- package/dist/profiler/ui/Ledger.svelte.d.ts +18 -0
- package/dist/profiler/ui/LocalCompare.svelte +48 -0
- package/dist/profiler/ui/LocalCompare.svelte.d.ts +8 -0
- package/dist/profiler/ui/LocalReports.svelte +68 -0
- package/dist/profiler/ui/LocalReports.svelte.d.ts +7 -0
- package/dist/profiler/ui/Login.svelte +13 -0
- package/dist/profiler/ui/Login.svelte.d.ts +12 -0
- package/dist/profiler/ui/LoginForm.svelte +42 -0
- package/dist/profiler/ui/LoginForm.svelte.d.ts +7 -0
- package/dist/profiler/ui/Message.svelte +10 -0
- package/dist/profiler/ui/Message.svelte.d.ts +12 -0
- package/dist/profiler/ui/OnThisPage.svelte +137 -0
- package/dist/profiler/ui/OnThisPage.svelte.d.ts +3 -0
- package/dist/profiler/ui/OneClock.svelte +297 -0
- package/dist/profiler/ui/OneClock.svelte.d.ts +27 -0
- package/dist/profiler/ui/Panel.svelte +10 -0
- package/dist/profiler/ui/Panel.svelte.d.ts +16 -0
- package/dist/profiler/ui/Paths.svelte +269 -0
- package/dist/profiler/ui/Paths.svelte.d.ts +15 -0
- package/dist/profiler/ui/Patterns.svelte +364 -0
- package/dist/profiler/ui/Patterns.svelte.d.ts +15 -0
- package/dist/profiler/ui/PermalinkGate.svelte +143 -0
- package/dist/profiler/ui/PermalinkGate.svelte.d.ts +10 -0
- package/dist/profiler/ui/RenderSteps.svelte +218 -0
- package/dist/profiler/ui/RenderSteps.svelte.d.ts +13 -0
- package/dist/profiler/ui/Report.svelte +28 -0
- package/dist/profiler/ui/Report.svelte.d.ts +20 -0
- package/dist/profiler/ui/ReportBody.svelte +2592 -0
- package/dist/profiler/ui/ReportBody.svelte.d.ts +21 -0
- package/dist/profiler/ui/Run.svelte +17 -0
- package/dist/profiler/ui/Run.svelte.d.ts +14 -0
- package/dist/profiler/ui/RunView.svelte +146 -0
- package/dist/profiler/ui/RunView.svelte.d.ts +10 -0
- package/dist/profiler/ui/ScoreCard.svelte +197 -0
- package/dist/profiler/ui/ScoreCard.svelte.d.ts +7 -0
- package/dist/profiler/ui/Scrub.svelte +293 -0
- package/dist/profiler/ui/Scrub.svelte.d.ts +18 -0
- package/dist/profiler/ui/SeedExplainer.svelte +193 -0
- package/dist/profiler/ui/SeedExplainer.svelte.d.ts +16 -0
- package/dist/profiler/ui/ShareLink.svelte +148 -0
- package/dist/profiler/ui/ShareLink.svelte.d.ts +8 -0
- package/dist/profiler/ui/Shell.svelte +50 -0
- package/dist/profiler/ui/Shell.svelte.d.ts +27 -0
- package/dist/profiler/ui/Sidebar.svelte +312 -0
- package/dist/profiler/ui/Sidebar.svelte.d.ts +7 -0
- package/dist/profiler/ui/Site.svelte +20 -0
- package/dist/profiler/ui/Site.svelte.d.ts +23 -0
- package/dist/profiler/ui/SiteView.svelte +385 -0
- package/dist/profiler/ui/SiteView.svelte.d.ts +27 -0
- package/dist/profiler/ui/ThemeSwitch.svelte +78 -0
- package/dist/profiler/ui/ThemeSwitch.svelte.d.ts +3 -0
- package/dist/profiler/ui/Timeline.svelte +672 -0
- package/dist/profiler/ui/Timeline.svelte.d.ts +22 -0
- package/dist/profiler/ui/Treemap.svelte +278 -0
- package/dist/profiler/ui/Treemap.svelte.d.ts +20 -0
- package/dist/profiler/ui/Upload.svelte +19 -0
- package/dist/profiler/ui/Upload.svelte.d.ts +10 -0
- package/dist/profiler/ui/UploadForm.svelte +59 -0
- package/dist/profiler/ui/UploadForm.svelte.d.ts +6 -0
- package/dist/profiler/ui/WaitingTable.svelte +55 -0
- package/dist/profiler/ui/WaitingTable.svelte.d.ts +14 -0
- package/dist/profiler/ui/Waterfall.svelte +371 -0
- package/dist/profiler/ui/Waterfall.svelte.d.ts +7 -0
- package/dist/profiler/ui/WhatIf.svelte +87 -0
- package/dist/profiler/ui/WhatIf.svelte.d.ts +18 -0
- package/dist/profiler/ui/fake-progress.svelte.d.ts +15 -0
- package/dist/profiler/ui/fake-progress.svelte.js +38 -0
- package/dist/profiler/ui/format.d.ts +20 -0
- package/dist/profiler/ui/format.js +71 -0
- package/dist/profiler/ui/permalink.d.ts +9 -0
- package/dist/profiler/ui/permalink.js +108 -0
- package/dist/profiler/ui/press.d.ts +5 -0
- package/dist/profiler/ui/press.js +19 -0
- package/dist/profiler/ui/profiler.css +650 -0
- package/dist/profiler/ui/report-data.d.ts +189 -0
- package/dist/profiler/ui/report-data.js +393 -0
- package/dist/profiler/ui/report-request.d.ts +6 -0
- package/dist/profiler/ui/report-request.js +50 -0
- package/dist/profiler/ui/row-anchor.svelte.d.ts +12 -0
- package/dist/profiler/ui/row-anchor.svelte.js +44 -0
- package/dist/profiler/ui/sort.svelte.d.ts +14 -0
- package/dist/profiler/ui/sort.svelte.js +37 -0
- package/dist/profiler/ui/store.d.ts +45 -0
- package/dist/profiler/ui/store.js +92 -0
- package/dist/profiler/visit.d.ts +389 -0
- package/dist/profiler/visit.js +1002 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.js +4 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +38 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +78 -0
- package/package.json +103 -3
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<script>import { density, range_hot, stack_at } from "../stacks.js";
|
|
2
|
+
import { fmt_ms, CATEGORY_COLOR } from "./format.js";
|
|
3
|
+
let { stacks, marks = [] } = $props();
|
|
4
|
+
const W = 960;
|
|
5
|
+
const H = 72;
|
|
6
|
+
const BINS = 240;
|
|
7
|
+
const bins = $derived(density(stacks, BINS));
|
|
8
|
+
const comp_bins = $derived(density(stacks, BINS, "component"));
|
|
9
|
+
const bin_ms = $derived(stacks.window_ms / BINS);
|
|
10
|
+
const max_bin = $derived(Math.max(...bins, bin_ms, .001));
|
|
11
|
+
const x_of = (t) => t / (stacks.window_ms || 1) * W;
|
|
12
|
+
const t_of = (x) => Math.max(0, Math.min(stacks.window_ms, x / W * stacks.window_ms));
|
|
13
|
+
let sel = $state(null);
|
|
14
|
+
let cursor = $state(null);
|
|
15
|
+
let drag = $state(null);
|
|
16
|
+
let view = $state("self");
|
|
17
|
+
const range = $derived(sel ? range_hot(stacks, sel.t0, sel.t1) : range_hot(stacks, 0, stacks.window_ms));
|
|
18
|
+
const at = $derived(cursor === null ? null : stack_at(stacks, cursor));
|
|
19
|
+
const rows = $derived(view === "self" ? range.self : view === "total" ? range.total : range.components);
|
|
20
|
+
const row_max = $derived(Math.max(...rows.map((r) => view === "self" ? r.self_ms : r.total_ms), .001));
|
|
21
|
+
function svg_x(e, el) {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
return (e.clientX - r.left) / r.width * W;
|
|
24
|
+
}
|
|
25
|
+
function down(e) {
|
|
26
|
+
const el = e.currentTarget;
|
|
27
|
+
el.setPointerCapture(e.pointerId);
|
|
28
|
+
drag = {
|
|
29
|
+
from: t_of(svg_x(e, el)),
|
|
30
|
+
moved: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function move(e) {
|
|
34
|
+
const el = e.currentTarget;
|
|
35
|
+
const t = t_of(svg_x(e, el));
|
|
36
|
+
cursor = t;
|
|
37
|
+
if (drag) {
|
|
38
|
+
const a = Math.min(drag.from, t);
|
|
39
|
+
const b = Math.max(drag.from, t);
|
|
40
|
+
if (b - a > stacks.window_ms * .002) {
|
|
41
|
+
drag.moved = true;
|
|
42
|
+
sel = {
|
|
43
|
+
t0: a,
|
|
44
|
+
t1: b
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function up() {
|
|
50
|
+
if (drag && !drag.moved) sel = null;
|
|
51
|
+
drag = null;
|
|
52
|
+
}
|
|
53
|
+
function leave() {
|
|
54
|
+
cursor = null;
|
|
55
|
+
if (drag) up();
|
|
56
|
+
}
|
|
57
|
+
const category_of = (r) => CATEGORY_COLOR[r.category] ?? "#6b7280";
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div class="scrub">
|
|
61
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
62
|
+
<svg
|
|
63
|
+
viewBox="0 0 {W} {H + 18}"
|
|
64
|
+
class="strip"
|
|
65
|
+
onpointerdown={down}
|
|
66
|
+
onpointermove={move}
|
|
67
|
+
onpointerup={up}
|
|
68
|
+
onpointerleave={leave}
|
|
69
|
+
role="img"
|
|
70
|
+
aria-label="CPU density over the render; drag to select a range"
|
|
71
|
+
>
|
|
72
|
+
{#each bins as b, i (i)}
|
|
73
|
+
<rect
|
|
74
|
+
x={(i / BINS) * W}
|
|
75
|
+
y={H - (b / max_bin) * H}
|
|
76
|
+
width={W / BINS + 0.3}
|
|
77
|
+
height={(b / max_bin) * H}
|
|
78
|
+
fill="#6f8378"
|
|
79
|
+
opacity="0.5"
|
|
80
|
+
/>
|
|
81
|
+
<rect
|
|
82
|
+
x={(i / BINS) * W}
|
|
83
|
+
y={H - (comp_bins[i] / max_bin) * H}
|
|
84
|
+
width={W / BINS + 0.3}
|
|
85
|
+
height={(comp_bins[i] / max_bin) * H}
|
|
86
|
+
fill="#6fe3b0"
|
|
87
|
+
opacity="0.85"
|
|
88
|
+
/>
|
|
89
|
+
{/each}
|
|
90
|
+
{#each marks as m, k (k)}
|
|
91
|
+
<line x1={x_of(m.t)} x2={x_of(m.t)} y1="0" y2={H} stroke="#d29922" stroke-dasharray="3 3">
|
|
92
|
+
<title>{m.label} at {fmt_ms(m.t)} ms</title>
|
|
93
|
+
</line>
|
|
94
|
+
{#if k < 3}<text x={x_of(m.t) + 3} y={H + 13} font-size="10" fill="#d29922">{m.label}</text
|
|
95
|
+
>{/if}
|
|
96
|
+
{/each}
|
|
97
|
+
{#if sel}
|
|
98
|
+
<rect
|
|
99
|
+
x={x_of(sel.t0)}
|
|
100
|
+
y="0"
|
|
101
|
+
width={Math.max(1, x_of(sel.t1) - x_of(sel.t0))}
|
|
102
|
+
height={H}
|
|
103
|
+
fill="#6fe3b0"
|
|
104
|
+
opacity="0.16"
|
|
105
|
+
stroke="#6fe3b0"
|
|
106
|
+
/>
|
|
107
|
+
{/if}
|
|
108
|
+
{#if cursor !== null}
|
|
109
|
+
<line x1={x_of(cursor)} x2={x_of(cursor)} y1="0" y2={H} stroke="#e6eee9" stroke-width="1" />
|
|
110
|
+
{/if}
|
|
111
|
+
<text x="2" y={H + 13} font-size="10" fill="#6f8378">0</text>
|
|
112
|
+
<text x={W - 2} y={H + 13} font-size="10" fill="#6f8378" text-anchor="end"
|
|
113
|
+
>{fmt_ms(stacks.window_ms)} ms</text
|
|
114
|
+
>
|
|
115
|
+
</svg>
|
|
116
|
+
<div class="scrub-bar">
|
|
117
|
+
<span class="hint">
|
|
118
|
+
{#if sel}
|
|
119
|
+
<b>{fmt_ms(sel.t0)}–{fmt_ms(sel.t1)} ms</b> · {fmt_ms(range.cpu_ms)} ms CPU, {fmt_ms(
|
|
120
|
+
range.idle_ms
|
|
121
|
+
)} ms idle
|
|
122
|
+
<button class="lnk" onclick={() => (sel = null)}>whole render</button>
|
|
123
|
+
{:else}
|
|
124
|
+
whole render · {fmt_ms(range.cpu_ms)} ms CPU · drag on the strip to slice it
|
|
125
|
+
{/if}
|
|
126
|
+
· grey is any CPU, green is a component's
|
|
127
|
+
</span>
|
|
128
|
+
<span class="views">
|
|
129
|
+
<button class:on={view === 'self'} onclick={() => (view = 'self')}>self</button>
|
|
130
|
+
<button class:on={view === 'total'} onclick={() => (view = 'total')}>owners</button>
|
|
131
|
+
<button class:on={view === 'components'} onclick={() => (view = 'components')}
|
|
132
|
+
>components</button
|
|
133
|
+
>
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="cols">
|
|
137
|
+
<table>
|
|
138
|
+
<thead
|
|
139
|
+
><tr
|
|
140
|
+
><th
|
|
141
|
+
>{view === 'total'
|
|
142
|
+
? 'on the stack'
|
|
143
|
+
: view === 'components'
|
|
144
|
+
? 'component'
|
|
145
|
+
: 'running'}</th
|
|
146
|
+
><th class="num">{view === 'self' ? 'self ms' : 'total ms'}</th><th class="num"
|
|
147
|
+
>of range</th
|
|
148
|
+
></tr
|
|
149
|
+
></thead
|
|
150
|
+
>
|
|
151
|
+
<tbody>
|
|
152
|
+
{#each rows.slice(0, 16) as r, k (k)}
|
|
153
|
+
<tr>
|
|
154
|
+
<td class="fn"
|
|
155
|
+
><i class="dot" style="background:{category_of(r)}"></i>{r.name}{#if r.file}<span
|
|
156
|
+
class="hint"
|
|
157
|
+
>
|
|
158
|
+
{r.file}</span
|
|
159
|
+
>{/if}</td
|
|
160
|
+
>
|
|
161
|
+
<td class="num">{fmt_ms(view === 'self' ? r.self_ms : r.total_ms)}</td>
|
|
162
|
+
<td class="num"
|
|
163
|
+
><div class="mini">
|
|
164
|
+
<div
|
|
165
|
+
style="width:{(
|
|
166
|
+
((view === 'self' ? r.self_ms : r.total_ms) / row_max) *
|
|
167
|
+
100
|
|
168
|
+
).toFixed(0)}%;background:{category_of(r)}"
|
|
169
|
+
></div>
|
|
170
|
+
</div>
|
|
171
|
+
{range.cpu_ms > 0
|
|
172
|
+
? (((view === 'self' ? r.self_ms : r.total_ms) / range.cpu_ms) * 100).toFixed(0)
|
|
173
|
+
: 0}%</td
|
|
174
|
+
>
|
|
175
|
+
</tr>
|
|
176
|
+
{:else}
|
|
177
|
+
<tr><td colspan="3" class="hint">no CPU sample in this range</td></tr>
|
|
178
|
+
{/each}
|
|
179
|
+
</tbody>
|
|
180
|
+
</table>
|
|
181
|
+
<div class="stack">
|
|
182
|
+
<div class="hint">
|
|
183
|
+
{#if at}
|
|
184
|
+
stack at <b>{fmt_ms(at.t)} ms</b> ({fmt_ms(at.ms)} ms sample), outermost first
|
|
185
|
+
{:else}
|
|
186
|
+
move over the strip for the stack at that instant
|
|
187
|
+
{/if}
|
|
188
|
+
</div>
|
|
189
|
+
{#if at}
|
|
190
|
+
<ol>
|
|
191
|
+
{#each at.frames as f, i (i)}
|
|
192
|
+
<li>
|
|
193
|
+
<i class="dot" style="background:{category_of(f)}"></i>{f.name}{#if f.file}<span
|
|
194
|
+
class="hint"
|
|
195
|
+
>
|
|
196
|
+
{f.file}</span
|
|
197
|
+
>{/if}
|
|
198
|
+
</li>
|
|
199
|
+
{:else}
|
|
200
|
+
<li class="hint">idle</li>
|
|
201
|
+
{/each}
|
|
202
|
+
</ol>
|
|
203
|
+
{/if}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<style>
|
|
209
|
+
.scrub {
|
|
210
|
+
margin: 6px 0 10px;
|
|
211
|
+
}
|
|
212
|
+
.strip {
|
|
213
|
+
width: 100%;
|
|
214
|
+
height: auto;
|
|
215
|
+
display: block;
|
|
216
|
+
background: var(--bg-sunken);
|
|
217
|
+
border: 1px solid var(--line);
|
|
218
|
+
border-radius: var(--r-sm);
|
|
219
|
+
cursor: crosshair;
|
|
220
|
+
touch-action: none;
|
|
221
|
+
user-select: none;
|
|
222
|
+
}
|
|
223
|
+
.scrub-bar {
|
|
224
|
+
display: flex;
|
|
225
|
+
justify-content: space-between;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 12px;
|
|
228
|
+
margin: 6px 0;
|
|
229
|
+
font-size: 12px;
|
|
230
|
+
flex-wrap: wrap;
|
|
231
|
+
}
|
|
232
|
+
.views button,
|
|
233
|
+
.lnk {
|
|
234
|
+
background: none;
|
|
235
|
+
border: 1px solid var(--line-strong);
|
|
236
|
+
color: inherit;
|
|
237
|
+
font: inherit;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
padding: 1px 8px;
|
|
240
|
+
border-radius: var(--r-sm);
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
margin-left: 4px;
|
|
243
|
+
}
|
|
244
|
+
.views button.on {
|
|
245
|
+
background: var(--accent-deep);
|
|
246
|
+
border-color: var(--accent-line);
|
|
247
|
+
color: var(--accent-strong);
|
|
248
|
+
}
|
|
249
|
+
.cols {
|
|
250
|
+
display: grid;
|
|
251
|
+
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
|
252
|
+
gap: 16px;
|
|
253
|
+
align-items: start;
|
|
254
|
+
}
|
|
255
|
+
@media (max-width: 800px) {
|
|
256
|
+
.cols {
|
|
257
|
+
grid-template-columns: 1fr;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
.dot {
|
|
261
|
+
display: inline-block;
|
|
262
|
+
width: 8px;
|
|
263
|
+
height: 8px;
|
|
264
|
+
border-radius: 2px;
|
|
265
|
+
margin-right: 6px;
|
|
266
|
+
vertical-align: middle;
|
|
267
|
+
}
|
|
268
|
+
.mini {
|
|
269
|
+
display: inline-block;
|
|
270
|
+
width: 48px;
|
|
271
|
+
height: 6px;
|
|
272
|
+
background: var(--bg-sunken);
|
|
273
|
+
border-radius: 2px;
|
|
274
|
+
margin-right: 6px;
|
|
275
|
+
vertical-align: middle;
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
}
|
|
278
|
+
.mini div {
|
|
279
|
+
height: 100%;
|
|
280
|
+
}
|
|
281
|
+
.stack ol {
|
|
282
|
+
margin: 6px 0 0;
|
|
283
|
+
padding-left: 22px;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
line-height: 1.5;
|
|
286
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
287
|
+
}
|
|
288
|
+
.stack li {
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
text-overflow: ellipsis;
|
|
292
|
+
}
|
|
293
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SCRUBBER — the render's CPU as a density strip over the stack index (analyze.ts
|
|
3
|
+
* `StackIndex`, queried by stacks.ts). Drag across it to ask "what ran between here and here":
|
|
4
|
+
* the self and inclusive tables answer for that range only. Move the cursor to ask "what was
|
|
5
|
+
* the stack at this instant". Every other table in the report is an aggregate over the whole
|
|
6
|
+
* window; this is the same data, sliced where the eye lands. A `wake:'load'` island.
|
|
7
|
+
*/
|
|
8
|
+
import type { StackIndex } from '../analyze.js';
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
stacks: StackIndex;
|
|
11
|
+
marks?: {
|
|
12
|
+
t: number;
|
|
13
|
+
label: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
declare const Scrub: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type Scrub = ReturnType<typeof Scrub>;
|
|
18
|
+
export default Scrub;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script>import { fmt_bytes } from "./format.js";
|
|
2
|
+
import { row_id, follow_hash } from "./row-anchor.svelte.js";
|
|
3
|
+
import { press_key } from "./press.js";
|
|
4
|
+
let { rows, whole_by, total, seed_bytes } = $props();
|
|
5
|
+
let picked = $state(null);
|
|
6
|
+
const shipped = $derived(rows.filter((r) => r.shipped));
|
|
7
|
+
const dropped = $derived(rows.filter((r) => !r.shipped));
|
|
8
|
+
const REASON = {
|
|
9
|
+
read: "read by an island",
|
|
10
|
+
referenced: "props point into it",
|
|
11
|
+
whole: "an island reads page.data whole"
|
|
12
|
+
};
|
|
13
|
+
$effect(() => follow_hash("seed", () => rows.map((r) => r.key), (k) => picked = k));
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
{#if whole_by.length}
|
|
17
|
+
<p class="whole">
|
|
18
|
+
<b>{whole_by.join(', ')}</b> read{whole_by.length === 1 ? 's' : ''} <code>page.data</code> whole,
|
|
19
|
+
so every key ships. Read the keys by name and seed shaping drops the rest.
|
|
20
|
+
</p>
|
|
21
|
+
{/if}
|
|
22
|
+
<div class="rows">
|
|
23
|
+
{#each rows as r (r.key)}
|
|
24
|
+
<div
|
|
25
|
+
class="row"
|
|
26
|
+
id={row_id('seed', r.key)}
|
|
27
|
+
class:picked={picked === r.key}
|
|
28
|
+
class:dropped={!r.shipped}
|
|
29
|
+
onclick={() => (picked = picked === r.key ? null : r.key)}
|
|
30
|
+
onkeydown={press_key}
|
|
31
|
+
role="button"
|
|
32
|
+
tabindex="0"
|
|
33
|
+
aria-expanded={picked === r.key}
|
|
34
|
+
>
|
|
35
|
+
<span class="key"><code>{r.key}</code></span>
|
|
36
|
+
<div class="track">
|
|
37
|
+
<div
|
|
38
|
+
class="fill"
|
|
39
|
+
class:ref={r.reason === 'referenced'}
|
|
40
|
+
class:whole={r.reason === 'whole'}
|
|
41
|
+
style="width:{Math.max(0.5, r.pct)}%"
|
|
42
|
+
></div>
|
|
43
|
+
</div>
|
|
44
|
+
<span class="num">{fmt_bytes(r.bytes)}</span>
|
|
45
|
+
<span class="why">
|
|
46
|
+
{#if !r.shipped}<span class="chip off">dropped</span>
|
|
47
|
+
{:else}<span
|
|
48
|
+
class="chip"
|
|
49
|
+
class:ref={r.reason === 'referenced'}
|
|
50
|
+
class:whole={r.reason === 'whole'}>{REASON[r.reason ?? ''] ?? 'ships'}</span
|
|
51
|
+
>{/if}
|
|
52
|
+
</span>
|
|
53
|
+
{#if picked === r.key}
|
|
54
|
+
<div class="detail">
|
|
55
|
+
{#if r.readers_names.length}<div>
|
|
56
|
+
<span class="k">read by</span>
|
|
57
|
+
{r.readers_names.join(', ')}
|
|
58
|
+
</div>{/if}
|
|
59
|
+
{#if r.referenced_names.length}<div>
|
|
60
|
+
<span class="k">referenced by the props of</span>
|
|
61
|
+
{r.referenced_names.join(', ')}
|
|
62
|
+
</div>{/if}
|
|
63
|
+
{#if !r.readers_names.length && !r.referenced_names.length}
|
|
64
|
+
<div class="hint">
|
|
65
|
+
{r.shipped
|
|
66
|
+
? 'Nothing asks for this key by name: it ships because an island reads page.data whole.'
|
|
67
|
+
: 'No island reads or references it, so seed shaping left it out of the seed.'}
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
{#if r.shipped && r.reason === 'read'}
|
|
71
|
+
<div class="hint">
|
|
72
|
+
If the island needs only a part, return that part from load under its own key, or pass
|
|
73
|
+
it as a prop.
|
|
74
|
+
</div>
|
|
75
|
+
{:else if r.shipped && r.reason === 'referenced'}
|
|
76
|
+
<div class="hint">
|
|
77
|
+
The cheap shape: the key ships once and the island's props point into it.
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
<p class="hint">
|
|
86
|
+
{shipped.length} of {rows.length} keys ship: {fmt_bytes(seed_bytes)} on the wire{#if dropped.length},
|
|
87
|
+
{fmt_bytes(dropped.reduce((s, r) => s + r.bytes, 0))} of {fmt_bytes(total)} left out by seed shaping{/if}.
|
|
88
|
+
Click a key for who asks for it.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.whole {
|
|
93
|
+
margin: 4px 0 8px;
|
|
94
|
+
padding: 6px 10px;
|
|
95
|
+
background: var(--bg-raised);
|
|
96
|
+
border-left: 3px solid var(--c-orange);
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
font-size: 12.5px;
|
|
99
|
+
}
|
|
100
|
+
.rows {
|
|
101
|
+
display: grid;
|
|
102
|
+
gap: 3px;
|
|
103
|
+
}
|
|
104
|
+
.row {
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: 180px 1fr 80px 200px;
|
|
107
|
+
gap: 10px;
|
|
108
|
+
align-items: center;
|
|
109
|
+
font-size: 12.5px;
|
|
110
|
+
padding: 3px 6px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
.row:hover,
|
|
115
|
+
.row.picked {
|
|
116
|
+
background: var(--bg-raised);
|
|
117
|
+
}
|
|
118
|
+
.row.dropped {
|
|
119
|
+
opacity: 0.6;
|
|
120
|
+
}
|
|
121
|
+
.key code {
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
}
|
|
124
|
+
.track {
|
|
125
|
+
height: 10px;
|
|
126
|
+
background: var(--bg-hover);
|
|
127
|
+
border-radius: 3px;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
}
|
|
130
|
+
.fill {
|
|
131
|
+
height: 100%;
|
|
132
|
+
background: var(--c-orange);
|
|
133
|
+
}
|
|
134
|
+
.fill.ref {
|
|
135
|
+
background: var(--c-blue);
|
|
136
|
+
}
|
|
137
|
+
.fill.whole {
|
|
138
|
+
background: var(--warn);
|
|
139
|
+
}
|
|
140
|
+
.dropped .fill {
|
|
141
|
+
background: #3a424e;
|
|
142
|
+
}
|
|
143
|
+
.num {
|
|
144
|
+
text-align: right;
|
|
145
|
+
font-variant-numeric: tabular-nums;
|
|
146
|
+
color: var(--text-dim);
|
|
147
|
+
}
|
|
148
|
+
.chip {
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
border-radius: 999px;
|
|
151
|
+
padding: 0 8px;
|
|
152
|
+
line-height: 16px;
|
|
153
|
+
border: 1px solid #5a3a2a;
|
|
154
|
+
color: var(--c-orange);
|
|
155
|
+
}
|
|
156
|
+
.chip.ref {
|
|
157
|
+
border-color: #2a3a5a;
|
|
158
|
+
color: var(--c-blue);
|
|
159
|
+
}
|
|
160
|
+
.chip.whole {
|
|
161
|
+
border-color: #5a4a20;
|
|
162
|
+
color: var(--warn);
|
|
163
|
+
}
|
|
164
|
+
.chip.off {
|
|
165
|
+
border-color: var(--line);
|
|
166
|
+
color: var(--text-faint);
|
|
167
|
+
}
|
|
168
|
+
.detail {
|
|
169
|
+
grid-column: 1 / -1;
|
|
170
|
+
padding: 4px 0 4px 8px;
|
|
171
|
+
border-left: 2px solid var(--line);
|
|
172
|
+
display: grid;
|
|
173
|
+
gap: 2px;
|
|
174
|
+
}
|
|
175
|
+
.k {
|
|
176
|
+
color: var(--text-faint);
|
|
177
|
+
font-size: 11px;
|
|
178
|
+
text-transform: uppercase;
|
|
179
|
+
letter-spacing: 0.04em;
|
|
180
|
+
}
|
|
181
|
+
.hint {
|
|
182
|
+
color: var(--text-faint);
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
}
|
|
185
|
+
@media (max-width: 800px) {
|
|
186
|
+
.row {
|
|
187
|
+
grid-template-columns: 120px 1fr 70px;
|
|
188
|
+
}
|
|
189
|
+
.why {
|
|
190
|
+
grid-column: 1 / -1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SEED EXPLAINER — every top-level `page.data` key, sized, and WHY it ships: read by an
|
|
3
|
+
* island's client code (the build's answer), referenced by an island's props (a seed reference),
|
|
4
|
+
* or because some island reads page.data whole. A dropped key shows too, so the shaping is
|
|
5
|
+
* visible. A finding links here by `#seed=<key>`.
|
|
6
|
+
*/
|
|
7
|
+
import type { SeedRow } from './report-data.js';
|
|
8
|
+
type $$ComponentProps = {
|
|
9
|
+
rows: SeedRow[];
|
|
10
|
+
whole_by: string[];
|
|
11
|
+
total: number;
|
|
12
|
+
seed_bytes: number;
|
|
13
|
+
};
|
|
14
|
+
declare const SeedExplainer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type SeedExplainer = ReturnType<typeof SeedExplainer>;
|
|
16
|
+
export default SeedExplainer;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script>/**
|
|
2
|
+
* "Share link" on your own report (`wake:'load'`). Fetches the report's plaintext dump (only when you
|
|
3
|
+
* click — the authed `.dump` endpoint), encrypts it with a password YOU set (see permalink.ts), and
|
|
4
|
+
* hands back a `…/report/<id>#<blob>` URL. The report lives in the `#fragment` — zero server storage,
|
|
5
|
+
* decrypted in the recipient's browser (PermalinkGate). Browser-gated: WebKit (Safari + iOS) caps
|
|
6
|
+
* URLs too low, so links are disabled there; Chromium/Firefox go up to ~1.5 MB, with a Safari-
|
|
7
|
+
* recipient disclaimer past the universal-safe size. No `$effect`; all state moves on click/submit.
|
|
8
|
+
*/
|
|
9
|
+
import { encode_permalink, is_webkit_capped, MAX_PERMALINK_CHARS } from "./permalink.js";
|
|
10
|
+
import { get_report } from "./store.js";
|
|
11
|
+
import { report_request } from "./report-request.js";
|
|
12
|
+
let { id, base, ogpB64 = "" } = $props();
|
|
13
|
+
// WebKit check needs `navigator` — false during SSR, corrected on hydrate.
|
|
14
|
+
const capped = typeof navigator !== "undefined" && is_webkit_capped();
|
|
15
|
+
// Under Safari's ~80 K hard cap (with margin) a link opens EVERYWHERE; above it, Chromium/Firefox only.
|
|
16
|
+
const SAFE_UNIVERSAL = 56e3;
|
|
17
|
+
let open = $state(false);
|
|
18
|
+
let password = $state("");
|
|
19
|
+
let busy = $state(false);
|
|
20
|
+
let url = $state("");
|
|
21
|
+
let note = $state("");
|
|
22
|
+
let err = $state("");
|
|
23
|
+
let copied = $state(false);
|
|
24
|
+
async function generate(e) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
if (busy || !password) return;
|
|
27
|
+
busy = true;
|
|
28
|
+
err = "";
|
|
29
|
+
url = "";
|
|
30
|
+
note = "";
|
|
31
|
+
try {
|
|
32
|
+
// this browser's kept copy first (no request at all), else ask with the report carried along:
|
|
33
|
+
// on a serverless host a plain fetch by id lands on an instance that never had it
|
|
34
|
+
const kept = await get_report(id);
|
|
35
|
+
let dump = kept?.dump;
|
|
36
|
+
if (!dump) {
|
|
37
|
+
const res = await report_request(base, id, "dump", ogpB64);
|
|
38
|
+
if (!res.ok) throw new Error("fetch");
|
|
39
|
+
dump = await res.json();
|
|
40
|
+
}
|
|
41
|
+
const blob = await encode_permalink(dump, password);
|
|
42
|
+
if (blob.length > MAX_PERMALINK_CHARS) {
|
|
43
|
+
err = `This report is ~${Math.round(blob.length / 1024)} KB encoded — too big for a link. Download the .ogp and share the file instead.`;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
url = `${location.origin}${base}/report/${id}#${blob}`;
|
|
47
|
+
if (blob.length > SAFE_UNIVERSAL) note = "Large link — opens in Chrome & Firefox; Safari recipients need the .ogp file.";
|
|
48
|
+
} catch {
|
|
49
|
+
err = "Could not build the link.";
|
|
50
|
+
} finally {
|
|
51
|
+
busy = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function copy() {
|
|
55
|
+
try {
|
|
56
|
+
await navigator.clipboard.writeText(url);
|
|
57
|
+
copied = true;
|
|
58
|
+
setTimeout(() => copied = false, 1500);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{#if capped}
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
class="btn"
|
|
67
|
+
disabled
|
|
68
|
+
title="Universal link sharing is off in Safari — share the .ogp file, or open in Chrome / Firefox"
|
|
69
|
+
>
|
|
70
|
+
Share link<span class="sub">Safari: use .ogp</span>
|
|
71
|
+
</button>
|
|
72
|
+
{:else}
|
|
73
|
+
<button type="button" class="btn" onclick={() => (open = !open)}>
|
|
74
|
+
Share link<span class="sub">URL + password</span>
|
|
75
|
+
</button>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
{#if open && !capped}
|
|
79
|
+
<div class="share-panel">
|
|
80
|
+
<p class="hint">
|
|
81
|
+
Anyone with this link <b>and</b> the password can read the report. It's encrypted into the link
|
|
82
|
+
— nothing is stored on any server.
|
|
83
|
+
</p>
|
|
84
|
+
<form class="share-form" onsubmit={generate}>
|
|
85
|
+
<input
|
|
86
|
+
type="password"
|
|
87
|
+
bind:value={password}
|
|
88
|
+
placeholder="set a password"
|
|
89
|
+
autocomplete="new-password"
|
|
90
|
+
disabled={busy}
|
|
91
|
+
/>
|
|
92
|
+
<button type="submit" disabled={busy || !password}>{busy ? 'Encrypting…' : 'Generate'}</button
|
|
93
|
+
>
|
|
94
|
+
</form>
|
|
95
|
+
{#if err}<p class="share-err">{err}</p>{/if}
|
|
96
|
+
{#if url}
|
|
97
|
+
<div class="share-out">
|
|
98
|
+
<input type="text" readonly value={url} onclick={(e) => e.currentTarget.select()} />
|
|
99
|
+
<button type="button" onclick={copy}>{copied ? 'copied ✓' : 'copy'}</button>
|
|
100
|
+
</div>
|
|
101
|
+
{#if note}<p class="share-note">{note}</p>{/if}
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
105
|
+
|
|
106
|
+
<style>
|
|
107
|
+
.share-panel {
|
|
108
|
+
margin: 4px 0 14px;
|
|
109
|
+
padding: 12px 14px;
|
|
110
|
+
border: 1px solid var(--line);
|
|
111
|
+
border-radius: 10px;
|
|
112
|
+
background: var(--bg-sunken);
|
|
113
|
+
max-width: 640px;
|
|
114
|
+
}
|
|
115
|
+
.share-out {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 8px;
|
|
118
|
+
margin: 10px 0 0;
|
|
119
|
+
}
|
|
120
|
+
.share-out input {
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
padding: 8px 10px;
|
|
124
|
+
border-radius: 8px;
|
|
125
|
+
border: 1px solid var(--line);
|
|
126
|
+
background: var(--bg-sunken);
|
|
127
|
+
color: #9fb0c4;
|
|
128
|
+
font:
|
|
129
|
+
12px ui-monospace,
|
|
130
|
+
monospace;
|
|
131
|
+
}
|
|
132
|
+
.share-out button {
|
|
133
|
+
padding: 8px 12px;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
border: 1px solid var(--line);
|
|
136
|
+
background: var(--bg-hover);
|
|
137
|
+
color: var(--text);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
font: inherit;
|
|
140
|
+
font-size: 12.5px;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
.share-note {
|
|
144
|
+
color: var(--warn);
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
margin: 8px 0 0;
|
|
147
|
+
}
|
|
148
|
+
</style>
|