@praxisjs/devtools 0.1.0 → 0.2.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +5 -7
  3. package/dist/index.js +1035 -860
  4. package/package.json +8 -7
  5. package/src/decorators/debug.ts +102 -77
  6. package/src/decorators/trace.ts +15 -11
  7. package/src/icons/ellipsis-vertical.tsx +25 -19
  8. package/src/icons/panel-bottom.tsx +24 -18
  9. package/src/icons/panel-left.tsx +24 -18
  10. package/src/icons/panel-right.tsx +24 -18
  11. package/src/icons/panel-top.tsx +24 -18
  12. package/src/icons/x.tsx +24 -18
  13. package/src/plugins/components/components/component-detail.tsx +59 -54
  14. package/src/plugins/components/components/component-row.tsx +36 -33
  15. package/src/plugins/components/components/detail-row.tsx +21 -18
  16. package/src/plugins/components/components/detail-section.tsx +14 -12
  17. package/src/plugins/components/components/status-dot.tsx +20 -11
  18. package/src/plugins/components/components-tab.tsx +66 -61
  19. package/src/plugins/signals/components/signal-detail.tsx +35 -27
  20. package/src/plugins/signals/components/signal-row.tsx +32 -28
  21. package/src/plugins/signals/signals-tab.tsx +92 -79
  22. package/src/plugins/timeline/components/badge.tsx +15 -9
  23. package/src/plugins/timeline/components/timeline-row.tsx +52 -45
  24. package/src/plugins/timeline/timeline-tab.tsx +90 -77
  25. package/src/plugins/types.ts +2 -2
  26. package/src/ui/dev-tools.tsx +45 -39
  27. package/src/ui/panel.tsx +152 -146
  28. package/src/ui/shared/empty-state.tsx +17 -11
  29. package/src/ui/shared/icon-button.tsx +28 -25
  30. package/src/ui/shared/panel-section.tsx +14 -12
  31. package/src/ui/shared/search-input.tsx +19 -15
  32. package/src/ui/shared/side-panel.tsx +16 -13
  33. package/vite.config.ts +3 -9
@@ -1,70 +1,75 @@
1
1
  import { SidePanel } from "@shared/side-panel";
2
2
  import { formatValue } from "@utils/format-value";
3
3
 
4
+ import { StatelessComponent } from "@praxisjs/core";
5
+ import { Component } from "@praxisjs/decorators";
6
+
7
+
4
8
  import { DetailRow } from "./detail-row";
5
9
  import { DetailSection } from "./detail-section";
6
10
  import { StatusDot } from "./status-dot";
7
11
 
8
12
  import type { ComponentEntry, SignalEntry } from "@core/types";
9
13
 
