@nwire/studio 0.9.1

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/components.json +19 -0
  4. package/index.html +12 -0
  5. package/package.json +66 -0
  6. package/src/App.vue +305 -0
  7. package/src/components/EmptyState.stories.ts +53 -0
  8. package/src/components/EmptyState.vue +28 -0
  9. package/src/components/ErrorBoundary.vue +60 -0
  10. package/src/components/FilterInput.stories.ts +32 -0
  11. package/src/components/FilterInput.vue +33 -0
  12. package/src/components/JsonView.stories.ts +38 -0
  13. package/src/components/JsonView.vue +34 -0
  14. package/src/components/KindBadge.stories.ts +72 -0
  15. package/src/components/KindBadge.vue +59 -0
  16. package/src/components/ListRow.stories.ts +56 -0
  17. package/src/components/ListRow.vue +48 -0
  18. package/src/components/MasterDetail.stories.ts +74 -0
  19. package/src/components/MasterDetail.vue +35 -0
  20. package/src/components/MonacoViewer.vue +143 -0
  21. package/src/components/PageHeader.stories.ts +45 -0
  22. package/src/components/PageHeader.vue +46 -0
  23. package/src/components/SchemaNode.vue +208 -0
  24. package/src/components/SchemaTree.vue +65 -0
  25. package/src/components/SourceDrawer.vue +136 -0
  26. package/src/components/SourcePill.vue +103 -0
  27. package/src/components/__tests__/EmptyState.test.ts +28 -0
  28. package/src/components/__tests__/ErrorBoundary.test.ts +52 -0
  29. package/src/components/__tests__/FilterInput.test.ts +38 -0
  30. package/src/components/__tests__/JsonView.test.ts +33 -0
  31. package/src/components/__tests__/KindBadge.test.ts +39 -0
  32. package/src/components/__tests__/ListRow.test.ts +39 -0
  33. package/src/components/__tests__/MasterDetail.test.ts +40 -0
  34. package/src/components/__tests__/PageHeader.test.ts +42 -0
  35. package/src/components/index.ts +17 -0
  36. package/src/components/ui/badge/Badge.vue +17 -0
  37. package/src/components/ui/badge/index.ts +25 -0
  38. package/src/components/ui/button/Button.vue +28 -0
  39. package/src/components/ui/button/index.ts +34 -0
  40. package/src/components/ui/card/Card.vue +14 -0
  41. package/src/components/ui/card/CardContent.vue +14 -0
  42. package/src/components/ui/card/CardDescription.vue +14 -0
  43. package/src/components/ui/card/CardFooter.vue +14 -0
  44. package/src/components/ui/card/CardHeader.vue +14 -0
  45. package/src/components/ui/card/CardTitle.vue +14 -0
  46. package/src/components/ui/card/index.ts +6 -0
  47. package/src/components/ui/dialog/Dialog.vue +15 -0
  48. package/src/components/ui/dialog/DialogClose.vue +12 -0
  49. package/src/components/ui/dialog/DialogContent.vue +47 -0
  50. package/src/components/ui/dialog/DialogDescription.vue +22 -0
  51. package/src/components/ui/dialog/DialogFooter.vue +12 -0
  52. package/src/components/ui/dialog/DialogHeader.vue +14 -0
  53. package/src/components/ui/dialog/DialogScrollContent.vue +60 -0
  54. package/src/components/ui/dialog/DialogTitle.vue +22 -0
  55. package/src/components/ui/dialog/DialogTrigger.vue +12 -0
  56. package/src/components/ui/dialog/index.ts +9 -0
  57. package/src/components/ui/input/Input.vue +32 -0
  58. package/src/components/ui/input/index.ts +1 -0
  59. package/src/components/ui/scroll-area/ScrollArea.vue +22 -0
  60. package/src/components/ui/scroll-area/ScrollBar.vue +32 -0
  61. package/src/components/ui/scroll-area/index.ts +2 -0
  62. package/src/components/ui/separator/Separator.vue +27 -0
  63. package/src/components/ui/separator/index.ts +1 -0
  64. package/src/components/ui/skeleton/Skeleton.vue +14 -0
  65. package/src/components/ui/skeleton/index.ts +1 -0
  66. package/src/components/ui/tabs/Tabs.vue +15 -0
  67. package/src/components/ui/tabs/TabsContent.vue +25 -0
  68. package/src/components/ui/tabs/TabsList.vue +25 -0
  69. package/src/components/ui/tabs/TabsTrigger.vue +29 -0
  70. package/src/components/ui/tabs/index.ts +4 -0
  71. package/src/components/ui/tooltip/Tooltip.vue +15 -0
  72. package/src/components/ui/tooltip/TooltipContent.vue +40 -0
  73. package/src/components/ui/tooltip/TooltipProvider.vue +12 -0
  74. package/src/components/ui/tooltip/TooltipTrigger.vue +12 -0
  75. package/src/components/ui/tooltip/index.ts +4 -0
  76. package/src/composables/useCopy.ts +31 -0
  77. package/src/lib/__tests__/normalize-cache.test.ts +104 -0
  78. package/src/lib/cache.ts +334 -0
  79. package/src/lib/normalize-cache.ts +92 -0
  80. package/src/lib/project-catalog.ts +125 -0
  81. package/src/lib/utils.ts +6 -0
  82. package/src/main.ts +112 -0
  83. package/src/pages/Actions.vue +180 -0
  84. package/src/pages/Commands.vue +262 -0
  85. package/src/pages/Dispatch.vue +431 -0
  86. package/src/pages/Events.vue +166 -0
  87. package/src/pages/Home.stories.ts +47 -0
  88. package/src/pages/Home.vue +485 -0
  89. package/src/pages/Hooks.vue +297 -0
  90. package/src/pages/Live.vue +249 -0
  91. package/src/pages/Modules.vue +174 -0
  92. package/src/pages/Overview.vue +159 -0
  93. package/src/pages/Plugins.stories.ts +44 -0
  94. package/src/pages/Plugins.vue +403 -0
  95. package/src/pages/Projects.vue +272 -0
  96. package/src/pages/Run.vue +479 -0
  97. package/src/pages/Topology.vue +164 -0
  98. package/src/pages/Trace.vue +511 -0
  99. package/src/pages/TraceNode.vue +166 -0
  100. package/src/pages/Workflows.vue +191 -0
  101. package/src/pages/__tests__/Actions.test.ts +98 -0
  102. package/src/pages/__tests__/Home.test.ts +98 -0
  103. package/src/pages/__tests__/Hooks.test.ts +119 -0
  104. package/src/pages/__tests__/Plugins.test.ts +80 -0
  105. package/src/style.css +40 -0
  106. package/tsconfig.json +20 -0
  107. package/vite.config.ts +892 -0
