@praxisjs/devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/assets/logo.svg +9 -0
  4. package/assets/uno.generated.css +205 -0
  5. package/dist/index.d.ts +135 -0
  6. package/dist/index.js +1051 -0
  7. package/package.json +35 -0
  8. package/src/core/event-bus.ts +19 -0
  9. package/src/core/registry.ts +219 -0
  10. package/src/core/types.ts +45 -0
  11. package/src/decorators/debug.ts +161 -0
  12. package/src/decorators/index.ts +3 -0
  13. package/src/decorators/trace.ts +64 -0
  14. package/src/icons/ellipsis-vertical.tsx +20 -0
  15. package/src/icons/index.ts +6 -0
  16. package/src/icons/panel-bottom.tsx +19 -0
  17. package/src/icons/panel-left.tsx +19 -0
  18. package/src/icons/panel-right.tsx +19 -0
  19. package/src/icons/panel-top.tsx +19 -0
  20. package/src/icons/x.tsx +19 -0
  21. package/src/index.ts +20 -0
  22. package/src/plugins/components/components/component-detail.tsx +70 -0
  23. package/src/plugins/components/components/component-row.tsx +42 -0
  24. package/src/plugins/components/components/detail-row.tsx +22 -0
  25. package/src/plugins/components/components/detail-section.tsx +18 -0
  26. package/src/plugins/components/components/status-dot.tsx +14 -0
  27. package/src/plugins/components/components-tab.tsx +85 -0
  28. package/src/plugins/components/index.ts +9 -0
  29. package/src/plugins/signals/components/signal-detail.tsx +35 -0
  30. package/src/plugins/signals/components/signal-row.tsx +37 -0
  31. package/src/plugins/signals/index.ts +9 -0
  32. package/src/plugins/signals/signals-tab.tsx +99 -0
  33. package/src/plugins/timeline/components/badge.tsx +14 -0
  34. package/src/plugins/timeline/components/timeline-row.tsx +55 -0
  35. package/src/plugins/timeline/constants.ts +24 -0
  36. package/src/plugins/timeline/index.ts +9 -0
  37. package/src/plugins/timeline/timeline-tab.tsx +101 -0
  38. package/src/plugins/types.ts +10 -0
  39. package/src/ui/dev-tools.tsx +121 -0
  40. package/src/ui/panel.tsx +225 -0
  41. package/src/ui/shared/empty-state.tsx +12 -0
  42. package/src/ui/shared/icon-button.tsx +30 -0
  43. package/src/ui/shared/panel-section.tsx +18 -0
  44. package/src/ui/shared/search-input.tsx +18 -0
  45. package/src/ui/shared/side-panel.tsx +18 -0
  46. package/src/utils/format-time.ts +7 -0
  47. package/src/utils/format-value.ts +13 -0
  48. package/src/vite-env.d.ts +6 -0
  49. package/tsconfig.json +21 -0
  50. package/uno.config.ts +57 -0
  51. package/vite.config.ts +42 -0
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@praxisjs/devtools",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "@unocss/reset": "^66.6.2",
15
+ "@praxisjs/core": "0.1.0",
16
+ "@praxisjs/jsx": "0.1.0",
17
+ "@praxisjs/runtime": "0.1.0",
18
+ "@praxisjs/shared": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.3.0",
22
+ "@unocss/cli": "^66.6.2",
23
+ "@unocss/preset-wind3": "^66.6.2",
24
+ "typescript": "^5.9.3",
25
+ "unocss": "^66.6.2",
26
+ "vite": "^7.3.1",
27
+ "vite-plugin-dts": "^4.5.4"
28
+ },
29
+ "scripts": {
30
+ "generate:css": "unocss \"src/**/*.{ts,tsx}\" -o assets/uno.generated.css",
31
+ "build": "vite build",
32
+ "prebuild": "pnpm run generate:css",
33
+ "dev": "vite build --watch"
34
+ }
35
+ }
@@ -0,0 +1,19 @@
1
+ type Handler<T = unknown> = (payload: T) => void;
2
+
3
+ export class EventBus {
4
+ private readonly handlers = new Map<string, Set<Handler>>();
5
+
6
+ on<T = unknown>(event: string, handler: Handler<T>): () => void {
7
+ if (!this.handlers.has(event)) {
8
+ this.handlers.set(event, new Set());
9
+ }
10
+ (this.handlers.get(event) as Set<Handler>).add(handler as Handler);
11
+ return () => this.handlers.get(event)?.delete(handler as Handler);
12
+ }
13
+
14
+ emit(event: string, payload: unknown): void {
15
+ this.handlers.get(event)?.forEach((h) => {
16
+ h(payload);
17
+ });
18
+ }
19
+ }
@@ -0,0 +1,219 @@
1
+ import { EventBus } from "./event-bus";
2
+
3
+ import type {
4
+ ComponentEntry,
5
+ LifecycleEvent,
6
+ SignalEntry,
7
+ TimelineEntry,
8
+ TimelineEventType,
9
+ } from "./types";
10
+
11
+ const MAX_HISTORY = 20;
12
+ const MAX_TIMELINE = 200;
13
+
14
+ let idCounter = 0;
15
+ function uid(): string {
16
+ return `v${(++idCounter).toString()}_${Date.now().toString(36)}`;
17
+ }
18
+
19
+ export class Registry {
20
+ private static _instance: Registry | null = null;
21
+
22
+ static get instance(): Registry {
23
+ Registry._instance ??= new Registry();
24
+ return Registry._instance;
25
+ }
26
+
27
+ readonly bus = new EventBus();
28
+
29
+ private readonly instanceIds = new WeakMap<object, string>();
30
+ private readonly signals = new Map<string, SignalEntry>();
31
+ private readonly components = new Map<string, ComponentEntry>();
32
+ private readonly timeline: TimelineEntry[] = [];
33
+
34
+ private getInstanceId(instance: object): string {
35
+ if (!this.instanceIds.has(instance)) {
36
+ this.instanceIds.set(instance, uid());
37
+ }
38
+ return this.instanceIds.get(instance) as string;
39
+ }
40
+
41
+ registerSignal(
42
+ instance: object,
43
+ key: string,
44
+ value: unknown,
45
+ componentName: string,
46
+ ): void {
47
+ const componentId = this.getInstanceId(instance);
48
+ const id = `${componentId}:${key}`;
49
+
50
+ const entry: SignalEntry = {
51
+ id,
52
+ label: key,
53
+ componentId,
54
+ componentName,
55
+ value,
56
+ history: [{ value, timestamp: Date.now() }],
57
+ changedAt: Date.now(),
58
+ };
59
+
60
+ this.signals.set(id, entry);
61
+ this.bus.emit("signal:registered", entry);
62
+ }
63
+
64
+ updateSignal(
65
+ instance: object,
66
+ key: string,
67
+ newValue: unknown,
68
+ oldValue: unknown,
69
+ ): void {
70
+ const componentId = this.getInstanceId(instance);
71
+ const id = `${componentId}:${key}`;
72
+
73
+ const entry = this.signals.get(id);
74
+ if (!entry) return;
75
+
76
+ const history = [
77
+ ...entry.history,
78
+ { value: newValue, timestamp: Date.now() },
79
+ ].slice(-MAX_HISTORY);
80
+
81
+ const updated: SignalEntry = {
82
+ ...entry,
83
+ value: newValue,
84
+ history,
85
+ changedAt: Date.now(),
86
+ };
87
+
88
+ this.signals.set(id, updated);
89
+ this.bus.emit("signal:changed", { entry: updated, oldValue });
90
+ this.pushTimeline({
91
+ type: "signal:change",
92
+ label: `${entry.componentName}.${key}`,
93
+ data: { old: oldValue, new: newValue, signalId: id },
94
+ });
95
+ }
96
+
97
+ registerComponent(instance: object, name: string): void {
98
+ const id = this.getInstanceId(instance);
99
+
100
+ const entry: ComponentEntry = {
101
+ id,
102
+ name,
103
+ renderCount: 0,
104
+ lastRenderDuration: 0,
105
+ mountedAt: Date.now(),
106
+ status: "mounted",
107
+ lifecycle: [],
108
+ };
109
+
110
+ this.components.set(id, entry);
111
+ this.bus.emit("component:registered", entry);
112
+ }
113
+
114
+ recordRender(instance: object, duration: number): void {
115
+ const id = this.getInstanceId(instance);
116
+ const entry = this.components.get(id);
117
+ if (!entry) return;
118
+
119
+ entry.renderCount++;
120
+ entry.lastRenderDuration = duration;
121
+
122
+ this.bus.emit("component:render", { ...entry });
123
+ this.pushTimeline({
124
+ type: "component:render",
125
+ label: `<${entry.name}>`,
126
+ data: {
127
+ componentId: id,
128
+ duration: +duration.toFixed(3),
129
+ renderCount: entry.renderCount,
130
+ },
131
+ });
132
+ }
133
+
134
+ recordLifecycle(instance: object, hook: string): void {
135
+ const id = this.getInstanceId(instance);
136
+ const entry = this.components.get(id);
137
+ if (!entry) return;
138
+
139
+ const event: LifecycleEvent = { hook, timestamp: Date.now() };
140
+ entry.lifecycle.push(event);
141
+
142
+ this.bus.emit("lifecycle", { componentId: id, name: entry.name, hook });
143
+
144
+ const type: TimelineEventType =
145
+ hook === "onUnmount"
146
+ ? "component:unmount"
147
+ : hook === "onBeforeMount"
148
+ ? "component:mount"
149
+ : "lifecycle";
150
+
151
+ this.pushTimeline({
152
+ type,
153
+ label: `<${entry.name}>.${hook}`,
154
+ data: { componentId: id, hook },
155
+ });
156
+
157
+ if (hook === "onUnmount") {
158
+ entry.status = "unmounted";
159
+ this.bus.emit("component:unmount", { ...entry });
160
+ this.components.delete(id);
161
+ for (const [sid, s] of this.signals) {
162
+ if (s.componentId === id) this.signals.delete(sid);
163
+ }
164
+ }
165
+ }
166
+
167
+ recordMethodCall(
168
+ instance: object,
169
+ method: string,
170
+ args: unknown[],
171
+ result: unknown,
172
+ duration: number,
173
+ componentName: string,
174
+ ): void {
175
+ const componentId = this.getInstanceId(instance);
176
+ this.pushTimeline({
177
+ type: "method:call",
178
+ label: `${componentName}.${method}()`,
179
+ data: {
180
+ componentId,
181
+ args,
182
+ result,
183
+ duration: +duration.toFixed(3),
184
+ },
185
+ });
186
+ }
187
+
188
+ getSignals(): SignalEntry[] {
189
+ return [...this.signals.values()];
190
+ }
191
+
192
+ getComponents(): ComponentEntry[] {
193
+ return [...this.components.values()];
194
+ }
195
+
196
+ getTimeline(): TimelineEntry[] {
197
+ return [...this.timeline];
198
+ }
199
+
200
+ getSignalsByComponent(componentId: string): SignalEntry[] {
201
+ return this.getSignals().filter((s) => s.componentId === componentId);
202
+ }
203
+
204
+ private pushTimeline(data: Omit<TimelineEntry, "id" | "timestamp">): void {
205
+ const entry: TimelineEntry = {
206
+ id: uid(),
207
+ timestamp: Date.now(),
208
+ ...data,
209
+ };
210
+
211
+ this.timeline.push(entry);
212
+
213
+ if (this.timeline.length > MAX_TIMELINE) {
214
+ this.timeline.shift();
215
+ }
216
+
217
+ this.bus.emit("timeline:push", entry);
218
+ }
219
+ }
@@ -0,0 +1,45 @@
1
+ export interface HistoryEntry {
2
+ value: unknown;
3
+ timestamp: number;
4
+ }
5
+
6
+ export interface SignalEntry {
7
+ id: string;
8
+ label: string;
9
+ componentId: string;
10
+ componentName: string;
11
+ value: unknown;
12
+ history: HistoryEntry[];
13
+ changedAt: number;
14
+ }
15
+
16
+ export interface LifecycleEvent {
17
+ hook: string;
18
+ timestamp: number;
19
+ }
20
+
21
+ export interface ComponentEntry {
22
+ id: string;
23
+ name: string;
24
+ renderCount: number;
25
+ lastRenderDuration: number;
26
+ mountedAt: number;
27
+ status: 'mounted' | 'unmounted';
28
+ lifecycle: LifecycleEvent[];
29
+ }
30
+
31
+ export type TimelineEventType =
32
+ | 'signal:change'
33
+ | 'component:render'
34
+ | 'component:mount'
35
+ | 'component:unmount'
36
+ | 'lifecycle'
37
+ | 'method:call';
38
+
39
+ export interface TimelineEntry {
40
+ id: string;
41
+ type: TimelineEventType;
42
+ label: string;
43
+ timestamp: number;
44
+ data: Record<string, unknown>;
45
+ }
@@ -0,0 +1,161 @@
1
+ import { Registry } from "../core/registry";
2
+
3
+ export interface DebugOptions {
4
+ label?: string;
5
+ }
6
+
7
+ // Duck-type check: callable with .subscribe but no .set → Computed
8
+ interface TrackedComputed {
9
+ (): unknown;
10
+ subscribe: (fn: (value: unknown) => void) => () => void;
11
+ }
12
+
13
+ function isComputed(value: unknown): value is TrackedComputed {
14
+ return (
15
+ typeof value === "function" &&
16
+ typeof (value as unknown as Record<string, unknown>).subscribe ===
17
+ "function" &&
18
+ !("set" in (value as object))
19
+ );
20
+ }
21
+
22
+ interface ComputedSlot {
23
+ computed: TrackedComputed;
24
+ unsub: () => void;
25
+ }
26
+
27
+ /**
28
+ * Tracks state, computed values, and methods in the devtools panel.
29
+ *
30
+ * On @State() properties (stacked):
31
+ * @Debug()
32
+ * @State() count = 0;
33
+ *
34
+ * On computed class fields:
35
+ * @Debug()
36
+ * doubled = computed(() => this.count * 2);
37
+ *
38
+ * On methods:
39
+ * @Debug()
40
+ * increment() { ... }
41
+ */
42
+ export function Debug(options: DebugOptions = {}) {
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;
50
+
51
+ // ── Method decorator ─────────────────────────────────────────────────
52
+ if (descriptor && typeof descriptor.value === "function") {
53
+ const original = descriptor.value as (...args: unknown[]) => unknown;
54
+
55
+ descriptor.value = function (this: object, ...args: unknown[]) {
56
+ const start = performance.now();
57
+ let result: unknown;
58
+ let threw = false;
59
+
60
+ try {
61
+ result = original.apply(this, args);
62
+ } catch (err) {
63
+ threw = true;
64
+ result = err;
65
+ throw err;
66
+ } finally {
67
+ const duration = performance.now() - start;
68
+ Registry.instance.recordMethodCall(
69
+ this,
70
+ label,
71
+ args,
72
+ threw ? `throw ${String(result)}` : result,
73
+ duration,
74
+ componentName,
75
+ );
76
+ }
77
+
78
+ return result;
79
+ };
80
+
81
+ return;
82
+ }
83
+
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
+ });
115
+
116
+ return;
117
+ }
118
+
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
+ );
137
+ return;
138
+ }
139
+
140
+ // subscribe() calls the callback immediately (synchronously), so we
141
+ // use a flag to skip the first call and register via registerSignal instead.
142
+ let skipFirst = true;
143
+ let prevValue = value();
144
+
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;
152
+ });
153
+
154
+ slots.set(this, { computed: value, unsub });
155
+ Registry.instance.registerSignal(this, label, prevValue, componentName);
156
+ },
157
+ enumerable: true,
158
+ configurable: true,
159
+ });
160
+ };
161
+ }
@@ -0,0 +1,3 @@
1
+ export { Debug } from './debug';
2
+ export type { DebugOptions } from './debug';
3
+ export { Trace } from './trace';
@@ -0,0 +1,64 @@
1
+ import { Registry } from '../core/registry';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ type AnyConstructor = new (...args: any[]) => any;
5
+
6
+ /**
7
+ * Instruments a component class to report renders and lifecycle events
8
+ * to the devtools panel.
9
+ *
10
+ * @Trace()
11
+ * @Component()
12
+ * class MyComponent extends BaseComponent { ... }
13
+ */
14
+ export function Trace() {
15
+ return function <T extends AnyConstructor>(constructor: T): T {
16
+ const name = constructor.name;
17
+ const registry = Registry.instance;
18
+ const proto = constructor.prototype as Record<string, unknown>;
19
+
20
+ // ── render() ──────────────────────────────────────────────────────────
21
+ const originalRender = proto.render as ((...args: unknown[]) => unknown) | undefined;
22
+
23
+ if (originalRender) {
24
+ proto.render = function (this: object, ...args: unknown[]) {
25
+ const start = performance.now();
26
+ const result = originalRender.call(this, ...args);
27
+ const duration = performance.now() - start;
28
+ registry.recordRender(this, duration);
29
+ return result;
30
+ };
31
+ }
32
+
33
+ // ── onBeforeMount – register the component instance ───────────────────
34
+ const originalOnBeforeMount = proto.onBeforeMount as
35
+ | ((...args: unknown[]) => unknown)
36
+ | undefined;
37
+
38
+ proto.onBeforeMount = function (this: object, ...args: unknown[]) {
39
+ registry.registerComponent(this, name);
40
+ registry.recordLifecycle(this, 'onBeforeMount');
41
+ return originalOnBeforeMount?.call(this, ...args);
42
+ };
43
+
44
+ // ── remaining lifecycle hooks ─────────────────────────────────────────
45
+ const hooks = [
46
+ 'onMount',
47
+ 'onUnmount',
48
+ 'onBeforeUpdate',
49
+ 'onUpdate',
50
+ 'onAfterUpdate',
51
+ ] as const;
52
+
53
+ for (const hook of hooks) {
54
+ const original = proto[hook] as ((...args: unknown[]) => unknown) | undefined;
55
+
56
+ proto[hook] = function (this: object, ...args: unknown[]) {
57
+ registry.recordLifecycle(this, hook);
58
+ return original?.call(this, ...args);
59
+ };
60
+ }
61
+
62
+ return constructor;
63
+ };
64
+ }
@@ -0,0 +1,20 @@
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
+ );
20
+ }
@@ -0,0 +1,6 @@
1
+ export { EllipsisVerticalIcon } from "./ellipsis-vertical";
2
+ export { PanelBottomIcon } from "./panel-bottom";
3
+ export { PanelLeftIcon } from "./panel-left";
4
+ export { PanelRightIcon } from "./panel-right";
5
+ export { PanelTopIcon } from "./panel-top";
6
+ export { XIcon } from "./x";
@@ -0,0 +1,19 @@
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
+ );
19
+ }
@@ -0,0 +1,19 @@
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
+ );
19
+ }
@@ -0,0 +1,19 @@
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
+ );
19
+ }
@@ -0,0 +1,19 @@
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
+ );
19
+ }
@@ -0,0 +1,19 @@
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
+ );
19
+ }