From 4094d4d824c152784e3571c015a00e113cd8062d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 23 Jul 2026 21:51:56 +0700 Subject: [PATCH] feat: add UI components for buttons, cards, fields, and input with Toaster integration --- app/app.vue | 5 ++ app/components/ui/button/Button.vue | 31 ++++++++++++ app/components/ui/button/index.ts | 40 +++++++++++++++ app/components/ui/card/Card.vue | 22 ++++++++ app/components/ui/card/CardAction.vue | 17 +++++++ app/components/ui/card/CardContent.vue | 17 +++++++ app/components/ui/card/CardDescription.vue | 17 +++++++ app/components/ui/card/CardFooter.vue | 17 +++++++ app/components/ui/card/CardHeader.vue | 17 +++++++ app/components/ui/card/CardTitle.vue | 17 +++++++ app/components/ui/card/index.ts | 7 +++ app/components/ui/field/Field.vue | 25 +++++++++ app/components/ui/field/FieldContent.vue | 20 ++++++++ app/components/ui/field/FieldDescription.vue | 22 ++++++++ app/components/ui/field/FieldError.vue | 53 ++++++++++++++++++++ app/components/ui/field/FieldGroup.vue | 20 ++++++++ app/components/ui/field/FieldLabel.vue | 23 +++++++++ app/components/ui/field/FieldLegend.vue | 24 +++++++++ app/components/ui/field/FieldSeparator.vue | 29 +++++++++++ app/components/ui/field/FieldSet.vue | 21 ++++++++ app/components/ui/field/FieldTitle.vue | 20 ++++++++ app/components/ui/field/index.ts | 39 ++++++++++++++ app/components/ui/input/Input.vue | 33 ++++++++++++ app/components/ui/input/index.ts | 1 + app/components/ui/label/Label.vue | 26 ++++++++++ app/components/ui/label/index.ts | 1 + app/components/ui/separator/Separator.vue | 29 +++++++++++ app/components/ui/separator/index.ts | 1 + app/components/ui/sonner/Sonner.vue | 42 ++++++++++++++++ app/components/ui/sonner/index.ts | 1 + bun.lock | 5 ++ package.json | 3 ++ 32 files changed, 645 insertions(+) create mode 100644 app/components/ui/button/Button.vue create mode 100644 app/components/ui/button/index.ts create mode 100644 app/components/ui/card/Card.vue create mode 100644 app/components/ui/card/CardAction.vue create mode 100644 app/components/ui/card/CardContent.vue create mode 100644 app/components/ui/card/CardDescription.vue create mode 100644 app/components/ui/card/CardFooter.vue create mode 100644 app/components/ui/card/CardHeader.vue create mode 100644 app/components/ui/card/CardTitle.vue create mode 100644 app/components/ui/card/index.ts create mode 100644 app/components/ui/field/Field.vue create mode 100644 app/components/ui/field/FieldContent.vue create mode 100644 app/components/ui/field/FieldDescription.vue create mode 100644 app/components/ui/field/FieldError.vue create mode 100644 app/components/ui/field/FieldGroup.vue create mode 100644 app/components/ui/field/FieldLabel.vue create mode 100644 app/components/ui/field/FieldLegend.vue create mode 100644 app/components/ui/field/FieldSeparator.vue create mode 100644 app/components/ui/field/FieldSet.vue create mode 100644 app/components/ui/field/FieldTitle.vue create mode 100644 app/components/ui/field/index.ts create mode 100644 app/components/ui/input/Input.vue create mode 100644 app/components/ui/input/index.ts create mode 100644 app/components/ui/label/Label.vue create mode 100644 app/components/ui/label/index.ts create mode 100644 app/components/ui/separator/Separator.vue create mode 100644 app/components/ui/separator/index.ts create mode 100644 app/components/ui/sonner/Sonner.vue create mode 100644 app/components/ui/sonner/index.ts diff --git a/app/app.vue b/app/app.vue index 09f935b..87beb38 100644 --- a/app/app.vue +++ b/app/app.vue @@ -2,5 +2,10 @@
+
+ + diff --git a/app/components/ui/button/Button.vue b/app/components/ui/button/Button.vue new file mode 100644 index 0000000..3763470 --- /dev/null +++ b/app/components/ui/button/Button.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/components/ui/button/index.ts b/app/components/ui/button/index.ts new file mode 100644 index 0000000..0a62178 --- /dev/null +++ b/app/components/ui/button/index.ts @@ -0,0 +1,40 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Button } from "./Button.vue" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + "default": "h-9 px-4 py-2 has-[>svg]:px-3", + "xs": "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + "lg": "h-10 rounded-md px-6 has-[>svg]:px-4", + "icon": "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/app/components/ui/card/Card.vue b/app/components/ui/card/Card.vue new file mode 100644 index 0000000..f5a0707 --- /dev/null +++ b/app/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/components/ui/card/CardAction.vue b/app/components/ui/card/CardAction.vue new file mode 100644 index 0000000..c91638b --- /dev/null +++ b/app/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardContent.vue b/app/components/ui/card/CardContent.vue new file mode 100644 index 0000000..dfbc552 --- /dev/null +++ b/app/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardDescription.vue b/app/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..71c1b8d --- /dev/null +++ b/app/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardFooter.vue b/app/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..9e3739e --- /dev/null +++ b/app/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardHeader.vue b/app/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..4fe4da4 --- /dev/null +++ b/app/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardTitle.vue b/app/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..5f479e7 --- /dev/null +++ b/app/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/index.ts b/app/components/ui/card/index.ts new file mode 100644 index 0000000..1627758 --- /dev/null +++ b/app/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/app/components/ui/field/Field.vue b/app/components/ui/field/Field.vue new file mode 100644 index 0000000..5519d37 --- /dev/null +++ b/app/components/ui/field/Field.vue @@ -0,0 +1,25 @@ + + + diff --git a/app/components/ui/field/FieldContent.vue b/app/components/ui/field/FieldContent.vue new file mode 100644 index 0000000..d9a23fd --- /dev/null +++ b/app/components/ui/field/FieldContent.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/components/ui/field/FieldDescription.vue b/app/components/ui/field/FieldDescription.vue new file mode 100644 index 0000000..7240a83 --- /dev/null +++ b/app/components/ui/field/FieldDescription.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/components/ui/field/FieldError.vue b/app/components/ui/field/FieldError.vue new file mode 100644 index 0000000..8a0a63f --- /dev/null +++ b/app/components/ui/field/FieldError.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/components/ui/field/FieldGroup.vue b/app/components/ui/field/FieldGroup.vue new file mode 100644 index 0000000..834d8ce --- /dev/null +++ b/app/components/ui/field/FieldGroup.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/components/ui/field/FieldLabel.vue b/app/components/ui/field/FieldLabel.vue new file mode 100644 index 0000000..ce6c498 --- /dev/null +++ b/app/components/ui/field/FieldLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/field/FieldLegend.vue b/app/components/ui/field/FieldLegend.vue new file mode 100644 index 0000000..c620fed --- /dev/null +++ b/app/components/ui/field/FieldLegend.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/components/ui/field/FieldSeparator.vue b/app/components/ui/field/FieldSeparator.vue new file mode 100644 index 0000000..97d0efa --- /dev/null +++ b/app/components/ui/field/FieldSeparator.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/components/ui/field/FieldSet.vue b/app/components/ui/field/FieldSet.vue new file mode 100644 index 0000000..7be4dc9 --- /dev/null +++ b/app/components/ui/field/FieldSet.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/field/FieldTitle.vue b/app/components/ui/field/FieldTitle.vue new file mode 100644 index 0000000..f564b8b --- /dev/null +++ b/app/components/ui/field/FieldTitle.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/components/ui/field/index.ts b/app/components/ui/field/index.ts new file mode 100644 index 0000000..162ba14 --- /dev/null +++ b/app/components/ui/field/index.ts @@ -0,0 +1,39 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + }, +) + +export type FieldVariants = VariantProps + +export { default as Field } from "./Field.vue" +export { default as FieldContent } from "./FieldContent.vue" +export { default as FieldDescription } from "./FieldDescription.vue" +export { default as FieldError } from "./FieldError.vue" +export { default as FieldGroup } from "./FieldGroup.vue" +export { default as FieldLabel } from "./FieldLabel.vue" +export { default as FieldLegend } from "./FieldLegend.vue" +export { default as FieldSeparator } from "./FieldSeparator.vue" +export { default as FieldSet } from "./FieldSet.vue" +export { default as FieldTitle } from "./FieldTitle.vue" diff --git a/app/components/ui/input/Input.vue b/app/components/ui/input/Input.vue new file mode 100644 index 0000000..9db9e85 --- /dev/null +++ b/app/components/ui/input/Input.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/components/ui/input/index.ts b/app/components/ui/input/index.ts new file mode 100644 index 0000000..9976b86 --- /dev/null +++ b/app/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue" diff --git a/app/components/ui/label/Label.vue b/app/components/ui/label/Label.vue new file mode 100644 index 0000000..ee63970 --- /dev/null +++ b/app/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/components/ui/label/index.ts b/app/components/ui/label/index.ts new file mode 100644 index 0000000..036e35c --- /dev/null +++ b/app/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue" diff --git a/app/components/ui/separator/Separator.vue b/app/components/ui/separator/Separator.vue new file mode 100644 index 0000000..78d60ec --- /dev/null +++ b/app/components/ui/separator/Separator.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/components/ui/separator/index.ts b/app/components/ui/separator/index.ts new file mode 100644 index 0000000..4407287 --- /dev/null +++ b/app/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from "./Separator.vue" diff --git a/app/components/ui/sonner/Sonner.vue b/app/components/ui/sonner/Sonner.vue new file mode 100644 index 0000000..45389fc --- /dev/null +++ b/app/components/ui/sonner/Sonner.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/components/ui/sonner/index.ts b/app/components/ui/sonner/index.ts new file mode 100644 index 0000000..6673112 --- /dev/null +++ b/app/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./Sonner.vue" diff --git a/bun.lock b/bun.lock index 4445132..7a0b181 100644 --- a/bun.lock +++ b/bun.lock @@ -6,13 +6,16 @@ "name": "web2", "dependencies": { "@lucide/vue": "^1.26.0", + "@vueuse/core": "^14.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "nuxt": "^4.5.0", + "reka-ui": "^2.10.1", "shadcn-nuxt": "2.8.0", "tailwind-merge": "^3.6.0", "vue": "^3.5.40", "vue-router": "^5.2.0", + "vue-sonner": "^2.0.9", }, "devDependencies": { "@nuxtjs/tailwindcss": "7.0.0-beta.1", @@ -1675,6 +1678,8 @@ "vue-router": ["vue-router@5.2.0", "", { "dependencies": { "@babel/generator": "^8.0.0", "@vue-macros/common": "^3.1.3", "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.9.0", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.2.1", "magic-string": "^0.30.21", "mlly": "^1.8.2", "muggle-string": "^0.4.1", "nostics": "^1.1.4", "pathe": "^2.0.3", "picomatch": "^4.0.5", "scule": "^1.3.0", "tinyglobby": "^0.2.17", "unplugin": "^3.3.0", "unplugin-utils": "^0.3.2", "yaml": "^2.9.0" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", "pinia": "^3.0.4 || ^4.0.2", "vite": "^7.3.0 || ^8.0.0", "vue": "^3.5.34 || ^4.0.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia", "vite"] }, "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw=="], + "vue-sonner": ["vue-sonner@2.0.9", "", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="], + "web-worker": ["web-worker@1.5.0", "", {}, "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], diff --git a/package.json b/package.json index 32171d4..1ef9e65 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,13 @@ }, "dependencies": { "@lucide/vue": "^1.26.0", + "@vueuse/core": "^14.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "nuxt": "^4.5.0", + "reka-ui": "^2.10.1", "shadcn-nuxt": "2.8.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "vue": "^3.5.40", "vue-router": "^5.2.0"