@shadow-js/devtools 0.0.0-stage → 1.0.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +576 -2
- package/dist/dev/agent.d.ts +239 -0
- package/dist/dev/agent.js +626 -0
- package/dist/dev/agent.js.map +1 -0
- package/dist/dev/backend.d.ts +53 -0
- package/dist/dev/backend.js +53 -0
- package/dist/dev/backend.js.map +1 -0
- package/dist/dev/hook.d.ts +83 -0
- package/dist/dev/hook.js +136 -0
- package/dist/dev/hook.js.map +1 -0
- package/dist/dev/index.d.ts +17 -0
- package/dist/dev/index.js +17 -0
- package/dist/dev/inspection.d.ts +226 -0
- package/dist/dev/inspection.js +263 -0
- package/dist/dev/inspection.js.map +1 -0
- package/dist/dev/inspector.d.ts +331 -0
- package/dist/dev/inspector.js +429 -0
- package/dist/dev/inspector.js.map +1 -0
- package/dist/dev/model.d.ts +418 -0
- package/dist/dev/model.js +559 -0
- package/dist/dev/model.js.map +1 -0
- package/dist/dev/mount.d.ts +207 -0
- package/dist/dev/mount.js +256 -0
- package/dist/dev/mount.js.map +1 -0
- package/dist/dev/overlay.d.ts +102 -0
- package/dist/dev/overlay.js +176 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/preview.d.ts +74 -0
- package/dist/dev/preview.js +122 -0
- package/dist/dev/preview.js.map +1 -0
- package/dist/dev/protocol.d.ts +288 -0
- package/dist/dev/protocol.js +109 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/ring.d.ts +50 -0
- package/dist/dev/ring.js +94 -0
- package/dist/dev/ring.js.map +1 -0
- package/dist/dev/transport.d.ts +347 -0
- package/dist/dev/transport.js +394 -0
- package/dist/dev/transport.js.map +1 -0
- package/dist/dev/ui/details.js +294 -0
- package/dist/dev/ui/details.js.map +1 -0
- package/dist/dev/ui/inspection-state.d.ts +43 -0
- package/dist/dev/ui/inspection-state.js +62 -0
- package/dist/dev/ui/inspection-state.js.map +1 -0
- package/dist/dev/ui/issues.js +85 -0
- package/dist/dev/ui/issues.js.map +1 -0
- package/dist/dev/ui/panel.d.ts +55 -0
- package/dist/dev/ui/panel.js +160 -0
- package/dist/dev/ui/panel.js.map +1 -0
- package/dist/dev/ui/rows.d.ts +44 -0
- package/dist/dev/ui/rows.js +132 -0
- package/dist/dev/ui/rows.js.map +1 -0
- package/dist/dev/ui/state-tab.js +88 -0
- package/dist/dev/ui/state-tab.js.map +1 -0
- package/dist/dev/ui/state.d.ts +192 -0
- package/dist/dev/ui/state.js +147 -0
- package/dist/dev/ui/state.js.map +1 -0
- package/dist/dev/ui/styles.d.ts +31 -0
- package/dist/dev/ui/styles.js +113 -0
- package/dist/dev/ui/styles.js.map +1 -0
- package/dist/dev/ui/timeline.js +135 -0
- package/dist/dev/ui/timeline.js.map +1 -0
- package/dist/dev/ui/tree.js +186 -0
- package/dist/dev/ui/tree.js.map +1 -0
- package/dist/prod/agent.js +2 -0
- package/dist/prod/agent.js.map +1 -0
- package/dist/prod/backend.js +2 -0
- package/dist/prod/backend.js.map +1 -0
- package/dist/prod/hook.js +2 -0
- package/dist/prod/hook.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/inspection.js +2 -0
- package/dist/prod/inspection.js.map +1 -0
- package/dist/prod/inspector.js +2 -0
- package/dist/prod/inspector.js.map +1 -0
- package/dist/prod/model.js +2 -0
- package/dist/prod/model.js.map +1 -0
- package/dist/prod/mount.js +2 -0
- package/dist/prod/mount.js.map +1 -0
- package/dist/prod/overlay.js +11 -0
- package/dist/prod/overlay.js.map +1 -0
- package/dist/prod/preview.js +2 -0
- package/dist/prod/preview.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/ring.js +2 -0
- package/dist/prod/ring.js.map +1 -0
- package/dist/prod/transport.js +2 -0
- package/dist/prod/transport.js.map +1 -0
- package/dist/prod/ui/details.js +2 -0
- package/dist/prod/ui/details.js.map +1 -0
- package/dist/prod/ui/inspection-state.js +2 -0
- package/dist/prod/ui/inspection-state.js.map +1 -0
- package/dist/prod/ui/issues.js +2 -0
- package/dist/prod/ui/issues.js.map +1 -0
- package/dist/prod/ui/panel.js +2 -0
- package/dist/prod/ui/panel.js.map +1 -0
- package/dist/prod/ui/rows.js +2 -0
- package/dist/prod/ui/rows.js.map +1 -0
- package/dist/prod/ui/state-tab.js +2 -0
- package/dist/prod/ui/state-tab.js.map +1 -0
- package/dist/prod/ui/state.js +2 -0
- package/dist/prod/ui/state.js.map +1 -0
- package/dist/prod/ui/styles.js +82 -0
- package/dist/prod/ui/styles.js.map +1 -0
- package/dist/prod/ui/timeline.js +2 -0
- package/dist/prod/ui/timeline.js.map +1 -0
- package/dist/prod/ui/tree.js +2 -0
- package/dist/prod/ui/tree.js.map +1 -0
- package/package.json +69 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ErrorRecord, HydrationRecord } from "../model.js";
|
|
2
|
+
import { ComponentStat, Explanation, FlameFlush, GraphSnapshot, SnapshotNode } from "../inspector.js";
|
|
3
|
+
import { Backend } from "../backend.js";
|
|
4
|
+
import { TreeRow } from "./rows.js";
|
|
5
|
+
import { Accessor } from "@shadow-js/core";
|
|
6
|
+
//#region src/ui/state.d.ts
|
|
7
|
+
/** The panel's tabs. @since 1.0.0 */
|
|
8
|
+
export type TabId = "tree" | "timeline" | "state" | "issues";
|
|
9
|
+
/**
|
|
10
|
+
* The panel's tabs in display order. Each entry has the tab's `id` and the label shown on it.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { TABS } from "@shadow-js/devtools";
|
|
14
|
+
* for (const tab of TABS) console.log(tab.id, tab.label);
|
|
15
|
+
* @since 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
export declare const TABS: ReadonlyArray<{
|
|
18
|
+
id: TabId;
|
|
19
|
+
label: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Everything the panel's components read and write. All state is signals; the heavy views are memos. @since 1.0.0 */
|
|
22
|
+
export interface PanelState {
|
|
23
|
+
/**
|
|
24
|
+
* The backend the panel reads from. The panel does not know whether it is local or remote.
|
|
25
|
+
*/
|
|
26
|
+
readonly backend: Backend;
|
|
27
|
+
/**
|
|
28
|
+
* The active tab.
|
|
29
|
+
*/
|
|
30
|
+
readonly tab: Accessor<TabId>;
|
|
31
|
+
/**
|
|
32
|
+
* Makes `tab` the active tab.
|
|
33
|
+
*/
|
|
34
|
+
setTab(tab: TabId): void;
|
|
35
|
+
/**
|
|
36
|
+
* Id of the selected node. `0` when no node is selected, because `NO_ID` never names a node.
|
|
37
|
+
*/
|
|
38
|
+
readonly selected: Accessor<number>;
|
|
39
|
+
/**
|
|
40
|
+
* Selects the node with this id.
|
|
41
|
+
*/
|
|
42
|
+
select(id: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* The tree's text filter.
|
|
45
|
+
*/
|
|
46
|
+
readonly filter: Accessor<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Sets the tree's text filter. Matching rows are shown with their ancestors.
|
|
49
|
+
*/
|
|
50
|
+
setFilter(text: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the tree shows components only.
|
|
53
|
+
*/
|
|
54
|
+
readonly componentsOnly: Accessor<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Shows components only when `on` is true, and every node otherwise.
|
|
57
|
+
*/
|
|
58
|
+
setComponentsOnly(on: boolean): void;
|
|
59
|
+
/**
|
|
60
|
+
* Ids of the nodes the user collapsed.
|
|
61
|
+
*/
|
|
62
|
+
readonly collapsed: Accessor<ReadonlySet<number>>;
|
|
63
|
+
/**
|
|
64
|
+
* Collapses the node when it is expanded, and expands it when it is collapsed.
|
|
65
|
+
*/
|
|
66
|
+
toggleCollapsed(id: number): void;
|
|
67
|
+
/**
|
|
68
|
+
* The most tree rows shown at once.
|
|
69
|
+
*/
|
|
70
|
+
readonly rowLimit: Accessor<number>;
|
|
71
|
+
/**
|
|
72
|
+
* Doubles the row limit. The tree's Show more button calls this.
|
|
73
|
+
*/
|
|
74
|
+
showMoreRows(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Whether components flash when they update.
|
|
77
|
+
*/
|
|
78
|
+
readonly flash: Accessor<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Switches flashing on or off, then calls `onFlash`.
|
|
81
|
+
*/
|
|
82
|
+
setFlash(on: boolean): void;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the user paused recording.
|
|
85
|
+
*/
|
|
86
|
+
readonly paused: Accessor<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Pauses or resumes recording. Sends `pause` or `resume` to the backend, then calls `onPause`.
|
|
89
|
+
*/
|
|
90
|
+
setPaused(on: boolean): void;
|
|
91
|
+
/** Counter bumped each time the model changed; views depend on it. */
|
|
92
|
+
readonly version: Accessor<number>;
|
|
93
|
+
/** Re-reads the model now. */
|
|
94
|
+
refresh(): void;
|
|
95
|
+
/**
|
|
96
|
+
* The graph snapshot of the backend's model. Recomputed when `version` changes.
|
|
97
|
+
*/
|
|
98
|
+
readonly snapshot: Accessor<GraphSnapshot>;
|
|
99
|
+
/**
|
|
100
|
+
* The nodes of {@link PanelState.snapshot}, by id.
|
|
101
|
+
*/
|
|
102
|
+
readonly nodes: Accessor<ReadonlyMap<number, SnapshotNode>>;
|
|
103
|
+
/**
|
|
104
|
+
* The tree rows for the current filter, switches, collapse state and row limit, and how many matching
|
|
105
|
+
* rows the limit cut.
|
|
106
|
+
*/
|
|
107
|
+
readonly rows: Accessor<{
|
|
108
|
+
rows: TreeRow[];
|
|
109
|
+
hidden: number;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* The selected node, or `undefined` when it is not in the snapshot.
|
|
113
|
+
*/
|
|
114
|
+
readonly selectedNode: Accessor<SnapshotNode | undefined>;
|
|
115
|
+
/**
|
|
116
|
+
* Why the selected memo or effect last ran, or `null` when the selected node is not a memo or effect.
|
|
117
|
+
*/
|
|
118
|
+
readonly explanation: Accessor<Explanation | null>;
|
|
119
|
+
/**
|
|
120
|
+
* The newest 12 flushes, laid out as flame lanes.
|
|
121
|
+
*/
|
|
122
|
+
readonly flames: Accessor<FlameFlush[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Up to 10 components that re-ran, most re-runs first.
|
|
125
|
+
*/
|
|
126
|
+
readonly hot: Accessor<ComponentStat[]>;
|
|
127
|
+
/**
|
|
128
|
+
* The errors the runtime reported, newest first.
|
|
129
|
+
*/
|
|
130
|
+
readonly errors: Accessor<ErrorRecord[]>;
|
|
131
|
+
/**
|
|
132
|
+
* The hydration mismatches, newest first.
|
|
133
|
+
*/
|
|
134
|
+
readonly hydration: Accessor<HydrationRecord[]>;
|
|
135
|
+
/** Highlights a component on the page. */
|
|
136
|
+
highlight(id: number | null): void;
|
|
137
|
+
/** The display name of a node, `#id` when it is gone. */
|
|
138
|
+
nameOf(id: number): string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Options of {@link createPanelState}. Every field is optional.
|
|
142
|
+
*
|
|
143
|
+
* @since 1.0.0
|
|
144
|
+
*/
|
|
145
|
+
export interface PanelStateOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Called when the user switches flashing on or off.
|
|
148
|
+
*/
|
|
149
|
+
onFlash?(on: boolean): void;
|
|
150
|
+
/**
|
|
151
|
+
* Called with the pause state the user chose. The state also sends `pause` or `resume` to the backend,
|
|
152
|
+
* so the agent stops or resumes recording either way.
|
|
153
|
+
*/
|
|
154
|
+
onPause?(paused: boolean): void;
|
|
155
|
+
/**
|
|
156
|
+
* Starting tab.
|
|
157
|
+
*
|
|
158
|
+
* @default "tree"
|
|
159
|
+
*/
|
|
160
|
+
tab?: TabId;
|
|
161
|
+
/**
|
|
162
|
+
* Starting row limit of the tree.
|
|
163
|
+
*
|
|
164
|
+
* @default 300
|
|
165
|
+
*/
|
|
166
|
+
rowLimit?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Starting value of the flash switch.
|
|
169
|
+
*
|
|
170
|
+
* @default true
|
|
171
|
+
*/
|
|
172
|
+
flash?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Creates the panel's state. Call it inside the panel's reactive root, because the memos belong to the
|
|
176
|
+
* current owner. The heavy views (snapshot, rows, flame lanes) are memos of the `version` counter, so a
|
|
177
|
+
* burst of model changes costs one recomputation per refresh.
|
|
178
|
+
*
|
|
179
|
+
* @param backend The backend to read from.
|
|
180
|
+
* @param options Initial values and callbacks.
|
|
181
|
+
* @returns The state.
|
|
182
|
+
* @example
|
|
183
|
+
* import { createAgent, createLocalBackend, createPanelState } from "@shadow-js/devtools";
|
|
184
|
+
* const backend = createLocalBackend(createAgent());
|
|
185
|
+
* // Inside the panel's reactive root (the one render() creates):
|
|
186
|
+
* const state = createPanelState(backend, { tab: "timeline", flash: false });
|
|
187
|
+
* state.setTab("issues");
|
|
188
|
+
* @since 1.0.0
|
|
189
|
+
*/
|
|
190
|
+
export declare function createPanelState(backend: Backend, options?: PanelStateOptions): PanelState;
|
|
191
|
+
//#endregion
|
|
192
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { componentStats, explainRun, flameTimeline, nodeLabel, snapshotOf } from "../inspector.js";
|
|
2
|
+
import { buildRows } from "./rows.js";
|
|
3
|
+
import { useMemo, useSignal } from "@shadow-js/core";
|
|
4
|
+
//#region src/ui/state.ts
|
|
5
|
+
/**
|
|
6
|
+
* The panel's tabs in display order. Each entry has the tab's `id` and the label shown on it.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { TABS } from "@shadow-js/devtools";
|
|
10
|
+
* for (const tab of TABS) console.log(tab.id, tab.label);
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
const TABS = [
|
|
14
|
+
{
|
|
15
|
+
id: "tree",
|
|
16
|
+
label: "Components"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "timeline",
|
|
20
|
+
label: "Timeline"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "state",
|
|
24
|
+
label: "State"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "issues",
|
|
28
|
+
label: "Issues"
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Creates the panel's state. Call it inside the panel's reactive root, because the memos belong to the
|
|
33
|
+
* current owner. The heavy views (snapshot, rows, flame lanes) are memos of the `version` counter, so a
|
|
34
|
+
* burst of model changes costs one recomputation per refresh.
|
|
35
|
+
*
|
|
36
|
+
* @param backend The backend to read from.
|
|
37
|
+
* @param options Initial values and callbacks.
|
|
38
|
+
* @returns The state.
|
|
39
|
+
* @example
|
|
40
|
+
* import { createAgent, createLocalBackend, createPanelState } from "@shadow-js/devtools";
|
|
41
|
+
* const backend = createLocalBackend(createAgent());
|
|
42
|
+
* // Inside the panel's reactive root (the one render() creates):
|
|
43
|
+
* const state = createPanelState(backend, { tab: "timeline", flash: false });
|
|
44
|
+
* state.setTab("issues");
|
|
45
|
+
* @since 1.0.0
|
|
46
|
+
*/
|
|
47
|
+
function createPanelState(backend, options = {}) {
|
|
48
|
+
const [tab, setTab] = useSignal(options.tab ?? "tree");
|
|
49
|
+
const [selected, setSelected] = useSignal(0);
|
|
50
|
+
const [filter, setFilter] = useSignal("");
|
|
51
|
+
const [componentsOnly, setComponentsOnly] = useSignal(false);
|
|
52
|
+
const [collapsed, setCollapsed] = useSignal(/* @__PURE__ */ new Set());
|
|
53
|
+
const [rowLimit, setRowLimit] = useSignal(options.rowLimit ?? 300);
|
|
54
|
+
const [flash, setFlashSignal] = useSignal(options.flash !== false);
|
|
55
|
+
const [paused, setPausedSignal] = useSignal(false);
|
|
56
|
+
const [version, setVersion] = useSignal(0);
|
|
57
|
+
const snapshot = useMemo(() => {
|
|
58
|
+
version();
|
|
59
|
+
return snapshotOf(backend.model);
|
|
60
|
+
});
|
|
61
|
+
const nodes = useMemo(() => new Map(snapshot().nodes.map((node) => [node.id, node])));
|
|
62
|
+
const rows = useMemo(() => buildRows(snapshot(), {
|
|
63
|
+
filter: filter(),
|
|
64
|
+
componentsOnly: componentsOnly(),
|
|
65
|
+
collapsed: collapsed(),
|
|
66
|
+
limit: rowLimit()
|
|
67
|
+
}));
|
|
68
|
+
const selectedNode = useMemo(() => nodes().get(selected()));
|
|
69
|
+
return {
|
|
70
|
+
backend,
|
|
71
|
+
tab,
|
|
72
|
+
setTab,
|
|
73
|
+
selected,
|
|
74
|
+
select: setSelected,
|
|
75
|
+
filter,
|
|
76
|
+
setFilter,
|
|
77
|
+
componentsOnly,
|
|
78
|
+
setComponentsOnly,
|
|
79
|
+
collapsed,
|
|
80
|
+
toggleCollapsed(id) {
|
|
81
|
+
setCollapsed((current) => {
|
|
82
|
+
const next = new Set(current);
|
|
83
|
+
if (!next.delete(id)) next.add(id);
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
rowLimit,
|
|
88
|
+
showMoreRows() {
|
|
89
|
+
setRowLimit((limit) => limit * 2);
|
|
90
|
+
},
|
|
91
|
+
flash,
|
|
92
|
+
setFlash(on) {
|
|
93
|
+
setFlashSignal(on);
|
|
94
|
+
options.onFlash?.(on);
|
|
95
|
+
},
|
|
96
|
+
paused,
|
|
97
|
+
setPaused(on) {
|
|
98
|
+
setPausedSignal(on);
|
|
99
|
+
backend.request({ type: on ? "pause" : "resume" });
|
|
100
|
+
options.onPause?.(on);
|
|
101
|
+
},
|
|
102
|
+
version,
|
|
103
|
+
refresh() {
|
|
104
|
+
setVersion((current) => current + 1);
|
|
105
|
+
},
|
|
106
|
+
snapshot,
|
|
107
|
+
nodes,
|
|
108
|
+
rows,
|
|
109
|
+
selectedNode,
|
|
110
|
+
explanation: useMemo(() => {
|
|
111
|
+
version();
|
|
112
|
+
const node = selectedNode();
|
|
113
|
+
if (node === void 0 || node.type !== "memo" && node.type !== "effect") return null;
|
|
114
|
+
return explainRun(backend.model, node.id);
|
|
115
|
+
}),
|
|
116
|
+
flames: useMemo(() => {
|
|
117
|
+
version();
|
|
118
|
+
return flameTimeline(backend.model, { last: 12 });
|
|
119
|
+
}),
|
|
120
|
+
hot: useMemo(() => {
|
|
121
|
+
version();
|
|
122
|
+
return componentStats(backend.model).filter((stat) => stat.updates > 0).slice(0, 10);
|
|
123
|
+
}),
|
|
124
|
+
errors: useMemo(() => {
|
|
125
|
+
version();
|
|
126
|
+
return backend.model.errors.toArray().reverse();
|
|
127
|
+
}),
|
|
128
|
+
hydration: useMemo(() => {
|
|
129
|
+
version();
|
|
130
|
+
return backend.model.hydration.toArray().reverse();
|
|
131
|
+
}),
|
|
132
|
+
highlight(id) {
|
|
133
|
+
backend.request({
|
|
134
|
+
type: "highlight",
|
|
135
|
+
id
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
nameOf(id) {
|
|
139
|
+
const node = nodes().get(id) ?? backend.model.node(id);
|
|
140
|
+
return node === void 0 ? `#${id}` : nodeLabel(node);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { TABS, createPanelState };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","names":[],"sources":["../../../src/ui/state.ts"],"sourcesContent":["import { type Accessor, useMemo, useSignal } from \"@shadow-js/core\";\nimport type { Backend } from \"../backend\";\nimport {\n type ComponentStat,\n componentStats,\n type Explanation,\n explainRun,\n type FlameFlush,\n flameTimeline,\n type GraphSnapshot,\n nodeLabel,\n type SnapshotNode,\n snapshotOf,\n} from \"../inspector\";\nimport type { ErrorRecord, HydrationRecord } from \"../model\";\nimport { buildRows, type TreeRow } from \"./rows\";\n\n/** The panel's tabs. @since 1.0.0 */\nexport type TabId = \"tree\" | \"timeline\" | \"state\" | \"issues\";\n\n/**\n * The panel's tabs in display order. Each entry has the tab's `id` and the label shown on it.\n *\n * @example\n * import { TABS } from \"@shadow-js/devtools\";\n * for (const tab of TABS) console.log(tab.id, tab.label);\n * @since 1.0.0\n */\nexport const TABS: ReadonlyArray<{ id: TabId; label: string }> = [\n { id: \"tree\", label: \"Components\" },\n { id: \"timeline\", label: \"Timeline\" },\n { id: \"state\", label: \"State\" },\n { id: \"issues\", label: \"Issues\" },\n];\n\n/** Everything the panel's components read and write. All state is signals; the heavy views are memos. @since 1.0.0 */\nexport interface PanelState {\n /**\n * The backend the panel reads from. The panel does not know whether it is local or remote.\n */\n readonly backend: Backend;\n /**\n * The active tab.\n */\n readonly tab: Accessor<TabId>;\n /**\n * Makes `tab` the active tab.\n */\n setTab(tab: TabId): void;\n /**\n * Id of the selected node. `0` when no node is selected, because `NO_ID` never names a node.\n */\n readonly selected: Accessor<number>;\n /**\n * Selects the node with this id.\n */\n select(id: number): void;\n /**\n * The tree's text filter.\n */\n readonly filter: Accessor<string>;\n /**\n * Sets the tree's text filter. Matching rows are shown with their ancestors.\n */\n setFilter(text: string): void;\n /**\n * Whether the tree shows components only.\n */\n readonly componentsOnly: Accessor<boolean>;\n /**\n * Shows components only when `on` is true, and every node otherwise.\n */\n setComponentsOnly(on: boolean): void;\n /**\n * Ids of the nodes the user collapsed.\n */\n readonly collapsed: Accessor<ReadonlySet<number>>;\n /**\n * Collapses the node when it is expanded, and expands it when it is collapsed.\n */\n toggleCollapsed(id: number): void;\n /**\n * The most tree rows shown at once.\n */\n readonly rowLimit: Accessor<number>;\n /**\n * Doubles the row limit. The tree's Show more button calls this.\n */\n showMoreRows(): void;\n /**\n * Whether components flash when they update.\n */\n readonly flash: Accessor<boolean>;\n /**\n * Switches flashing on or off, then calls `onFlash`.\n */\n setFlash(on: boolean): void;\n /**\n * Whether the user paused recording.\n */\n readonly paused: Accessor<boolean>;\n /**\n * Pauses or resumes recording. Sends `pause` or `resume` to the backend, then calls `onPause`.\n */\n setPaused(on: boolean): void;\n /** Counter bumped each time the model changed; views depend on it. */\n readonly version: Accessor<number>;\n /** Re-reads the model now. */\n refresh(): void;\n /**\n * The graph snapshot of the backend's model. Recomputed when `version` changes.\n */\n readonly snapshot: Accessor<GraphSnapshot>;\n /**\n * The nodes of {@link PanelState.snapshot}, by id.\n */\n readonly nodes: Accessor<ReadonlyMap<number, SnapshotNode>>;\n /**\n * The tree rows for the current filter, switches, collapse state and row limit, and how many matching\n * rows the limit cut.\n */\n readonly rows: Accessor<{ rows: TreeRow[]; hidden: number }>;\n /**\n * The selected node, or `undefined` when it is not in the snapshot.\n */\n readonly selectedNode: Accessor<SnapshotNode | undefined>;\n /**\n * Why the selected memo or effect last ran, or `null` when the selected node is not a memo or effect.\n */\n readonly explanation: Accessor<Explanation | null>;\n /**\n * The newest 12 flushes, laid out as flame lanes.\n */\n readonly flames: Accessor<FlameFlush[]>;\n /**\n * Up to 10 components that re-ran, most re-runs first.\n */\n readonly hot: Accessor<ComponentStat[]>;\n /**\n * The errors the runtime reported, newest first.\n */\n readonly errors: Accessor<ErrorRecord[]>;\n /**\n * The hydration mismatches, newest first.\n */\n readonly hydration: Accessor<HydrationRecord[]>;\n /** Highlights a component on the page. */\n highlight(id: number | null): void;\n /** The display name of a node, `#id` when it is gone. */\n nameOf(id: number): string;\n}\n\n/**\n * Options of {@link createPanelState}. Every field is optional.\n *\n * @since 1.0.0\n */\nexport interface PanelStateOptions {\n /**\n * Called when the user switches flashing on or off.\n */\n onFlash?(on: boolean): void;\n /**\n * Called with the pause state the user chose. The state also sends `pause` or `resume` to the backend,\n * so the agent stops or resumes recording either way.\n */\n onPause?(paused: boolean): void;\n /**\n * Starting tab.\n *\n * @default \"tree\"\n */\n tab?: TabId;\n /**\n * Starting row limit of the tree.\n *\n * @default 300\n */\n rowLimit?: number;\n /**\n * Starting value of the flash switch.\n *\n * @default true\n */\n flash?: boolean;\n}\n\n/**\n * Creates the panel's state. Call it inside the panel's reactive root, because the memos belong to the\n * current owner. The heavy views (snapshot, rows, flame lanes) are memos of the `version` counter, so a\n * burst of model changes costs one recomputation per refresh.\n *\n * @param backend The backend to read from.\n * @param options Initial values and callbacks.\n * @returns The state.\n * @example\n * import { createAgent, createLocalBackend, createPanelState } from \"@shadow-js/devtools\";\n * const backend = createLocalBackend(createAgent());\n * // Inside the panel's reactive root (the one render() creates):\n * const state = createPanelState(backend, { tab: \"timeline\", flash: false });\n * state.setTab(\"issues\");\n * @since 1.0.0\n */\nexport function createPanelState(backend: Backend, options: PanelStateOptions = {}): PanelState {\n const [tab, setTab] = useSignal<TabId>(options.tab ?? \"tree\");\n const [selected, setSelected] = useSignal(0);\n const [filter, setFilter] = useSignal(\"\");\n const [componentsOnly, setComponentsOnly] = useSignal(false);\n const [collapsed, setCollapsed] = useSignal<ReadonlySet<number>>(new Set());\n const [rowLimit, setRowLimit] = useSignal(options.rowLimit ?? 300);\n const [flash, setFlashSignal] = useSignal(options.flash !== false);\n const [paused, setPausedSignal] = useSignal(false);\n const [version, setVersion] = useSignal(0);\n\n const snapshot = useMemo(() => {\n version();\n return snapshotOf(backend.model);\n });\n const nodes = useMemo(() => new Map(snapshot().nodes.map((node) => [node.id, node] as const)));\n const rows = useMemo(() =>\n buildRows(snapshot(), {\n filter: filter(),\n componentsOnly: componentsOnly(),\n collapsed: collapsed(),\n limit: rowLimit(),\n }),\n );\n const selectedNode = useMemo(() => nodes().get(selected()));\n const explanation = useMemo(() => {\n version();\n const node = selectedNode();\n if (node === undefined || (node.type !== \"memo\" && node.type !== \"effect\")) return null;\n return explainRun(backend.model, node.id);\n });\n const flames = useMemo(() => {\n version();\n return flameTimeline(backend.model, { last: 12 });\n });\n const hot = useMemo(() => {\n version();\n return componentStats(backend.model)\n .filter((stat) => stat.updates > 0)\n .slice(0, 10);\n });\n const errors = useMemo(() => {\n version();\n return backend.model.errors.toArray().reverse();\n });\n const hydration = useMemo(() => {\n version();\n return backend.model.hydration.toArray().reverse();\n });\n\n return {\n backend,\n tab,\n setTab,\n selected,\n select: setSelected,\n filter,\n setFilter,\n componentsOnly,\n setComponentsOnly,\n collapsed,\n toggleCollapsed(id): void {\n setCollapsed((current) => {\n const next = new Set(current);\n if (!next.delete(id)) next.add(id);\n return next;\n });\n },\n rowLimit,\n showMoreRows(): void {\n setRowLimit((limit) => limit * 2);\n },\n flash,\n setFlash(on): void {\n setFlashSignal(on);\n options.onFlash?.(on);\n },\n paused,\n setPaused(on): void {\n setPausedSignal(on);\n // Through the backend, so a remote agent pauses too; the callback is for page-side hooks.\n void backend.request({ type: on ? \"pause\" : \"resume\" });\n options.onPause?.(on);\n },\n version,\n refresh(): void {\n setVersion((current) => current + 1);\n },\n snapshot,\n nodes,\n rows,\n selectedNode,\n explanation,\n flames,\n hot,\n errors,\n hydration,\n highlight(id): void {\n void backend.request({ type: \"highlight\", id });\n },\n nameOf(id): string {\n const node = nodes().get(id) ?? backend.model.node(id);\n return node === undefined ? `#${id}` : nodeLabel(node);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,MAAa,OAAoD;CAC/D;EAAE,IAAI;EAAQ,OAAO;CAAa;CAClC;EAAE,IAAI;EAAY,OAAO;CAAW;CACpC;EAAE,IAAI;EAAS,OAAO;CAAQ;CAC9B;EAAE,IAAI;EAAU,OAAO;CAAS;AAClC;;;;;;;;;;;;;;;;;AA0KA,SAAgB,iBAAiB,SAAkB,UAA6B,CAAC,GAAe;CAC9F,MAAM,CAAC,KAAK,UAAU,UAAiB,QAAQ,OAAO,MAAM;CAC5D,MAAM,CAAC,UAAU,eAAe,UAAU,CAAC;CAC3C,MAAM,CAAC,QAAQ,aAAa,UAAU,EAAE;CACxC,MAAM,CAAC,gBAAgB,qBAAqB,UAAU,KAAK;CAC3D,MAAM,CAAC,WAAW,gBAAgB,0BAA+B,IAAI,IAAI,CAAC;CAC1E,MAAM,CAAC,UAAU,eAAe,UAAU,QAAQ,YAAY,GAAG;CACjE,MAAM,CAAC,OAAO,kBAAkB,UAAU,QAAQ,UAAU,KAAK;CACjE,MAAM,CAAC,QAAQ,mBAAmB,UAAU,KAAK;CACjD,MAAM,CAAC,SAAS,cAAc,UAAU,CAAC;CAEzC,MAAM,WAAW,cAAc;EAC7B,QAAQ;EACR,OAAO,WAAW,QAAQ,KAAK;CACjC,CAAC;CACD,MAAM,QAAQ,cAAc,IAAI,IAAI,SAAS,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAU,CAAC,CAAC;CAC7F,MAAM,OAAO,cACX,UAAU,SAAS,GAAG;EACpB,QAAQ,OAAO;EACf,gBAAgB,eAAe;EAC/B,WAAW,UAAU;EACrB,OAAO,SAAS;CAClB,CAAC,CACH;CACA,MAAM,eAAe,cAAc,MAAM,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC;CA0B1D,OAAO;EACL;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA,gBAAgB,IAAU;GACxB,cAAc,YAAY;IACxB,MAAM,OAAO,IAAI,IAAI,OAAO;IAC5B,IAAI,CAAC,KAAK,OAAO,EAAE,GAAG,KAAK,IAAI,EAAE;IACjC,OAAO;GACT,CAAC;EACH;EACA;EACA,eAAqB;GACnB,aAAa,UAAU,QAAQ,CAAC;EAClC;EACA;EACA,SAAS,IAAU;GACjB,eAAe,EAAE;GACjB,QAAQ,UAAU,EAAE;EACtB;EACA;EACA,UAAU,IAAU;GAClB,gBAAgB,EAAE;GAElB,QAAa,QAAQ,EAAE,MAAM,KAAK,UAAU,SAAS,CAAC;GACtD,QAAQ,UAAU,EAAE;EACtB;EACA;EACA,UAAgB;GACd,YAAY,YAAY,UAAU,CAAC;EACrC;EACA;EACA;EACA;EACA;EACA,aAnEkB,cAAc;GAChC,QAAQ;GACR,MAAM,OAAO,aAAa;GAC1B,IAAI,SAAS,KAAA,KAAc,KAAK,SAAS,UAAU,KAAK,SAAS,UAAW,OAAO;GACnF,OAAO,WAAW,QAAQ,OAAO,KAAK,EAAE;EAC1C,CA8DY;EACV,QA9Da,cAAc;GAC3B,QAAQ;GACR,OAAO,cAAc,QAAQ,OAAO,EAAE,MAAM,GAAG,CAAC;EAClD,CA2DO;EACL,KA3DU,cAAc;GACxB,QAAQ;GACR,OAAO,eAAe,QAAQ,KAAK,CAAC,CACjC,QAAQ,SAAS,KAAK,UAAU,CAAC,CAAC,CAClC,MAAM,GAAG,EAAE;EAChB,CAsDI;EACF,QAtDa,cAAc;GAC3B,QAAQ;GACR,OAAO,QAAQ,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ;EAChD,CAmDO;EACL,WAnDgB,cAAc;GAC9B,QAAQ;GACR,OAAO,QAAQ,MAAM,UAAU,QAAQ,CAAC,CAAC,QAAQ;EACnD,CAgDU;EACR,UAAU,IAAU;GAClB,QAAa,QAAQ;IAAE,MAAM;IAAa;GAAG,CAAC;EAChD;EACA,OAAO,IAAY;GACjB,MAAM,OAAO,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,QAAQ,MAAM,KAAK,EAAE;GACrD,OAAO,SAAS,KAAA,IAAY,IAAI,OAAO,UAAU,IAAI;EACvD;CACF;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/ui/styles.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The panel's stylesheet. It lives inside the shadow root, so page styles cannot reach the panel and the
|
|
4
|
+
* panel's styles cannot leak out. Colors follow `prefers-color-scheme`. Development tool: `mountDevtools()`
|
|
5
|
+
* inserts it in development builds only.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { LAUNCHER_CSS, PANEL_CSS } from "@shadow-js/devtools";
|
|
9
|
+
* const host = document.createElement("div");
|
|
10
|
+
* const root = host.attachShadow({ mode: "open" });
|
|
11
|
+
* const style = document.createElement("style");
|
|
12
|
+
* style.textContent = `${PANEL_CSS}${LAUNCHER_CSS}`;
|
|
13
|
+
* root.append(style);
|
|
14
|
+
* document.body.append(host);
|
|
15
|
+
* @since 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
export declare const PANEL_CSS: string;
|
|
18
|
+
/**
|
|
19
|
+
* Styles of the small button that opens the panel. Like {@link PANEL_CSS}, it is inserted into the shadow
|
|
20
|
+
* root by `mountDevtools()` in development builds only.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* import { LAUNCHER_CSS } from "@shadow-js/devtools";
|
|
24
|
+
* const style = document.createElement("style");
|
|
25
|
+
* style.textContent = LAUNCHER_CSS;
|
|
26
|
+
* document.head.append(style);
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
*/
|
|
29
|
+
export declare const LAUNCHER_CSS: string;
|
|
30
|
+
//#endregion
|
|
31
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//#region src/ui/styles.ts
|
|
2
|
+
/**
|
|
3
|
+
* The panel's stylesheet. It lives inside the shadow root, so page styles cannot reach the panel and the
|
|
4
|
+
* panel's styles cannot leak out. Colors follow `prefers-color-scheme`. Development tool: `mountDevtools()`
|
|
5
|
+
* inserts it in development builds only.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { LAUNCHER_CSS, PANEL_CSS } from "@shadow-js/devtools";
|
|
9
|
+
* const host = document.createElement("div");
|
|
10
|
+
* const root = host.attachShadow({ mode: "open" });
|
|
11
|
+
* const style = document.createElement("style");
|
|
12
|
+
* style.textContent = `${PANEL_CSS}${LAUNCHER_CSS}`;
|
|
13
|
+
* root.append(style);
|
|
14
|
+
* document.body.append(host);
|
|
15
|
+
* @since 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
const PANEL_CSS = `
|
|
18
|
+
:host { all: initial; }
|
|
19
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
20
|
+
.panel {
|
|
21
|
+
--bg: #1e1f22; --bg2: #26282c; --bg3: #2f3237; --fg: #d7dae0; --muted: #8d939c; --line: #3a3d42;
|
|
22
|
+
--accent: #4da3ff; --good: #6bcb77; --warn: #ffc107; --bad: #ff6b6b; --select: #254a73;
|
|
23
|
+
position: fixed; z-index: 2147483647; width: 460px; height: 380px; min-width: 300px; min-height: 200px;
|
|
24
|
+
display: flex; flex-direction: column; overflow: hidden; resize: both;
|
|
25
|
+
background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 8px;
|
|
26
|
+
box-shadow: 0 8px 30px rgba(0,0,0,.45); font: 12px/1.45 ui-sans-serif, system-ui, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
@media (prefers-color-scheme: light) {
|
|
29
|
+
.panel { --bg: #fbfbfc; --bg2: #f0f1f3; --bg3: #e4e6ea; --fg: #22252a; --muted: #6b717b; --line: #cfd3d9;
|
|
30
|
+
--accent: #0b6bcb; --good: #1f8a3b; --warn: #a66b00; --bad: #c62828; --select: #cfe3fb; }
|
|
31
|
+
}
|
|
32
|
+
.panel[hidden] { display: none; }
|
|
33
|
+
.header { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: var(--bg2); border-bottom: 1px solid var(--line); cursor: grab; user-select: none; touch-action: none; }
|
|
34
|
+
.header.dragging { cursor: grabbing; }
|
|
35
|
+
.title { font-weight: 600; letter-spacing: .02em; }
|
|
36
|
+
.spacer { flex: 1; }
|
|
37
|
+
.tabs { display: flex; gap: 2px; padding: 4px 6px 0; background: var(--bg2); border-bottom: 1px solid var(--line); }
|
|
38
|
+
.tab { border: 0; background: transparent; color: var(--muted); padding: 5px 10px; border-radius: 6px 6px 0 0; cursor: pointer; font: inherit; }
|
|
39
|
+
.tab[aria-selected="true"] { color: var(--fg); background: var(--bg); box-shadow: inset 0 -2px 0 var(--accent); }
|
|
40
|
+
.badge { display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px; border-radius: 8px; background: var(--bg3); color: var(--fg); text-align: center; font-size: 10px; }
|
|
41
|
+
.badge.bad { background: var(--bad); color: #fff; }
|
|
42
|
+
.btn { border: 1px solid var(--line); background: var(--bg3); color: var(--fg); border-radius: 5px; padding: 2px 8px; cursor: pointer; font: inherit; }
|
|
43
|
+
.btn:hover { border-color: var(--accent); }
|
|
44
|
+
.btn[aria-pressed="true"] { background: var(--select); border-color: var(--accent); }
|
|
45
|
+
.body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
46
|
+
.toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
47
|
+
.input, .textarea { background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 5px; padding: 3px 6px; font: inherit; }
|
|
48
|
+
.input { flex: 1; min-width: 0; }
|
|
49
|
+
.textarea { width: 100%; min-height: 70px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
|
|
50
|
+
.split { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
51
|
+
.list { flex: 1; min-height: 60px; overflow: auto; }
|
|
52
|
+
.detail { max-height: 45%; overflow: auto; border-top: 1px solid var(--line); padding: 8px; background: var(--bg2); }
|
|
53
|
+
.row { display: flex; align-items: center; gap: 4px; padding: 1px 8px; cursor: default; white-space: nowrap; }
|
|
54
|
+
.row:hover { background: var(--bg3); }
|
|
55
|
+
.row[aria-selected="true"] { background: var(--select); }
|
|
56
|
+
.twisty { width: 14px; padding: 0; border: 0; background: none; text-align: center; color: var(--muted); cursor: pointer; flex: none; font: inherit; }
|
|
57
|
+
.twisty[hidden] { display: none; }
|
|
58
|
+
.row:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
59
|
+
.kind { font-size: 10px; padding: 0 4px; border-radius: 3px; background: var(--bg3); color: var(--muted); flex: none; }
|
|
60
|
+
.kind.component { color: var(--accent); }
|
|
61
|
+
.kind.signal { color: var(--good); }
|
|
62
|
+
.kind.memo { color: var(--warn); }
|
|
63
|
+
.name { overflow: hidden; text-overflow: ellipsis; }
|
|
64
|
+
.name.component { color: var(--accent); font-weight: 600; }
|
|
65
|
+
.value { color: var(--muted); overflow: hidden; text-overflow: ellipsis; }
|
|
66
|
+
.updates { margin-left: auto; color: var(--warn); font-variant-numeric: tabular-nums; flex: none; }
|
|
67
|
+
.more { padding: 6px 8px; }
|
|
68
|
+
.section { margin-bottom: 8px; }
|
|
69
|
+
.section h4 { margin: 0 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
|
|
70
|
+
.chain { list-style: none; margin: 0; padding: 0; }
|
|
71
|
+
.chain li { padding: 1px 0; }
|
|
72
|
+
.chain li::before { content: "→ "; color: var(--muted); }
|
|
73
|
+
.chain li:first-child::before { content: ""; }
|
|
74
|
+
.link { color: var(--accent); cursor: pointer; background: none; border: 0; padding: 0; font: inherit; text-decoration: underline; }
|
|
75
|
+
.flush { padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
76
|
+
.flush-head { display: flex; gap: 8px; color: var(--muted); margin-bottom: 4px; }
|
|
77
|
+
.lanes { position: relative; background: var(--bg2); border-radius: 4px; overflow: hidden; }
|
|
78
|
+
.bar { position: absolute; height: 14px; overflow: hidden; white-space: nowrap; font-size: 10px; line-height: 14px; padding: 0 2px; border-radius: 2px; background: var(--accent); color: #fff; cursor: pointer; }
|
|
79
|
+
.bar.memo { background: var(--warn); color: #000; }
|
|
80
|
+
.bar.unchanged { opacity: .55; }
|
|
81
|
+
.table { width: 100%; border-collapse: collapse; }
|
|
82
|
+
.table th, .table td { text-align: left; padding: 2px 8px; border-bottom: 1px solid var(--line); }
|
|
83
|
+
.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
84
|
+
.empty { padding: 16px; color: var(--muted); text-align: center; }
|
|
85
|
+
.status { color: var(--muted); }
|
|
86
|
+
.status.bad { color: var(--bad); }
|
|
87
|
+
.status.good { color: var(--good); }
|
|
88
|
+
.issue { padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
89
|
+
.issue code { color: var(--bad); }
|
|
90
|
+
.footer { display: flex; gap: 12px; padding: 4px 8px; border-top: 1px solid var(--line); background: var(--bg2); color: var(--muted); }
|
|
91
|
+
`;
|
|
92
|
+
/**
|
|
93
|
+
* Styles of the small button that opens the panel. Like {@link PANEL_CSS}, it is inserted into the shadow
|
|
94
|
+
* root by `mountDevtools()` in development builds only.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* import { LAUNCHER_CSS } from "@shadow-js/devtools";
|
|
98
|
+
* const style = document.createElement("style");
|
|
99
|
+
* style.textContent = LAUNCHER_CSS;
|
|
100
|
+
* document.head.append(style);
|
|
101
|
+
* @since 1.0.0
|
|
102
|
+
*/
|
|
103
|
+
const LAUNCHER_CSS = `
|
|
104
|
+
.launcher { position: fixed; right: 12px; bottom: 12px; z-index: 2147483647; width: 36px; height: 36px; border-radius: 50%;
|
|
105
|
+
border: 1px solid #3a3d42; background: #1e1f22; color: #4da3ff; font: 600 14px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer;
|
|
106
|
+
box-shadow: 0 4px 14px rgba(0,0,0,.4); }
|
|
107
|
+
.launcher:hover { border-color: #4da3ff; }
|
|
108
|
+
.launcher[hidden] { display: none; }
|
|
109
|
+
`;
|
|
110
|
+
//#endregion
|
|
111
|
+
export { LAUNCHER_CSS, PANEL_CSS };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","names":[],"sources":["../../../src/ui/styles.ts"],"sourcesContent":["/**\n * The panel's stylesheet. It lives inside the shadow root, so page styles cannot reach the panel and the\n * panel's styles cannot leak out. Colors follow `prefers-color-scheme`. Development tool: `mountDevtools()`\n * inserts it in development builds only.\n *\n * @example\n * import { LAUNCHER_CSS, PANEL_CSS } from \"@shadow-js/devtools\";\n * const host = document.createElement(\"div\");\n * const root = host.attachShadow({ mode: \"open\" });\n * const style = document.createElement(\"style\");\n * style.textContent = `${PANEL_CSS}${LAUNCHER_CSS}`;\n * root.append(style);\n * document.body.append(host);\n * @since 1.0.0\n */\nexport const PANEL_CSS: string = `\n:host { all: initial; }\n*, *::before, *::after { box-sizing: border-box; }\n.panel {\n --bg: #1e1f22; --bg2: #26282c; --bg3: #2f3237; --fg: #d7dae0; --muted: #8d939c; --line: #3a3d42;\n --accent: #4da3ff; --good: #6bcb77; --warn: #ffc107; --bad: #ff6b6b; --select: #254a73;\n position: fixed; z-index: 2147483647; width: 460px; height: 380px; min-width: 300px; min-height: 200px;\n display: flex; flex-direction: column; overflow: hidden; resize: both;\n background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 8px;\n box-shadow: 0 8px 30px rgba(0,0,0,.45); font: 12px/1.45 ui-sans-serif, system-ui, sans-serif;\n}\n@media (prefers-color-scheme: light) {\n .panel { --bg: #fbfbfc; --bg2: #f0f1f3; --bg3: #e4e6ea; --fg: #22252a; --muted: #6b717b; --line: #cfd3d9;\n --accent: #0b6bcb; --good: #1f8a3b; --warn: #a66b00; --bad: #c62828; --select: #cfe3fb; }\n}\n.panel[hidden] { display: none; }\n.header { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: var(--bg2); border-bottom: 1px solid var(--line); cursor: grab; user-select: none; touch-action: none; }\n.header.dragging { cursor: grabbing; }\n.title { font-weight: 600; letter-spacing: .02em; }\n.spacer { flex: 1; }\n.tabs { display: flex; gap: 2px; padding: 4px 6px 0; background: var(--bg2); border-bottom: 1px solid var(--line); }\n.tab { border: 0; background: transparent; color: var(--muted); padding: 5px 10px; border-radius: 6px 6px 0 0; cursor: pointer; font: inherit; }\n.tab[aria-selected=\"true\"] { color: var(--fg); background: var(--bg); box-shadow: inset 0 -2px 0 var(--accent); }\n.badge { display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px; border-radius: 8px; background: var(--bg3); color: var(--fg); text-align: center; font-size: 10px; }\n.badge.bad { background: var(--bad); color: #fff; }\n.btn { border: 1px solid var(--line); background: var(--bg3); color: var(--fg); border-radius: 5px; padding: 2px 8px; cursor: pointer; font: inherit; }\n.btn:hover { border-color: var(--accent); }\n.btn[aria-pressed=\"true\"] { background: var(--select); border-color: var(--accent); }\n.body { flex: 1; min-height: 0; display: flex; flex-direction: column; }\n.toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: 1px solid var(--line); }\n.input, .textarea { background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 5px; padding: 3px 6px; font: inherit; }\n.input { flex: 1; min-width: 0; }\n.textarea { width: 100%; min-height: 70px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }\n.split { flex: 1; min-height: 0; display: flex; flex-direction: column; }\n.list { flex: 1; min-height: 60px; overflow: auto; }\n.detail { max-height: 45%; overflow: auto; border-top: 1px solid var(--line); padding: 8px; background: var(--bg2); }\n.row { display: flex; align-items: center; gap: 4px; padding: 1px 8px; cursor: default; white-space: nowrap; }\n.row:hover { background: var(--bg3); }\n.row[aria-selected=\"true\"] { background: var(--select); }\n.twisty { width: 14px; padding: 0; border: 0; background: none; text-align: center; color: var(--muted); cursor: pointer; flex: none; font: inherit; }\n.twisty[hidden] { display: none; }\n.row:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }\n.kind { font-size: 10px; padding: 0 4px; border-radius: 3px; background: var(--bg3); color: var(--muted); flex: none; }\n.kind.component { color: var(--accent); }\n.kind.signal { color: var(--good); }\n.kind.memo { color: var(--warn); }\n.name { overflow: hidden; text-overflow: ellipsis; }\n.name.component { color: var(--accent); font-weight: 600; }\n.value { color: var(--muted); overflow: hidden; text-overflow: ellipsis; }\n.updates { margin-left: auto; color: var(--warn); font-variant-numeric: tabular-nums; flex: none; }\n.more { padding: 6px 8px; }\n.section { margin-bottom: 8px; }\n.section h4 { margin: 0 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }\n.chain { list-style: none; margin: 0; padding: 0; }\n.chain li { padding: 1px 0; }\n.chain li::before { content: \"→ \"; color: var(--muted); }\n.chain li:first-child::before { content: \"\"; }\n.link { color: var(--accent); cursor: pointer; background: none; border: 0; padding: 0; font: inherit; text-decoration: underline; }\n.flush { padding: 6px 8px; border-bottom: 1px solid var(--line); }\n.flush-head { display: flex; gap: 8px; color: var(--muted); margin-bottom: 4px; }\n.lanes { position: relative; background: var(--bg2); border-radius: 4px; overflow: hidden; }\n.bar { position: absolute; height: 14px; overflow: hidden; white-space: nowrap; font-size: 10px; line-height: 14px; padding: 0 2px; border-radius: 2px; background: var(--accent); color: #fff; cursor: pointer; }\n.bar.memo { background: var(--warn); color: #000; }\n.bar.unchanged { opacity: .55; }\n.table { width: 100%; border-collapse: collapse; }\n.table th, .table td { text-align: left; padding: 2px 8px; border-bottom: 1px solid var(--line); }\n.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }\n.empty { padding: 16px; color: var(--muted); text-align: center; }\n.status { color: var(--muted); }\n.status.bad { color: var(--bad); }\n.status.good { color: var(--good); }\n.issue { padding: 6px 8px; border-bottom: 1px solid var(--line); }\n.issue code { color: var(--bad); }\n.footer { display: flex; gap: 12px; padding: 4px 8px; border-top: 1px solid var(--line); background: var(--bg2); color: var(--muted); }\n`;\n\n/**\n * Styles of the small button that opens the panel. Like {@link PANEL_CSS}, it is inserted into the shadow\n * root by `mountDevtools()` in development builds only.\n *\n * @example\n * import { LAUNCHER_CSS } from \"@shadow-js/devtools\";\n * const style = document.createElement(\"style\");\n * style.textContent = LAUNCHER_CSS;\n * document.head.append(style);\n * @since 1.0.0\n */\nexport const LAUNCHER_CSS: string = `\n.launcher { position: fixed; right: 12px; bottom: 12px; z-index: 2147483647; width: 36px; height: 36px; border-radius: 50%;\n border: 1px solid #3a3d42; background: #1e1f22; color: #4da3ff; font: 600 14px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer;\n box-shadow: 0 4px 14px rgba(0,0,0,.4); }\n.launcher:hover { border-color: #4da3ff; }\n.launcher[hidden] { display: none; }\n`;\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,MAAa,YAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFjC,MAAa,eAAuB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { For, Show, addEventListener, anchor, className, createComponent, effect, insert, setAttribute, style, template } from "@shadow-js/web";
|
|
2
|
+
//#region src/ui/timeline.tsx
|
|
3
|
+
const _tmpl$ = /*#__PURE__*/ template("<div></div>");
|
|
4
|
+
const _tmpl$2 = /*#__PURE__*/ template("<div class=\"empty\">No flushes recorded yet.</div>");
|
|
5
|
+
const _tmpl$3 = /*#__PURE__*/ template("<span>queue: render / user</span>");
|
|
6
|
+
const _tmpl$4 = /*#__PURE__*/ template("<div class=\"flush\"><div class=\"flush-head\"><strong></strong><span> runs</span><span> ms</span></div><div class=\"lanes\"></div></div>");
|
|
7
|
+
const _tmpl$5 = /*#__PURE__*/ template("<tr><td><button type=\"button\" class=\"link\"></button></td><td class=\"num\"></td><td class=\"num\"></td></tr>");
|
|
8
|
+
const _tmpl$6 = /*#__PURE__*/ template("<div class=\"flush\"><div class=\"flush-head\"><strong>Components that re-ran most</strong></div><table class=\"table\" aria-label=\"Component update counts\"><thead><tr><th>Component</th><th class=\"num\">Updates</th><th class=\"num\">Total ms</th></tr></thead><tbody></tbody></table></div>");
|
|
9
|
+
const _tmpl$7 = /*#__PURE__*/ template("<section class=\"list\" aria-label=\"Timeline\"></section>");
|
|
10
|
+
const LANE_HEIGHT = 16;
|
|
11
|
+
/** One bar of the flame graph: positioned by time inside its flush, one lane per nesting level. */
|
|
12
|
+
const Bar = (props) => {
|
|
13
|
+
const _el$ = _tmpl$();
|
|
14
|
+
effect(() => {
|
|
15
|
+
className(_el$, `bar ${props.span.kind}${props.span.changed ? "" : " unchanged"}`);
|
|
16
|
+
});
|
|
17
|
+
effect(() => {
|
|
18
|
+
setAttribute(_el$, "data-span", props.span.seq);
|
|
19
|
+
});
|
|
20
|
+
effect(() => {
|
|
21
|
+
setAttribute(_el$, "title", `${props.span.label}: ${props.span.duration.toFixed(2)} ms${props.span.changed ? "" : " (value unchanged)"}`);
|
|
22
|
+
});
|
|
23
|
+
effect(() => ({
|
|
24
|
+
left: `${props.flush.duration <= 0 ? 0 : (props.span.start - props.flush.start) / props.flush.duration * 100}%`,
|
|
25
|
+
width: `${props.flush.duration <= 0 ? 100 : Math.max(props.span.duration / props.flush.duration * 100, 1)}%`,
|
|
26
|
+
top: `${props.span.depth * LANE_HEIGHT}px`
|
|
27
|
+
}), (_v$, _p$) => {
|
|
28
|
+
style(_el$, _v$, _p$);
|
|
29
|
+
});
|
|
30
|
+
addEventListener(_el$, "click", () => props.state.select(props.span.id));
|
|
31
|
+
insert(_el$, () => props.span.label);
|
|
32
|
+
return _el$;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The Timeline tab. Props: `state` is the panel's {@link PanelState}. It shows the newest 12 flushes, each
|
|
36
|
+
* with its run count and duration (flushes also show their queue sizes), and its runs laid out as flame lanes (a
|
|
37
|
+
* memo pulled by an effect sits one lane below the effect). Clicking a bar selects its node. Below the
|
|
38
|
+
* flushes it lists up to 10 components that re-ran most, with their update counts and total time.
|
|
39
|
+
*
|
|
40
|
+
* Internal to the panel: the package index does not export this component. Used in development builds
|
|
41
|
+
* only.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
const TimelineTab = (props) => {
|
|
46
|
+
const _el$ = _tmpl$7(), _el$2 = anchor(_el$, null);
|
|
47
|
+
insert(_el$, createComponent(Show, {
|
|
48
|
+
get when() {
|
|
49
|
+
return props.state.flames().length > 0;
|
|
50
|
+
},
|
|
51
|
+
get fallback() {
|
|
52
|
+
return _tmpl$2();
|
|
53
|
+
},
|
|
54
|
+
get children() {
|
|
55
|
+
return createComponent(For, {
|
|
56
|
+
get each() {
|
|
57
|
+
return props.state.flames().slice().reverse();
|
|
58
|
+
},
|
|
59
|
+
key: (flush) => flush.flush,
|
|
60
|
+
children: (flush) => {
|
|
61
|
+
const _el$ = _tmpl$4(), _el$2 = _el$.firstChild, _el$4 = _el$2.firstChild, _el$5 = _el$4.nextSibling, _el$7 = _el$5.firstChild, _el$6 = _el$5.nextSibling, _el$8 = _el$6.firstChild, _el$3 = _el$2.nextSibling;
|
|
62
|
+
effect(() => {
|
|
63
|
+
setAttribute(_el$, "data-flush", flush().flush);
|
|
64
|
+
});
|
|
65
|
+
insert(_el$4, () => flush().flush === 0 ? "outside a flush" : `flush #${flush().flush}`);
|
|
66
|
+
insert(_el$5, () => flush().spans.length, _el$7);
|
|
67
|
+
insert(_el$6, () => flush().duration.toFixed(2), _el$8);
|
|
68
|
+
insert(_el$2, createComponent(Show, {
|
|
69
|
+
get when() {
|
|
70
|
+
return flush().flush !== 0;
|
|
71
|
+
},
|
|
72
|
+
get children() {
|
|
73
|
+
const _el$ = _tmpl$3(), _el$3 = _el$.firstChild.splitText(7), _el$4 = _el$3.splitText(10);
|
|
74
|
+
insert(_el$, () => flush().render, _el$3);
|
|
75
|
+
insert(_el$, () => flush().user, _el$4);
|
|
76
|
+
return _el$;
|
|
77
|
+
}
|
|
78
|
+
}, "Show"));
|
|
79
|
+
effect(() => ({ height: `${Math.max(flush().lanes, 1) * LANE_HEIGHT}px` }), (_v$, _p$) => {
|
|
80
|
+
style(_el$3, _v$, _p$);
|
|
81
|
+
});
|
|
82
|
+
insert(_el$3, createComponent(For, {
|
|
83
|
+
get each() {
|
|
84
|
+
return flush().spans;
|
|
85
|
+
},
|
|
86
|
+
key: (span) => span.seq,
|
|
87
|
+
children: (span) => createComponent(Bar, {
|
|
88
|
+
get state() {
|
|
89
|
+
return props.state;
|
|
90
|
+
},
|
|
91
|
+
get flush() {
|
|
92
|
+
return flush();
|
|
93
|
+
},
|
|
94
|
+
get span() {
|
|
95
|
+
return span();
|
|
96
|
+
}
|
|
97
|
+
}, "Bar")
|
|
98
|
+
}, "For"));
|
|
99
|
+
return _el$;
|
|
100
|
+
}
|
|
101
|
+
}, "For");
|
|
102
|
+
}
|
|
103
|
+
}, "Show"), _el$2);
|
|
104
|
+
insert(_el$, createComponent(Show, {
|
|
105
|
+
get when() {
|
|
106
|
+
return props.state.hot().length > 0;
|
|
107
|
+
},
|
|
108
|
+
get children() {
|
|
109
|
+
const _el$ = _tmpl$6(), _el$3 = _el$.firstChild.nextSibling.firstChild.nextSibling;
|
|
110
|
+
insert(_el$3, createComponent(For, {
|
|
111
|
+
get each() {
|
|
112
|
+
return props.state.hot();
|
|
113
|
+
},
|
|
114
|
+
key: (stat) => stat.id,
|
|
115
|
+
children: (stat) => {
|
|
116
|
+
const _el$ = _tmpl$5(), _el$2 = _el$.firstChild, _el$5 = _el$2.firstChild, _el$3 = _el$2.nextSibling, _el$4 = _el$3.nextSibling;
|
|
117
|
+
effect(() => {
|
|
118
|
+
setAttribute(_el$, "data-component", stat().id);
|
|
119
|
+
});
|
|
120
|
+
addEventListener(_el$5, "click", () => props.state.select(stat().id));
|
|
121
|
+
insert(_el$5, () => stat().name);
|
|
122
|
+
insert(_el$3, () => stat().updates);
|
|
123
|
+
insert(_el$4, () => stat().totalDuration.toFixed(2));
|
|
124
|
+
return _el$;
|
|
125
|
+
}
|
|
126
|
+
}, "For"));
|
|
127
|
+
return _el$;
|
|
128
|
+
}
|
|
129
|
+
}, "Show"));
|
|
130
|
+
return _el$;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
export { TimelineTab };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=timeline.js.map
|