@ddd-ts/event-tree-viewer 0.0.0-eventviz.10

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/cli.mjs +20 -0
  4. package/index.html +13 -0
  5. package/package.json +67 -0
  6. package/src/App.tsx +153 -0
  7. package/src/application/trpc-client.ts +6 -0
  8. package/src/application/use-direction.ts +18 -0
  9. package/src/application/use-domains.ts +9 -0
  10. package/src/application/use-expansion.ts +42 -0
  11. package/src/application/use-filters.ts +78 -0
  12. package/src/application/use-graph.ts +38 -0
  13. package/src/application/use-reveal.ts +26 -0
  14. package/src/application/use-selection.ts +15 -0
  15. package/src/application/use-settings.ts +84 -0
  16. package/src/application/use-view-mode.ts +14 -0
  17. package/src/assets/fonts/Monor_Regular.otf +0 -0
  18. package/src/assets/fonts/Supreme-Variable.woff2 +0 -0
  19. package/src/assets/fonts/Supreme-VariableItalic.woff2 +0 -0
  20. package/src/assets/fonts/monor-bold.otf +0 -0
  21. package/src/assets/react.svg +1 -0
  22. package/src/cli.ts +29 -0
  23. package/src/components/direction-toggle.tsx +28 -0
  24. package/src/components/domain-header.tsx +44 -0
  25. package/src/components/export-dialog.tsx +164 -0
  26. package/src/components/filter-bar.tsx +17 -0
  27. package/src/components/header.tsx +37 -0
  28. package/src/components/inspector.tsx +183 -0
  29. package/src/components/kind-filter.tsx +70 -0
  30. package/src/components/node-badge.tsx +19 -0
  31. package/src/components/node-name.tsx +66 -0
  32. package/src/components/settings-menu.tsx +147 -0
  33. package/src/components/ui/badge.tsx +52 -0
  34. package/src/components/ui/button.tsx +56 -0
  35. package/src/components/ui/card.tsx +103 -0
  36. package/src/components/ui/checkbox.tsx +28 -0
  37. package/src/components/ui/dialog.tsx +108 -0
  38. package/src/components/ui/input.tsx +20 -0
  39. package/src/components/ui/popover.tsx +88 -0
  40. package/src/components/ui/scroll-area.tsx +54 -0
  41. package/src/components/ui/select.tsx +88 -0
  42. package/src/components/ui/separator.tsx +23 -0
  43. package/src/components/ui/toggle-group.tsx +89 -0
  44. package/src/components/ui/toggle.tsx +43 -0
  45. package/src/components/view-switcher.tsx +28 -0
  46. package/src/components/views/graph-view.tsx +1203 -0
  47. package/src/components/views/list-view.tsx +109 -0
  48. package/src/components/views/tree-view.tsx +485 -0
  49. package/src/domain/cypher-export.ts +66 -0
  50. package/src/domain/direction.ts +1 -0
  51. package/src/domain/domain-grouping.ts +217 -0
  52. package/src/domain/edge.ts +37 -0
  53. package/src/domain/filter.ts +21 -0
  54. package/src/domain/flatten-tree.ts +167 -0
  55. package/src/domain/graph.ts +42 -0
  56. package/src/domain/node.ts +28 -0
  57. package/src/domain/roots.ts +18 -0
  58. package/src/domain/traversal.ts +60 -0
  59. package/src/index.css +205 -0
  60. package/src/lib/utils.ts +6 -0
  61. package/src/main.tsx +16 -0
  62. package/src/server/router.ts +87 -0
  63. package/src/server/vite-plugin.ts +99 -0
  64. package/vite.config.ts +36 -0
