@praxisjs/devtools 0.1.1 → 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 +22 -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/devtools",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,17 +12,18 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@unocss/reset": "^66.6.2",
15
- "@praxisjs/core": "0.2.0",
16
- "@praxisjs/jsx": "0.1.0",
17
- "@praxisjs/runtime": "0.1.1",
18
- "@praxisjs/shared": "0.1.0"
15
+ "@praxisjs/core": "0.3.0",
16
+ "@praxisjs/decorators": "0.3.0",
17
+ "@praxisjs/jsx": "0.2.0",
18
+ "@praxisjs/runtime": "0.2.0",
19
+ "@praxisjs/shared": "0.2.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@types/node": "^25.3.0",
22
23
  "@unocss/cli": "^66.6.2",
23
- "@unocss/preset-wind3": "^66.6.2",
24
+ "@unocss/preset-wind3": "^66.6.6",
24
25
  "typescript": "^5.9.3",
25
- "unocss": "^66.6.2",
26
+ "unocss": "^66.6.6",
26
27
  "vite": "^7.3.1",
27
28
  "vite-plugin-dts": "^4.5.4"
28
29
  },
@@ -41,18 +41,17 @@ interface ComputedSlot {
41
41
  */
42
42
  export function Debug(options: DebugOptions = {}) {
43
43
  return function (
44
- target: object,
45
- key: string,
46
- descriptor?: PropertyDescriptor,
47
- ): void {
48
- const componentName = (target.constructor as { name: string }).name;
49
- const label = options.label ?? key;
44
+ value: unknown,
45
+ context: ClassMethodDecoratorContext | ClassFieldDecoratorContext,
46
+ ) {
47
+ const label = options.label ?? (context.name as string);
50
48
 
51
49
  // ── Method decorator ─────────────────────────────────────────────────
52
- if (descriptor && typeof descriptor.value === "function") {
53
- const original = descriptor.value as (...args: unknown[]) => unknown;
50
+ if (context.kind === "method") {
51
+ const original = value as (...args: unknown[]) => unknown;
54
52
 
55
- descriptor.value = function (this: object, ...args: unknown[]) {
53
+ return function (this: object, ...args: unknown[]) {
54
+ const componentName = (this.constructor as { name: string }).name;
56
55
  const start = performance.now();
57
56
  let result: unknown;
58
57
  let threw = false;
@@ -77,85 +76,111 @@ export function Debug(options: DebugOptions = {}) {
77
76
 
78
77
  return result;
79
78
  };
80
-
81
- return;
82
79
  }
83
80
 
84
- const existingDesc = Object.getOwnPropertyDescriptor(target, key);
85
-
86
- // ── Property decorator (wrapping @State()) ───────────────────────────
87
- if (existingDesc?.get && existingDesc.set) {
88
- const originalGet = existingDesc.get.bind(existingDesc);
89
- const originalSet = existingDesc.set.bind(existingDesc);
90
- const initialized = new WeakSet();
91
-
92
- Object.defineProperty(target, key, {
93
- get(this: object) {
94
- return originalGet.call(this) as unknown;
95
- },
96
- set(this: object, newValue: unknown) {
97
- if (!initialized.has(this)) {
98
- initialized.add(this);
99
- originalSet.call(this, newValue);
100
- Registry.instance.registerSignal(
101
- this,
102
- label,
103
- newValue,
104
- componentName,
105
- );
106
- } else {
107
- const oldValue: unknown = originalGet.call(this);
108
- originalSet.call(this, newValue);
109
- Registry.instance.updateSignal(this, label, newValue, oldValue);
110
- }
111
- },
112
- enumerable: true,
113
- configurable: true,
114
- });
81
+ // ── Field decorator ───────────────────────────────────────────────────
82
+ context.addInitializer(function (
83
+ this: unknown,
84
+ ) {
85
+ const instance = this as object & Record<string, unknown>;
86
+ const name = context.name as string;
87
+ const componentName = (
88
+ instance.constructor as { name: string }
89
+ ).name;
90
+
91
+ // ── Wrapping @State() getter/setter ─────────────────────────────
92
+ // @State() initializer runs before @Debug() initializer (inner-first),
93
+ // so the getter/setter is already installed on the instance here.
94
+ const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
95
+
96
+ if (existingDesc?.get && existingDesc.set) {
97
+ // eslint-disable-next-line @typescript-eslint/unbound-method
98
+ const originalGet = existingDesc.get;
99
+ // eslint-disable-next-line @typescript-eslint/unbound-method
100
+ const originalSet = existingDesc.set;
101
+
102
+ Registry.instance.registerSignal(
103
+ instance,
104
+ label,
105
+ originalGet.call(instance),
106
+ componentName,
107
+ );
115
108
 
116
- return;
117
- }
109
+ Object.defineProperty(instance, name, {
110
+ get(this: object) {
111
+ return originalGet.call(this) as unknown;
112
+ },
113
+ set(this: object, newValue: unknown) {
114
+ const oldValue: unknown = originalGet.call(this);
115
+ originalSet.call(this, newValue);
116
+ Registry.instance.updateSignal(this, label, newValue, oldValue);
117
+ },
118
+ enumerable: true,
119
+ configurable: true,
120
+ });
118
121
 
119
- // ── Computed class field ──────────────────────────────────────────────
120
- // No existing descriptor means it's a plain class field (e.g. `doubled = computed(...)`).
121
- // Intercept the assignment so we can subscribe to the computed's value changes.
122
- const slots = new WeakMap<object, ComputedSlot>();
123
-
124
- Object.defineProperty(target, key, {
125
- get(this: object) {
126
- return slots.get(this)?.computed;
127
- },
128
- set(this: object, value: unknown) {
129
- // Clean up previous subscription on re-assignment
130
- slots.get(this)?.unsub();
131
-
132
- if (!isComputed(value)) {
133
- console.warn(
134
- `[PraxisJS DevTools] @Debug() on "${componentName}.${key}": ` +
135
- `expected a computed() value but got ${typeof value}. Skipping.`,
136
- );
122
+ return;
123
+ }
124
+
125
+ // ── Computed class field ─────────────────────────────────────────
126
+ // The field initializer has already run, so read the assigned value.
127
+ const initialValue = instance[name];
128
+ Reflect.deleteProperty(instance, name);
129
+
130
+ if (!isComputed(initialValue)) {
131
+ if (initialValue !== undefined) {
132
+ console.warn(
133
+ `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
134
+ `expected a computed() value but got ${typeof initialValue}. Skipping.`,
135
+ );
136
+ }
137
+ instance[name] = initialValue;
137
138
  return;
138
139
  }
139
140
 
140
141
  // subscribe() calls the callback immediately (synchronously), so we
141
142
  // use a flag to skip the first call and register via registerSignal instead.
142
- let skipFirst = true;
143
- let prevValue = value();
143
+ let slot: ComputedSlot;
144
+
145
+ function subscribe(computed: TrackedComputed): () => void {
146
+ let skipFirst = true;
147
+ let prevValue = computed();
148
+
149
+ const unsub = computed.subscribe((newValue) => {
150
+ if (skipFirst) {
151
+ skipFirst = false;
152
+ return;
153
+ }
154
+ Registry.instance.updateSignal(instance, label, newValue, prevValue);
155
+ prevValue = newValue;
156
+ });
157
+
158
+ Registry.instance.registerSignal(instance, label, prevValue, componentName);
159
+ return unsub;
160
+ }
144
161
 
145
- const unsub = value.subscribe((newValue) => {
146
- if (skipFirst) {
147
- skipFirst = false;
148
- return;
149
- }
150
- Registry.instance.updateSignal(this, label, newValue, prevValue);
151
- prevValue = newValue;
162
+ slot = { computed: initialValue, unsub: subscribe(initialValue) };
163
+
164
+ Object.defineProperty(instance, name, {
165
+ get() {
166
+ return slot.computed;
167
+ },
168
+ set(newValue: unknown) {
169
+ slot.unsub();
170
+
171
+ if (!isComputed(newValue)) {
172
+ console.warn(
173
+ `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
174
+ `expected a computed() value but got ${typeof newValue}. Skipping.`,
175
+ );
176
+ return;
177
+ }
178
+
179
+ slot = { computed: newValue, unsub: subscribe(newValue) };
180
+ },
181
+ enumerable: true,
182
+ configurable: true,
152
183
  });
153
-
154
- slots.set(this, { computed: value, unsub });
155
- Registry.instance.registerSignal(this, label, prevValue, componentName);
156
- },
157
- enumerable: true,
158
- configurable: true,
159
184
  });
160
185
  };
161
186
  }
@@ -1,4 +1,4 @@
1
- import { Registry } from '../core/registry';
1
+ import { Registry } from "../core/registry";
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
4
  type AnyConstructor = new (...args: any[]) => any;
@@ -9,16 +9,18 @@ type AnyConstructor = new (...args: any[]) => any;
9
9
  *
10
10
  * @Trace()
11
11
  * @Component()
12
- * class MyComponent extends BaseComponent { ... }
12
+ * class MyComponent extends StatefulComponent { ... }
13
13
  */
14
14
  export function Trace() {
15
- return function <T extends AnyConstructor>(constructor: T): T {
15
+ return function <T extends AnyConstructor>(constructor: T, _context: ClassDecoratorContext): T {
16
16
  const name = constructor.name;
17
17
  const registry = Registry.instance;
18
18
  const proto = constructor.prototype as Record<string, unknown>;
19
19
 
20
20
  // ── render() ──────────────────────────────────────────────────────────
21
- const originalRender = proto.render as ((...args: unknown[]) => unknown) | undefined;
21
+ const originalRender = proto.render as
22
+ | ((...args: unknown[]) => unknown)
23
+ | undefined;
22
24
 
23
25
  if (originalRender) {
24
26
  proto.render = function (this: object, ...args: unknown[]) {
@@ -37,21 +39,23 @@ export function Trace() {
37
39
 
38
40
  proto.onBeforeMount = function (this: object, ...args: unknown[]) {
39
41
  registry.registerComponent(this, name);
40
- registry.recordLifecycle(this, 'onBeforeMount');
42
+ registry.recordLifecycle(this, "onBeforeMount");
41
43
  return originalOnBeforeMount?.call(this, ...args);
42
44
  };
43
45
 
44
46
  // ── remaining lifecycle hooks ─────────────────────────────────────────
45
47
  const hooks = [
46
- 'onMount',
47
- 'onUnmount',
48
- 'onBeforeUpdate',
49
- 'onUpdate',
50
- 'onAfterUpdate',
48
+ "onMount",
49
+ "onUnmount",
50
+ "onBeforeUpdate",
51
+ "onUpdate",
52
+ "onAfterUpdate",
51
53
  ] as const;
52
54
 
53
55
  for (const hook of hooks) {
54
- const original = proto[hook] as ((...args: unknown[]) => unknown) | undefined;
56
+ const original = proto[hook] as
57
+ | ((...args: unknown[]) => unknown)
58
+ | undefined;
55
59
 
56
60
  proto[hook] = function (this: object, ...args: unknown[]) {
57
61
  registry.recordLifecycle(this, hook);
@@ -1,20 +1,26 @@
1
- export function EllipsisVerticalIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-ellipsis-vertical-icon lucide-ellipsis-vertical"
14
- >
15
- <circle cx="12" cy="12" r="1" />
16
- <circle cx="12" cy="5" r="1" />
17
- <circle cx="12" cy="19" r="1" />
18
- </svg>
19
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class EllipsisVerticalIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-ellipsis-vertical-icon lucide-ellipsis-vertical"
19
+ >
20
+ <circle cx="12" cy="12" r="1" />
21
+ <circle cx="12" cy="5" r="1" />
22
+ <circle cx="12" cy="19" r="1" />
23
+ </svg>
24
+ );
25
+ }
20
26
  }
@@ -1,19 +1,25 @@
1
- export function PanelBottomIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-panel-bottom-icon lucide-panel-bottom"
14
- >
15
- <rect width="18" height="18" x="3" y="3" rx="2" />
16
- <path d="M3 15h18" />
17
- </svg>
18
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class PanelBottomIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-panel-bottom-icon lucide-panel-bottom"
19
+ >
20
+ <rect width="18" height="18" x="3" y="3" rx="2" />
21
+ <path d="M3 15h18" />
22
+ </svg>
23
+ );
24
+ }
19
25
  }
@@ -1,19 +1,25 @@
1
- export function PanelLeftIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-panel-left-icon lucide-panel-left"
14
- >
15
- <rect width="18" height="18" x="3" y="3" rx="2" />
16
- <path d="M9 3v18" />
17
- </svg>
18
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class PanelLeftIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-panel-left-icon lucide-panel-left"
19
+ >
20
+ <rect width="18" height="18" x="3" y="3" rx="2" />
21
+ <path d="M9 3v18" />
22
+ </svg>
23
+ );
24
+ }
19
25
  }
@@ -1,19 +1,25 @@
1
- export function PanelRightIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-panel-right-icon lucide-panel-right"
14
- >
15
- <rect width="18" height="18" x="3" y="3" rx="2" />
16
- <path d="M15 3v18" />
17
- </svg>
18
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class PanelRightIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-panel-right-icon lucide-panel-right"
19
+ >
20
+ <rect width="18" height="18" x="3" y="3" rx="2" />
21
+ <path d="M15 3v18" />
22
+ </svg>
23
+ );
24
+ }
19
25
  }
@@ -1,19 +1,25 @@
1
- export function PanelTopIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-panel-top-icon lucide-panel-top"
14
- >
15
- <rect width="18" height="18" x="3" y="3" rx="2" />
16
- <path d="M3 9h18" />
17
- </svg>
18
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class PanelTopIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-panel-top-icon lucide-panel-top"
19
+ >
20
+ <rect width="18" height="18" x="3" y="3" rx="2" />
21
+ <path d="M3 9h18" />
22
+ </svg>
23
+ );
24
+ }
19
25
  }
package/src/icons/x.tsx CHANGED
@@ -1,19 +1,25 @@
1
- export function XIcon() {
2
- return (
3
- <svg
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- class="lucide lucide-x-icon lucide-x"
14
- >
15
- <path d="M18 6 6 18" />
16
- <path d="m6 6 12 12" />
17
- </svg>
18
- );
1
+ import { StatelessComponent } from "@praxisjs/core";
2
+ import { Component } from "@praxisjs/decorators";
3
+
4
+ @Component()
5
+ export class XIcon extends StatelessComponent {
6
+ render() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ class="lucide lucide-x-icon lucide-x"
19
+ >
20
+ <path d="M18 6 6 18" />
21
+ <path d="m6 6 12 12" />
22
+ </svg>
23
+ );
24
+ }
19
25
  }