@@ -0,0 +1 @@
1
+ export { default as Input } from "./Input.vue";
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { ScrollAreaRootProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+ import ScrollBar from "./ScrollBar.vue";
8
+
9
+ const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>();
10
+
11
+ const delegatedProps = reactiveOmit(props, "class");
12
+ </script>
13
+
14
+ <template>
15
+ <ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
16
+ <ScrollAreaViewport class="h-full w-full rounded-[inherit]">
17
+ <slot />
18
+ </ScrollAreaViewport>
19
+ <ScrollBar />
20
+ <ScrollAreaCorner />
21
+ </ScrollAreaRoot>
22
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import type { ScrollAreaScrollbarProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = withDefaults(
9
+ defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(),
10
+ {
11
+ orientation: "vertical",
12
+ },
13
+ );
14
+
15
+ const delegatedProps = reactiveOmit(props, "class");
16
+ </script>
17
+
18
+ <template>
19
+ <ScrollAreaScrollbar
20
+ v-bind="delegatedProps"
21
+ :class="
22
+ cn(
23
+ 'flex touch-none select-none transition-colors',
24
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
25
+ orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
26
+ props.class,
27
+ )
28
+ "
29
+ >
30
+ <ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
31
+ </ScrollAreaScrollbar>
32
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as ScrollArea } from "./ScrollArea.vue";
2
+ export { default as ScrollBar } from "./ScrollBar.vue";
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { SeparatorProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { Separator } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>(), {
9
+ orientation: "horizontal",
10
+ decorative: true,
11
+ });
12
+
13
+ const delegatedProps = reactiveOmit(props, "class");
14
+ </script>
15
+
16
+ <template>
17
+ <Separator
18
+ v-bind="delegatedProps"
19
+ :class="
20
+ cn(
21
+ 'shrink-0 bg-border',
22
+ props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
23
+ props.class,
24
+ )
25
+ "
26
+ />
27
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Separator } from "./Separator.vue";
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface SkeletonProps {
6
+ class?: HTMLAttributes["class"];
7
+ }
8
+
9
+ const props = defineProps<SkeletonProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <div :class="cn('animate-pulse rounded-md bg-muted', props.class)" />
14
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Skeleton } from "./Skeleton.vue";
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import type { TabsRootEmits, TabsRootProps } from "reka-ui";
3
+ import { TabsRoot, useForwardPropsEmits } from "reka-ui";
4
+
5
+ const props = defineProps<TabsRootProps>();
6
+ const emits = defineEmits<TabsRootEmits>();
7
+
8
+ const forwarded = useForwardPropsEmits(props, emits);
9
+ </script>
10
+
11
+ <template>
12
+ <TabsRoot v-bind="forwarded">
13
+ <slot />
14
+ </TabsRoot>
15
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import type { TabsContentProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { TabsContent } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+ </script>
12
+
13
+ <template>
14
+ <TabsContent
15
+ :class="
16
+ cn(
17
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
18
+ props.class,
19
+ )
20
+ "
21
+ v-bind="delegatedProps"
22
+ >
23
+ <slot />
24
+ </TabsContent>
25
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import type { TabsListProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { TabsList } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = defineProps<TabsListProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+ </script>
12
+
13
+ <template>
14
+ <TabsList
15
+ v-bind="delegatedProps"
16
+ :class="
17
+ cn(
18
+ 'inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
19
+ props.class,
20
+ )
21
+ "
22
+ >
23
+ <slot />
24
+ </TabsList>
25
+ </template>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import type { TabsTriggerProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { TabsTrigger, useForwardProps } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps);
13
+ </script>
14
+
15
+ <template>
16
+ <TabsTrigger
17
+ v-bind="forwardedProps"
18
+ :class="
19
+ cn(
20
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
21
+ props.class,
22
+ )
23
+ "
24
+ >
25
+ <span class="truncate">
26
+ <slot />
27
+ </span>
28
+ </TabsTrigger>
29
+ </template>
@@ -0,0 +1,4 @@
1
+ export { default as Tabs } from "./Tabs.vue";
2
+ export { default as TabsContent } from "./TabsContent.vue";
3
+ export { default as TabsList } from "./TabsList.vue";
4
+ export { default as TabsTrigger } from "./TabsTrigger.vue";
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import type { TooltipRootEmits, TooltipRootProps } from "reka-ui";
3
+ import { TooltipRoot, useForwardPropsEmits } from "reka-ui";
4
+
5
+ const props = defineProps<TooltipRootProps>();
6
+ const emits = defineEmits<TooltipRootEmits>();
7
+
8
+ const forwarded = useForwardPropsEmits(props, emits);
9
+ </script>
10
+
11
+ <template>
12
+ <TooltipRoot v-bind="forwarded">
13
+ <slot />
14
+ </TooltipRoot>
15
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import type { TooltipContentEmits, TooltipContentProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ defineOptions({
9
+ inheritAttrs: false,
10
+ });
11
+
12
+ const props = withDefaults(
13
+ defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(),
14
+ {
15
+ sideOffset: 4,
16
+ },
17
+ );
18
+
19
+ const emits = defineEmits<TooltipContentEmits>();
20
+
21
+ const delegatedProps = reactiveOmit(props, "class");
22
+
23
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
24
+ </script>
25
+
26
+ <template>
27
+ <TooltipPortal>
28
+ <TooltipContent
29
+ v-bind="{ ...forwarded, ...$attrs }"
30
+ :class="
31
+ cn(
32
+ 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
33
+ props.class,
34
+ )
35
+ "
36
+ >
37
+ <slot />
38
+ </TooltipContent>
39
+ </TooltipPortal>
40
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { TooltipProviderProps } from "reka-ui";
3
+ import { TooltipProvider } from "reka-ui";
4
+
5
+ const props = defineProps<TooltipProviderProps>();
6
+ </script>
7
+
8
+ <template>
9
+ <TooltipProvider v-bind="props">
10
+ <slot />
11
+ </TooltipProvider>
12
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { TooltipTriggerProps } from "reka-ui";
3
+ import { TooltipTrigger } from "reka-ui";
4
+
5
+ const props = defineProps<TooltipTriggerProps>();
6
+ </script>
7
+
8
+ <template>
9
+ <TooltipTrigger v-bind="props">
10
+ <slot />
11
+ </TooltipTrigger>
12
+ </template>
@@ -0,0 +1,4 @@
1
+ export { default as Tooltip } from "./Tooltip.vue";
2
+ export { default as TooltipContent } from "./TooltipContent.vue";
3
+ export { default as TooltipProvider } from "./TooltipProvider.vue";
4
+ export { default as TooltipTrigger } from "./TooltipTrigger.vue";
@@ -0,0 +1,31 @@
1
+ import { ref } from "vue";
2
+
3
+ /**
4
+ * Clipboard helper with a 1.5s "copied" flash for the UI.
5
+ *
6
+ * const { copy, copied } = useCopy();
7
+ * copy("hello"); // → fires navigator.clipboard.writeText
8
+ * // copied.value flips to true for 1500ms
9
+ */
10
+ export function useCopy(flashMs = 1500) {
11
+ const copied = ref(false);
12
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
13
+
14
+ async function copy(text: string): Promise<void> {
15
+ try {
16
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
17
+ await navigator.clipboard.writeText(text);
18
+ }
19
+ copied.value = true;
20
+ if (timeoutId) clearTimeout(timeoutId);
21
+ timeoutId = setTimeout(() => {
22
+ copied.value = false;
23
+ }, flashMs);
24
+ } catch {
25
+ // Clipboard may be unavailable (iframe, insecure context, denied
26
+ // permission). Silently fail — the chip just won't flash.
27
+ }
28
+ }
29
+
30
+ return { copy, copied };
31
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeCache } from "../normalize-cache";
3
+
4
+ describe("normalizeCache", () => {
5
+ it("fills every expected array when input is empty", () => {
6
+ const { cache, missingFields } = normalizeCache({});
7
+ expect(cache).not.toBeNull();
8
+ expect(cache?.apps).toEqual([]);
9
+ expect(cache?.modules).toEqual([]);
10
+ expect(cache?.actions).toEqual([]);
11
+ expect(cache?.events).toEqual([]);
12
+ expect(cache?.actors).toEqual([]);
13
+ expect(cache?.projections).toEqual([]);
14
+ expect(cache?.queries).toEqual([]);
15
+ expect(cache?.resolvers).toEqual([]);
16
+ expect(cache?.routes).toEqual([]);
17
+ expect(cache?.workflows).toEqual([]);
18
+ expect(cache?.graph).toEqual({ events: [] });
19
+ expect(missingFields).toContain("resolvers");
20
+ expect(missingFields).toContain("workflows");
21
+ expect(missingFields).toContain("graph");
22
+ });
23
+
24
+ it("preserves arrays that ARE present", () => {
25
+ const input = {
26
+ apps: [{ name: "x", modules: [] }],
27
+ modules: [],
28
+ actions: [],
29
+ events: [],
30
+ actors: [],
31
+ projections: [],
32
+ queries: [],
33
+ resolvers: [{ operation: "Op", version: 1 }],
34
+ routes: [],
35
+ workflows: [],
36
+ externalCalls: [],
37
+ inboundWebhooks: [],
38
+ outboxes: [],
39
+ inboxes: [],
40
+ crons: [],
41
+ hooks: [],
42
+ plugins: [],
43
+ graph: { events: [] },
44
+ generatedAt: "2026-05-17T00:00:00Z",
45
+ };
46
+ const { cache, missingFields } = normalizeCache(input);
47
+ expect(cache?.apps).toEqual(input.apps);
48
+ expect(cache?.resolvers).toEqual(input.resolvers);
49
+ expect(missingFields).toEqual([]);
50
+ });
51
+
52
+ it("reports the exact list of missing array fields", () => {
53
+ const { missingFields } = normalizeCache({
54
+ apps: [],
55
+ modules: [],
56
+ actions: [],
57
+ events: [],
58
+ actors: [],
59
+ projections: [],
60
+ queries: [],
61
+ // resolvers + workflows + graph missing
62
+ routes: [],
63
+ externalCalls: [],
64
+ inboundWebhooks: [],
65
+ outboxes: [],
66
+ inboxes: [],
67
+ crons: [],
68
+ generatedAt: "2026-05-17T00:00:00Z",
69
+ });
70
+ expect([...missingFields].sort()).toEqual([
71
+ "graph",
72
+ "hooks",
73
+ "plugins",
74
+ "resolvers",
75
+ "workflows",
76
+ ]);
77
+ });
78
+
79
+ it("ignores non-array values masquerading as the field", () => {
80
+ const { cache, missingFields } = normalizeCache({ resolvers: "not an array" });
81
+ expect(cache?.resolvers).toEqual([]);
82
+ expect(missingFields).toContain("resolvers");
83
+ });
84
+
85
+ it("returns null + fatal error when input is not an object", () => {
86
+ expect(normalizeCache(null).cache).toBeNull();
87
+ expect(normalizeCache(null).fatalError).toMatch(/not a JSON object/i);
88
+ expect(normalizeCache([]).cache).toBeNull();
89
+ expect(normalizeCache("oops").cache).toBeNull();
90
+ expect(normalizeCache(42).cache).toBeNull();
91
+ });
92
+
93
+ it("synthesises generatedAt when missing", () => {
94
+ const { cache, missingFields } = normalizeCache({});
95
+ expect(cache?.generatedAt).toBeTypeOf("string");
96
+ expect(missingFields).toContain("generatedAt");
97
+ });
98
+
99
+ it("handles graph.events missing while graph exists", () => {
100
+ const { cache, missingFields } = normalizeCache({ graph: {} });
101
+ expect(cache?.graph).toEqual({ events: [] });
102
+ expect(missingFields).toContain("graph.events");
103
+ });
104
+ });