package/src/index.css ADDED
@@ -0,0 +1,205 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
2
+ @import "tailwindcss";
3
+ @import "tw-animate-css";
4
+ @import "shadcn/tailwind.css";
5
+
6
+ @font-face {
7
+ font-family: "Supreme Variable";
8
+ src: url("./assets/fonts/Supreme-Variable.woff2") format("woff2-variations");
9
+ font-weight: 100 800;
10
+ font-style: normal;
11
+ font-display: swap;
12
+ }
13
+
14
+ @font-face {
15
+ font-family: "Supreme Variable";
16
+ src: url("./assets/fonts/Supreme-VariableItalic.woff2") format("woff2-variations");
17
+ font-weight: 100 800;
18
+ font-style: italic;
19
+ font-display: swap;
20
+ }
21
+
22
+ @font-face {
23
+ font-family: "Monor";
24
+ src: url("./assets/fonts/Monor_Regular.otf") format("opentype");
25
+ font-weight: 400;
26
+ font-style: normal;
27
+ font-display: swap;
28
+ }
29
+
30
+ @font-face {
31
+ font-family: "Monor";
32
+ src: url("./assets/fonts/monor-bold.otf") format("opentype");
33
+ font-weight: 700;
34
+ font-style: normal;
35
+ font-display: swap;
36
+ }
37
+
38
+ @custom-variant dark (&:is(.dark *));
39
+
40
+ .theme-switching,
41
+ .theme-switching *,
42
+ .theme-switching *::before,
43
+ .theme-switching *::after {
44
+ transition: none !important;
45
+ }
46
+
47
+ @theme inline {
48
+ --font-heading: 'Monor', ui-serif, Georgia, serif;
49
+ --font-display: 'Monor', ui-serif, Georgia, serif;
50
+ --font-sans: 'Supreme Variable', ui-sans-serif, system-ui, sans-serif;
51
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
52
+ --color-sidebar-ring: var(--sidebar-ring);
53
+ --color-sidebar-border: var(--sidebar-border);
54
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
55
+ --color-sidebar-accent: var(--sidebar-accent);
56
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
57
+ --color-sidebar-primary: var(--sidebar-primary);
58
+ --color-sidebar-foreground: var(--sidebar-foreground);
59
+ --color-sidebar: var(--sidebar);
60
+ --color-chart-5: var(--chart-5);
61
+ --color-chart-4: var(--chart-4);
62
+ --color-chart-3: var(--chart-3);
63
+ --color-chart-2: var(--chart-2);
64
+ --color-chart-1: var(--chart-1);
65
+ --color-ring: var(--ring);
66
+ --color-input: var(--input);
67
+ --color-border: var(--border);
68
+ --color-destructive: var(--destructive);
69
+ --color-accent-foreground: var(--accent-foreground);
70
+ --color-accent: var(--accent);
71
+ --color-muted-foreground: var(--muted-foreground);
72
+ --color-muted: var(--muted);
73
+ --color-secondary-foreground: var(--secondary-foreground);
74
+ --color-secondary: var(--secondary);
75
+ --color-primary-foreground: var(--primary-foreground);
76
+ --color-primary: var(--primary);
77
+ --color-popover-foreground: var(--popover-foreground);
78
+ --color-popover: var(--popover);
79
+ --color-card-foreground: var(--card-foreground);
80
+ --color-card: var(--card);
81
+ --color-foreground: var(--foreground);
82
+ --color-background: var(--background);
83
+ --color-graph-edge: var(--graph-edge);
84
+ --color-graph-dim: var(--graph-dim);
85
+ --radius-sm: calc(var(--radius) * 0.6);
86
+ --radius-md: calc(var(--radius) * 0.8);
87
+ --radius-lg: var(--radius);
88
+ --radius-xl: calc(var(--radius) * 1.4);
89
+ --radius-2xl: calc(var(--radius) * 1.8);
90
+ --radius-3xl: calc(var(--radius) * 2.2);
91
+ --radius-4xl: calc(var(--radius) * 2.6);
92
+ }
93
+
94
+ :root {
95
+ --background: oklch(1 0 0);
96
+ --foreground: oklch(0.145 0 0);
97
+ --card: oklch(1 0 0);
98
+ --card-foreground: oklch(0.145 0 0);
99
+ --popover: oklch(1 0 0);
100
+ --popover-foreground: oklch(0.145 0 0);
101
+ --primary: oklch(0.205 0 0);
102
+ --primary-foreground: oklch(0.985 0 0);
103
+ --secondary: oklch(0.97 0 0);
104
+ --secondary-foreground: oklch(0.205 0 0);
105
+ --muted: oklch(0.97 0 0);
106
+ --muted-foreground: oklch(0.556 0 0);
107
+ --accent: oklch(0.97 0 0);
108
+ --accent-foreground: oklch(0.205 0 0);
109
+ --destructive: oklch(0.577 0.245 27.325);
110
+ --border: oklch(0.922 0 0);
111
+ --input: oklch(0.922 0 0);
112
+ --graph-edge: oklch(0.73 0 0);
113
+ --graph-dim: oklch(0.9 0 0);
114
+ --ring: oklch(0.708 0 0);
115
+ --chart-1: oklch(0.87 0 0);
116
+ --chart-2: oklch(0.556 0 0);
117
+ --chart-3: oklch(0.439 0 0);
118
+ --chart-4: oklch(0.371 0 0);
119
+ --chart-5: oklch(0.269 0 0);
120
+ --radius: 0.625rem;
121
+ --sidebar: oklch(0.985 0 0);
122
+ --sidebar-foreground: oklch(0.145 0 0);
123
+ --sidebar-primary: oklch(0.205 0 0);
124
+ --sidebar-primary-foreground: oklch(0.985 0 0);
125
+ --sidebar-accent: oklch(0.97 0 0);
126
+ --sidebar-accent-foreground: oklch(0.205 0 0);
127
+ --sidebar-border: oklch(0.922 0 0);
128
+ --sidebar-ring: oklch(0.708 0 0);
129
+ }
130
+
131
+ .dark {
132
+ --background: oklch(0.1913 0 0);
133
+ --foreground: oklch(0.9851 0 0);
134
+ --card: oklch(0.2308 0 0);
135
+ --card-foreground: oklch(0.9851 0 0);
136
+ --popover: oklch(0.2308 0 0);
137
+ --popover-foreground: oklch(0.9851 0 0);
138
+ --primary: oklch(0.9851 0 0);
139
+ --primary-foreground: oklch(0.2308 0 0);
140
+ --secondary: oklch(0.2603 0 0);
141
+ --secondary-foreground: oklch(0.9097 0 0);
142
+ --muted: oklch(0.2308 0 0);
143
+ --muted-foreground: oklch(1 0 0 / 50%);
144
+ --accent: oklch(0.2809 0 0);
145
+ --accent-foreground: oklch(0.9851 0 0);
146
+ --destructive: oklch(0.704 0.191 22.216);
147
+ --border: oklch(1 0 0 / 7%);
148
+ --input: oklch(1 0 0 / 12%);
149
+ --ring: oklch(1 0 0 / 30%);
150
+ --graph-edge: oklch(0.5 0 0);
151
+ --graph-dim: oklch(0.3 0 0);
152
+ --chart-1: oklch(0.87 0 0);
153
+ --chart-2: oklch(0.556 0 0);
154
+ --chart-3: oklch(0.439 0 0);
155
+ --chart-4: oklch(0.371 0 0);
156
+ --chart-5: oklch(0.269 0 0);
157
+ --sidebar: oklch(0.2308 0 0);
158
+ --sidebar-foreground: oklch(0.9851 0 0);
159
+ --sidebar-primary: oklch(0.488 0.243 264.376);
160
+ --sidebar-primary-foreground: oklch(0.9851 0 0);
161
+ --sidebar-accent: oklch(0.2809 0 0);
162
+ --sidebar-accent-foreground: oklch(0.9851 0 0);
163
+ --sidebar-border: oklch(1 0 0 / 7%);
164
+ --sidebar-ring: oklch(1 0 0 / 30%);
165
+ }
166
+
167
+ .dark [data-slot="popover-content"],
168
+ .dark [data-slot="popover-positioner"] > [role="dialog"] {
169
+ box-shadow:
170
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.07),
171
+ 0 2px 8px -1px rgba(0, 0, 0, 0.1),
172
+ 0 12px 28px -4px rgba(0, 0, 0, 0.2),
173
+ 0 24px 48px -12px rgba(0, 0, 0, 0.2);
174
+ }
175
+
176
+ .dark .surface-elevated {
177
+ box-shadow:
178
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.07),
179
+ 0 2px 8px -1px rgba(0, 0, 0, 0.1),
180
+ 0 12px 28px -4px rgba(0, 0, 0, 0.2);
181
+ }
182
+
183
+ @layer base {
184
+ * {
185
+ @apply border-border outline-ring/50;
186
+ }
187
+ body {
188
+ @apply bg-background text-foreground;
189
+ }
190
+ button:not(:disabled), [role="button"]:not(:disabled) {
191
+ cursor: pointer;
192
+ }
193
+ html {
194
+ @apply font-sans;
195
+ }
196
+ }
197
+
198
+ @utility scrollbar-hidden {
199
+ scrollbar-width: none;
200
+ -ms-overflow-style: none;
201
+ &::-webkit-scrollbar {
202
+ display: none;
203
+ }
204
+ }
205
+
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4
+
5
+ import "./index.css"
6
+ import App from "./App.tsx"
7
+
8
+ const queryClient = new QueryClient()
9
+
10
+ createRoot(document.getElementById("root")!).render(
11
+ <StrictMode>
12
+ <QueryClientProvider client={queryClient}>
13
+ <App />
14
+ </QueryClientProvider>
15
+ </StrictMode>
16
+ )
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { initTRPC, TRPCError } from "@trpc/server"
4
+ import launchEditor from "launch-editor"
5
+ import { z } from "zod"
6
+ import { engine, type Edge, type Node } from "@ddd-ts/event-tree"
7
+
8
+ const t = initTRPC.create()
9
+
10
+ export interface Graph {
11
+ nodes: readonly Node[]
12
+ edges: readonly Edge[]
13
+ }
14
+
15
+ let cache: { root: string; graph: Graph } | null = null
16
+
17
+ function scan(root: string): Graph {
18
+ if (cache && cache.root === root) return cache.graph
19
+ engine.reset()
20
+ engine.run(root)
21
+ const toRel = (file: string) =>
22
+ path.isAbsolute(file) ? path.relative(root, file) : file
23
+ const nodes = engine.getNodes().map((n) => ({
24
+ ...n,
25
+ source: { ...n.source, file: toRel(n.source.file) },
26
+ })) as Node[]
27
+ const edges = engine.getEdges().map((e) => ({
28
+ ...e,
29
+ source: { ...e.source, file: toRel(e.source.file) },
30
+ })) as Edge[]
31
+ const graph: Graph = { nodes, edges }
32
+ cache = { root, graph }
33
+ return graph
34
+ }
35
+
36
+ export function invalidate() {
37
+ cache = null
38
+ }
39
+
40
+ export function createRouter(scanRoot: string) {
41
+ return t.router({
42
+ graph: t.router({
43
+ get: t.procedure.query(() => scan(scanRoot)),
44
+ }),
45
+ editor: t.router({
46
+ open: t.procedure
47
+ .input(
48
+ z.object({
49
+ file: z.string().min(1),
50
+ offset: z.number().int().nonnegative().optional(),
51
+ })
52
+ )
53
+ .mutation(({ input }) => {
54
+ const absolute = path.isAbsolute(input.file)
55
+ ? input.file
56
+ : path.resolve(scanRoot, input.file)
57
+ if (!fs.existsSync(absolute)) {
58
+ throw new TRPCError({
59
+ code: "NOT_FOUND",
60
+ message: `file not found: ${input.file}`,
61
+ })
62
+ }
63
+ let spec = absolute
64
+ if (input.offset != null) {
65
+ const text = fs.readFileSync(absolute, "utf8")
66
+ const before = text.slice(0, input.offset)
67
+ const line = before.split("\n").length
68
+ const column = input.offset - before.lastIndexOf("\n")
69
+ spec = `${absolute}:${line}:${column}`
70
+ }
71
+ return new Promise<{ ok: true }>((resolve, reject) => {
72
+ launchEditor(spec, (fileName, error) => {
73
+ reject(
74
+ new TRPCError({
75
+ code: "INTERNAL_SERVER_ERROR",
76
+ message: `launch-editor failed for ${fileName}: ${error}`,
77
+ })
78
+ )
79
+ })
80
+ queueMicrotask(() => resolve({ ok: true }))
81
+ })
82
+ }),
83
+ }),
84
+ })
85
+ }
86
+
87
+ export type AppRouter = ReturnType<typeof createRouter>
@@ -0,0 +1,99 @@
1
+ import path from "node:path"
2
+ import type { PluginOption, ViteDevServer } from "vite"
3
+
4
+ function nodeRequestToFetch(
5
+ req: import("node:http").IncomingMessage,
6
+ base: string
7
+ ): Request {
8
+ const url = new URL(req.url ?? "/", base)
9
+ const headers = new Headers()
10
+ for (const [k, v] of Object.entries(req.headers)) {
11
+ if (v == null) continue
12
+ if (Array.isArray(v)) v.forEach((vv) => headers.append(k, vv))
13
+ else headers.set(k, v)
14
+ }
15
+ const init: RequestInit = { method: req.method, headers }
16
+ if (req.method && !["GET", "HEAD"].includes(req.method)) {
17
+ const chunks: Buffer[] = []
18
+ return new Request(url, {
19
+ ...init,
20
+ body: new ReadableStream({
21
+ start(controller) {
22
+ req.on("data", (c) => chunks.push(c))
23
+ req.on("end", () => {
24
+ controller.enqueue(Buffer.concat(chunks))
25
+ controller.close()
26
+ })
27
+ req.on("error", (e) => controller.error(e))
28
+ },
29
+ }),
30
+ duplex: "half",
31
+ } as RequestInit & { duplex: "half" })
32
+ }
33
+ return new Request(url, init)
34
+ }
35
+
36
+ async function writeFetchResponseToNode(
37
+ fetchRes: Response,
38
+ res: import("node:http").ServerResponse
39
+ ) {
40
+ res.statusCode = fetchRes.status
41
+ fetchRes.headers.forEach((v, k) => res.setHeader(k, v))
42
+ if (fetchRes.body) {
43
+ const reader = fetchRes.body.getReader()
44
+ while (true) {
45
+ const { done, value } = await reader.read()
46
+ if (done) break
47
+ res.write(value)
48
+ }
49
+ }
50
+ res.end()
51
+ }
52
+
53
+ export function trpcPlugin(scanRoot: string): PluginOption {
54
+ return {
55
+ name: "event-tree-trpc",
56
+ apply: "serve",
57
+ async configureServer(server: ViteDevServer) {
58
+ const [{ createRouter, invalidate }, { fetchRequestHandler }] =
59
+ await Promise.all([
60
+ server.ssrLoadModule("/src/server/router.ts") as Promise<
61
+ typeof import("./router")
62
+ >,
63
+ import("@trpc/server/adapters/fetch"),
64
+ ])
65
+ const router = createRouter(scanRoot)
66
+ const endpoint = "/trpc"
67
+
68
+ server.middlewares.use(endpoint, async (req, res) => {
69
+ try {
70
+ req.url = endpoint + (req.url ?? "")
71
+ const request = nodeRequestToFetch(
72
+ req,
73
+ `http://${req.headers.host ?? "localhost"}`
74
+ )
75
+ const response = await fetchRequestHandler({
76
+ endpoint,
77
+ req: request,
78
+ router,
79
+ })
80
+ await writeFetchResponseToNode(response, res)
81
+ } catch (error) {
82
+ server.config.logger.error(
83
+ `[event-tree-trpc] ${(error as Error).message}`
84
+ )
85
+ res.statusCode = 500
86
+ res.end(String(error))
87
+ }
88
+ })
89
+
90
+ server.watcher.add(path.resolve(scanRoot, "**/*.ts"))
91
+ const onChange = (file: string) => {
92
+ if (file.startsWith(scanRoot)) invalidate()
93
+ }
94
+ server.watcher.on("change", onChange)
95
+ server.watcher.on("add", onChange)
96
+ server.watcher.on("unlink", onChange)
97
+ },
98
+ }
99
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import path from "node:path"
2
+ import tailwindcss from "@tailwindcss/vite"
3
+ import react from "@vitejs/plugin-react"
4
+ import { defineConfig } from "vite"
5
+ import { trpcPlugin } from "./src/server/vite-plugin"
6
+
7
+ const SCAN_ROOT = path.resolve(process.env.EVENTVIZ_SCAN_ROOT ?? process.cwd())
8
+
9
+ // https://vite.dev/config/
10
+ export default defineConfig({
11
+ plugins: [react(), tailwindcss(), trpcPlugin(SCAN_ROOT)],
12
+ resolve: {
13
+ alias: {
14
+ "@": path.resolve(__dirname, "./src"),
15
+ },
16
+ },
17
+ optimizeDeps: {
18
+ include: [
19
+ "use-sync-external-store/shim",
20
+ "use-sync-external-store/shim/with-selector",
21
+ "elkjs/lib/elk.bundled.js",
22
+ "@base-ui/react/button",
23
+ "@base-ui/react/checkbox",
24
+ "@base-ui/react/dialog",
25
+ "@base-ui/react/input",
26
+ "@base-ui/react/merge-props",
27
+ "@base-ui/react/popover",
28
+ "@base-ui/react/scroll-area",
29
+ "@base-ui/react/select",
30
+ "@base-ui/react/separator",
31
+ "@base-ui/react/toggle",
32
+ "@base-ui/react/toggle-group",
33
+ "@base-ui/react/use-render",
34
+ ],
35
+ },
36
+ })