paseo-plugin-genui 0.0.0-stage → 0.1.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 +202 -0
- package/OVERVIEW.md +21 -0
- package/README.md +77 -2
- package/assets/fonts/Excalifont-OFL.txt +109 -0
- package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +1 -0
- package/assets/screenshot-chat.png +0 -0
- package/assets/screenshot-gallery.png +0 -0
- package/assets/screenshot-playground.png +0 -0
- package/client/components/content.tsx +468 -0
- package/client/components/data.tsx +967 -0
- package/client/components/dev.tsx +998 -0
- package/client/components/diagram.tsx +513 -0
- package/client/components/inputs.tsx +994 -0
- package/client/components/layout.tsx +239 -0
- package/client/components/media.tsx +428 -0
- package/client/components/project.tsx +641 -0
- package/client/components/widgets.tsx +836 -0
- package/client/gallery.tsx +449 -0
- package/client/live-ui.tsx +525 -0
- package/client/loaders.ts +342 -0
- package/client/markdown.tsx +172 -0
- package/client/node-renderer.tsx +149 -0
- package/client/render.tsx +186 -0
- package/client/runtime.ts +103 -0
- package/client/settings.tsx +57 -0
- package/client/web.ts +186 -0
- package/gallery/Acceptance-baseline.ui +1 -0
- package/gallery/Acceptance-checklist.ui +14 -0
- package/gallery/Acceptance-evidence.ui +9 -0
- package/gallery/Acceptance.ui +1 -0
- package/gallery/Badge.ui +3 -0
- package/gallery/Benchmark-context.ui +8 -0
- package/gallery/Benchmark-hyperfine.ui +1 -0
- package/gallery/Benchmark-runs-list.ui +9 -0
- package/gallery/Benchmark-runs.ui +1 -0
- package/gallery/Benchmark-vitest.ui +1 -0
- package/gallery/Benchmark.ui +9 -0
- package/gallery/Board-bb-tasks.ui +1 -0
- package/gallery/Board-gh-issues.ui +1 -0
- package/gallery/Board-gh-prs.ui +1 -0
- package/gallery/Board.ui +12 -0
- package/gallery/Button-destructive.ui +4 -0
- package/gallery/Button.ui +5 -0
- package/gallery/Calendar.ui +15 -0
- package/gallery/CalendarHeatmap-short.ui +11 -0
- package/gallery/CalendarHeatmap.ui +4 -0
- package/gallery/Callout.ui +4 -0
- package/gallery/Card.ui +4 -0
- package/gallery/Carousel.ui +8 -0
- package/gallery/Changes-paths.ui +1 -0
- package/gallery/Changes-uncommitted.ui +1 -0
- package/gallery/Changes.ui +1 -0
- package/gallery/Chart-area.ui +3 -0
- package/gallery/Chart-funnel.ui +3 -0
- package/gallery/Chart-heatmap.ui +11 -0
- package/gallery/Chart-pie.ui +3 -0
- package/gallery/Chart-radar.ui +3 -0
- package/gallery/Chart-scatter.ui +4 -0
- package/gallery/Chart-stacked.ui +5 -0
- package/gallery/Chart.ui +3 -0
- package/gallery/Checklist.ui +6 -0
- package/gallery/Choice.ui +8 -0
- package/gallery/Code.ui +3 -0
- package/gallery/Commits-limit.ui +1 -0
- package/gallery/Commits.ui +1 -0
- package/gallery/Compare-conditions.ui +4 -0
- package/gallery/Compare-three.ui +7 -0
- package/gallery/Compare.ui +4 -0
- package/gallery/Coverage-go.ui +1 -0
- package/gallery/Coverage-lcov.ui +1 -0
- package/gallery/Coverage-python.ui +1 -0
- package/gallery/Coverage.ui +1 -0
- package/gallery/DateInput.ui +5 -0
- package/gallery/Decision-percent.ui +10 -0
- package/gallery/Decision.ui +12 -0
- package/gallery/Dependencies-audit.ui +1 -0
- package/gallery/Dependencies-both.ui +1 -0
- package/gallery/Dependencies-list.ui +7 -0
- package/gallery/Dependencies-pip.ui +1 -0
- package/gallery/Dependencies-pnpm.ui +1 -0
- package/gallery/Dependencies.ui +1 -0
- package/gallery/Details.ui +4 -0
- package/gallery/Diagram-flow.ui +16 -0
- package/gallery/Diagram-sequence.ui +15 -0
- package/gallery/Diagram.ui +16 -0
- package/gallery/Diff.ui +3 -0
- package/gallery/Divider.ui +5 -0
- package/gallery/EditableTable.ui +9 -0
- package/gallery/Entity-list.ui +6 -0
- package/gallery/Entity.ui +8 -0
- package/gallery/Environments.ui +1 -0
- package/gallery/Estimate-points.ui +6 -0
- package/gallery/Estimate.ui +8 -0
- package/gallery/File.ui +4 -0
- package/gallery/Findings-biome.ui +1 -0
- package/gallery/Findings-eslint.ui +1 -0
- package/gallery/Findings-oxlint.ui +1 -0
- package/gallery/Findings-review.ui +1 -0
- package/gallery/Findings-ruff.ui +1 -0
- package/gallery/Findings-sarif.ui +1 -0
- package/gallery/Findings-ship.ui +11 -0
- package/gallery/Findings-tsc.ui +1 -0
- package/gallery/Findings.ui +7 -0
- package/gallery/Flashcards.ui +3 -0
- package/gallery/FollowUps.ui +4 -0
- package/gallery/Form.ui +8 -0
- package/gallery/Gallery-links.ui +8 -0
- package/gallery/Gallery-strip.ui +14 -0
- package/gallery/Gallery.ui +10 -0
- package/gallery/Gauge.ui +7 -0
- package/gallery/Grid.ui +8 -0
- package/gallery/Hotspots-photo.ui +8 -0
- package/gallery/Hotspots.ui +11 -0
- package/gallery/Icon.ui +5 -0
- package/gallery/Image.ui +4 -0
- package/gallery/ImageSearch-aspects.ui +6 -0
- package/gallery/ImageSearch.ui +4 -0
- package/gallery/Input.ui +5 -0
- package/gallery/Json-api.ui +1 -0
- package/gallery/Json.ui +8 -0
- package/gallery/KeyValue.ui +3 -0
- package/gallery/List-dots.ui +8 -0
- package/gallery/List.ui +8 -0
- package/gallery/ListEditor.ui +5 -0
- package/gallery/Logs-jsonl.ui +1 -0
- package/gallery/Logs-lines.ui +11 -0
- package/gallery/Logs-python.ui +1 -0
- package/gallery/Logs.ui +1 -0
- package/gallery/Map-city.ui +8 -0
- package/gallery/Map.ui +8 -0
- package/gallery/Metric.ui +3 -0
- package/gallery/OptionCards-multi.ui +10 -0
- package/gallery/OptionCards.ui +10 -0
- package/gallery/Pipeline-checks.ui +1 -0
- package/gallery/Pipeline-passed.ui +1 -0
- package/gallery/Pipeline-running.ui +1 -0
- package/gallery/Pipeline-stages.ui +13 -0
- package/gallery/Pipeline.ui +1 -0
- package/gallery/Plan.ui +9 -0
- package/gallery/Progress.ui +4 -0
- package/gallery/Provenance-deploy.ui +6 -0
- package/gallery/Provenance-no-receipt.ui +1 -0
- package/gallery/Provenance.ui +1 -0
- package/gallery/PullRequest.ui +1 -0
- package/gallery/Quiz.ui +6 -0
- package/gallery/Radio.ui +5 -0
- package/gallery/Rating.ui +5 -0
- package/gallery/Roadmap-sprint.ui +8 -0
- package/gallery/Roadmap-year.ui +9 -0
- package/gallery/Roadmap.ui +12 -0
- package/gallery/Row.ui +3 -0
- package/gallery/SegmentedBar-status.ui +8 -0
- package/gallery/SegmentedBar.ui +8 -0
- package/gallery/Select.ui +5 -0
- package/gallery/Slider.ui +7 -0
- package/gallery/Snippet.ui +3 -0
- package/gallery/Sources-chips.ui +4 -0
- package/gallery/Sources.ui +8 -0
- package/gallery/Sparkline.ui +6 -0
- package/gallery/Stack.ui +7 -0
- package/gallery/StackTrace-aggregate.ui +1 -0
- package/gallery/StackTrace-browser.ui +1 -0
- package/gallery/StackTrace-go.ui +1 -0
- package/gallery/StackTrace-java.ui +1 -0
- package/gallery/StackTrace-python.ui +1 -0
- package/gallery/StackTrace-rust.ui +1 -0
- package/gallery/StackTrace.ui +1 -0
- package/gallery/Status.ui +9 -0
- package/gallery/Stepper.ui +5 -0
- package/gallery/Steps.ui +8 -0
- package/gallery/Swatches-long-names.ui +4 -0
- package/gallery/Swatches-picker.ui +5 -0
- package/gallery/Swatches.ui +10 -0
- package/gallery/Table.ui +7 -0
- package/gallery/Tabs.ui +8 -0
- package/gallery/Tests-junit.ui +1 -0
- package/gallery/Tests-vitest-junit.ui +1 -0
- package/gallery/Tests-vitest.ui +1 -0
- package/gallery/Tests.ui +9 -0
- package/gallery/Text.ui +4 -0
- package/gallery/TextArea.ui +5 -0
- package/gallery/Threads-ids.ui +1 -0
- package/gallery/Threads.ui +1 -0
- package/gallery/Timeline.ui +8 -0
- package/gallery/Timer.ui +4 -0
- package/gallery/Toggle.ui +5 -0
- package/gallery/Transcript-text.ui +1 -0
- package/gallery/Transcript-untimed.ui +1 -0
- package/gallery/Transcript-whisper.ui +1 -0
- package/gallery/Transcript.ui +1 -0
- package/gallery/Tree.ui +3 -0
- package/gallery/Treemap-du.ui +1 -0
- package/gallery/Treemap-esbuild.ui +1 -0
- package/gallery/Treemap-tree.ui +7 -0
- package/gallery/Treemap.ui +18 -0
- package/gallery/Walkthrough.ui +9 -0
- package/gallery/Waterfall-har.ui +1 -0
- package/gallery/Waterfall-trace.ui +1 -0
- package/gallery/Waterfall.ui +11 -0
- package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
- package/gallery/data/acceptance/coil-gate.json +153 -0
- package/gallery/data/acceptance/orbit-baseline.json +163 -0
- package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
- package/gallery/data/acceptance/shots/gate-light.png +0 -0
- package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
- package/gallery/data/activity.json +1 -0
- package/gallery/data/bb-tasks-orb.json +420 -0
- package/gallery/data/biome.json +229 -0
- package/gallery/data/compare/status-after.png +0 -0
- package/gallery/data/compare/status-before.png +0 -0
- package/gallery/data/compare/status-dark-2x.png +0 -0
- package/gallery/data/compare/status-rerender.png +0 -0
- package/gallery/data/coverage-py.json +1 -0
- package/gallery/data/coverage-summary.json +1 -0
- package/gallery/data/coverage.out +23 -0
- package/gallery/data/deploys.json +1 -0
- package/gallery/data/device-runs.json +285 -0
- package/gallery/data/du-node-modules.txt +359 -0
- package/gallery/data/esbuild-meta.json +292 -0
- package/gallery/data/eslint.json +266 -0
- package/gallery/data/eslint.sarif +343 -0
- package/gallery/data/findings-review.json +76 -0
- package/gallery/data/gh-issues-nextjs.json +293 -0
- package/gallery/data/gh-pr-checks.json +272 -0
- package/gallery/data/gh-prs-nextjs.json +419 -0
- package/gallery/data/gh-run-failed.json +1 -0
- package/gallery/data/gh-run-passed.json +1 -0
- package/gallery/data/gh-run-running.json +1 -0
- package/gallery/data/github-release.json +57 -0
- package/gallery/data/hyperfine-startup.json +187 -0
- package/gallery/data/lcov.info +234 -0
- package/gallery/data/logs/media-sync.log +127 -0
- package/gallery/data/logs/thumbnailer.jsonl +17 -0
- package/gallery/data/logs/vitest-run.log +89 -0
- package/gallery/data/npm-audit.json +672 -0
- package/gallery/data/npm-outdated.json +179 -0
- package/gallery/data/oxlint.json +156 -0
- package/gallery/data/pip-outdated.json +68 -0
- package/gallery/data/pnpm-outdated.json +58 -0
- package/gallery/data/provenance/devicectl-install.json +35 -0
- package/gallery/data/provenance/install-timeout.log +4 -0
- package/gallery/data/provenance/iphone-install.json +72 -0
- package/gallery/data/provenance/iphone-no-receipt.json +57 -0
- package/gallery/data/provenance/live-home.jpg +0 -0
- package/gallery/data/provenance/live-probe.txt +10 -0
- package/gallery/data/provenance/xcodebuild-2811.log +19 -0
- package/gallery/data/provenance/xcodebuild-4712.log +19 -0
- package/gallery/data/pytest-junit.xml +64 -0
- package/gallery/data/python-org-trace.json +35 -0
- package/gallery/data/python-org.har +31 -0
- package/gallery/data/retry.ts +13 -0
- package/gallery/data/ruff.json +261 -0
- package/gallery/data/src/durations.py +63 -0
- package/gallery/data/src/json-logic.ts +156 -0
- package/gallery/data/src/mapMath.ts +135 -0
- package/gallery/data/src/semver/bump.go +73 -0
- package/gallery/data/traces/browser-error.json +5 -0
- package/gallery/data/traces/go/main.go +27 -0
- package/gallery/data/traces/go-panic.txt +16 -0
- package/gallery/data/traces/java-caused-by.txt +15 -0
- package/gallery/data/traces/node-aggregate.txt +30 -0
- package/gallery/data/traces/node-cause.txt +21 -0
- package/gallery/data/traces/py/deploy.json +5 -0
- package/gallery/data/traces/py/deploy.py +17 -0
- package/gallery/data/traces/py/settings.py +19 -0
- package/gallery/data/traces/python-chain.txt +30 -0
- package/gallery/data/traces/rust/main.rs +11 -0
- package/gallery/data/traces/rust-panic.txt +16 -0
- package/gallery/data/traces/shop/cart.mjs +19 -0
- package/gallery/data/traces/shop/pricing.mjs +12 -0
- package/gallery/data/traces/shop/status.mjs +9 -0
- package/gallery/data/traces/web/app.js +11 -0
- package/gallery/data/traces/web/index.html +1 -0
- package/gallery/data/traces/web/search.js +10 -0
- package/gallery/data/transcript/call-noisy.wav +0 -0
- package/gallery/data/transcript/call.json +362 -0
- package/gallery/data/transcript/call.wav +0 -0
- package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
- package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
- package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
- package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
- package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
- package/gallery/data/tsc.txt +4 -0
- package/gallery/data/vitest-bench.json +119 -0
- package/gallery/data/vitest-junit.xml +133 -0
- package/gallery/data/vitest-report.json +612 -0
- package/gallery/images/bell.svg +10 -0
- package/gallery/images/bike.svg +47 -0
- package/gallery/images/footer-planet.webp +0 -0
- package/gallery/images/front-light.svg +13 -0
- package/gallery/images/golden-hour.webp +0 -0
- package/gallery/images/option-flowers.webp +0 -0
- package/gallery/images/option-hilltop.webp +0 -0
- package/gallery/images/option-meadow.webp +0 -0
- package/gallery/images/option-sky.webp +0 -0
- package/gallery/images/u-lock.svg +10 -0
- package/index.client.tsx +164 -0
- package/index.server.ts +216 -0
- package/package.json +50 -4
- package/paseo-plugin.json +14 -0
- package/server/data-source.ts +407 -0
- package/server/project.ts +981 -0
- package/server/web/address.ts +57 -0
- package/server/web/cache.ts +85 -0
- package/server/web/commons.ts +126 -0
- package/server/web/geocode.ts +40 -0
- package/server/web/image.ts +35 -0
- package/server/web/lookups.ts +248 -0
- package/server/web/net.ts +210 -0
- package/shared/core/acceptance.ts +744 -0
- package/shared/core/ast.ts +76 -0
- package/shared/core/benchmark.ts +743 -0
- package/shared/core/catalog.ts +733 -0
- package/shared/core/compare.ts +225 -0
- package/shared/core/coverage.ts +456 -0
- package/shared/core/data.ts +334 -0
- package/shared/core/decisions.ts +527 -0
- package/shared/core/dependencies.ts +604 -0
- package/shared/core/diagnostics.ts +24 -0
- package/shared/core/environments.ts +212 -0
- package/shared/core/evaluate.ts +737 -0
- package/shared/core/fields.ts +66 -0
- package/shared/core/findings.ts +1172 -0
- package/shared/core/form.ts +69 -0
- package/shared/core/index.ts +44 -0
- package/shared/core/junit.ts +252 -0
- package/shared/core/lexer.ts +275 -0
- package/shared/core/logs.ts +496 -0
- package/shared/core/markdown.ts +2374 -0
- package/shared/core/parser.ts +355 -0
- package/shared/core/perf-units.ts +228 -0
- package/shared/core/pipeline.ts +679 -0
- package/shared/core/project.ts +472 -0
- package/shared/core/provenance.ts +725 -0
- package/shared/core/stack-trace.ts +718 -0
- package/shared/core/streaming.ts +136 -0
- package/shared/core/test-results.ts +562 -0
- package/shared/core/thread-activity.ts +203 -0
- package/shared/core/tracking.ts +887 -0
- package/shared/core/transcript.ts +1364 -0
- package/shared/core/treemap.ts +446 -0
- package/shared/core/types.ts +70 -0
- package/shared/core/validate.ts +574 -0
- package/shared/core/waterfall.ts +536 -0
- package/shared/diagram/flowchart.ts +205 -0
- package/shared/diagram/layout.ts +440 -0
- package/shared/diagram/markdown.ts +135 -0
- package/shared/diagram/sequence.ts +122 -0
- package/shared/diagram/zoom.ts +114 -0
- package/shared/fallback.ts +120 -0
- package/shared/genui.test.ts +122 -0
- package/shared/instructions.ts +144 -0
- package/shared/logic/calendarMath.ts +130 -0
- package/shared/logic/chartMath.ts +289 -0
- package/shared/logic/choose-logic.ts +196 -0
- package/shared/logic/compare-logic.ts +270 -0
- package/shared/logic/content-model.ts +227 -0
- package/shared/logic/environments-logic.ts +53 -0
- package/shared/logic/explain-logic.ts +315 -0
- package/shared/logic/fieldLogic.ts +138 -0
- package/shared/logic/gallery-search.ts +71 -0
- package/shared/logic/icons.ts +322 -0
- package/shared/logic/json-logic.ts +153 -0
- package/shared/logic/mapMath.ts +133 -0
- package/shared/logic/project-logic.ts +39 -0
- package/shared/logic/references-model.ts +201 -0
- package/shared/logic/sketch.ts +165 -0
- package/shared/logic/supersession.ts +77 -0
- package/shared/logic/values.ts +133 -0
- package/shared/logic/widget-logic.ts +163 -0
- package/shared/rpc.ts +252 -0
- package/shared/segment.ts +81 -0
- package/shared/settings.ts +12 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// Sizes as Treemap shows them: path/size lists, nested trees, esbuild
|
|
2
|
+
// metafiles and `du` output read into one tree, and the squarified layout
|
|
3
|
+
// (Bruls, Huizing & van Wijk 2000) that turns a level of it into tiles.
|
|
4
|
+
// Pure; shared by the renderer and the markdown fallback.
|
|
5
|
+
import { objectFields, type Fields } from "./perf-units";
|
|
6
|
+
import type { Value } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface TreeNode {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** Names from the root down, joined with "/": a stable key. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** The node's size, its descendants' sum for a group. */
|
|
13
|
+
readonly value: number;
|
|
14
|
+
/** Largest first. */
|
|
15
|
+
readonly children: readonly TreeNode[];
|
|
16
|
+
/** A node_modules package (esbuild metafiles). */
|
|
17
|
+
readonly package: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Treemap {
|
|
21
|
+
readonly source: "list" | "tree" | "esbuild" | "du";
|
|
22
|
+
readonly root: TreeNode;
|
|
23
|
+
/** "B" for metafiles and du, the given unit otherwise; null for plain numbers. */
|
|
24
|
+
readonly unit: string | null;
|
|
25
|
+
/** Items without a name or a positive size, left out. */
|
|
26
|
+
readonly ignored: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TreemapRead = { readonly ok: true; readonly treemap: Treemap } | { readonly ok: false; readonly error: string };
|
|
30
|
+
|
|
31
|
+
const USAGE = "Treemap needs [{path, size}], [{name, children}], an esbuild metafile or du -k output";
|
|
32
|
+
|
|
33
|
+
/** A tree under construction: sizes of its own (a file, a du total) and children by name. */
|
|
34
|
+
interface Draft {
|
|
35
|
+
name: string;
|
|
36
|
+
own: number;
|
|
37
|
+
/** A size the input gave for the whole node (du totals, nested `size`); its children's sum may fall short. */
|
|
38
|
+
total: number | null;
|
|
39
|
+
children: Map<string, Draft>;
|
|
40
|
+
package: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function draft(name: string): Draft {
|
|
44
|
+
return { name, own: 0, total: null, children: new Map(), package: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function child(parent: Draft, name: string): Draft {
|
|
48
|
+
let found = parent.children.get(name);
|
|
49
|
+
if (found === undefined) {
|
|
50
|
+
found = draft(name);
|
|
51
|
+
parent.children.set(name, found);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Path segments: `/` or `\` separated, without empty, `.` and leading `..` parts. */
|
|
57
|
+
export function pathSegments(path: string): string[] {
|
|
58
|
+
const parts = path.split(/[/\\]+/).filter((part) => part !== "" && part !== ".");
|
|
59
|
+
while (parts[0] === "..") parts.shift();
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What the rest of a group holds beyond its listed children. */
|
|
64
|
+
const REST_NAME = "(rest)";
|
|
65
|
+
|
|
66
|
+
/** Freezes a draft: values summed bottom-up, a `total` beyond the children's sum kept as a "(rest)" child, children largest first. */
|
|
67
|
+
function freeze(node: Draft, path: string): TreeNode {
|
|
68
|
+
const children = [...node.children.values()].map((kid) => freeze(kid, path === "" ? kid.name : `${path}/${kid.name}`));
|
|
69
|
+
let sum = children.reduce((total, kid) => total + kid.value, 0);
|
|
70
|
+
if (children.length > 0 && node.own > 0) {
|
|
71
|
+
children.push({ name: REST_NAME, path: `${path}/${REST_NAME}`, value: node.own, children: [], package: false });
|
|
72
|
+
sum += node.own;
|
|
73
|
+
} else if (children.length === 0) {
|
|
74
|
+
sum = node.own;
|
|
75
|
+
}
|
|
76
|
+
if (node.total !== null && node.total > sum) {
|
|
77
|
+
if (children.length > 0) children.push({ name: REST_NAME, path: `${path}/${REST_NAME}`, value: node.total - sum, children: [], package: false });
|
|
78
|
+
sum = node.total;
|
|
79
|
+
}
|
|
80
|
+
// Two "(rest)" children (own and total) merge into one.
|
|
81
|
+
const merged: TreeNode[] = [];
|
|
82
|
+
for (const kid of children) {
|
|
83
|
+
const same = kid.name === REST_NAME ? merged.find((other) => other.name === REST_NAME) : undefined;
|
|
84
|
+
if (same === undefined) merged.push(kid);
|
|
85
|
+
else merged[merged.indexOf(same)] = { ...same, value: same.value + kid.value };
|
|
86
|
+
}
|
|
87
|
+
merged.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name));
|
|
88
|
+
return { name: node.name, path, value: sum, children: merged, package: node.package };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The root, or the one group under it while it is the root's only child: "node_modules", not "(all) › node_modules". */
|
|
92
|
+
function trimRoot(root: TreeNode): TreeNode {
|
|
93
|
+
let node = root;
|
|
94
|
+
while (node.children.length === 1 && node.children[0].children.length > 0) node = node.children[0];
|
|
95
|
+
return node;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function positive(value: Value | undefined): number | null {
|
|
99
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function label(value: Value | undefined): string {
|
|
103
|
+
return typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Inputs
|
|
108
|
+
|
|
109
|
+
function readList(items: readonly Value[]): { root: Draft; ignored: number; bytes: boolean; nested: boolean } {
|
|
110
|
+
const root = draft("");
|
|
111
|
+
let ignored = 0;
|
|
112
|
+
let bytes = false;
|
|
113
|
+
let nested = false;
|
|
114
|
+
const addNested = (parent: Draft, fields: Fields): boolean => {
|
|
115
|
+
const name = label(fields.name ?? fields.label ?? fields.path);
|
|
116
|
+
const size = positive(fields.size ?? fields.value ?? fields.bytes);
|
|
117
|
+
const kids = Array.isArray(fields.children) ? fields.children : null;
|
|
118
|
+
if (name === "" || (size === null && (kids === null || kids.length === 0))) return false;
|
|
119
|
+
const node = child(parent, name);
|
|
120
|
+
if (kids !== null && kids.length > 0) {
|
|
121
|
+
nested = true;
|
|
122
|
+
if (size !== null) node.total = (node.total ?? 0) + size;
|
|
123
|
+
for (const kid of kids) {
|
|
124
|
+
const kidFields = objectFields(kid);
|
|
125
|
+
if (kidFields === null || !addNested(node, kidFields)) ignored++;
|
|
126
|
+
}
|
|
127
|
+
} else if (size !== null) {
|
|
128
|
+
node.own += size;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
const fields = objectFields(item);
|
|
134
|
+
if (fields === null) {
|
|
135
|
+
ignored++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (fields.bytes !== undefined) bytes = true;
|
|
139
|
+
if (Array.isArray(fields.children)) {
|
|
140
|
+
if (!addNested(root, fields)) ignored++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const path = label(fields.path ?? fields.name ?? fields.label);
|
|
144
|
+
const size = positive(fields.size ?? fields.value ?? fields.bytes);
|
|
145
|
+
const parts = pathSegments(path);
|
|
146
|
+
if (parts.length === 0 || size === null) {
|
|
147
|
+
ignored++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let node = root;
|
|
151
|
+
for (const part of parts) node = child(node, part);
|
|
152
|
+
node.own += size;
|
|
153
|
+
}
|
|
154
|
+
return { root, ignored, bytes, nested };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A node_modules path as [package, ...rest]: the last node_modules wins, scoped names keep their scope. */
|
|
158
|
+
export function packagePath(path: string): { package: string; rest: string[] } | null {
|
|
159
|
+
const parts = pathSegments(path);
|
|
160
|
+
const at = parts.lastIndexOf("node_modules");
|
|
161
|
+
if (at === -1 || at + 1 >= parts.length) return null;
|
|
162
|
+
const scoped = parts[at + 1].startsWith("@") && at + 2 < parts.length;
|
|
163
|
+
const name = scoped ? `${parts[at + 1]}/${parts[at + 2]}` : parts[at + 1];
|
|
164
|
+
return { package: name, rest: parts.slice(at + (scoped ? 3 : 2)) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** esbuild --metafile: each output's `inputs[path].bytesInOutput`, packages grouped under node_modules. */
|
|
168
|
+
function readMetafile(outputs: Fields): { root: Draft; ignored: number } {
|
|
169
|
+
const root = draft("");
|
|
170
|
+
let ignored = 0;
|
|
171
|
+
const files = Object.entries(outputs).flatMap(([name, output]) => {
|
|
172
|
+
const inputs = name.endsWith(".map") ? null : objectFields(objectFields(output)?.inputs);
|
|
173
|
+
return inputs === null ? [] : [{ name, inputs }];
|
|
174
|
+
});
|
|
175
|
+
for (const { name: outputName, inputs } of files) {
|
|
176
|
+
// Several outputs (code splitting, css next to js): one group per output.
|
|
177
|
+
const base = files.length > 1 ? child(root, pathSegments(outputName).pop() ?? outputName) : root;
|
|
178
|
+
for (const [inputPath, input] of Object.entries(inputs)) {
|
|
179
|
+
const bytes = objectFields(input)?.bytesInOutput;
|
|
180
|
+
const size = positive(bytes);
|
|
181
|
+
if (size === null) {
|
|
182
|
+
// An input tree-shaken away (0 bytes) is a fact, not a problem.
|
|
183
|
+
if (bytes !== 0) ignored++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const pkg = packagePath(inputPath);
|
|
187
|
+
let node = base;
|
|
188
|
+
if (pkg !== null) {
|
|
189
|
+
node = child(child(node, "node_modules"), pkg.package);
|
|
190
|
+
node.package = true;
|
|
191
|
+
for (const part of pkg.rest) node = child(node, part);
|
|
192
|
+
} else {
|
|
193
|
+
const parts = pathSegments(inputPath);
|
|
194
|
+
for (const part of parts.length === 0 ? [inputPath] : parts) node = child(node, part);
|
|
195
|
+
}
|
|
196
|
+
node.own += size;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { root, ignored };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const DU_SUFFIX: Record<string, number> = { "": 1024, b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4, p: 1024 ** 5 };
|
|
203
|
+
|
|
204
|
+
/** One `du` line: `size<TAB>path`; sizes in KB (`du -k`) or with a K/M/G suffix (`du -h`). */
|
|
205
|
+
function duLine(line: string): { bytes: number; path: string } | null {
|
|
206
|
+
const match = /^\s*(\d+(?:[.,]\d+)?)\s*([bkmgtp])?i?b?\s+(.+?)\s*$/i.exec(line);
|
|
207
|
+
if (match === null) return null;
|
|
208
|
+
const amount = Number(match[1].replace(",", "."));
|
|
209
|
+
const factor = DU_SUFFIX[(match[2] ?? "").toLowerCase()];
|
|
210
|
+
return { bytes: amount * factor, path: match[3] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* `du` output: every line a directory's (or with -a a file's) total, parents
|
|
215
|
+
* after their children. A directory's own files are its total minus its
|
|
216
|
+
* listed children: they show as "(rest)".
|
|
217
|
+
*/
|
|
218
|
+
function readDu(text: string): { root: Draft; ignored: number; lines: number } {
|
|
219
|
+
const root = draft("");
|
|
220
|
+
let ignored = 0;
|
|
221
|
+
let lines = 0;
|
|
222
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
223
|
+
if (raw.trim() === "") continue;
|
|
224
|
+
const parsed = duLine(raw);
|
|
225
|
+
const parts = parsed === null ? [] : pathSegments(parsed.path);
|
|
226
|
+
if (parsed === null || parts.length === 0) {
|
|
227
|
+
ignored++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
lines++;
|
|
231
|
+
let node = root;
|
|
232
|
+
for (const part of parts) node = child(node, part);
|
|
233
|
+
if (parsed.bytes > 0) node.total = Math.max(node.total ?? 0, parsed.bytes);
|
|
234
|
+
}
|
|
235
|
+
return { root, ignored, lines };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Reads any input Treemap takes; `unit` overrides the default ("B" for metafiles and du). */
|
|
239
|
+
export function readTreemap(value: Value | undefined, unit: string | null): TreemapRead {
|
|
240
|
+
if (value === undefined || value === null) return { ok: false, error: USAGE };
|
|
241
|
+
let result: { root: Draft; ignored: number } | null = null;
|
|
242
|
+
let source: Treemap["source"] = "list";
|
|
243
|
+
let defaultUnit: string | null = null;
|
|
244
|
+
const fields = objectFields(value);
|
|
245
|
+
const outputs = objectFields(fields?.outputs);
|
|
246
|
+
if (typeof value === "string") {
|
|
247
|
+
const du = readDu(value);
|
|
248
|
+
if (du.lines === 0) return { ok: false, error: "No du lines (size<TAB>path) in this text" };
|
|
249
|
+
result = du;
|
|
250
|
+
source = "du";
|
|
251
|
+
defaultUnit = "B";
|
|
252
|
+
} else if (outputs !== null) {
|
|
253
|
+
result = readMetafile(outputs);
|
|
254
|
+
source = "esbuild";
|
|
255
|
+
defaultUnit = "B";
|
|
256
|
+
} else if (fields !== null && Array.isArray(fields.children)) {
|
|
257
|
+
const list = readList([value]);
|
|
258
|
+
result = list;
|
|
259
|
+
source = "tree";
|
|
260
|
+
if (list.bytes) defaultUnit = "B";
|
|
261
|
+
} else if (Array.isArray(value)) {
|
|
262
|
+
const list = readList(value);
|
|
263
|
+
result = list;
|
|
264
|
+
source = list.nested ? "tree" : "list";
|
|
265
|
+
if (list.bytes) defaultUnit = "B";
|
|
266
|
+
}
|
|
267
|
+
if (result === null) return { ok: false, error: USAGE };
|
|
268
|
+
const root = trimRoot(freeze(result.root, ""));
|
|
269
|
+
return { ok: true, treemap: { source, root, unit: unit ?? defaultUnit, ignored: result.ignored } };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The node at `path` under `root` (a TreeNode path), or null. */
|
|
273
|
+
export function findNode(root: TreeNode, path: string): TreeNode | null {
|
|
274
|
+
if (path === root.path) return root;
|
|
275
|
+
for (const kid of root.children) {
|
|
276
|
+
if (path === kid.path || path.startsWith(`${kid.path}/`)) return findNode(kid, path);
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The nodes from `root` down to the one at `path`, both included; just [root] when it isn't there. */
|
|
282
|
+
export function trail(root: TreeNode, path: string): TreeNode[] {
|
|
283
|
+
const chain: TreeNode[] = [root];
|
|
284
|
+
for (let node = root; node.path !== path; ) {
|
|
285
|
+
const next = node.children.find((kid) => path === kid.path || path.startsWith(`${kid.path}/`));
|
|
286
|
+
if (next === undefined) return [root];
|
|
287
|
+
chain.push(next);
|
|
288
|
+
node = next;
|
|
289
|
+
}
|
|
290
|
+
return chain;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Layout
|
|
295
|
+
|
|
296
|
+
export interface Rect {
|
|
297
|
+
readonly x: number;
|
|
298
|
+
readonly y: number;
|
|
299
|
+
readonly w: number;
|
|
300
|
+
readonly h: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The worst aspect ratio of a row of `areas` laid along a side `side` long. */
|
|
304
|
+
function worst(areas: readonly number[], side: number): number {
|
|
305
|
+
const sum = areas.reduce((total, area) => total + area, 0);
|
|
306
|
+
if (sum === 0 || side === 0) return Number.POSITIVE_INFINITY;
|
|
307
|
+
const max = Math.max(...areas);
|
|
308
|
+
const min = Math.min(...areas);
|
|
309
|
+
return Math.max((side * side * max) / (sum * sum), (sum * sum) / (side * side * min));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Squarified treemap layout: rects for `values` (in their order) filling
|
|
314
|
+
* `rect`, each with an area proportional to its value and as close to a
|
|
315
|
+
* square as the greedy row-by-row method gets. Values are laid largest
|
|
316
|
+
* first; zero and negative values get empty rects.
|
|
317
|
+
*/
|
|
318
|
+
export function squarify(values: readonly number[], rect: Rect): Rect[] {
|
|
319
|
+
const out: Rect[] = values.map(() => ({ x: rect.x, y: rect.y, w: 0, h: 0 }));
|
|
320
|
+
const order = values
|
|
321
|
+
.map((value, index) => ({ value: Number.isFinite(value) && value > 0 ? value : 0, index }))
|
|
322
|
+
.filter((entry) => entry.value > 0)
|
|
323
|
+
.sort((a, b) => b.value - a.value);
|
|
324
|
+
const total = order.reduce((sum, entry) => sum + entry.value, 0);
|
|
325
|
+
if (total === 0 || rect.w <= 0 || rect.h <= 0) return out;
|
|
326
|
+
const scale = (rect.w * rect.h) / total;
|
|
327
|
+
let { x, y, w, h } = rect;
|
|
328
|
+
let row: { area: number; index: number }[] = [];
|
|
329
|
+
const place = () => {
|
|
330
|
+
const sum = row.reduce((total, entry) => total + entry.area, 0);
|
|
331
|
+
if (w >= h) {
|
|
332
|
+
// A column along the left side.
|
|
333
|
+
const width = h === 0 ? 0 : sum / h;
|
|
334
|
+
let at = y;
|
|
335
|
+
for (const entry of row) {
|
|
336
|
+
const height = width === 0 ? 0 : entry.area / width;
|
|
337
|
+
out[entry.index] = { x, y: at, w: width, h: height };
|
|
338
|
+
at += height;
|
|
339
|
+
}
|
|
340
|
+
x += width;
|
|
341
|
+
w = Math.max(0, w - width);
|
|
342
|
+
} else {
|
|
343
|
+
// A row along the top.
|
|
344
|
+
const height = w === 0 ? 0 : sum / w;
|
|
345
|
+
let at = x;
|
|
346
|
+
for (const entry of row) {
|
|
347
|
+
const width = height === 0 ? 0 : entry.area / height;
|
|
348
|
+
out[entry.index] = { x: at, y, w: width, h: height };
|
|
349
|
+
at += width;
|
|
350
|
+
}
|
|
351
|
+
y += height;
|
|
352
|
+
h = Math.max(0, h - height);
|
|
353
|
+
}
|
|
354
|
+
row = [];
|
|
355
|
+
};
|
|
356
|
+
for (const entry of order) {
|
|
357
|
+
const area = entry.value * scale;
|
|
358
|
+
const side = Math.min(w, h);
|
|
359
|
+
const areas = row.map((item) => item.area);
|
|
360
|
+
if (row.length === 0 || worst([...areas, area], side) <= worst(areas, side)) {
|
|
361
|
+
row.push({ area, index: entry.index });
|
|
362
|
+
} else {
|
|
363
|
+
place();
|
|
364
|
+
row.push({ area, index: entry.index });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (row.length > 0) place();
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface Tile extends Rect {
|
|
372
|
+
readonly node: TreeNode;
|
|
373
|
+
/** 1 for the shown root's children, 2 for theirs. */
|
|
374
|
+
readonly level: 1 | 2;
|
|
375
|
+
/** The level-1 group this tile belongs to (its position among the root's children): its colour. */
|
|
376
|
+
readonly group: number;
|
|
377
|
+
/** Children folded into this one tile ("12 more"), when there were too many to draw. */
|
|
378
|
+
readonly folded: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface TreemapLayout {
|
|
382
|
+
readonly tiles: readonly Tile[];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Tiles per group before the rest fold into one "N more" tile. */
|
|
386
|
+
const MAX_CHILDREN = 24;
|
|
387
|
+
/** A child smaller than this share of its parent folds into "N more". */
|
|
388
|
+
const MIN_SHARE = 0.004;
|
|
389
|
+
/** The strip a level-1 group keeps for its name above its children. */
|
|
390
|
+
export const GROUP_HEADER = 18;
|
|
391
|
+
const GROUP_PADDING = 2;
|
|
392
|
+
/** The smallest level-1 group that shows its children inside. */
|
|
393
|
+
const NEST_MIN_WIDTH = 64;
|
|
394
|
+
const NEST_MIN_HEIGHT = 48;
|
|
395
|
+
|
|
396
|
+
/** A node's children as drawn: the largest up to MAX_CHILDREN, the rest folded into one node. */
|
|
397
|
+
export function visibleChildren(node: TreeNode, limit = MAX_CHILDREN): { nodes: TreeNode[]; folded: number[] } {
|
|
398
|
+
const kept: TreeNode[] = [];
|
|
399
|
+
const rest: TreeNode[] = [];
|
|
400
|
+
for (const kid of node.children) {
|
|
401
|
+
if (kept.length < limit && kid.value >= node.value * MIN_SHARE) kept.push(kid);
|
|
402
|
+
else rest.push(kid);
|
|
403
|
+
}
|
|
404
|
+
// Folding one child gains nothing: show it.
|
|
405
|
+
if (rest.length === 1) {
|
|
406
|
+
kept.push(rest[0]);
|
|
407
|
+
rest.length = 0;
|
|
408
|
+
}
|
|
409
|
+
if (rest.length === 0) return { nodes: kept, folded: kept.map(() => 0) };
|
|
410
|
+
const value = rest.reduce((sum, kid) => sum + kid.value, 0);
|
|
411
|
+
const more: TreeNode = { name: `${rest.length} more`, path: `${node.path}/…more`, value, children: [], package: false };
|
|
412
|
+
return { nodes: [...kept, more], folded: [...kept.map(() => 0), rest.length] };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Two levels of tiles for `root` in a `width` × `height` box: its children,
|
|
417
|
+
* and inside each child big enough, a name strip and its own children.
|
|
418
|
+
*/
|
|
419
|
+
export function layoutTreemap(root: TreeNode, width: number, height: number): TreemapLayout {
|
|
420
|
+
const tiles: Tile[] = [];
|
|
421
|
+
const top = visibleChildren(root);
|
|
422
|
+
const rects = squarify(
|
|
423
|
+
top.nodes.map((node) => node.value),
|
|
424
|
+
{ x: 0, y: 0, w: width, h: height },
|
|
425
|
+
);
|
|
426
|
+
top.nodes.forEach((node, group) => {
|
|
427
|
+
const rect = rects[group];
|
|
428
|
+
if (rect.w <= 0 || rect.h <= 0) return;
|
|
429
|
+
tiles.push({ ...rect, node, level: 1, group, folded: top.folded[group] });
|
|
430
|
+
if (node.children.length === 0 || top.folded[group] > 0 || rect.w < NEST_MIN_WIDTH || rect.h < NEST_MIN_HEIGHT) return;
|
|
431
|
+
const inner = {
|
|
432
|
+
x: rect.x + GROUP_PADDING,
|
|
433
|
+
y: rect.y + GROUP_HEADER,
|
|
434
|
+
w: rect.w - GROUP_PADDING * 2,
|
|
435
|
+
h: rect.h - GROUP_HEADER - GROUP_PADDING,
|
|
436
|
+
};
|
|
437
|
+
const kids = visibleChildren(node, 16);
|
|
438
|
+
squarify(
|
|
439
|
+
kids.nodes.map((kid) => kid.value),
|
|
440
|
+
inner,
|
|
441
|
+
).forEach((kidRect, index) => {
|
|
442
|
+
if (kidRect.w > 0 && kidRect.h > 0) tiles.push({ ...kidRect, node: kids.nodes[index], level: 2, group, folded: kids.folded[index] });
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
return { tiles };
|
|
446
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Shared contract between the language core (core/), the React renderer (ui/),
|
|
2
|
+
// and the server tool (server.ts). Pure data: no React, no DOM, no Node APIs.
|
|
3
|
+
|
|
4
|
+
import type { ComponentName } from "./catalog";
|
|
5
|
+
|
|
6
|
+
/** A compile-time or evaluation-time problem, positioned in the UI source. */
|
|
7
|
+
export interface Diagnostic {
|
|
8
|
+
severity: "error" | "warning";
|
|
9
|
+
/** 1-based line in the UI source. */
|
|
10
|
+
line: number;
|
|
11
|
+
/** 1-based column in the UI source. */
|
|
12
|
+
column: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Scalar = string | number | boolean | null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Every value the renderer can receive as a component prop. Lambdas never
|
|
20
|
+
* reach the renderer: they only exist while evaluating `map`/`filter`.
|
|
21
|
+
*/
|
|
22
|
+
export type Value =
|
|
23
|
+
| Scalar
|
|
24
|
+
| readonly Value[]
|
|
25
|
+
| { readonly [key: string]: Value }
|
|
26
|
+
| UiNode
|
|
27
|
+
| Action
|
|
28
|
+
| Binding;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One evaluated component instance. `props` are keyed by the catalog param
|
|
32
|
+
* name; omitted optional params are absent (never `undefined` values).
|
|
33
|
+
* `nodes` params arrive normalized as `UiNode[]`, `actions` params as
|
|
34
|
+
* `Action[]`, and `binding` params as a `Binding`.
|
|
35
|
+
*/
|
|
36
|
+
export interface UiNode {
|
|
37
|
+
readonly kind: "node";
|
|
38
|
+
/** A catalog component; `"Error"` when this node failed to evaluate; `"Loading"` while a file it reads with Load is still being read. */
|
|
39
|
+
readonly type: ComponentName | "Error" | "Loading";
|
|
40
|
+
/**
|
|
41
|
+
* Stable across re-evaluations of the same program so React keeps
|
|
42
|
+
* component identity: derived from the source position and list index.
|
|
43
|
+
*/
|
|
44
|
+
readonly key: string;
|
|
45
|
+
/** For `type: "Error"`: `{ message: string }`; for `"Loading"`: `{ path: string, component: ComponentName | null }` (null for a bare expression). */
|
|
46
|
+
readonly props: Readonly<Record<string, Value>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A two-way binding to one `$state` variable. `name` excludes the `$`. */
|
|
50
|
+
export interface Binding {
|
|
51
|
+
readonly kind: "binding";
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly value: Value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Side effects a button can trigger. Evaluated at render time. */
|
|
57
|
+
export type Action =
|
|
58
|
+
| { readonly kind: "action"; readonly type: "send"; readonly text: string }
|
|
59
|
+
| {
|
|
60
|
+
readonly kind: "action";
|
|
61
|
+
readonly type: "set";
|
|
62
|
+
/** State name without the `$`. */
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly value: Value;
|
|
65
|
+
}
|
|
66
|
+
| { readonly kind: "action"; readonly type: "openUrl"; readonly url: string }
|
|
67
|
+
| { readonly kind: "action"; readonly type: "copy"; readonly text: string };
|
|
68
|
+
|
|
69
|
+
/** Mutable state of one rendered UI: `$name` → value (keys exclude the `$`). */
|
|
70
|
+
export type UiState = Readonly<Record<string, Value>>;
|