10
- export function ComponentDetail({
11
- entry,
12
- signals,
13
- }: {
14
+ @Component()
15
+ export class ComponentDetail extends StatelessComponent<{
14
16
  entry: ComponentEntry;
15
17
  signals: SignalEntry[];
16
- }) {
17
- return (
18
- <SidePanel>
19
- <div class="px-3 py-2 border-b border-border flex items-center gap-2 bg-bg shrink-0">
20
- <StatusDot status={entry.status} />
21
- <span class="text-accent font-mono text-[11px] font-semibold truncate pl-1">
22
- &lt;{entry.name}&gt;
23
- </span>
24
- </div>
25
-
26
- <div class="flex-1 overflow-y-auto">
27
- <DetailSection label="Stats">
28
- <DetailRow k="renders" v={String(entry.renderCount)} />
29
- <DetailRow
30
- k="last render"
31
- v={`${entry.lastRenderDuration.toFixed(3)}ms`}
32
- />
33
- <DetailRow k="status" v={entry.status} />
34
- </DetailSection>
18
+ }> {
19
+ render() {
20
+ const { entry, signals } = this.props;
21
+ return (
22
+ <SidePanel>
23
+ <div class="px-3 py-2 border-b border-border flex items-center gap-2 bg-bg shrink-0">
24
+ <StatusDot status={entry.status} />
25
+ <span class="text-accent font-mono text-[11px] font-semibold truncate pl-1">
26
+ &lt;{entry.name}&gt;
27
+ </span>
28
+ </div>
35
29
 
36
- {signals.length > 0 && (
37
- <DetailSection label="State">
38
- {signals.map((s) => (
39
- <DetailRow
40
- key={s.id}
41
- k={s.label}
42
- v={formatValue(s.value)}
43
- signal
44
- />
45
- ))}
30
+ <div class="flex-1 overflow-y-auto">
31
+ <DetailSection label="Stats">
32
+ <DetailRow k="renders" v={String(entry.renderCount)} />
33
+ <DetailRow
34
+ k="last render"
35
+ v={`${entry.lastRenderDuration.toFixed(3)}ms`}
36
+ />
37
+ <DetailRow k="status" v={entry.status} />
46
38
  </DetailSection>
47
- )}
48
39
 
49
- {entry.lifecycle.length > 0 && (
50
- <DetailSection label="Lifecycle">
51
- {[...entry.lifecycle]
52
- .reverse()
53
- .slice(0, 20)
54
- .map((ev, i) => (
55
- <div
56
- key={String(i)}
57
- class="px-3 py-2 flex justify-between items-center border-b border-border"
58
- >
59
- <span class="text-[11px] text-text font-mono">{ev.hook}</span>
60
- <span class="text-[10px] text-subtle tabular-nums">
61
- {new Date(ev.timestamp).toLocaleTimeString()}
62
- </span>
63
- </div>
40
+ {signals.length > 0 && (
41
+ <DetailSection label="State">
42
+ {signals.map((s) => (
43
+ <DetailRow
44
+ key={s.id}
45
+ k={s.label}
46
+ v={formatValue(s.value)}
47
+ signal
48
+ />
64
49
  ))}
65
- </DetailSection>
66
- )}
67
- </div>
68
- </SidePanel>
69
- );
50
+ </DetailSection>
51
+ )}
52
+
53
+ {entry.lifecycle.length > 0 && (
54
+ <DetailSection label="Lifecycle">
55
+ {[...entry.lifecycle]
56
+ .reverse()
57
+ .slice(0, 20)
58
+ .map((ev, i) => (
59
+ <div
60
+ key={String(i)}
61
+ class="px-3 py-2 flex justify-between items-center border-b border-border"
62
+ >
63
+ <span class="text-[11px] text-text font-mono">{ev.hook}</span>
64
+ <span class="text-[10px] text-subtle tabular-nums">
65
+ {new Date(ev.timestamp).toLocaleTimeString()}
66
+ </span>
67
+ </div>
68
+ ))}
69
+ </DetailSection>
70
+ )}
71
+ </div>
72
+ </SidePanel>
73
+ );
74
+ }
70
75
  }
@@ -1,42 +1,45 @@
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
1
4
  import { StatusDot } from "./status-dot";
2
5
 
3
6
  import type { ComponentEntry } from "@core/types";
4
7
 
5
- export function ComponentRow({
6
- entry,
7
- selected,
8
- onClick,
9
- }: {
8
+ @Component()
9
+ export class ComponentRow extends StatelessComponent<{
10
10
  entry: ComponentEntry;
11
11
  selected: () => boolean;
12
12
  onClick: () => void;
13
- }) {
14
- return (
15
- <div
16
- onClick={onClick}
17
- class={() =>
18
- `relative flex items-center gap-2 px-3 py-2 cursor-pointer border-b border-border transition-colors duration-100 ${
19
- selected() ? "bg-selected" : "hover:bg-section"
20
- }`
21
- }
22
- >
23
- {() =>
24
- selected() && (
25
- <span class="absolute left-0 top-0 bottom-0 w-[2px] bg-accent rounded-r" />
26
- )
27
- }
28
- <StatusDot status={entry.status} />
29
- <span class="text-accent font-mono text-[11px] flex-1 truncate pl-1">
30
- &lt;{entry.name}&gt;
31
- </span>
32
- <span class="text-muted text-[11px] tabular-nums">
33
- ×{entry.renderCount}
34
- </span>
35
- <span
36
- class={`text-[10px] tabular-nums w-12 text-right ${entry.lastRenderDuration > 16 ? "text-warn" : "text-subtle"}`}
13
+ }> {
14
+ render() {
15
+ const { entry, selected, onClick } = this.props;
16
+ return (
17
+ <div
18
+ onClick={onClick}
19
+ class={() =>
20
+ `relative flex items-center gap-2 px-3 py-2 cursor-pointer border-b border-border transition-colors duration-100 ${
21
+ selected() ? "bg-selected" : "hover:bg-section"
22
+ }`
23
+ }
37
24
  >
38
- {entry.lastRenderDuration.toFixed(1)}ms
39
- </span>
40
- </div>
41
- );
25
+ {() =>
26
+ selected() && (
27
+ <span class="absolute left-0 top-0 bottom-0 w-[2px] bg-accent rounded-r" />
28
+ )
29
+ }
30
+ <StatusDot status={entry.status} />
31
+ <span class="text-accent font-mono text-[11px] flex-1 truncate pl-1">
32
+ &lt;{entry.name}&gt;
33
+ </span>
34
+ <span class="text-muted text-[11px] tabular-nums">
35
+ ×{entry.renderCount}
36
+ </span>
37
+ <span
38
+ class={`text-[10px] tabular-nums w-12 text-right ${entry.lastRenderDuration > 16 ? "text-warn" : "text-subtle"}`}
39
+ >
40
+ {entry.lastRenderDuration.toFixed(1)}ms
41
+ </span>
42
+ </div>
43
+ );
44
+ }
42
45
  }
@@ -1,22 +1,25 @@
1
- export function DetailRow({
2
- k,
3
- v,
4
- signal: isSignal,
5
- }: {
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class DetailRow extends StatelessComponent<{
6
6
  k: string;
7
7
  v: string;
8
8
  signal?: boolean;
9
- }) {
10
- return (
11
- <div class="flex justify-between items-center px-3 py-2 border-b border-border gap-3">
12
- <span
13
- class={`text-[11px] font-mono shrink-0 ${isSignal ? "text-accent" : "text-muted"}`}
14
- >
15
- {k}
16
- </span>
17
- <span class="text-[11px] text-text font-mono truncate text-right">
18
- {v}
19
- </span>
20
- </div>
21
- );
9
+ }> {
10
+ render() {
11
+ const { k, v, signal: isSignal } = this.props;
12
+ return (
13
+ <div class="flex justify-between items-center px-3 py-2 border-b border-border gap-3">
14
+ <span
15
+ class={`text-[11px] font-mono shrink-0 ${isSignal ? "text-accent" : "text-muted"}`}
16
+ >
17
+ {k}
18
+ </span>
19
+ <span class="text-[11px] text-text font-mono truncate text-right">
20
+ {v}
21
+ </span>
22
+ </div>
23
+ );
24
+ }
22
25
  }
@@ -1,18 +1,20 @@
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
1
3
  import type { Children } from "@praxisjs/shared";
2
4
 
3
- export function DetailSection({
4
- label,
5
- children,
6
- }: {
5
+ @Component()
6
+ export class DetailSection extends StatelessComponent<{
7
7
  label: string;
8
8
  children: Children;
9
- }) {
10
- return (
11
- <div class="border-b border-border">
12
- <div class="px-3 py-[4px] text-[9px] text-subtle font-bold tracking-[0.12em] uppercase bg-section border-b border-border">
13
- {label}
9
+ }> {
10
+ render() {
11
+ return (
12
+ <div class="border-b border-border">
13
+ <div class="px-3 py-[4px] text-[9px] text-subtle font-bold tracking-[0.12em] uppercase bg-section border-b border-border">
14
+ {this.props.label}
15
+ </div>
16
+ {this.props.children}
14
17
  </div>
15
- {children}
16
- </div>
17
- );
18
+ );
19
+ }
18
20
  }
@@ -1,14 +1,23 @@
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
1
4
  import type { ComponentEntry } from "@core/types";
2
5
 
3
- export function StatusDot({ status }: { status: ComponentEntry["status"] }) {
4
- return (
5
- <span
6
- class={`inline-block w-[6px] h-[6px] rounded-full shrink-0 ${status === "mounted" ? "bg-success" : "bg-subtle"}`}
7
- style={
8
- status === "mounted"
9
- ? { boxShadow: "0 0 6px rgba(14,165,122,0.7)" }
10
- : undefined
11
- }
12
- />
13
- );
6
+ @Component()
7
+ export class StatusDot extends StatelessComponent<{
8
+ status: ComponentEntry["status"];
9
+ }> {
10
+ render() {
11
+ const { status } = this.props;
12
+ return (
13
+ <span
14
+ class={`inline-block w-[6px] h-[6px] rounded-full shrink-0 ${status === "mounted" ? "bg-success" : "bg-subtle"}`}
15
+ style={
16
+ status === "mounted"
17
+ ? { boxShadow: "0 0 6px rgba(14,165,122,0.7)" }
18
+ : undefined
19
+ }
20
+ />
21
+ );
22
+ }
14
23
  }
@@ -1,85 +1,90 @@
1
1
  import { EmptyState } from "@shared/empty-state";
2
2
 
3
- import { signal, effect, peek, onMount, onUnmount } from "@praxisjs/core";
3
+ import { StatefulComponent } from "@praxisjs/core";
4
+ import { Component, State } from "@praxisjs/decorators";
5
+
4
6
 
5
7
  import { ComponentDetail } from "./components/component-detail";
6
8
  import { ComponentRow } from "./components/component-row";
7
9
 
8
10
  import type { Registry } from "@core/registry";
9
- import type { ComponentEntry, SignalEntry } from "@core/types";
11
+ import type { ComponentEntry } from "@core/types";
12
+
13
+ @Component()
14
+ export class ComponentsTab extends StatefulComponent {
15
+ @State() components: ComponentEntry[] = [];
16
+ @State() selectedId: string | null = null;
10
17
 
11
- export function ComponentsTab({ registry }: { registry: Registry }) {
12
- const components = signal<ComponentEntry[]>(registry.getComponents());
13
- const selectedId = signal<string | null>(null);
14
- const sigs = signal<SignalEntry[]>([]);
18
+ private _handlers: Array<() => void> = [];
15
19
 
16
- const stopSigEffect = effect(() => {
17
- const id = selectedId();
18
- sigs.set(id ? registry.getSignalsByComponent(id) : []);
19
- });
20
+ private get registry() {
21
+ return this.props.registry as Registry;
22
+ }
23
+
24
+ onMount() {
25
+ this.components = this.registry.getComponents();
20
26
 
21
- let handlers: Array<() => void> = [];
22
- onMount(() => {
23
27
  const refresh = () => {
24
- components.set(registry.getComponents());
25
- const id = peek(selectedId);
26
- if (id) sigs.set(registry.getSignalsByComponent(id));
28
+ this.components = this.registry.getComponents();
27
29
  };
28
- handlers = [
30
+
31
+ this._handlers = [
29
32
  "component:registered",
30
33
  "component:render",
31
34
  "component:unmount",
32
35
  "lifecycle",
33
36
  "signal:registered",
34
37
  "signal:changed",
35
- ].map((ev) => registry.bus.on(ev, refresh));
36
- });
38
+ ].map((ev) => this.registry.bus.on(ev, refresh));
39
+ }
37
40
 
38
- onUnmount(() => {
39
- stopSigEffect();
40
- handlers.forEach((off) => {
41
- off();
42
- });
43
- });
41
+ onUnmount() {
42
+ this._handlers.forEach((off) => { off(); });
43
+ }
44
44
 
45
- return (
46
- <div class="flex h-full overflow-hidden">
47
- <div class="flex-1 flex flex-col overflow-hidden min-w-0">
48
- <div class="flex items-center px-3 h-7 text-[9px] text-subtle font-bold tracking-[0.12em] uppercase border-b border-border bg-section gap-2 shrink-0">
49
- <span class="flex-1">Component</span>
50
- <span>Renders</span>
51
- <span class="w-12 text-right">Last</span>
52
- </div>
45
+ render() {
46
+ const { registry } = this;
47
+ return (
48
+ <div class="flex h-full overflow-hidden">
49
+ <div class="flex-1 flex flex-col overflow-hidden min-w-0">
50
+ <div class="flex items-center px-3 h-7 text-[9px] text-subtle font-bold tracking-[0.12em] uppercase border-b border-border bg-section gap-2 shrink-0">
51
+ <span class="flex-1">Component</span>
52
+ <span>Renders</span>
53
+ <span class="w-12 text-right">Last</span>
54
+ </div>
53
55
 
54
- <div class="flex-1 overflow-y-auto">
55
- {() =>
56
- components().length === 0 ? (
57
- <EmptyState message="No components tracked. Add @Trace() to component classes." />
58
- ) : (
59
- components().map((c) => (
60
- <ComponentRow
61
- key={c.id}
62
- entry={c}
63
- selected={() => selectedId() === c.id}
64
- onClick={() => {
65
- selectedId.update((id) => (id === c.id ? null : c.id));
66
- }}
67
- />
68
- ))
69
- )
70
- }
56
+ <div class="flex-1 overflow-y-auto">
57
+ {() =>
58
+ this.components.length === 0 ? (
59
+ <EmptyState message="No components tracked. Add @Trace() to component classes." />
60
+ ) : (
61
+ this.components.map((c) => (
62
+ <ComponentRow
63
+ key={c.id}
64
+ entry={c}
65
+ selected={() => this.selectedId === c.id}
66
+ onClick={() => {
67
+ this.selectedId =
68
+ this.selectedId === c.id ? null : c.id;
69
+ }}
70
+ />
71
+ ))
72
+ )
73
+ }
74
+ </div>
71
75
  </div>
72
- </div>
73
76
 
74
- {() => {
75
- const id = selectedId();
76
- const entry = id
77
- ? (components().find((c) => c.id === id) ?? null)
78
- : null;
79
- return entry ? (
80
- <ComponentDetail entry={entry} signals={sigs()} />
81
- ) : null;
82
- }}
83
- </div>
84
- );
77
+ {() => {
78
+ const id = this.selectedId;
79
+ const entry = id
80
+ ? (this.components.find((c) => c.id === id) ?? null)
81
+ : null;
82
+ const sigs = id ? registry.getSignalsByComponent(id) : [];
83
+ return entry ? (
84
+ <ComponentDetail entry={entry} signals={sigs} />
85
+ ) : null;
86
+ }}
87
+ </div>
88
+ );
89
+ }
85
90
  }
@@ -1,35 +1,43 @@
1
+
1
2
  import { PanelSection } from "@shared/panel-section";
2
3
  import { SidePanel } from "@shared/side-panel";
3
4
  import { time } from "@utils/format-time";
4
5
  import { formatValue } from "@utils/format-value";
5
6
 
7
+ import { StatelessComponent } from "@praxisjs/core";
8
+ import { Component } from "@praxisjs/decorators";
9
+
6
10
  import type { SignalEntry } from "@core/types";
7
11
 
8
- export function SignalDetail({ entry }: { entry: SignalEntry }) {
9
- return (
10
- <SidePanel width="260px">
11
- <PanelSection label="History">
12
- <div class="overflow-y-auto">
13
- {[...entry.history].reverse().map((h, i) => (
14
- <div
15
- key={String(i)}
16
- class="px-3 py-2 border-b border-border flex justify-between items-center gap-3"
17
- >
18
- <span class="font-mono text-[11px] text-text truncate">
19
- {formatValue(h.value)}
20
- </span>
21
- <span class="text-[10px] text-subtle shrink-0 tabular-nums">
22
- {time(h.timestamp, "ago")}
23
- </span>
24
- </div>
25
- ))}
26
- {entry.history.length === 0 && (
27
- <p class="px-3 py-6 text-[11px] text-subtle text-center">
28
- No history yet.
29
- </p>
30
- )}
31
- </div>
32
- </PanelSection>
33
- </SidePanel>
34
- );
12
+ @Component()
13
+ export class SignalDetail extends StatelessComponent<{ entry: SignalEntry }> {
14
+ render() {
15
+ const { entry } = this.props;
16
+ return (
17
+ <SidePanel width="260px">
18
+ <PanelSection label="History">
19
+ <div class="overflow-y-auto">
20
+ {[...entry.history].reverse().map((h, i) => (
21
+ <div
22
+ key={String(i)}
23
+ class="px-3 py-2 border-b border-border flex justify-between items-center gap-3"
24
+ >
25
+ <span class="font-mono text-[11px] text-text truncate">
26
+ {formatValue(h.value)}
27
+ </span>
28
+ <span class="text-[10px] text-subtle shrink-0 tabular-nums">
29
+ {time(h.timestamp, "ago")}
30
+ </span>
31
+ </div>
32
+ ))}
33
+ {entry.history.length === 0 && (
34
+ <p class="px-3 py-6 text-[11px] text-subtle text-center">
35
+ No history yet.
36
+ </p>
37
+ )}
38
+ </div>
39
+ </PanelSection>
40
+ </SidePanel>
41
+ );
42
+ }
35
43
  }
@@ -1,37 +1,41 @@
1
1
  import { time } from "@utils/format-time";
2
2
  import { formatValue } from "@utils/format-value";
3
3
 
4
+ import { StatelessComponent } from "@praxisjs/core";
5
+ import { Component } from "@praxisjs/decorators";
6
+
7
+
4
8
  import type { SignalEntry } from "@core/types";
5
9
 
6
- export function SignalRow({
7
- entry,
8
- selected,
9
- onClick,
10
- }: {
10
+ @Component()
11
+ export class SignalRow extends StatelessComponent<{
11
12
  entry: SignalEntry;
12
13
  selected: boolean;
13
14
  onClick: () => void;
14
- }) {
15
- return (
16
- <div
17
- onClick={onClick}
18
- class={`relative grid grid-cols-[1.2fr_0.8fr_1fr_auto] items-center px-3 py-2 cursor-pointer border-b border-border transition-colors duration-100 ${
19
- selected ? "bg-selected" : "hover:bg-section"
20
- }`}
21
- >
22
- {selected && (
23
- <span class="absolute left-0 top-0 bottom-0 w-[2px] bg-accent rounded-r" />
24
- )}
25
- <span class="text-accent font-mono text-[11px] truncate pl-1">
26
- {entry.label}
27
- </span>
28
- <span class="text-muted text-[11px] truncate">{entry.componentName}</span>
29
- <span class="text-text font-mono text-[11px] truncate">
30
- {formatValue(entry.value)}
31
- </span>
32
- <span class="text-subtle text-[10px] text-right tabular-nums">
33
- {time(entry.changedAt, "ago")}
34
- </span>
35
- </div>
36
- );
15
+ }> {
16
+ render() {
17
+ const { entry, selected, onClick } = this.props;
18
+ return (
19
+ <div
20
+ onClick={onClick}
21
+ class={`relative grid grid-cols-[1.2fr_0.8fr_1fr_auto] items-center px-3 py-2 cursor-pointer border-b border-border transition-colors duration-100 ${
22
+ selected ? "bg-selected" : "hover:bg-section"
23
+ }`}
24
+ >
25
+ {selected && (
26
+ <span class="absolute left-0 top-0 bottom-0 w-[2px] bg-accent rounded-r" />
27
+ )}
28
+ <span class="text-accent font-mono text-[11px] truncate pl-1">
29
+ {entry.label}
30
+ </span>
31
+ <span class="text-muted text-[11px] truncate">{entry.componentName}</span>
32
+ <span class="text-text font-mono text-[11px] truncate">
33
+ {formatValue(entry.value)}
34
+ </span>
35
+ <span class="text-subtle text-[10px] text-right tabular-nums">
36
+ {time(entry.changedAt, "ago")}
37
+ </span>
38
+ </div>
39
+ );
40
+ }
37
41
  }