@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,39 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import KindBadge from "../KindBadge.vue";
4
+
5
+ describe("KindBadge", () => {
6
+ it("renders slot content", () => {
7
+ const wrapper = mount(KindBadge, { slots: { default: "active" } });
8
+ expect(wrapper.text()).toBe("active");
9
+ });
10
+
11
+ it("applies the active variant by default style set", () => {
12
+ const wrapper = mount(KindBadge, {
13
+ props: { variant: "active" },
14
+ slots: { default: "active" },
15
+ });
16
+ expect(wrapper.classes().join(" ")).toMatch(/emerald/);
17
+ });
18
+
19
+ it("applies the sunset variant style", () => {
20
+ const wrapper = mount(KindBadge, {
21
+ props: { variant: "sunset" },
22
+ slots: { default: "sunset" },
23
+ });
24
+ expect(wrapper.classes().join(" ")).toMatch(/rose/);
25
+ });
26
+
27
+ it("falls back to neutral when no variant", () => {
28
+ const wrapper = mount(KindBadge, { slots: { default: "n" } });
29
+ expect(wrapper.classes().join(" ")).toMatch(/zinc/);
30
+ });
31
+
32
+ it("can disable uppercase formatting", () => {
33
+ const wrapper = mount(KindBadge, {
34
+ props: { uppercase: false },
35
+ slots: { default: "n" },
36
+ });
37
+ expect(wrapper.classes().join(" ")).not.toMatch(/uppercase/);
38
+ });
39
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import ListRow from "../ListRow.vue";
4
+
5
+ describe("ListRow", () => {
6
+ it("emits click", async () => {
7
+ const wrapper = mount(ListRow, {
8
+ slots: { title: "row", meta: "tag" },
9
+ });
10
+ await wrapper.get("[data-testid=list-row]").trigger("click");
11
+ expect(wrapper.emitted("click")).toBeTruthy();
12
+ });
13
+
14
+ it("renders title + meta + description slots", () => {
15
+ const wrapper = mount(ListRow, {
16
+ slots: {
17
+ title: '<span class="t">title-text</span>',
18
+ meta: '<span class="m">meta-text</span>',
19
+ description: '<span class="d">desc-text</span>',
20
+ },
21
+ });
22
+ expect(wrapper.find(".t").text()).toBe("title-text");
23
+ expect(wrapper.find(".m").text()).toBe("meta-text");
24
+ expect(wrapper.find(".d").text()).toBe("desc-text");
25
+ });
26
+
27
+ it("applies the selected class", () => {
28
+ const wrapper = mount(ListRow, {
29
+ props: { selected: true },
30
+ slots: { title: "row" },
31
+ });
32
+ expect(wrapper.get("[data-testid=list-row]").classes()).toContain("bg-zinc-900");
33
+ });
34
+
35
+ it("omits description block when slot is empty", () => {
36
+ const wrapper = mount(ListRow, { slots: { title: "row" } });
37
+ expect(wrapper.find(".line-clamp-2").exists()).toBe(false);
38
+ });
39
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import MasterDetail from "../MasterDetail.vue";
4
+
5
+ describe("MasterDetail", () => {
6
+ it("renders list + listHeader slots", () => {
7
+ const wrapper = mount(MasterDetail, {
8
+ slots: {
9
+ listHeader: '<div class="lh">header</div>',
10
+ list: '<div class="l">items</div>',
11
+ },
12
+ });
13
+ expect(wrapper.find(".lh").exists()).toBe(true);
14
+ expect(wrapper.find(".l").exists()).toBe(true);
15
+ });
16
+
17
+ it("falls back to the empty slot when detail is absent", () => {
18
+ const wrapper = mount(MasterDetail, {
19
+ slots: {
20
+ list: '<div class="l">items</div>',
21
+ empty: "pick something",
22
+ },
23
+ });
24
+ // ScrollArea injects its own <style> block — assert on contains, not equality.
25
+ expect(wrapper.get("[data-testid=detail-pane]").text()).toContain("pick something");
26
+ });
27
+
28
+ it("renders detail slot when provided", () => {
29
+ const wrapper = mount(MasterDetail, {
30
+ slots: {
31
+ list: '<div class="l">items</div>',
32
+ detail: '<div class="d">selected detail</div>',
33
+ empty: "pick something",
34
+ },
35
+ });
36
+ expect(wrapper.find(".d").exists()).toBe(true);
37
+ // When `detail` is provided, the empty fallback never renders.
38
+ expect(wrapper.findAll(".d")).toHaveLength(1);
39
+ });
40
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import { Network } from "lucide-vue-next";
4
+ import PageHeader from "../PageHeader.vue";
5
+
6
+ describe("PageHeader", () => {
7
+ it("renders title + subtitle", () => {
8
+ const wrapper = mount(PageHeader, {
9
+ props: { title: "Resolvers", subtitle: "Interface layer" },
10
+ });
11
+ expect(wrapper.text()).toContain("Resolvers");
12
+ expect(wrapper.text()).toContain("Interface layer");
13
+ });
14
+
15
+ it("renders the count badge when total is set", () => {
16
+ const wrapper = mount(PageHeader, {
17
+ props: { title: "Resolvers", count: 3, total: 10 },
18
+ });
19
+ expect(wrapper.get("[data-testid=page-count]").text()).toBe("3 / 10");
20
+ });
21
+
22
+ it("falls back to total when count is undefined", () => {
23
+ const wrapper = mount(PageHeader, {
24
+ props: { title: "Resolvers", total: 10 },
25
+ });
26
+ expect(wrapper.get("[data-testid=page-count]").text()).toBe("10 / 10");
27
+ });
28
+
29
+ it("renders the lucide icon when supplied", () => {
30
+ const wrapper = mount(PageHeader, {
31
+ props: { title: "Resolvers", icon: Network },
32
+ });
33
+ expect(wrapper.findComponent(Network).exists()).toBe(true);
34
+ });
35
+
36
+ it("omits the count badge when total is undefined", () => {
37
+ const wrapper = mount(PageHeader, {
38
+ props: { title: "Resolvers" },
39
+ });
40
+ expect(wrapper.find("[data-testid=page-count]").exists()).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Studio's reusable component library — small, named, behaviorally honest.
3
+ * Pages compose these. Storybook documents them. Playwright + Vitest test them.
4
+ */
5
+ export { default as PageHeader } from "./PageHeader.vue";
6
+ export { default as FilterInput } from "./FilterInput.vue";
7
+ export { default as KindBadge } from "./KindBadge.vue";
8
+ export { default as JsonView } from "./JsonView.vue";
9
+ export { default as EmptyState } from "./EmptyState.vue";
10
+ export { default as MasterDetail } from "./MasterDetail.vue";
11
+ export { default as ListRow } from "./ListRow.vue";
12
+ export { default as ErrorBoundary } from "./ErrorBoundary.vue";
13
+ export { default as SourcePill } from "./SourcePill.vue";
14
+ export { default as SchemaTree } from "./SchemaTree.vue";
15
+ export { default as SchemaNode } from "./SchemaNode.vue";
16
+ export { default as MonacoViewer } from "./MonacoViewer.vue";
17
+ export { default as SourceDrawer } from "./SourceDrawer.vue";
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import type { BadgeVariants } from ".";
4
+ import { cn } from "@/lib/utils";
5
+ import { badgeVariants } from ".";
6
+
7
+ const props = defineProps<{
8
+ variant?: BadgeVariants["variant"];
9
+ class?: HTMLAttributes["class"];
10
+ }>();
11
+ </script>
12
+
13
+ <template>
14
+ <div :class="cn(badgeVariants({ variant }), props.class)">
15
+ <slot />
16
+ </div>
17
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
+
4
+ export { default as Badge } from "./Badge.vue";
5
+
6
+ export const badgeVariants = cva(
7
+ "inline-flex gap-1 items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ },
23
+ );
24
+
25
+ export type BadgeVariants = VariantProps<typeof badgeVariants>;
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { PrimitiveProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import type { ButtonVariants } from ".";
5
+ import { Primitive } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+ import { buttonVariants } from ".";
8
+
9
+ interface Props extends PrimitiveProps {
10
+ variant?: ButtonVariants["variant"];
11
+ size?: ButtonVariants["size"];
12
+ class?: HTMLAttributes["class"];
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ as: "button",
17
+ });
18
+ </script>
19
+
20
+ <template>
21
+ <Primitive
22
+ :as="as"
23
+ :as-child="asChild"
24
+ :class="cn(buttonVariants({ variant, size }), props.class)"
25
+ >
26
+ <slot />
27
+ </Primitive>
28
+ </template>
@@ -0,0 +1,34 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
+
4
+ export { default as Button } from "./Button.vue";
5
+
6
+ export const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
14
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ ghost: "hover:bg-accent hover:text-accent-foreground",
16
+ link: "text-primary underline-offset-4 hover:underline",
17
+ },
18
+ size: {
19
+ default: "h-10 px-4 py-2",
20
+ sm: "h-9 rounded-md px-3",
21
+ lg: "h-11 rounded-md px-8",
22
+ icon: "h-10 w-10",
23
+ "icon-sm": "size-9",
24
+ "icon-lg": "size-11",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ size: "default",
30
+ },
31
+ },
32
+ );
33
+
34
+ export type ButtonVariants = VariantProps<typeof buttonVariants>;
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <div :class="cn('rounded-lg border bg-card text-card-foreground shadow-sm', props.class)">
12
+ <slot />
13
+ </div>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <div :class="cn('p-6 pt-0', props.class)">
12
+ <slot />
13
+ </div>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <p :class="cn('text-sm text-muted-foreground', props.class)">
12
+ <slot />
13
+ </p>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <div :class="cn('flex items-center p-6 pt-0', props.class)">
12
+ <slot />
13
+ </div>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
12
+ <slot />
13
+ </div>
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <h3 :class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)">
12
+ <slot />
13
+ </h3>
14
+ </template>
@@ -0,0 +1,6 @@
1
+ export { default as Card } from "./Card.vue";
2
+ export { default as CardContent } from "./CardContent.vue";
3
+ export { default as CardDescription } from "./CardDescription.vue";
4
+ export { default as CardFooter } from "./CardFooter.vue";
5
+ export { default as CardHeader } from "./CardHeader.vue";
6
+ export { default as CardTitle } from "./CardTitle.vue";
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import type { DialogRootEmits, DialogRootProps } from "reka-ui";
3
+ import { DialogRoot, useForwardPropsEmits } from "reka-ui";
4
+
5
+ const props = defineProps<DialogRootProps>();
6
+ const emits = defineEmits<DialogRootEmits>();
7
+
8
+ const forwarded = useForwardPropsEmits(props, emits);
9
+ </script>
10
+
11
+ <template>
12
+ <DialogRoot v-bind="forwarded">
13
+ <slot />
14
+ </DialogRoot>
15
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { DialogCloseProps } from "reka-ui";
3
+ import { DialogClose } from "reka-ui";
4
+
5
+ const props = defineProps<DialogCloseProps>();
6
+ </script>
7
+
8
+ <template>
9
+ <DialogClose v-bind="props">
10
+ <slot />
11
+ </DialogClose>
12
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import type { DialogContentEmits, DialogContentProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { X } from "lucide-vue-next";
6
+ import {
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogOverlay,
10
+ DialogPortal,
11
+ useForwardPropsEmits,
12
+ } from "reka-ui";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>();
16
+ const emits = defineEmits<DialogContentEmits>();
17
+
18
+ const delegatedProps = reactiveOmit(props, "class");
19
+
20
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
21
+ </script>
22
+
23
+ <template>
24
+ <DialogPortal>
25
+ <DialogOverlay
26
+ class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
27
+ />
28
+ <DialogContent
29
+ v-bind="forwarded"
30
+ :class="
31
+ cn(
32
+ 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
33
+ props.class,
34
+ )
35
+ "
36
+ >
37
+ <slot />
38
+
39
+ <DialogClose
40
+ class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
41
+ >
42
+ <X class="w-4 h-4" />
43
+ <span class="sr-only">Close</span>
44
+ </DialogClose>
45
+ </DialogContent>
46
+ </DialogPortal>
47
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { DialogDescriptionProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { DialogDescription, useForwardProps } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps);
13
+ </script>
14
+
15
+ <template>
16
+ <DialogDescription
17
+ v-bind="forwardedProps"
18
+ :class="cn('text-sm text-muted-foreground', props.class)"
19
+ >
20
+ <slot />
21
+ </DialogDescription>
22
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{ class?: HTMLAttributes["class"] }>();
6
+ </script>
7
+
8
+ <template>
9
+ <div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
10
+ <slot />
11
+ </div>
12
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"];
7
+ }>();
8
+ </script>
9
+
10
+ <template>
11
+ <div :class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)">
12
+ <slot />
13
+ </div>
14
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import type { DialogContentEmits, DialogContentProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { X } from "lucide-vue-next";
6
+ import {
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogOverlay,
10
+ DialogPortal,
11
+ useForwardPropsEmits,
12
+ } from "reka-ui";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>();
16
+ const emits = defineEmits<DialogContentEmits>();
17
+
18
+ const delegatedProps = reactiveOmit(props, "class");
19
+
20
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
21
+ </script>
22
+
23
+ <template>
24
+ <DialogPortal>
25
+ <DialogOverlay
26
+ class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
27
+ >
28
+ <DialogContent
29
+ :class="
30
+ cn(
31
+ 'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
32
+ props.class,
33
+ )
34
+ "
35
+ v-bind="forwarded"
36
+ @pointer-down-outside="
37
+ (event) => {
38
+ const originalEvent = event.detail.originalEvent;
39
+ const target = originalEvent.target as HTMLElement;
40
+ if (
41
+ originalEvent.offsetX > target.clientWidth ||
42
+ originalEvent.offsetY > target.clientHeight
43
+ ) {
44
+ event.preventDefault();
45
+ }
46
+ }
47
+ "
48
+ >
49
+ <slot />
50
+
51
+ <DialogClose
52
+ class="absolute top-3 right-3 p-0.5 transition-colors rounded-md hover:bg-secondary"
53
+ >
54
+ <X class="w-4 h-4" />
55
+ <span class="sr-only">Close</span>
56
+ </DialogClose>
57
+ </DialogContent>
58
+ </DialogOverlay>
59
+ </DialogPortal>
60
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { DialogTitleProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { DialogTitle, useForwardProps } from "reka-ui";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps);
13
+ </script>
14
+
15
+ <template>
16
+ <DialogTitle
17
+ v-bind="forwardedProps"
18
+ :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)"
19
+ >
20
+ <slot />
21
+ </DialogTitle>
22
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { DialogTriggerProps } from "reka-ui";
3
+ import { DialogTrigger } from "reka-ui";
4
+
5
+ const props = defineProps<DialogTriggerProps>();
6
+ </script>
7
+
8
+ <template>
9
+ <DialogTrigger v-bind="props">
10
+ <slot />
11
+ </DialogTrigger>
12
+ </template>
@@ -0,0 +1,9 @@
1
+ export { default as Dialog } from "./Dialog.vue";
2
+ export { default as DialogClose } from "./DialogClose.vue";
3
+ export { default as DialogContent } from "./DialogContent.vue";
4
+ export { default as DialogDescription } from "./DialogDescription.vue";
5
+ export { default as DialogFooter } from "./DialogFooter.vue";
6
+ export { default as DialogHeader } from "./DialogHeader.vue";
7
+ export { default as DialogScrollContent } from "./DialogScrollContent.vue";
8
+ export { default as DialogTitle } from "./DialogTitle.vue";
9
+ export { default as DialogTrigger } from "./DialogTrigger.vue";
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { useVModel } from "@vueuse/core";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const props = defineProps<{
7
+ defaultValue?: string | number;
8
+ modelValue?: string | number;
9
+ class?: HTMLAttributes["class"];
10
+ }>();
11
+
12
+ const emits = defineEmits<{
13
+ (e: "update:modelValue", payload: string | number): void;
14
+ }>();
15
+
16
+ const modelValue = useVModel(props, "modelValue", emits, {
17
+ passive: true,
18
+ defaultValue: props.defaultValue,
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <input
24
+ v-model="modelValue"
25
+ :class="
26
+ cn(
27
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
28
+ props.class,
29
+ )
30
+ "
31
+ />
32
+ </template>