@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
@@ -0,0 +1,10 @@
1
+ import type { FunctionComponent } from "@praxisjs/shared";
2
+
3
+ import type { Registry } from "@core/registry";
4
+
5
+ export interface DevtoolsPlugin {
6
+ id: string;
7
+ label: string;
8
+ setup?: (registry: Registry) => void;
9
+ component: FunctionComponent<{ registry: Registry }>;
10
+ }
@@ -0,0 +1,121 @@
1
+ import logo from "@assets/logo.svg";
2
+ import unoStyles from "@assets/uno.generated.css?inline";
3
+ import { Registry } from "@core/registry";
4
+ import { ComponentsPlugin } from "@plugins/components";
5
+ import { SignalsPlugin } from "@plugins/signals";
6
+ import { TimelinePlugin } from "@plugins/timeline";
7
+ import unoReset from "@unocss/reset/tailwind-v4.css?inline";
8
+
9
+ import { signal } from "@praxisjs/core";
10
+ import { render } from "@praxisjs/runtime";
11
+
12
+ import { Panel } from "./panel";
13
+
14
+ import type { DevtoolsPlugin } from "@plugins/types";
15
+
16
+ const DEFAULT_PLUGINS: DevtoolsPlugin[] = [
17
+ SignalsPlugin,
18
+ ComponentsPlugin,
19
+ TimelinePlugin,
20
+ ];
21
+
22
+ export interface DevToolsOptions {
23
+ plugins?: DevtoolsPlugin[];
24
+ }
25
+
26
+ function DevToolsApp({
27
+ plugins,
28
+ registry,
29
+ }: {
30
+ plugins: DevtoolsPlugin[];
31
+ registry: Registry;
32
+ }) {
33
+ const open = signal(false);
34
+
35
+ return (
36
+ <div>
37
+ {() =>
38
+ open() ? (
39
+ <Panel
40
+ plugins={plugins}
41
+ registry={registry}
42
+ onClose={() => {
43
+ open.set(false);
44
+ }}
45
+ />
46
+ ) : (
47
+ <button
48
+ onClick={() => {
49
+ open.set(true);
50
+ }}
51
+ class="fixed bottom-5 right-5 z-[2147483647] flex items-center gap-1 pl-[10px] pr-[14px] h-[32px] rounded-xl font-sans font-semibold text-[12px] text-accent bg-header border border-border cursor-pointer select-none shadow-[0_8px_32px_rgba(0,0,0,0.7)] transition-all duration-200 hover:border-accent hover:bg-soft hover:shadow-[0_8px_32px_rgba(56,189,248,0.1)]"
52
+ >
53
+ <img class="h-[13px] w-[13px]" src={logo} />
54
+ <span class="text-[10px] font-bold text-accent tracking-widest uppercase">
55
+ devtools
56
+ </span>
57
+ </button>
58
+ )
59
+ }
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export const DevTools = {
65
+ plugins: [...DEFAULT_PLUGINS] as DevtoolsPlugin[],
66
+ host: null as HTMLElement | null,
67
+ cleanup: null as (() => void) | null,
68
+
69
+ init(options: DevToolsOptions = {}) {
70
+ if (this.host) return;
71
+
72
+ if (options.plugins) {
73
+ this.plugins = options.plugins;
74
+ }
75
+
76
+ this.plugins.forEach((p) => p.setup?.(Registry.instance));
77
+
78
+ const host = document.createElement("div");
79
+ host.id = "praxisjs-devtools";
80
+ document.body.appendChild(host);
81
+ this.host = host;
82
+
83
+ const shadow = host.attachShadow({ mode: "open" });
84
+
85
+ const styleEl = document.createElement("style");
86
+ styleEl.textContent = unoReset + unoStyles;
87
+ shadow.appendChild(styleEl);
88
+
89
+ const container = document.createElement("div");
90
+ shadow.appendChild(container);
91
+
92
+ this.cleanup = render(
93
+ <DevToolsApp plugins={this.plugins} registry={Registry.instance} />,
94
+ container,
95
+ );
96
+ },
97
+
98
+ registerPlugin(plugin: DevtoolsPlugin) {
99
+ if (this.plugins.find((p) => p.id === plugin.id)) return;
100
+ plugin.setup?.(Registry.instance);
101
+ this.plugins.push(plugin);
102
+ this.remount();
103
+ },
104
+
105
+ get registry() {
106
+ return Registry.instance;
107
+ },
108
+
109
+ remount() {
110
+ if (!this.host) return;
111
+
112
+ const shadow = this.host.shadowRoot as ShadowRoot;
113
+ const container = shadow.querySelector("div:last-child") as HTMLElement;
114
+
115
+ this.cleanup?.();
116
+ this.cleanup = render(
117
+ <DevToolsApp plugins={this.plugins} registry={Registry.instance} />,
118
+ container,
119
+ );
120
+ },
121
+ };
@@ -0,0 +1,225 @@
1
+ import logo from "@assets/logo.svg";
2
+ import {
3
+ EllipsisVerticalIcon,
4
+ PanelBottomIcon,
5
+ PanelLeftIcon,
6
+ PanelRightIcon,
7
+ PanelTopIcon,
8
+ XIcon,
9
+ } from "@icons";
10
+ import { IconButton } from "@shared/icon-button";
11
+
12
+ import { signal } from "@praxisjs/core";
13
+ import type { Component } from "@praxisjs/shared";
14
+
15
+ import type { Registry } from "@core/registry";
16
+ import type { DevtoolsPlugin } from "@plugins/types";
17
+
18
+ interface PanelProps {
19
+ plugins: DevtoolsPlugin[];
20
+ registry: Registry;
21
+ onClose: () => void;
22
+ }
23
+
24
+ type Dock = "bottom" | "top" | "left" | "right";
25
+
26
+ const MIN_HEIGHT = 180;
27
+ const DEFAULT_HEIGHT = 300;
28
+ const MIN_WIDTH = 240;
29
+ const DEFAULT_WIDTH = 380;
30
+
31
+ const DOCK_ICONS: Record<Dock, Component> = {
32
+ bottom: PanelBottomIcon,
33
+ top: PanelTopIcon,
34
+ left: PanelLeftIcon,
35
+ right: PanelRightIcon,
36
+ };
37
+
38
+ export function Panel({ plugins, registry, onClose }: PanelProps) {
39
+ const activeTab = signal(plugins[0]?.id ?? "");
40
+ const dock = signal<Dock>("bottom");
41
+ const dockMenuOpen = signal(false);
42
+ const bottomSize = signal(DEFAULT_HEIGHT);
43
+ const vertSize = signal(DEFAULT_WIDTH);
44
+
45
+ const handleMouseDown = (e: MouseEvent) => {
46
+ e.preventDefault();
47
+ const startX = e.clientX;
48
+ const startY = e.clientY;
49
+ const d = dock();
50
+ const startSize = d === "bottom" || d === "top" ? bottomSize() : vertSize();
51
+
52
+ const onMove = (ev: MouseEvent) => {
53
+ const d = dock();
54
+ if (d === "bottom") {
55
+ bottomSize.set(
56
+ Math.max(
57
+ MIN_HEIGHT,
58
+ Math.min(
59
+ window.innerHeight * 0.85,
60
+ startSize + (startY - ev.clientY),
61
+ ),
62
+ ),
63
+ );
64
+ } else if (d === "top") {
65
+ bottomSize.set(
66
+ Math.max(
67
+ MIN_HEIGHT,
68
+ Math.min(
69
+ window.innerHeight * 0.85,
70
+ startSize + (ev.clientY - startY),
71
+ ),
72
+ ),
73
+ );
74
+ } else if (d === "left") {
75
+ vertSize.set(
76
+ Math.max(
77
+ MIN_WIDTH,
78
+ Math.min(
79
+ window.innerWidth * 0.6,
80
+ startSize + (ev.clientX - startX),
81
+ ),
82
+ ),
83
+ );
84
+ } else {
85
+ vertSize.set(
86
+ Math.max(
87
+ MIN_WIDTH,
88
+ Math.min(
89
+ window.innerWidth * 0.6,
90
+ startSize + (startX - ev.clientX),
91
+ ),
92
+ ),
93
+ );
94
+ }
95
+ };
96
+
97
+ const onUp = () => {
98
+ document.removeEventListener("mousemove", onMove);
99
+ document.removeEventListener("mouseup", onUp);
100
+ };
101
+
102
+ document.addEventListener("mousemove", onMove);
103
+ document.addEventListener("mouseup", onUp);
104
+ };
105
+
106
+ return (
107
+ <div
108
+ class={() => {
109
+ const d = dock();
110
+ if (d === "top")
111
+ return "flex flex-col-reverse fixed top-0 left-0 w-full z-[2147483647]";
112
+ if (d === "left")
113
+ return "flex flex-row-reverse fixed left-0 top-0 h-screen z-[2147483647]";
114
+ if (d === "right")
115
+ return "flex fixed right-0 top-0 h-screen z-[2147483647]";
116
+ return "flex flex-col fixed bottom-0 left-0 w-full z-[2147483647]";
117
+ }}
118
+ >
119
+ <div
120
+ class={() => {
121
+ const d = dock();
122
+ if (d === "bottom" || d === "top")
123
+ return "h-1 cursor-[ns-resize] bg-transparent hover:bg-accent transition-colors duration-200";
124
+ return "w-1 cursor-[ew-resize] bg-transparent hover:bg-accent transition-colors duration-200";
125
+ }}
126
+ onMouseDown={handleMouseDown}
127
+ />
128
+ <div
129
+ class={() => {
130
+ const d = dock();
131
+ if (d === "top") return "w-full h-full bg-bg border-b border-border";
132
+ if (d === "left") return "w-full h-full bg-bg border-r border-border";
133
+ if (d === "right")
134
+ return "w-full h-full bg-bg border-l border-border";
135
+ return "w-full h-full bg-bg border-t border-border";
136
+ }}
137
+ style={() => {
138
+ const d = dock();
139
+ return d === "bottom" || d === "top"
140
+ ? { height: `${bottomSize().toString()}px` }
141
+ : { width: `${vertSize().toString()}px` };
142
+ }}
143
+ >
144
+ <div class="flex flex-col flex-1 w-full h-full overflow-hidden">
145
+ <div class="flex h-10 items-stretch border-b border-border bg-header shrink-0 px-2 gap-[2px]">
146
+ <div class="flex items-center gap-[6px] pr-3 mr-[2px] border-r border-border shrink-0">
147
+ <img class="h-[13px] w-[13px]" src={logo} />
148
+ <span class="text-[10px] font-bold text-accent tracking-widest uppercase">
149
+ praxisjs
150
+ </span>
151
+ </div>
152
+
153
+ <div class="flex items-stretch flex-1">
154
+ {plugins.map((p) => (
155
+ <button
156
+ key={p.id}
157
+ class={() =>
158
+ activeTab() === p.id
159
+ ? "relative flex items-center px-3 text-[11px] font-semibold text-accent cursor-pointer"
160
+ : "flex items-center px-3 text-[11px] text-muted cursor-pointer hover:text-text transition-colors duration-150"
161
+ }
162
+ onClick={() => {
163
+ activeTab.set(p.id);
164
+ }}
165
+ >
166
+ {p.label}
167
+ {() =>
168
+ activeTab() === p.id ? (
169
+ <span class="absolute bottom-0 left-0 right-0 h-[2px] bg-accent" />
170
+ ) : null
171
+ }
172
+ </button>
173
+ ))}
174
+ </div>
175
+
176
+ <div class="flex items-center gap-[2px] shrink-0">
177
+ <div class="relative">
178
+ <IconButton
179
+ title="Dock position"
180
+ icon={EllipsisVerticalIcon}
181
+ active={() => dockMenuOpen()}
182
+ onClick={() => {
183
+ dockMenuOpen.set(!dockMenuOpen());
184
+ }}
185
+ />
186
+ {() =>
187
+ dockMenuOpen() ? (
188
+ <div class="absolute right-0 top-full mt-1 flex gap-[2px] bg-bg border border-border rounded-md shadow-lg p-1 z-10">
189
+ {(["bottom", "top", "left", "right"] as Dock[]).map(
190
+ (v) => (
191
+ <IconButton
192
+ key={v}
193
+ title={`Dock ${v}`}
194
+ active={() => dock() === v}
195
+ icon={DOCK_ICONS[v]}
196
+ onClick={() => {
197
+ dock.set(v);
198
+ dockMenuOpen.set(false);
199
+ }}
200
+ />
201
+ ),
202
+ )}
203
+ </div>
204
+ ) : null
205
+ }
206
+ </div>
207
+
208
+ <div class="w-px h-3 bg-border mx-1" />
209
+
210
+ <IconButton title="Close" icon={XIcon} onClick={onClose} />
211
+ </div>
212
+ </div>
213
+
214
+ <div class="flex-1 overflow-hidden flex flex-col">
215
+ {() => {
216
+ const plugin = plugins.find((p) => p.id === activeTab());
217
+ const Active = plugin?.component;
218
+ return Active ? <Active registry={registry} /> : null;
219
+ }}
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ );
225
+ }
@@ -0,0 +1,12 @@
1
+ export function EmptyState({ message }: { message: string }) {
2
+ return (
3
+ <div class="flex flex-col items-center justify-center gap-2 py-12">
4
+ <span class="text-subtle text-[20px] leading-none select-none font-mono">
5
+
6
+ </span>
7
+ <p class="text-subtle text-[11px] text-center leading-relaxed max-w-[200px]">
8
+ {message}
9
+ </p>
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,30 @@
1
+ import type { Component } from "@praxisjs/shared";
2
+
3
+ export function IconButton({
4
+ title,
5
+ active,
6
+ icon: Icon,
7
+ onClick,
8
+ }: {
9
+ title?: string;
10
+ active?: boolean | (() => boolean);
11
+ icon: Component;
12
+ onClick: () => void;
13
+ }) {
14
+ return (
15
+ <button
16
+ title={title}
17
+ onClick={onClick}
18
+ class={() => {
19
+ const isActive = typeof active === "function" ? active() : active;
20
+ return `w-[22px] h-[22px] flex items-center justify-center rounded-md text-[11px] cursor-pointer transition-colors duration-150 ${
21
+ isActive
22
+ ? "text-accent bg-soft color-white"
23
+ : "text-subtle hover:color-muted hover:bg-section"
24
+ }`;
25
+ }}
26
+ >
27
+ <Icon />
28
+ </button>
29
+ );
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { Children } from "@praxisjs/shared";
2
+
3
+ export function PanelSection({
4
+ label,
5
+ children,
6
+ }: {
7
+ label: string;
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}
14
+ </div>
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ export function SearchInput({
2
+ placeholder,
3
+ onInput,
4
+ }: {
5
+ placeholder: string;
6
+ onInput: (value: string) => void;
7
+ }) {
8
+ return (
9
+ <input
10
+ type="text"
11
+ placeholder={placeholder}
12
+ class="w-full bg-input border border-border rounded-lg text-text text-[11px] px-3 py-[5px] outline-none box-border transition-shadow duration-150"
13
+ onInput={(e: InputEvent) => {
14
+ onInput((e.target as HTMLInputElement).value);
15
+ }}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ import type { Children } from "@praxisjs/shared";
2
+
3
+ export function SidePanel({
4
+ children,
5
+ width = "280px",
6
+ }: {
7
+ children?: Children;
8
+ width?: string;
9
+ }) {
10
+ return (
11
+ <div
12
+ class="shrink-0 border-l border-border flex flex-col overflow-hidden"
13
+ style={{ width }}
14
+ >
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,7 @@
1
+ export function time(ts: number, prefix?: string): string {
2
+ const localPrefix = prefix ? ` ${prefix}` : "";
3
+ const diff = Date.now() - ts;
4
+ if (diff < 1000) return `${diff.toString()}ms${localPrefix}`;
5
+ if (diff < 60_000) return `${(diff / 1000).toFixed(1)}s${localPrefix}`;
6
+ return `${Math.floor(diff / 60_000).toString()}m${localPrefix}`;
7
+ }
@@ -0,0 +1,13 @@
1
+ export function formatValue(value: unknown): string {
2
+ if (value === null) return "null";
3
+ if (value === undefined) return "undefined";
4
+ if (typeof value === "string") return `"${value}"`;
5
+ if (typeof value === "object") {
6
+ try {
7
+ return JSON.stringify(value);
8
+ } catch {
9
+ return "[object]";
10
+ }
11
+ }
12
+ return String(value as unknown);
13
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module "*.css?inline" {
4
+ const content: string;
5
+ export default content;
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./",
6
+ "jsxImportSource": "@praxisjs/jsx",
7
+ "module": "ESNext",
8
+ "moduleResolution": "bundler",
9
+ "paths": {
10
+ "@/*": ["./src/*"],
11
+ "@shared/*": ["./src/ui/shared/*"],
12
+ "@assets/*": ["./assets/*"],
13
+ "@core/*": ["./src/core/*"],
14
+ "@plugins/*": ["./src/plugins/*"],
15
+ "@decorators/*": ["./src/decorators/*"],
16
+ "@utils/*": ["./src/utils/*"],
17
+ "@icons": ["./src/icons/index.ts"]
18
+ }
19
+ },
20
+ "include": ["src", "uno.config.ts", "vite.config.ts"]
21
+ }
package/uno.config.ts ADDED
@@ -0,0 +1,57 @@
1
+ import presetWind3 from "@unocss/preset-wind3";
2
+ import { defineConfig } from "unocss";
3
+
4
+ export default defineConfig({
5
+ presets: [presetWind3()],
6
+
7
+ theme: {
8
+ colors: {
9
+ bg: "#0f0d17",
10
+ header: "#161326",
11
+ section: "#1b1830",
12
+ border: "rgba(155,144,230,0.25)",
13
+
14
+ text: "#f4f2ff",
15
+ muted: "#c7c3e6",
16
+ subtle: "#6f6a8f",
17
+
18
+ selected: "#221e3d",
19
+ input: "#1b1830",
20
+
21
+ accent: "#9b90e6",
22
+ accent2: "#7c6dd6",
23
+ accent3: "#6d5bbd",
24
+ soft: "rgba(155,144,230,0.18)",
25
+
26
+ success: "#0ea57a",
27
+ warn: "#d97706",
28
+ error: "#dc2626",
29
+ important: "#9b90e6",
30
+ },
31
+ },
32
+
33
+ preflights: [
34
+ {
35
+ getCSS: () => `
36
+ :host { all: initial; display: block; font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; }
37
+ * { box-sizing: border-box; }
38
+
39
+ input::placeholder { color: #6f6a8f; }
40
+ input:focus {
41
+ outline: none;
42
+ box-shadow: 0 0 0 2px rgba(155,144,230,0.3);
43
+ }
44
+
45
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
46
+ ::-webkit-scrollbar-track { background: transparent; }
47
+ ::-webkit-scrollbar-thumb {
48
+ background: rgba(155,144,230,0.2);
49
+ border-radius: 2px;
50
+ }
51
+ ::-webkit-scrollbar-thumb:hover {
52
+ background: rgba(155,144,230,0.4);
53
+ }
54
+ `,
55
+ },
56
+ ],
57
+ });
package/vite.config.ts ADDED
@@ -0,0 +1,42 @@
1
+ import path from "path";
2
+
3
+ import UnoCSS from "unocss/vite";
4
+ import { defineConfig } from "vite";
5
+ import dts from "vite-plugin-dts";
6
+
7
+ export default defineConfig({
8
+ plugins: [UnoCSS(), dts({ rollupTypes: true })],
9
+ esbuild: {
10
+ jsxImportSource: "@praxisjs/jsx",
11
+ jsx: "automatic",
12
+ },
13
+ build: {
14
+ lib: {
15
+ entry: "src/index.ts",
16
+ formats: ["es"],
17
+ fileName: "index",
18
+ },
19
+ rollupOptions: {
20
+ external: [
21
+ "@praxisjs/core",
22
+ "@praxisjs/jsx",
23
+ "@praxisjs/jsx/jsx-runtime",
24
+ "@praxisjs/runtime",
25
+ "@praxisjs/shared",
26
+ ],
27
+ },
28
+ cssCodeSplit: false,
29
+ },
30
+ resolve: {
31
+ alias: {
32
+ "@": path.resolve(__dirname, "src"),
33
+ "@shared": path.resolve(__dirname, "src/ui/shared"),
34
+ "@assets": path.resolve(__dirname, "assets"),
35
+ "@core": path.resolve(__dirname, "src/core"),
36
+ "@plugins": path.resolve(__dirname, "src/plugins"),
37
+ "@decorators": path.resolve(__dirname, "src/decorators"),
38
+ "@utils": path.resolve(__dirname, "src/utils"),
39
+ "@icons": path.resolve(__dirname, "src/icons"),
40
+ },
41
+ },
42
+ });