+
+
{
+ /** A square button that holds only an icon. */
+ icon?: boolean;
+ /** The one filled action of a bar. */
+ primary?: boolean;
+}
+
+/** An action of the Health view, in the size and weight the prototype gives it. */
+export function HealthButton(props: HealthButtonProps): JSX.Element {
+ const rest = omit(props, 'icon', 'primary', 'children');
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/src/dev-toolbar/health/HealthRow.tsx b/src/dev-toolbar/health/HealthRow.tsx
new file mode 100644
index 0000000..8867c23
--- /dev/null
+++ b/src/dev-toolbar/health/HealthRow.tsx
@@ -0,0 +1,45 @@
+import type { JSX } from '@solidjs/web';
+import { omit } from 'solid-js';
+
+export type Tone = 'error' | 'warn' | 'info';
+
+const GLYPHS: Record = { error: '✕', warn: '!', info: 'i' };
+
+/** The glyph of a thrown error or an issue: red cross, amber bang or blue i. */
+export function Glyph(props: { tone: Tone }): JSX.Element {
+ return (
+
+ {GLYPHS[props.tone]}
+
+ );
+}
+
+export interface HealthRowProps extends Omit, 'title'> {
+ tone: Tone;
+ title: string;
+ subtitle: string;
+ /** The age, or the count of a repeated issue. */
+ time: string;
+ selected: boolean;
+}
+
+/** One entry of the Health list: glyph, title and message, and when. */
+export function HealthRow(props: HealthRowProps): JSX.Element {
+ const rest = omit(props, 'tone', 'title', 'subtitle', 'time', 'selected');
+ return (
+
+
+
+ {props.title}
+ {props.subtitle}
+
+ {props.time}
+
+ );
+}
diff --git a/src/dev-toolbar/health/describe-error.ts b/src/dev-toolbar/health/describe-error.ts
new file mode 100644
index 0000000..c0c07d3
--- /dev/null
+++ b/src/dev-toolbar/health/describe-error.ts
@@ -0,0 +1,10 @@
+/** The first line of a thrown value, split into its name and message. */
+export function describeError(error: unknown): { name: string; message: string } {
+ if (error instanceof Error) {
+ return { name: error.name || 'Error', message: error.message };
+ }
+ if (error instanceof ErrorEvent) {
+ return { name: 'Error', message: error.message };
+ }
+ return { name: 'Thrown value', message: String(error) };
+}
diff --git a/src/dev-toolbar/health/index.tsx b/src/dev-toolbar/health/index.tsx
new file mode 100644
index 0000000..a072551
--- /dev/null
+++ b/src/dev-toolbar/health/index.tsx
@@ -0,0 +1,290 @@
+import type { JSX } from '@solidjs/web';
+import { clientOnly } from '@solidjs/web';
+import { createEffect, createMemo, createSignal, For, Show } from 'solid-js';
+import { ResizeHandle } from '../../ui/ResizeHandle.js';
+import { useResizableDetail } from '../../ui/use-resizable-detail.js';
+import type { ErrorRecord } from '../error-record.js';
+import { IssueDetail, matchesIssue, TONES } from '../issues/index.js';
+import { isIssuesAvailable, issueText, type IssueEntry } from '../issues/tracker.js';
+import { describeError } from './describe-error.js';
+import { HealthRow } from './HealthRow.js';
+import { leadingPane } from './leading-pane.js';
+import { formatAge } from './time.js';
+import './styles.css';
+
+export { describeError } from './describe-error.js';
+
+// The error panel is the heaviest part of the view, so it loads when an error is opened.
+const ErrorViewer = clientOnly(() => import('../error-viewer/index.js'), { lazy: true });
+
+/** What the detail pane shows: a thrown error by its index, or an issue by its key. */
+type Selection = { kind: 'error'; index: number } | { kind: 'issue'; key: string };
+
+/** Which entries the list shows. */
+type Filter = 'all' | 'error' | 'warn' | 'info';
+
+const FILTERS: { value: Filter; label: string }[] = [
+ { value: 'all', label: 'All' },
+ { value: 'error', label: 'Errors' },
+ { value: 'warn', label: 'Warnings' },
+ { value: 'info', label: 'Info' },
+];
+
+/** How often the ages in the list move on. */
+const TICK_MS = 5000;
+
+export interface HealthViewerProps {
+ show?: boolean;
+ /** The errors the toolbar caught, newest first. */
+ errors: ErrorRecord[];
+ resetError: () => void;
+ issues: IssueEntry[];
+ onClearIssues: () => void;
+ onViewOwner?: (owner: object) => void;
+ onViewInGraph?: (node: object) => void;
+ /** The label of the action an error's origin belongs to, such as `setCurrency("XYZ")`. */
+ describeOrigin?: (origin: object) => string | undefined;
+ /** Opens the action an error's origin belongs to in the Activity view. */
+ onShowInActivity?: (origin: object) => void;
+}
+
+/** Thrown errors and the runtime's diagnostics in one list, with the selected entry beside it. */
+export default function HealthViewer(props: HealthViewerProps): JSX.Element {
+ const [filter, setFilter] = createSignal('all');
+ const [query, setQuery] = createSignal('');
+ const [selected, setSelected] = createSignal();
+ const [tick, setTick] = createSignal(0);
+
+ // The handle is the left edge of the detail, so it sizes the list before it.
+ const listWidth = leadingPane(
+ useResizableDetail({
+ storageKey: 'solid-start-devtools:health-list-width',
+ defaultWidth: 320,
+ minWidth: 200,
+ }),
+ );
+
+ // The ages in the rows move on without a change to the lists, so a clock
+ // runs while the view is shown.
+ createEffect(
+ () => props.show,
+ (show) => {
+ if (!show) return;
+ const timer = setInterval(() => setTick((value) => value + 1), TICK_MS);
+ return () => clearInterval(timer);
+ },
+ );
+
+ // A new error lands at the top of the list and is opened, as the old Errors
+ // panel did. The newest record is compared, so a capped list still works.
+ let newest: ErrorRecord | undefined;
+ createEffect(
+ () => props.errors[0],
+ (first) => {
+ if (first && first !== newest) setSelected({ kind: 'error', index: 0 });
+ newest = first;
+ },
+ );
+
+ // Thrown errors count as errors, so only the All and Errors chips list them.
+ const shownErrors = createMemo(() => {
+ const result: { record: ErrorRecord; index: number }[] = [];
+ if (filter() !== 'all' && filter() !== 'error') return result;
+ const search = query().trim().toLowerCase();
+ let index = 0;
+ for (const record of props.errors) {
+ const { name, message } = describeError(record.error);
+ if (!search || `${name} ${message}`.toLowerCase().includes(search)) {
+ result.push({ record, index });
+ }
+ index++;
+ }
+ return result;
+ });
+
+ const shownIssues = createMemo(() => {
+ const search = query().trim().toLowerCase();
+ const result: IssueEntry[] = [];
+ for (const entry of props.issues) {
+ if (filter() !== 'all' && entry.event.severity !== filter()) continue;
+ if (matchesIssue(entry, search)) result.push(entry);
+ }
+ return result;
+ });
+
+ const total = () => props.errors.length + props.issues.length;
+ const shownCount = () => shownErrors().length + shownIssues().length;
+
+ // What the detail shows: the pick, else the first row. A cleared pick falls back the same way.
+ const current = createMemo(() => {
+ const value = selected();
+ if (value?.kind === 'error' && value.index < props.errors.length) return value;
+ if (value?.kind === 'issue') {
+ const entry = props.issues.find((item) => item.key === value.key);
+ if (entry) return { kind: 'issue' as const, entry };
+ }
+ const first = shownErrors()[0];
+ if (first) return { kind: 'error' as const, index: first.index };
+ const issue = shownIssues()[0];
+ return issue ? { kind: 'issue' as const, entry: issue } : undefined;
+ });
+
+ function clearAll(): void {
+ setSelected(undefined);
+ props.resetError();
+ props.onClearIssues();
+ }
+
+ // The age of a thrown error is on the clock the timeline uses, an issue's on the wall clock.
+ function errorAge(record: ErrorRecord): string {
+ tick();
+ return formatAge(performance.now() - record.at);
+ }
+
+ function issueAge(entry: IssueEntry): string {
+ tick();
+ if (entry.count > 1) return `×${entry.count}`;
+ return formatAge(Date.now() - entry.lastAt);
+ }
+
+ function emptyText(): string {
+ return isIssuesAvailable()
+ ? 'No errors or issues so far.'
+ : 'No errors so far. Issues need a development build of solid-js.';
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ setQuery(event.currentTarget.value)}
+ />
+
+
{`${shownCount()} of ${total()}`}
+
+
+
+ {(item) => (
+ setFilter(item.value)}
+ >
+ {item.label}
+
+ )}
+
+
+ Clear
+
+
+
+ {(item) => {
+ const info = () => describeError(item.record.error);
+ const isSelected = () => {
+ const value = current();
+ return value?.kind === 'error' && value.index === item.index;
+ };
+ return (
+ setSelected({ kind: 'error', index: item.index })}
+ />
+ );
+ }}
+
+
+ {(entry) => {
+ const isSelected = () => {
+ const value = current();
+ return value?.kind === 'issue' && value.entry.key === entry.key;
+ };
+ return (
+ setSelected({ kind: 'issue', key: entry.key })}
+ />
+ );
+ }}
+
+
+
+
+
+ {total() ? 'Nothing matches this filter.' : emptyText()}
+
+ }
+ >
+ {(value) => (
+
+ }
+ >
+ {(error) => (
+
record.error)}
+ records={props.errors}
+ index={error().index}
+ onIndexChange={(index: number) => setSelected({ kind: 'error', index })}
+ describeOrigin={props.describeOrigin}
+ onShowInActivity={props.onShowInActivity}
+ resetError={() => {
+ setSelected(undefined);
+ props.resetError();
+ }}
+ />
+ )}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/dev-toolbar/health/leading-pane.ts b/src/dev-toolbar/health/leading-pane.ts
new file mode 100644
index 0000000..bf4f21d
--- /dev/null
+++ b/src/dev-toolbar/health/leading-pane.ts
@@ -0,0 +1,16 @@
+import type { ResizableDetail } from '../../ui/use-resizable-detail.js';
+
+/**
+ * Turns a detail width into the width of the pane before the handle.
+ *
+ * The handle sits on the left edge of the pane after it, so dragging it to
+ * the left makes the pane before it narrower, where it would widen a detail
+ * pane that sits at the right edge.
+ */
+export function leadingPane(detail: ResizableDetail): ResizableDetail {
+ return {
+ ...detail,
+ drag: (offsetX) => detail.drag(-offsetX),
+ step: (deltaX, panelWidth) => detail.step(-deltaX, panelWidth),
+ };
+}
diff --git a/src/dev-toolbar/health/styles.css b/src/dev-toolbar/health/styles.css
new file mode 100644
index 0000000..9ddde01
--- /dev/null
+++ b/src/dev-toolbar/health/styles.css
@@ -0,0 +1,245 @@
+/* the prototype's split: the list on the left, the selected entry beside it */
+[data-solid-health-viewer] {
+ display: flex;
+ flex-direction: row;
+
+ height: 100%;
+ min-height: 0;
+ min-width: 0;
+
+ color: var(--start-dt-text);
+ font:
+ 13px/1.45 system-ui,
+ -apple-system,
+ sans-serif;
+}
+
+[data-solid-health-list] {
+ box-sizing: border-box;
+ flex: none;
+ /* keeps the detail usable when the window shrinks below a saved width */
+ max-width: calc(100% - 240px);
+ overflow: auto;
+ padding: 6px;
+}
+
+[data-solid-health-searchrow] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 4px 0;
+}
+
+[data-solid-health-search] {
+ box-sizing: border-box;
+ display: flex;
+ flex: 1;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ height: 28px;
+ padding: 0 8px;
+
+ border: 1px solid var(--start-dt-line);
+ border-radius: 7px;
+ background: var(--start-dt-well);
+ color: var(--start-dt-faint);
+}
+
+[data-solid-health-search]:focus-within {
+ border-color: var(--start-dt-solid);
+}
+
+[data-solid-dev-toolbar] [data-solid-health-search] svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.6;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+[data-solid-health-search] input {
+ flex: 1;
+ min-width: 0;
+ padding: 0;
+
+ border: 0;
+ outline: 0;
+ background: none;
+ color: var(--start-dt-text);
+ font: inherit;
+ font-size: 12px;
+}
+
+[data-solid-health-search] input::placeholder {
+ color: var(--start-dt-faint);
+}
+
+[data-solid-health-count] {
+ flex: none;
+ color: var(--start-dt-faint);
+ font: 11px var(--start-dt-mono);
+}
+
+[data-solid-health-filters] {
+ flex-wrap: wrap;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 4px;
+}
+
+[data-solid-health-chip] {
+ padding: 3px 9px;
+
+ border: 1px solid var(--start-dt-line-2);
+ border-radius: 99px;
+ background: none;
+ color: var(--start-dt-text-muted);
+ font: inherit;
+ font-size: 11.5px;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+[data-solid-health-chip]:hover:not(:disabled) {
+ color: var(--start-dt-text);
+}
+
+[data-solid-health-chip][data-on] {
+ background: var(--start-dt-raise-2);
+ border-color: transparent;
+ color: var(--start-dt-text);
+}
+
+[data-solid-health-chip][data-ghost] {
+ margin-left: auto;
+ border-style: dashed;
+ color: var(--start-dt-faint);
+}
+
+[data-solid-health-chip]:disabled {
+ cursor: default;
+ opacity: 0.6;
+}
+
+[data-solid-health-chip]:focus-visible {
+ outline: 2px solid var(--start-dt-solid);
+ outline-offset: 1px;
+}
+
+/* a row: glyph, title over message, then the age. Selection is a fill, never a side rule. */
+[data-solid-health-row] {
+ display: grid;
+ grid-template-columns: 18px minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+
+ box-sizing: border-box;
+ width: 100%;
+ padding: 7px 10px;
+
+ border: 0;
+ border-radius: 7px;
+ background: none;
+ color: var(--start-dt-text);
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+[data-solid-health-row]:hover {
+ background: oklch(1 0 0 / 0.04);
+}
+
+[data-solid-health-row][data-selected] {
+ background: var(--start-dt-raise-2);
+}
+
+[data-solid-health-row]:focus-visible {
+ outline: 2px solid var(--start-dt-solid);
+ outline-offset: -2px;
+}
+
+[data-solid-health-row-what] {
+ min-width: 0;
+}
+
+[data-solid-health-row-what] > b {
+ display: block;
+ overflow: hidden;
+
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+[data-solid-health-row-sub] {
+ display: block;
+ overflow: hidden;
+
+ color: var(--start-dt-faint);
+ font-size: 11.5px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+[data-solid-health-row-time] {
+ color: var(--start-dt-faint);
+ font: 11px var(--start-dt-mono);
+ text-align: right;
+}
+
+/* the 18px glyph: red cross, amber bang, blue i */
+[data-solid-health-glyph] {
+ display: grid;
+ place-items: center;
+ flex: none;
+
+ width: 18px;
+ height: 18px;
+
+ border-radius: 5px;
+ font: 700 10px var(--start-dt-mono);
+}
+
+[data-solid-health-glyph='error'] {
+ background: var(--start-dt-red-soft);
+ color: var(--start-dt-red);
+}
+
+[data-solid-health-glyph='warn'] {
+ background: var(--start-dt-amber-soft);
+ color: var(--start-dt-amber);
+}
+
+[data-solid-health-glyph='info'] {
+ background: var(--start-dt-solid-soft);
+ color: var(--start-dt-solid);
+}
+
+/* the right pane; the resize handle sits on its left edge */
+[data-solid-health-detail] {
+ position: relative;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+ min-width: 0;
+
+ border-left: 1px solid var(--start-dt-line);
+}
+
+[data-solid-health-detail] > [data-solid-dev-toolbar-panel] {
+ flex: 1;
+ min-height: 0;
+}
+
+[data-solid-health-empty] {
+ padding: 16px;
+ color: var(--start-dt-faint);
+ font-size: 12px;
+}
diff --git a/src/dev-toolbar/health/time.test.ts b/src/dev-toolbar/health/time.test.ts
new file mode 100644
index 0000000..76524f2
--- /dev/null
+++ b/src/dev-toolbar/health/time.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+import { formatAge, formatClock, wallTime } from './time.js';
+
+describe('formatAge', () => {
+ it('rounds down to the largest unit', () => {
+ expect(formatAge(1000)).toBe('now');
+ expect(formatAge(12_000)).toBe('12s');
+ expect(formatAge(3 * 60_000 + 20_000)).toBe('3m');
+ expect(formatAge(2 * 3_600_000)).toBe('2h');
+ expect(formatAge(3 * 86_400_000)).toBe('3d');
+ });
+
+ it('treats a time in the future as now', () => {
+ expect(formatAge(-500)).toBe('now');
+ });
+});
+
+describe('wallTime', () => {
+ it('moves a performance.now() time onto the wall clock', () => {
+ expect(wallTime(900, 10_000, 1000)).toBe(9900);
+ });
+});
+
+describe('formatClock', () => {
+ it('pads each part to two digits', () => {
+ expect(formatClock(new Date(2026, 0, 1, 4, 5, 9).getTime())).toBe('04:05:09');
+ });
+});
diff --git a/src/dev-toolbar/health/time.ts b/src/dev-toolbar/health/time.ts
new file mode 100644
index 0000000..30a4b5b
--- /dev/null
+++ b/src/dev-toolbar/health/time.ts
@@ -0,0 +1,25 @@
+/** The wall-clock time a record made on the `performance.now()` clock was made at. */
+export function wallTime(at: number, now = Date.now(), tick = performance.now()): number {
+ return now - (tick - at);
+}
+
+/** How long ago, as the list shows it: "now", "12s", "3m", "2h", "4d". */
+export function formatAge(milliseconds: number): string {
+ const seconds = Math.max(0, Math.floor(milliseconds / 1000));
+ if (seconds < 5) return 'now';
+ if (seconds < 60) return `${seconds}s`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h`;
+ return `${Math.floor(hours / 24)}d`;
+}
+
+/** The time of day as hh:mm:ss on a 24-hour clock. */
+export function formatClock(time: number): string {
+ const date = new Date(time);
+ const parts = [date.getHours(), date.getMinutes(), date.getSeconds()];
+ const padded: string[] = [];
+ for (const part of parts) padded.push(String(part).padStart(2, '0'));
+ return padded.join(':');
+}
diff --git a/src/dev-toolbar/index.css b/src/dev-toolbar/index.css
index f884faa..af55039 100644
--- a/src/dev-toolbar/index.css
+++ b/src/dev-toolbar/index.css
@@ -14,6 +14,25 @@
--start-dt-danger: oklch(0.63 0.21 25);
--start-dt-shadow: 0 12px 32px oklch(0 0 0 / 0.4), 0 2px 8px oklch(0 0 0 / 0.25);
+ /* the redesign's palette: raised surfaces, hairlines, text steps and status hues */
+ --start-dt-raise: oklch(0.21 0.017 265);
+ --start-dt-raise-2: oklch(0.26 0.022 265);
+ --start-dt-line: oklch(1 0 0 / 0.08);
+ --start-dt-line-2: oklch(1 0 0 / 0.14);
+ --start-dt-faint: oklch(0.55 0.015 265);
+ --start-dt-well: oklch(0 0 0 / 0.28);
+ --start-dt-solid: oklch(0.7 0.14 245);
+ --start-dt-solid-soft: oklch(0.7 0.14 245 / 0.15);
+ --start-dt-red: oklch(0.68 0.2 25);
+ --start-dt-red-soft: oklch(0.68 0.2 25 / 0.16);
+ --start-dt-amber: oklch(0.8 0.15 80);
+ --start-dt-amber-soft: oklch(0.8 0.15 80 / 0.16);
+ --start-dt-green: oklch(0.78 0.15 155);
+ --start-dt-green-soft: oklch(0.78 0.15 155 / 0.15);
+ --start-dt-violet: oklch(0.72 0.15 300);
+ --start-dt-violet-soft: oklch(0.72 0.15 300 / 0.17);
+ --start-dt-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, monospace;
+
position: fixed;
left: 0;
right: 0;
@@ -157,6 +176,20 @@
text-align: center;
}
+[data-solid-dev-toolbar-handle-issues] {
+ min-width: 1.125rem;
+ padding: 0 0.3125rem;
+
+ border-radius: 9999px;
+ background: oklch(0.8 0.15 80 / 0.18);
+ color: oklch(0.85 0.12 80);
+
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.6875rem;
+ line-height: 1.125rem;
+ text-align: center;
+}
+
@media (prefers-reduced-motion: reduce) {
[data-solid-dev-toolbar-handle-grip],
[data-solid-dev-toolbar-handle-content] {
@@ -166,17 +199,21 @@
[data-solid-dev-toolbar-version] {
border-radius: 9999px;
- color: var(--start-dt-accent);
- background: var(--start-dt-accent-soft);
+ color: var(--start-dt-text-muted);
+ background: oklch(1 0 0 / 0.06);
padding: 0.0625rem 0.5rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 0.75rem;
+ font-size: 0.6875rem;
line-height: 1.25rem;
- font-weight: 600;
white-space: nowrap;
}
+[data-solid-dev-toolbar-version] b {
+ color: var(--start-dt-text);
+ font-weight: 600;
+}
+
/* open: a dock along the bottom of the page */
[data-solid-dev-toolbar-dock] {
@@ -223,112 +260,110 @@
[data-solid-dev-toolbar-header] {
display: flex;
align-items: center;
- gap: 0.75rem;
+ gap: 8px;
- padding: 0.25rem 0.5rem 0.25rem 0.75rem;
+ height: 42px;
+ flex-shrink: 0;
+ box-sizing: border-box;
+ padding: 0 6px 0 12px;
- background: var(--start-dt-surface);
- border-bottom: var(--start-dt-border-soft) 1px solid;
+ font-size: 13px;
+ background: var(--start-dt-bg);
+ border-bottom: 1px solid var(--start-dt-line);
white-space: nowrap;
overflow: hidden;
}
-[data-solid-dev-toolbar-brand] {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-
- font-size: 0.8125rem;
- font-weight: 600;
- letter-spacing: -0.01em;
+[data-solid-dev-toolbar-header] > svg {
+ width: 18px;
+ height: 18px;
}
-[data-solid-dev-toolbar-route] {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
+[data-solid-dev-toolbar-header] [data-solid-icon-button] {
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ display: grid;
+ place-items: center;
+ border-radius: 7px;
+}
- color: var(--start-dt-text-muted);
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 0.75rem;
+[data-solid-dev-toolbar-header] [data-solid-icon-button] svg {
+ width: 16px;
+ height: 16px;
}
-[data-solid-dev-toolbar-funnel] {
+[data-solid-dev-toolbar-search] {
display: flex;
align-items: center;
- gap: 0.375rem;
- padding: 0.125rem 0.5rem;
- border-radius: 9999px;
- background: var(--start-dt-accent-soft);
- border: 1px solid var(--start-dt-border-soft);
- font-size: 0.75rem;
- line-height: 1.25rem;
- white-space: nowrap;
-}
+ gap: 8px;
-[data-solid-dev-toolbar-funnel-label] {
- color: var(--start-dt-text-muted);
- font-size: 0.6875rem;
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
+ width: 250px;
+ min-width: 0;
+ height: 28px;
+ padding: 0 8px;
-[data-solid-dev-toolbar-funnel-name] {
- background: none;
- border: none;
- padding: 0;
font: inherit;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-weight: 600;
- color: var(--start-dt-accent);
+ font-size: 13px;
+ color: var(--start-dt-faint);
cursor: pointer;
- text-decoration: underline;
- text-decoration-style: dotted;
- text-underline-offset: 2px;
+
+ background: var(--start-dt-well);
+ border: 1px solid var(--start-dt-line);
+ border-radius: 7px;
}
-[data-solid-dev-toolbar-funnel-name]:hover {
- text-decoration-style: solid;
+[data-solid-dev-toolbar-search]:hover {
+ border-color: var(--start-dt-line-2);
}
-[data-solid-dev-toolbar-funnel-action] {
- background: none;
- border: none;
- padding: 0 0.25rem;
- font: inherit;
- font-size: 0.6875rem;
- color: var(--start-dt-text);
- cursor: pointer;
- text-decoration: underline;
- opacity: 0.85;
+[data-solid-dev-toolbar-search]:focus-visible {
+ outline: 2px solid var(--start-dt-accent);
+ outline-offset: -2px;
}
-[data-solid-dev-toolbar-funnel-action]:hover {
- opacity: 1;
- color: var(--start-dt-accent);
+[data-solid-dev-toolbar-search] svg {
+ width: 16px;
+ height: 16px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.6;
+ stroke-linecap: round;
}
-[data-solid-dev-toolbar-funnel-clear] {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 1rem;
- height: 1rem;
- border-radius: 9999px;
- background: none;
- border: none;
- padding: 0;
- font: inherit;
- font-size: 0.8125rem;
- line-height: 1;
- color: var(--start-dt-text-muted);
- cursor: pointer;
+[data-solid-dev-toolbar-search] span {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+}
+
+[data-solid-dev-toolbar-search] kbd {
+ padding: 0 4px;
+ border: 1px solid var(--start-dt-line-2);
+ border-radius: 4px;
+ color: var(--start-dt-faint);
+ font: 10px var(--start-dt-mono);
}
-[data-solid-dev-toolbar-funnel-clear]:hover {
+[data-solid-dev-toolbar-route] {
+ min-width: 0;
+ max-width: 16rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0 2px;
+
color: var(--start-dt-text);
- background: var(--start-dt-surface-hover);
+ font: 12px var(--start-dt-mono);
+}
+
+/* the hairline between the version and the route */
+[data-solid-dev-toolbar-header] [data-solid-dev-toolbar-version] + [data-solid-dev-toolbar-route] {
+ margin-left: 3px;
+ padding-left: 11px;
+ border-left: 1px solid var(--start-dt-line-2);
}
[data-solid-icon-button][data-active] {
@@ -340,105 +375,84 @@
flex: 1;
}
-[data-solid-dev-toolbar-main] {
- flex: 1;
- display: flex;
- min-height: 0;
-}
+/* the views and the tabs of the current view: segmented controls in the header */
-[data-solid-dev-toolbar-nav] {
+[data-solid-dev-toolbar-views] {
display: flex;
- flex-direction: column;
- gap: 0.125rem;
+ align-items: center;
+ gap: 2px;
- width: 11rem;
- flex-shrink: 0;
- padding: 0.375rem;
- overflow: auto;
+ margin-left: 6px;
+ padding: 3px;
- background: var(--start-dt-surface);
- border-right: var(--start-dt-border-soft) 1px solid;
+ border-radius: 9px;
+ background: var(--start-dt-well);
}
-[data-solid-dev-toolbar-tab] {
+[data-solid-dev-toolbar-view] {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: 6px;
- padding: 0.375rem 0.5rem;
+ height: 26px;
+ padding: 0 11px;
font: inherit;
- font-size: 0.8125rem;
- text-align: left;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
color: var(--start-dt-text-muted);
cursor: pointer;
background: none;
border: none;
- border-radius: 0.375rem;
+ border-radius: 6px;
transition-property: color, background-color, box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
-[data-solid-dev-toolbar-tab] svg {
- height: 1rem;
- width: 1rem;
-}
-
-[data-solid-dev-toolbar-tab]:hover {
+[data-solid-dev-toolbar-view]:hover {
color: var(--start-dt-text);
- background: var(--start-dt-surface-hover);
}
-[data-solid-dev-toolbar-tab]:focus-visible {
+[data-solid-dev-toolbar-view]:focus-visible {
outline: 2px solid var(--start-dt-accent);
outline-offset: -2px;
}
-[data-solid-dev-toolbar-tab][aria-selected='true'] {
+[data-solid-dev-toolbar-view][aria-selected='true'] {
color: var(--start-dt-text);
- background: var(--start-dt-surface-active);
-}
-
-[data-solid-dev-toolbar-tab][aria-selected='true'] svg {
- color: var(--start-dt-accent);
-}
-
-[data-solid-dev-toolbar-tab-label] {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ background: var(--start-dt-raise-2);
+ box-shadow:
+ 0 1px 2px oklch(0 0 0 / 0.4),
+ inset 0 1px 0 oklch(1 0 0 / 0.06);
}
[data-solid-dev-toolbar-tab-badge] {
- min-width: 1.125rem;
- padding: 0 0.3125rem;
+ padding: 0 5px;
border-radius: 9999px;
- background: var(--start-dt-surface-hover);
- color: var(--start-dt-text-muted);
+ color: var(--start-dt-faint);
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 0.6875rem;
- line-height: 1.125rem;
- text-align: center;
+ font: 600 10.5px/16px var(--start-dt-mono);
}
[data-solid-dev-toolbar-tab-badge='failure'] {
- background: oklch(0.63 0.21 25 / 0.2);
- color: oklch(0.78 0.14 25);
+ background: var(--start-dt-red-soft);
+ color: var(--start-dt-red);
}
[data-solid-dev-toolbar-tab-badge='warning'] {
- background: oklch(0.8 0.15 80 / 0.18);
- color: oklch(0.85 0.12 80);
+ background: var(--start-dt-amber-soft);
+ color: var(--start-dt-amber);
}
[data-solid-dev-toolbar-body] {
flex: 1;
+ display: flex;
+ flex-direction: column;
min-width: 0;
min-height: 0;
overflow: hidden;
@@ -452,14 +466,9 @@
overflow: hidden;
}
-/* narrow pages fold the nav down to its icons */
-@media (max-width: 640px) {
- [data-solid-dev-toolbar-nav] {
- width: auto;
- }
-
- [data-solid-dev-toolbar-tab-label],
- [data-solid-dev-toolbar-tab-badge],
+/* narrow pages drop the route and the version */
+@media (max-width: 720px) {
+ [data-solid-dev-toolbar-header] [data-solid-dev-toolbar-version],
[data-solid-dev-toolbar-route] {
display: none;
}
diff --git a/src/dev-toolbar/index.tsx b/src/dev-toolbar/index.tsx
index 1dcceb8..59e3bc8 100644
--- a/src/dev-toolbar/index.tsx
+++ b/src/dev-toolbar/index.tsx
@@ -15,21 +15,9 @@ import {
} from 'solid-js';
import version from '../version.js';
import IconButton from '../ui/IconButton.js';
-import { type ServerFunctionInstance, ServerFunctionViewer } from './functions/index.js';
+import type { ServerFunctionInstance } from './functions/index.js';
import { captureServerFunctionCall } from './functions/tracker.js';
-import {
- AsyncIcon,
- ErrorIcon,
- FunctionIcon,
- GraphIcon,
- HighlightUpdatesIcon,
- InspectIcon,
- IssueIcon,
- MinimizeIcon,
- SolidIcon,
- TimelineIcon,
- TreeIcon,
-} from './icons.js';
+import { HighlightUpdatesIcon, InspectIcon, MinimizeIcon, SolidIcon } from './icons.js';
import { AsyncHistory, startAsyncCapture, type AsyncRecord } from './async/tracker.js';
import { HighlightUpdatesOverlay } from './highlight-updates/overlay.js';
import { installElementTracking, type InspectedComponent } from './inspector/lookup.js';
@@ -37,33 +25,65 @@ import { InspectorOverlay } from './inspector/overlay.js';
import { addIssues, captureIssues, type IssueEntry } from './issues/tracker.js';
import { isExtensionError } from './extension-errors.js';
import { addError } from './error-list.js';
-import { openInEditor } from './open-in-editor.js';
import { CopyReportButton } from './report/CopyReportButton.js';
import { pageRoute } from './report/markdown.js';
+import { createErrorRecord, type ErrorRecord } from './error-record.js';
import { excludeOwner, includeOwner } from './ownership/registry.js';
import { excludeReactiveOwner, includeReactiveOwner } from './reactivity/registry.js';
import { startTimeline, TimelineBuilder, type TimelineEntry } from './timeline/tracker.js';
+import type { PaletteTarget } from './palette/search.js';
+import { SignalHistory } from './structure/signal-history.js';
import './index.css';
-const ErrorViewer = clientOnly(() => import('./error-viewer/index.js'), { lazy: true });
-const ReactivityViewer = clientOnly(() => import('./reactivity/index.js'), { lazy: true });
-const OwnershipViewer = clientOnly(() => import('./ownership/index.js'), { lazy: true });
-const IssuesViewer = clientOnly(() => import('./issues/index.js'), { lazy: true });
-const TimelineViewer = clientOnly(() => import('./timeline/index.js'), { lazy: true });
-const AsyncViewer = clientOnly(() => import('./async/index.js'), { lazy: true });
-
-type Tab = 'err' | 'issues' | 'tl' | 'async' | 'fn' | 'rx' | 'own';
-
-const TABS: { value: Tab; label: string; icon: typeof ErrorIcon }[] = [
- { value: 'err', label: 'Errors', icon: ErrorIcon },
- { value: 'issues', label: 'Issues', icon: IssueIcon },
- { value: 'tl', label: 'Timeline', icon: TimelineIcon },
- { value: 'async', label: 'Async', icon: AsyncIcon },
- { value: 'fn', label: 'Server functions', icon: FunctionIcon },
- { value: 'rx', label: 'Reactivity', icon: GraphIcon },
- { value: 'own', label: 'Ownership', icon: TreeIcon },
+const HealthViewer = clientOnly(() => import('./health/index.js'), { lazy: true });
+const ActivityViewer = clientOnly(() => import('./activity/index.js'), { lazy: true });
+const StructureViewer = clientOnly(() => import('./structure/index.js'), { lazy: true });
+const CommandPalette = clientOnly(() => import('./palette/index.js'), { lazy: true });
+
+/** A view of the dock. */
+type Tab = 'health' | 'activity' | 'structure';
+type StructureMode = 'owners' | 'graph';
+
+const VIEWS: { value: Tab; label: string }[] = [
+ { value: 'health', label: 'Health' },
+ { value: 'activity', label: 'Activity' },
+ { value: 'structure', label: 'Structure' },
];
+/** The panels of older versions, and the view each now lives in. */
+const OLD_TABS: Record
= {
+ err: 'health',
+ issues: 'health',
+ tl: 'activity',
+ async: 'activity',
+ fn: 'activity',
+ own: 'structure',
+ rx: 'structure',
+};
+
+/** Reads a saved tab, mapping the panels of older versions to their view. */
+function savedTab(value: unknown): Tab | undefined {
+ if (typeof value !== 'string') return undefined;
+ if (value in OLD_TABS) return OLD_TABS[value];
+ for (const view of VIEWS) {
+ if (view.value === value) return view.value;
+ }
+ return undefined;
+}
+
+/** The solid-js version the app runs, read from its package once the toolbar loads. */
+async function loadSolidVersion(): Promise {
+ try {
+ // No import attribute: Vite serves the file as a module, and a browser
+ // rejects that module when the import asks for JSON.
+ const pkg = await import('solid-js/package.json');
+ return (pkg.default as { version?: string }).version;
+ } catch {
+ // A bundler that cannot import the package file leaves the version out.
+ return undefined;
+ }
+}
+
// The dock remembers whether it was open, its tab and its height, so a full
// reload during development comes back to the same view.
const STORAGE_KEY = 'solid-start-devtools:dock';
@@ -75,6 +95,7 @@ const DRAG_THRESHOLD = 4;
interface DockState {
open: boolean;
tab: Tab;
+ structureMode?: StructureMode;
height: number;
highlightUpdates?: boolean;
}
@@ -117,6 +138,15 @@ function currentRoute(): string {
return location.pathname + location.search;
}
+/** The solid-js version chip, in the pull tab and the header. */
+function SolidVersion(props: { value: string }): JSX.Element {
+ return (
+
+ solid-js {props.value}
+
+ );
+}
+
export interface DevToolbarProps {
children?: JSX.Element;
}
@@ -146,7 +176,10 @@ export function DevToolbar(props: DevToolbarProps) {
if (owner) OBSERVE?.exclude(owner);
const [open, setOpen] = createSignal(false);
- const [tab, setTab] = createSignal('fn');
+ const [tab, setTab] = createSignal('activity');
+ const [structureMode, setStructureMode] = createSignal('owners');
+ const [paletteOpen, setPaletteOpen] = createSignal(false);
+ const [solidVersion, setSolidVersion] = createSignal();
const [height, setHeight] = createSignal(DEFAULT_HEIGHT);
const [highlightUpdates, setHighlightUpdates] = createSignal(false);
const [route, setRoute] = createSignal('');
@@ -160,11 +193,9 @@ export function DevToolbar(props: DevToolbarProps) {
function selectComponent(comp: InspectedComponent) {
setInspecting(false);
setSelectedComponent(comp);
+ // The pick opens in the ownership tree, which selects and shows it.
setOwnershipFocus({ owner: comp.owner });
- if (!open()) {
- setOpen(true);
- persist({ open: true });
- }
+ showOwner(comp.owner);
}
function clearComponentFunnel() {
@@ -185,6 +216,7 @@ export function DevToolbar(props: DevToolbarProps) {
writeDockState({
open: open(),
tab: tab(),
+ structureMode: structureMode(),
height: height(),
highlightUpdates: highlightUpdates(),
...next,
@@ -197,6 +229,24 @@ export function DevToolbar(props: DevToolbarProps) {
persist({ open: true, tab: value });
}
+ function changeStructureMode(mode: StructureMode) {
+ setStructureMode(mode);
+ persist({ structureMode: mode });
+ }
+
+ // Other views link into Structure: an owner opens the tree, a node the graph.
+ function showOwner(owner: object) {
+ setOwnershipFocus({ owner });
+ changeStructureMode('owners');
+ show('structure');
+ }
+
+ function showInGraph(node: object) {
+ setGraphFocus({ node });
+ changeStructureMode('graph');
+ show('structure');
+ }
+
function close() {
// The focused control hides with the dock, so focus moves to the handle.
const hadFocus = !!dock?.contains(document.activeElement);
@@ -213,7 +263,7 @@ export function DevToolbar(props: DevToolbarProps) {
function openAndFocus() {
show(tab());
flush();
- dock?.querySelector(`[data-solid-dev-toolbar-tab="${tab()}"]`)?.focus();
+ dock?.querySelector(`[data-solid-dev-toolbar-view="${tab()}"]`)?.focus();
}
// A panel is live only while the dock is open on its tab, so the graph and
@@ -222,7 +272,12 @@ export function DevToolbar(props: DevToolbarProps) {
onSettled(() => {
const saved = readDockState();
- if (TABS.some((entry) => entry.value === saved.tab)) setTab(saved.tab!);
+ const restored = savedTab(saved.tab);
+ if (restored) setTab(restored);
+ if (saved.structureMode === 'owners' || saved.structureMode === 'graph') {
+ setStructureMode(saved.structureMode);
+ }
+ void loadSolidVersion().then((value) => setSolidVersion(value));
if (typeof saved.height === 'number') setHeight(clampHeight(saved.height));
if (typeof saved.highlightUpdates === 'boolean') setHighlightUpdates(saved.highlightUpdates);
if (saved.open) setOpen(true);
@@ -299,6 +354,13 @@ export function DevToolbar(props: DevToolbarProps) {
onSettled(() => {
const onKeyDown = (event: KeyboardEvent) => {
+ // ⌘K or Ctrl+K opens the palette, only while the dock is open so the
+ // app keeps the shortcut otherwise.
+ if ((event.metaKey || event.ctrlKey) && event.code === 'KeyK' && open()) {
+ event.preventDefault();
+ setPaletteOpen((current) => !current);
+ return;
+ }
// `code`, because Alt changes the character `key` reports on macOS.
if (!event.altKey || !event.shiftKey) return;
if (event.repeat || isEditable(event.composedPath()[0] ?? null)) return;
@@ -323,19 +385,24 @@ export function DevToolbar(props: DevToolbarProps) {
return () => window.removeEventListener('keydown', onKeyDown);
});
- function onTabKeyDown(event: KeyboardEvent) {
- const index = TABS.findIndex((entry) => entry.value === tab());
- let target: number;
- if (event.key === 'ArrowDown') target = (index + 1) % TABS.length;
- else if (event.key === 'ArrowUp') target = (index - 1 + TABS.length) % TABS.length;
- else if (event.key === 'Home') target = 0;
- else if (event.key === 'End') target = TABS.length - 1;
- else return;
+ // Arrow keys move along a tab list, and Home and End jump to its ends.
+ function nextIndex(event: KeyboardEvent, index: number, length: number): number | undefined {
+ if (event.key === 'ArrowRight') return (index + 1) % length;
+ if (event.key === 'ArrowLeft') return (index - 1 + length) % length;
+ if (event.key === 'Home') return 0;
+ if (event.key === 'End') return length - 1;
+ return undefined;
+ }
+
+ function onViewKeyDown(event: KeyboardEvent) {
+ const index = VIEWS.findIndex((entry) => entry.value === tab());
+ const target = nextIndex(event, index, VIEWS.length);
+ if (target === undefined) return;
event.preventDefault();
- const next = TABS[target]!;
+ const next = VIEWS[target]!;
show(next.value);
const list = event.currentTarget as HTMLElement;
- list.querySelector(`[data-solid-dev-toolbar-tab="${next.value}"]`)?.focus();
+ list.querySelector(`[data-solid-dev-toolbar-view="${next.value}"]`)?.focus();
}
// A panel can ask the graph to show a node. Each request is a new object, so
@@ -343,10 +410,22 @@ export function DevToolbar(props: DevToolbarProps) {
const [graphFocus, setGraphFocus] = createSignal<{ node: object }>();
// The graph can ask the ownership tree to show an owner the same way.
const [ownershipFocus, setOwnershipFocus] = createSignal<{ owner: object }>();
- // The timeline can ask the server-function panel to show a call.
- const [functionsFocus, setFunctionsFocus] = createSignal<{ instance: string }>();
+ // Health, Structure and the palette can ask Activity to show an action, a call or an owner.
+ const [activityFocus, setActivityFocus] = createSignal<{
+ origin?: object;
+ instance?: string;
+ ownerName?: string;
+ }>();
+
+ function showInActivity(focus: { origin?: object; instance?: string; ownerName?: string }) {
+ setActivityFocus(focus);
+ show('activity');
+ }
- const [errors, setErrors] = createSignal([]);
+ // The structure view shows the last change of a signal it lists.
+ const signalHistory = new SignalHistory();
+
+ const [errors, setErrors] = createSignal([]);
function resetError() {
setErrors([]);
@@ -354,9 +433,10 @@ export function DevToolbar(props: DevToolbarProps) {
function pushError(error: unknown) {
console.error(error);
- setErrors((current) => addError(current, error));
+ const record = createErrorRecord(error);
+ setErrors((current) => addError(current, record));
- show('err');
+ show('health');
}
onSettled(() => {
@@ -424,8 +504,6 @@ export function DevToolbar(props: DevToolbarProps) {
}),
);
- const callCount = createMemo(() => Object.keys(instances()).length);
-
async function copySession(): Promise {
// Loaded on use: mapping stacks needs the source-map reader the error panel loads lazily.
const [{ resolveErrorFrames }, { SESSION_ERRORS, sessionReport }] = await Promise.all([
@@ -434,7 +512,7 @@ export function DevToolbar(props: DevToolbarProps) {
]);
const recent = errors().slice(0, SESSION_ERRORS);
const resolved = await Promise.all(
- recent.map(async (error) => ({ error, frames: await resolveErrorFrames(error) })),
+ recent.map(async ({ error }) => ({ error, frames: await resolveErrorFrames(error) })),
);
return sessionReport({
errors: resolved,
@@ -448,12 +526,30 @@ export function DevToolbar(props: DevToolbarProps) {
});
}
- const badge = (value: Tab): number => {
- if (value === 'err') return errors().length;
- if (value === 'issues') return issueCount();
- if (value === 'fn') return callCount();
- return 0;
- };
+ // The pull tab reads out the counts it shows.
+ function handleLabel(): string {
+ const counts: string[] = [];
+ if (errors().length) counts.push(`${errors().length} errors`);
+ if (issueCount()) counts.push(`${issueCount()} issues`);
+ return ['Open Start Devtools', ...counts].join(', ');
+ }
+
+ // An action's label, for an error that was thrown during it.
+ function describeOrigin(origin: object): string | undefined {
+ for (const entry of timelineEntries()) {
+ if (entry.origin === origin) return entry.label;
+ }
+ return undefined;
+ }
+
+ function pickFromPalette(target: PaletteTarget) {
+ if (target.kind === 'component') showOwner(target.owner);
+ else if (target.kind === 'memo' || target.kind === 'signal') showInGraph(target.node);
+ else if (target.kind === 'action') {
+ if (target.origin) showInActivity({ origin: target.origin });
+ else show('activity');
+ } else if (target.kind === 'call') showInActivity({ instance: target.instance });
+ }
return (
<>
@@ -466,11 +562,7 @@ export function DevToolbar(props: DevToolbarProps) {
type="button"
data-solid-dev-toolbar-handle
data-errors={errors().length ? '' : undefined}
- aria-label={
- errors().length
- ? `Open Start Devtools, ${errors().length} errors`
- : 'Open Start Devtools'
- }
+ aria-label={handleLabel()}
title="Start Devtools (Alt+Shift+D)"
hidden={open()}
onPointerDown={(event) => onDragHeight(event, true)}
@@ -483,10 +575,13 @@ export function DevToolbar(props: DevToolbarProps) {
- {version}
+ {(value) => }
{(count) => {count()} }
+
+ {(count) => {count()} }
+
@@ -504,58 +599,64 @@ export function DevToolbar(props: DevToolbarProps) {
onPointerDown={(event) => onDragHeight(event, false)}
/>
-
-
-
-
- Start Devtools
-
-
- {version}
-
+
+
+
+
{(value) => }
{route()}
-
- {(comp) => (
-
- Inspecting
- {
- setOwnershipFocus({ owner: comp().owner });
- show('own');
- }}
- >
- {comp().name}
-
-
- {(loc) => (
- openInEditor(loc())}
- >
- Open in editor
-
- )}
-
-
- ×
-
-
- )}
-
+
+
+ {(entry) => {
+ const selected = () => tab() === entry.value;
+ return (
+ show(entry.value)}
+ >
+ {entry.label}
+
+ {(count) => (
+ {count()}
+ )}
+
+
+ {(count) => (
+ {count()}
+ )}
+
+
+ );
+ }}
+
+
+
setPaletteOpen(true)}
+ >
+
+
+
+
+ Find signal, component, call
+ ⌘K
+
-
-
-
- {(entry) => {
- const Icon = entry.icon;
- return (
- show(entry.value)}
- >
-
- {entry.label}
-
- {(count) => (
-
- {count()}
-
- )}
-
-
- );
- }}
-
-
-
-
-
setIssues([])}
- onViewOwner={(owner) => {
- setOwnershipFocus({ owner });
- show('own');
- }}
- onViewInGraph={(node) => {
- setGraphFocus({ node });
- show('rx');
- }}
- />
- {
- timeline.clear();
- setTimelineEntries([]);
- }}
- onViewCall={(instance) => {
- setFunctionsFocus({ instance });
- show('fn');
- }}
- onViewInGraph={(node) => {
- setGraphFocus({ node });
- show('rx');
- }}
- />
- {
- asyncHistory.clear();
- setAsyncRecords([]);
- }}
- onViewInGraph={(node) => {
- setGraphFocus({ node });
- show('rx');
- }}
- onViewOwner={(owner) => {
- setOwnershipFocus({ owner });
- show('own');
- }}
- />
- {
- setOwnershipFocus({ owner });
- show('own');
- }}
- />
- {
- setGraphFocus({ node });
- show('rx');
- }}
- />
- {
- setInstances((current) => {
- const next = { ...current };
- delete next[value];
- return next;
- });
- }}
- />
-
+
+
setIssues([])}
+ onViewOwner={showOwner}
+ onViewInGraph={showInGraph}
+ describeOrigin={describeOrigin}
+ onShowInActivity={(origin: object) => showInActivity({ origin })}
+ />
+ {
+ timeline.clear();
+ setTimelineEntries([]);
+ asyncHistory.clear();
+ setAsyncRecords([]);
+ }}
+ onDeleteInstance={(value: string) => {
+ setInstances((current) => {
+ const next = { ...current };
+ delete next[value];
+ return next;
+ });
+ }}
+ onViewInGraph={showInGraph}
+ onViewOwner={showOwner}
+ onShowInStructure={showInGraph}
+ onShowError={() => show('health')}
+ />
+
+ node ? signalHistory.lastChange(node) : undefined
+ }
+ onShowActivity={(_owner: object, name: string) =>
+ showInActivity({ ownerName: name })
+ }
+ />
+
+
+ setPaletteOpen(false)}
+ />
+
+