瀏覽代碼

Реализован первичный frontend

Artem Kastrov 2 月之前
父節點
當前提交
30d8f8734c
共有 96 個文件被更改,包括 2976 次插入209 次删除
  1. 3 0
      .editorconfig
  2. 21 0
      components.json
  3. 447 193
      package-lock.json
  4. 8 0
      package.json
  5. 172 0
      resources/css/app.css
  6. 76 0
      resources/js/Components/AppSidebar.vue
  7. 17 8
      resources/js/Composables/useAppearance.ts
  8. 21 2
      resources/js/Layouts/AppLayout.vue
  9. 29 0
      resources/js/Packages/Shadcn/Components/ui/button/Button.vue
  10. 36 0
      resources/js/Packages/Shadcn/Components/ui/button/index.js
  11. 39 0
      resources/js/Packages/Shadcn/Components/ui/button/index.ts
  12. 18 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenu.vue
  13. 38 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
  14. 36 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuContent.vue
  15. 15 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuGroup.vue
  16. 31 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuItem.vue
  17. 23 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuLabel.vue
  18. 22 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
  19. 39 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuRadioItem.vue
  20. 24 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSeparator.vue
  21. 17 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuShortcut.vue
  22. 19 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSub.vue
  23. 28 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSubContent.vue
  24. 31 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
  25. 17 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuTrigger.vue
  26. 16 0
      resources/js/Packages/Shadcn/Components/ui/dropdown-menu/index.ts
  27. 20 0
      resources/js/Packages/Shadcn/Components/ui/empty/Empty.vue
  28. 20 0
      resources/js/Packages/Shadcn/Components/ui/empty/EmptyContent.vue
  29. 20 0
      resources/js/Packages/Shadcn/Components/ui/empty/EmptyDescription.vue
  30. 20 0
      resources/js/Packages/Shadcn/Components/ui/empty/EmptyHeader.vue
  31. 21 0
      resources/js/Packages/Shadcn/Components/ui/empty/EmptyMedia.vue
  32. 21 0
      resources/js/Packages/Shadcn/Components/ui/empty/EmptyTitle.vue
  33. 26 0
      resources/js/Packages/Shadcn/Components/ui/empty/index.ts
  34. 35 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroup.vue
  35. 36 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupAddon.vue
  36. 21 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupButton.vue
  37. 19 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupInput.vue
  38. 19 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupText.vue
  39. 19 0
      resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupTextarea.vue
  40. 47 0
      resources/js/Packages/Shadcn/Components/ui/input-group/index.js
  41. 59 0
      resources/js/Packages/Shadcn/Components/ui/input-group/index.ts
  42. 33 0
      resources/js/Packages/Shadcn/Components/ui/input/Input.vue
  43. 1 0
      resources/js/Packages/Shadcn/Components/ui/input/index.js
  44. 1 0
      resources/js/Packages/Shadcn/Components/ui/input/index.ts
  45. 29 0
      resources/js/Packages/Shadcn/Components/ui/separator/Separator.vue
  46. 1 0
      resources/js/Packages/Shadcn/Components/ui/separator/index.ts
  47. 18 0
      resources/js/Packages/Shadcn/Components/ui/sheet/Sheet.vue
  48. 15 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetClose.vue
  49. 63 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetContent.vue
  50. 21 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetDescription.vue
  51. 16 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetFooter.vue
  52. 15 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetHeader.vue
  53. 21 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetOverlay.vue
  54. 21 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetTitle.vue
  55. 15 0
      resources/js/Packages/Shadcn/Components/ui/sheet/SheetTrigger.vue
  56. 8 0
      resources/js/Packages/Shadcn/Components/ui/sheet/index.ts
  57. 96 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/Sidebar.vue
  58. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarContent.vue
  59. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarFooter.vue
  60. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroup.vue
  61. 27 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupAction.vue
  62. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupContent.vue
  63. 25 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupLabel.vue
  64. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarHeader.vue
  65. 22 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarInput.vue
  66. 21 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarInset.vue
  67. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenu.vue
  68. 35 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuAction.vue
  69. 26 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuBadge.vue
  70. 48 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuButton.vue
  71. 36 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuButtonChild.vue
  72. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuItem.vue
  73. 35 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSkeleton.vue
  74. 22 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSub.vue
  75. 36 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSubButton.vue
  76. 18 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSubItem.vue
  77. 82 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarProvider.vue
  78. 33 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarRail.vue
  79. 19 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarSeparator.vue
  80. 27 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarTrigger.vue
  81. 60 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/index.ts
  82. 19 0
      resources/js/Packages/Shadcn/Components/ui/sidebar/utils.ts
  83. 17 0
      resources/js/Packages/Shadcn/Components/ui/skeleton/Skeleton.vue
  84. 1 0
      resources/js/Packages/Shadcn/Components/ui/skeleton/index.ts
  85. 28 0
      resources/js/Packages/Shadcn/Components/ui/textarea/Textarea.vue
  86. 1 0
      resources/js/Packages/Shadcn/Components/ui/textarea/index.js
  87. 1 0
      resources/js/Packages/Shadcn/Components/ui/textarea/index.ts
  88. 18 0
      resources/js/Packages/Shadcn/Components/ui/tooltip/Tooltip.vue
  89. 34 0
      resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipContent.vue
  90. 14 0
      resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipProvider.vue
  91. 15 0
      resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipTrigger.vue
  92. 4 0
      resources/js/Packages/Shadcn/Components/ui/tooltip/index.ts
  93. 6 0
      resources/js/Packages/Shadcn/Lib/utils.ts
  94. 93 4
      resources/js/Pages/Chat.vue
  95. 1 2
      resources/js/app.js
  96. 6 0
      vite.config.js

+ 3 - 0
.editorconfig

@@ -16,3 +16,6 @@ indent_size = 2
 
 [docker-compose.yml]
 indent_size = 4
+
+[components.json]
+indent_size = 2

+ 21 - 0
components.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://shadcn-vue.com/schema.json",
+  "style": "new-york",
+  "typescript": true,
+  "tailwind": {
+    "config": "",
+    "css": "resources/css/app.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "iconLibrary": "lucide",
+  "aliases": {
+    "components": "@/Packages/Shadcn/Components",
+    "utils": "@/Packages/Shadcn/Lib/utils",
+    "ui": "@/Packages/Shadcn/Components/ui",
+    "lib": "@/Packages/Shadcn/Lib",
+    "composables": "@/Packages/Shadcn/Composables"
+  },
+  "registries": {}
+}

+ 447 - 193
package-lock.json

@@ -7,14 +7,22 @@
             "dependencies": {
                 "@inertiajs/vue3": "^2.2.15",
                 "@vitejs/plugin-vue": "^6.0.1",
+                "@vueuse/core": "^14.0.0",
                 "chokidar": "^4.0.3",
+                "class-variance-authority": "^0.7.1",
+                "clsx": "^2.1.1",
                 "laravel-vite-plugin": "^2.0.0",
+                "lucide-vue-next": "^0.553.0",
+                "reka-ui": "^2.6.0",
+                "tailwind-merge": "^3.3.1",
                 "tailwindcss": "^4.0.0",
+                "tw-animate-css": "^1.4.0",
                 "vue": "^3.5.24"
             },
             "devDependencies": {
                 "@tailwindcss/vite": "^4.0.0",
                 "concurrently": "^9.0.1",
+                "typescript": "^5.9.3",
                 "vite": "^7.2.2"
             }
         },
@@ -480,6 +488,68 @@
                 "node": ">=18"
             }
         },
+        "node_modules/@floating-ui/core": {
+            "version": "1.7.3",
+            "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+            "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+            "license": "MIT",
+            "dependencies": {
+                "@floating-ui/utils": "^0.2.10"
+            }
+        },
+        "node_modules/@floating-ui/dom": {
+            "version": "1.7.4",
+            "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+            "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+            "license": "MIT",
+            "dependencies": {
+                "@floating-ui/core": "^1.7.3",
+                "@floating-ui/utils": "^0.2.10"
+            }
+        },
+        "node_modules/@floating-ui/utils": {
+            "version": "0.2.10",
+            "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+            "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+            "license": "MIT"
+        },
+        "node_modules/@floating-ui/vue": {
+            "version": "1.1.9",
+            "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.9.tgz",
+            "integrity": "sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@floating-ui/dom": "^1.7.4",
+                "@floating-ui/utils": "^0.2.10",
+                "vue-demi": ">=0.13.0"
+            }
+        },
+        "node_modules/@floating-ui/vue/node_modules/vue-demi": {
+            "version": "0.14.10",
+            "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+            "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+            "hasInstallScript": true,
+            "license": "MIT",
+            "bin": {
+                "vue-demi-fix": "bin/vue-demi-fix.js",
+                "vue-demi-switch": "bin/vue-demi-switch.js"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            },
+            "peerDependencies": {
+                "@vue/composition-api": "^1.0.0-rc.1",
+                "vue": "^3.0.0-0 || ^2.6.0"
+            },
+            "peerDependenciesMeta": {
+                "@vue/composition-api": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/@inertiajs/core": {
             "version": "2.2.15",
             "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.2.15.tgz",
@@ -506,6 +576,24 @@
                 "vue": "^3.0.0"
             }
         },
+        "node_modules/@internationalized/date": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
+            "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@swc/helpers": "^0.5.0"
+            }
+        },
+        "node_modules/@internationalized/number": {
+            "version": "3.6.5",
+            "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
+            "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@swc/helpers": "^0.5.0"
+            }
+        },
         "node_modules/@jridgewell/gen-mapping": {
             "version": "0.3.13",
             "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -847,6 +935,15 @@
                 "win32"
             ]
         },
+        "node_modules/@swc/helpers": {
+            "version": "0.5.17",
+            "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
+            "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "tslib": "^2.8.0"
+            }
+        },
         "node_modules/@tailwindcss/node": {
             "version": "4.1.17",
             "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
@@ -1119,6 +1216,32 @@
                 "vite": "^5.2.0 || ^6 || ^7"
             }
         },
+        "node_modules/@tanstack/virtual-core": {
+            "version": "3.13.12",
+            "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
+            "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
+            "license": "MIT",
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/tannerlinsley"
+            }
+        },
+        "node_modules/@tanstack/vue-virtual": {
+            "version": "3.13.12",
+            "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
+            "integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
+            "license": "MIT",
+            "dependencies": {
+                "@tanstack/virtual-core": "3.13.12"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/tannerlinsley"
+            },
+            "peerDependencies": {
+                "vue": "^2.7.0 || ^3.0.0"
+            }
+        },
         "node_modules/@types/estree": {
             "version": "1.0.8",
             "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1140,6 +1263,12 @@
                 "@types/lodash": "*"
             }
         },
+        "node_modules/@types/web-bluetooth": {
+            "version": "0.0.21",
+            "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+            "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+            "license": "MIT"
+        },
         "node_modules/@vitejs/plugin-vue": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@@ -1256,11 +1385,60 @@
             "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==",
             "license": "MIT"
         },
+        "node_modules/@vueuse/core": {
+            "version": "14.0.0",
+            "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz",
+            "integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/web-bluetooth": "^0.0.21",
+                "@vueuse/metadata": "14.0.0",
+                "@vueuse/shared": "14.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            },
+            "peerDependencies": {
+                "vue": "^3.5.0"
+            }
+        },
+        "node_modules/@vueuse/metadata": {
+            "version": "14.0.0",
+            "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz",
+            "integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
+        "node_modules/@vueuse/shared": {
+            "version": "14.0.0",
+            "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0.tgz",
+            "integrity": "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            },
+            "peerDependencies": {
+                "vue": "^3.5.0"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/ansi-styles": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
             "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "color-convert": "^2.0.1"
             },
@@ -1271,6 +1449,18 @@
                 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
+        "node_modules/aria-hidden": {
+            "version": "1.2.6",
+            "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+            "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+            "license": "MIT",
+            "dependencies": {
+                "tslib": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/asynckit": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1322,6 +1512,7 @@
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
             "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "ansi-styles": "^4.1.0",
                 "supports-color": "^7.1.0"
@@ -1338,6 +1529,7 @@
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
             "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "has-flag": "^4.0.0"
             },
@@ -1360,11 +1552,24 @@
                 "url": "https://paulmillr.com/funding/"
             }
         },
+        "node_modules/class-variance-authority": {
+            "version": "0.7.1",
+            "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+            "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "clsx": "^2.1.1"
+            },
+            "funding": {
+                "url": "https://polar.sh/cva"
+            }
+        },
         "node_modules/cliui": {
             "version": "8.0.1",
             "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
             "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
                 "string-width": "^4.2.0",
                 "strip-ansi": "^6.0.1",
@@ -1374,62 +1579,13 @@
                 "node": ">=12"
             }
         },
-        "node_modules/cliui/node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/cliui/node_modules/emoji-regex": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
-        },
-        "node_modules/cliui/node_modules/string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
-            "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/cliui/node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/cliui/node_modules/wrap-ansi": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
-            },
+        "node_modules/clsx": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+            "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+            "license": "MIT",
             "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+                "node": ">=6"
             }
         },
         "node_modules/color-convert": {
@@ -1437,6 +1593,7 @@
             "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
             "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "color-name": "~1.1.4"
             },
@@ -1448,7 +1605,8 @@
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
             "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/combined-stream": {
             "version": "1.0.8",
@@ -1463,18 +1621,18 @@
             }
         },
         "node_modules/concurrently": {
-            "version": "9.1.2",
-            "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
-            "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
+            "version": "9.2.1",
+            "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+            "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "chalk": "^4.1.2",
-                "lodash": "^4.17.21",
-                "rxjs": "^7.8.1",
-                "shell-quote": "^1.8.1",
-                "supports-color": "^8.1.1",
-                "tree-kill": "^1.2.2",
-                "yargs": "^17.7.2"
+                "chalk": "4.1.2",
+                "rxjs": "7.8.2",
+                "shell-quote": "1.8.3",
+                "supports-color": "8.1.1",
+                "tree-kill": "1.2.2",
+                "yargs": "17.7.2"
             },
             "bin": {
                 "conc": "dist/bin/concurrently.js",
@@ -1493,6 +1651,12 @@
             "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
             "license": "MIT"
         },
+        "node_modules/defu": {
+            "version": "6.1.4",
+            "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+            "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+            "license": "MIT"
+        },
         "node_modules/delayed-stream": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1526,6 +1690,13 @@
                 "node": ">= 0.4"
             }
         },
+        "node_modules/emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/enhanced-resolve": {
             "version": "5.18.3",
             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -1643,6 +1814,7 @@
             "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
             "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6"
             }
@@ -1653,16 +1825,34 @@
             "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
             "license": "MIT"
         },
+        "node_modules/fdir": {
+            "version": "6.5.0",
+            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+            "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "peerDependencies": {
+                "picomatch": "^3 || ^4"
+            },
+            "peerDependenciesMeta": {
+                "picomatch": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/follow-redirects": {
-            "version": "1.15.9",
-            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
-            "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+            "version": "1.15.11",
+            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+            "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
             "funding": [
                 {
                     "type": "individual",
                     "url": "https://github.com/sponsors/RubenVerborgh"
                 }
             ],
+            "license": "MIT",
             "engines": {
                 "node": ">=4.0"
             },
@@ -1693,6 +1883,7 @@
             "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
             "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
             "hasInstallScript": true,
+            "license": "MIT",
             "optional": true,
             "os": [
                 "darwin"
@@ -1705,6 +1896,7 @@
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
             "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+            "license": "MIT",
             "funding": {
                 "url": "https://github.com/sponsors/ljharb"
             }
@@ -1714,6 +1906,7 @@
             "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
             "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
             "dev": true,
+            "license": "ISC",
             "engines": {
                 "node": "6.* || 8.* || >= 10.*"
             }
@@ -1779,6 +1972,7 @@
             "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
             "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=8"
             }
@@ -1814,6 +2008,7 @@
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
             "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+            "license": "MIT",
             "dependencies": {
                 "function-bind": "^1.1.2"
             },
@@ -1826,6 +2021,7 @@
             "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
             "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=8"
             }
@@ -2109,18 +2305,21 @@
                 "url": "https://opencollective.com/parcel"
             }
         },
-        "node_modules/lodash": {
-            "version": "4.17.21",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-            "dev": true
-        },
         "node_modules/lodash-es": {
             "version": "4.17.21",
             "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
             "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
             "license": "MIT"
         },
+        "node_modules/lucide-vue-next": {
+            "version": "0.553.0",
+            "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.553.0.tgz",
+            "integrity": "sha512-0tg9XT+VCElTT+7EXXbBRhWe1nU7Doa32Xv/dHP5/LCleFVgV6cAqziM3C7AetqmsYIsfAtNwRYdtvs4Ds7aUg==",
+            "license": "ISC",
+            "peerDependencies": {
+                "vue": ">=3.0.1"
+            }
+        },
         "node_modules/magic-string": {
             "version": "0.30.21",
             "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2190,17 +2389,25 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/ohash": {
+            "version": "2.0.11",
+            "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+            "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+            "license": "MIT"
+        },
         "node_modules/picocolors": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+            "license": "ISC"
         },
         "node_modules/picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+            "license": "MIT",
             "engines": {
-                "node": ">=8.6"
+                "node": ">=12"
             },
             "funding": {
                 "url": "https://github.com/sponsors/jonschlinkert"
@@ -2237,7 +2444,8 @@
         "node_modules/proxy-from-env": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+            "license": "MIT"
         },
         "node_modules/qs": {
             "version": "6.14.0",
@@ -2267,11 +2475,69 @@
                 "url": "https://paulmillr.com/funding/"
             }
         },
+        "node_modules/reka-ui": {
+            "version": "2.6.0",
+            "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.0.tgz",
+            "integrity": "sha512-NrGMKrABD97l890mFS3TNUzB0BLUfbL3hh0NjcJRIUSUljb288bx3Mzo31nOyUcdiiW0HqFGXJwyCBh9cWgb0w==",
+            "license": "MIT",
+            "dependencies": {
+                "@floating-ui/dom": "^1.6.13",
+                "@floating-ui/vue": "^1.1.6",
+                "@internationalized/date": "^3.5.0",
+                "@internationalized/number": "^3.5.0",
+                "@tanstack/vue-virtual": "^3.12.0",
+                "@vueuse/core": "^12.5.0",
+                "@vueuse/shared": "^12.5.0",
+                "aria-hidden": "^1.2.4",
+                "defu": "^6.1.4",
+                "ohash": "^2.0.11"
+            },
+            "peerDependencies": {
+                "vue": ">= 3.2.0"
+            }
+        },
+        "node_modules/reka-ui/node_modules/@vueuse/core": {
+            "version": "12.8.2",
+            "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
+            "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/web-bluetooth": "^0.0.21",
+                "@vueuse/metadata": "12.8.2",
+                "@vueuse/shared": "12.8.2",
+                "vue": "^3.5.13"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
+        "node_modules/reka-ui/node_modules/@vueuse/metadata": {
+            "version": "12.8.2",
+            "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
+            "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
+        "node_modules/reka-ui/node_modules/@vueuse/shared": {
+            "version": "12.8.2",
+            "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
+            "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
+            "license": "MIT",
+            "dependencies": {
+                "vue": "^3.5.13"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
         "node_modules/require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
             "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=0.10.0"
             }
@@ -2318,19 +2584,21 @@
             }
         },
         "node_modules/rxjs": {
-            "version": "7.8.1",
-            "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
-            "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+            "version": "7.8.2",
+            "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+            "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
             "dev": true,
+            "license": "Apache-2.0",
             "dependencies": {
                 "tslib": "^2.1.0"
             }
         },
         "node_modules/shell-quote": {
-            "version": "1.8.2",
-            "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
-            "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+            "version": "1.8.3",
+            "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+            "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">= 0.4"
             },
@@ -2414,15 +2682,45 @@
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
             "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+            "license": "BSD-3-Clause",
             "engines": {
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/supports-color": {
             "version": "8.1.1",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
             "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "has-flag": "^4.0.0"
             },
@@ -2433,6 +2731,16 @@
                 "url": "https://github.com/chalk/supports-color?sponsor=1"
             }
         },
+        "node_modules/tailwind-merge": {
+            "version": "3.3.1",
+            "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
+            "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
+            "license": "MIT",
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/dcastil"
+            }
+        },
         "node_modules/tailwindcss": {
             "version": "4.1.17",
             "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
@@ -2469,40 +2777,12 @@
                 "url": "https://github.com/sponsors/SuperchupuDev"
             }
         },
-        "node_modules/tinyglobby/node_modules/fdir": {
-            "version": "6.5.0",
-            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
-            "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=12.0.0"
-            },
-            "peerDependencies": {
-                "picomatch": "^3 || ^4"
-            },
-            "peerDependenciesMeta": {
-                "picomatch": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/tinyglobby/node_modules/picomatch": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=12"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/jonschlinkert"
-            }
-        },
         "node_modules/tree-kill": {
             "version": "1.2.2",
             "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
             "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
             "dev": true,
+            "license": "MIT",
             "bin": {
                 "tree-kill": "cli.js"
             }
@@ -2511,7 +2791,30 @@
             "version": "2.8.1",
             "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
             "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-            "dev": true
+            "license": "0BSD"
+        },
+        "node_modules/tw-animate-css": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+            "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/Wombosvideo"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "5.9.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+            "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+            "devOptional": true,
+            "license": "Apache-2.0",
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=14.17"
+            }
         },
         "node_modules/vite": {
             "version": "7.2.2",
@@ -2591,35 +2894,19 @@
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz",
             "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==",
+            "license": "MIT",
             "dependencies": {
                 "picocolors": "^1.0.0",
                 "picomatch": "^2.3.1"
             }
         },
-        "node_modules/vite/node_modules/fdir": {
-            "version": "6.5.0",
-            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
-            "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=12.0.0"
-            },
-            "peerDependencies": {
-                "picomatch": "^3 || ^4"
-            },
-            "peerDependenciesMeta": {
-                "picomatch": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/vite/node_modules/picomatch": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+        "node_modules/vite-plugin-full-reload/node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
             "license": "MIT",
             "engines": {
-                "node": ">=12"
+                "node": ">=8.6"
             },
             "funding": {
                 "url": "https://github.com/sponsors/jonschlinkert"
@@ -2646,33 +2933,40 @@
                 }
             }
         },
+        "node_modules/wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
         "node_modules/y18n": {
             "version": "5.0.8",
             "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
             "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
             "dev": true,
+            "license": "ISC",
             "engines": {
                 "node": ">=10"
             }
         },
-        "node_modules/yaml": {
-            "version": "2.7.0",
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
-            "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
-            "optional": true,
-            "peer": true,
-            "bin": {
-                "yaml": "bin.mjs"
-            },
-            "engines": {
-                "node": ">= 14"
-            }
-        },
         "node_modules/yargs": {
             "version": "17.7.2",
             "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
             "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "cliui": "^8.0.1",
                 "escalade": "^3.1.1",
@@ -2691,50 +2985,10 @@
             "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
             "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
             "dev": true,
+            "license": "ISC",
             "engines": {
                 "node": ">=12"
             }
-        },
-        "node_modules/yargs/node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/yargs/node_modules/emoji-regex": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
-        },
-        "node_modules/yargs/node_modules/string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
-            "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/yargs/node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
         }
     }
 }

+ 8 - 0
package.json

@@ -8,14 +8,22 @@
     "devDependencies": {
         "@tailwindcss/vite": "^4.0.0",
         "concurrently": "^9.0.1",
+        "typescript": "^5.9.3",
         "vite": "^7.2.2"
     },
     "dependencies": {
         "@inertiajs/vue3": "^2.2.15",
         "@vitejs/plugin-vue": "^6.0.1",
+        "@vueuse/core": "^14.0.0",
         "chokidar": "^4.0.3",
+        "class-variance-authority": "^0.7.1",
+        "clsx": "^2.1.1",
         "laravel-vite-plugin": "^2.0.0",
+        "lucide-vue-next": "^0.553.0",
+        "reka-ui": "^2.6.0",
+        "tailwind-merge": "^3.3.1",
         "tailwindcss": "^4.0.0",
+        "tw-animate-css": "^1.4.0",
         "vue": "^3.5.24"
     }
 }

+ 172 - 0
resources/css/app.css

@@ -1,4 +1,7 @@
 @import 'tailwindcss';
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
 
 @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
 @source '../../storage/framework/views/*.php';
@@ -9,3 +12,172 @@
     --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
     'Segoe UI Symbol', 'Noto Color Emoji';
 }
+
+:root {
+    --radius: 1rem;
+    --background: oklch(1 0 0);
+    --foreground: oklch(0.141 0.005 285.823);
+    --card: oklch(1 0 0);
+    --card-foreground: oklch(0.141 0.005 285.823);
+    --popover: oklch(1 0 0);
+    --popover-foreground: oklch(0.141 0.005 285.823);
+    --primary: oklch(0.645 0.246 16.439);
+    --primary-foreground: oklch(0.969 0.015 12.422);
+    --secondary: oklch(0.967 0.001 286.375);
+    --secondary-foreground: oklch(0.21 0.006 285.885);
+    --muted: oklch(0.967 0.001 286.375);
+    --muted-foreground: oklch(0.552 0.016 285.938);
+    --accent: oklch(0.967 0.001 286.375);
+    --accent-foreground: oklch(0.21 0.006 285.885);
+    --destructive: oklch(0.577 0.245 27.325);
+    --destructive-foreground: oklch(1.0000 0 0);
+    --border: oklch(0.92 0.004 286.32);
+    --input: oklch(0.92 0.004 286.32);
+    --ring: oklch(0.645 0.246 16.439);
+    --chart-1: oklch(0.646 0.222 41.116);
+    --chart-2: oklch(0.6 0.118 184.704);
+    --chart-3: oklch(0.398 0.07 227.392);
+    --chart-4: oklch(0.828 0.189 84.429);
+    --chart-5: oklch(0.769 0.188 70.08);
+    --sidebar: oklch(0.985 0 0);
+    --sidebar-foreground: oklch(0.141 0.005 285.823);
+    --sidebar-primary: oklch(0.645 0.246 16.439);
+    --sidebar-primary-foreground: oklch(0.969 0.015 12.422);
+    --sidebar-accent: oklch(0.967 0.001 286.375);
+    --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+    --sidebar-border: oklch(0.92 0.004 286.32);
+    --sidebar-ring: oklch(0.645 0.246 16.439);
+    --font-sans: Inter, sans-serif;
+    --font-serif: Lora, serif;
+    --font-mono: JetBrains Mono, monospace;
+    --spacing: 0.25rem;
+    --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+    --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+    --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+    --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+    --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
+    --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
+    --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
+    --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
+    --tracking-normal: 0em;
+}
+
+.dark {
+    --background: oklch(0.141 0.005 285.823);
+    --foreground: oklch(0.985 0 0);
+    --card: oklch(0.21 0.006 285.885);
+    --card-foreground: oklch(0.985 0 0);
+    --popover: oklch(0.21 0.006 285.885);
+    --popover-foreground: oklch(0.985 0 0);
+    --primary: oklch(0.645 0.246 16.439);
+    --primary-foreground: oklch(0.969 0.015 12.422);
+    --secondary: oklch(0.274 0.006 286.033);
+    --secondary-foreground: oklch(0.985 0 0);
+    --muted: oklch(0.274 0.006 286.033);
+    --muted-foreground: oklch(0.705 0.015 286.067);
+    --accent: oklch(0.274 0.006 286.033);
+    --accent-foreground: oklch(0.985 0 0);
+    --destructive: oklch(0.704 0.191 22.216);
+    --destructive-foreground: oklch(1.0000 0 0);
+    --border: oklch(1 0 0 / 10%);
+    --input: oklch(1 0 0 / 15%);
+    --ring: oklch(0.645 0.246 16.439);
+    --chart-1: oklch(0.488 0.243 264.376);
+    --chart-2: oklch(0.696 0.17 162.48);
+    --chart-3: oklch(0.769 0.188 70.08);
+    --chart-4: oklch(0.627 0.265 303.9);
+    --chart-5: oklch(0.645 0.246 16.439);
+    --sidebar: oklch(0.21 0.006 285.885);
+    --sidebar-foreground: oklch(0.985 0 0);
+    --sidebar-primary: oklch(0.645 0.246 16.439);
+    --sidebar-primary-foreground: oklch(0.969 0.015 12.422);
+    --sidebar-accent: oklch(0.274 0.006 286.033);
+    --sidebar-accent-foreground: oklch(0.985 0 0);
+    --sidebar-border: oklch(1 0 0 / 10%);
+    --sidebar-ring: oklch(0.645 0.246 16.439);
+    --font-sans: Inter, sans-serif;
+    --font-serif: Lora, serif;
+    --font-mono: JetBrains Mono, monospace;
+    --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+    --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+    --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+    --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+    --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
+    --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
+    --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
+    --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
+}
+
+@theme inline {
+    --radius: var(--radius);
+    --radius-sm: calc(var(--radius) - 4px);
+    --radius-md: calc(var(--radius) - 2px);
+    --radius-lg: var(--radius);
+    --radius-xl: calc(var(--radius) + 4px);
+    --color-background: var(--background);
+    --color-foreground: var(--foreground);
+    --color-card: var(--card);
+    --color-card-foreground: var(--card-foreground);
+    --color-popover: var(--popover);
+    --color-popover-foreground: var(--popover-foreground);
+    --color-primary: var(--primary);
+    --color-primary-foreground: var(--primary-foreground);
+    --color-secondary: var(--secondary);
+    --color-secondary-foreground: var(--secondary-foreground);
+    --color-muted: var(--muted);
+    --color-muted-foreground: var(--muted-foreground);
+    --color-accent: var(--accent);
+    --color-accent-foreground: var(--accent-foreground);
+    --color-destructive: var(--destructive);
+    --color-destructive-foreground: var(--destructive-foreground);
+    --color-border: var(--border);
+    --color-input: var(--input);
+    --color-ring: var(--ring);
+    --color-chart-1: var(--chart-1);
+    --color-chart-2: var(--chart-2);
+    --color-chart-3: var(--chart-3);
+    --color-chart-4: var(--chart-4);
+    --color-chart-5: var(--chart-5);
+    --color-sidebar: var(--sidebar);
+    --color-sidebar-foreground: var(--sidebar-foreground);
+    --color-sidebar-primary: var(--sidebar-primary);
+    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+    --color-sidebar-accent: var(--sidebar-accent);
+    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+    --color-sidebar-border: var(--sidebar-border);
+    --color-sidebar-ring: var(--sidebar-ring);
+    --font-sans: Inter, sans-serif;
+    --font-serif: Lora, serif;
+    --font-mono: JetBrains Mono, monospace;
+    --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
+    --tracking-tight: calc(var(--tracking-normal) - 0.025em);
+    --tracking-wide: calc(var(--tracking-normal) + 0.025em);
+    --tracking-wider: calc(var(--tracking-normal) + 0.05em);
+    --tracking-widest: calc(var(--tracking-normal) + 0.1em);
+    --tracking-normal: var(--tracking-normal);
+    --shadow-2xl: var(--shadow-2xl);
+    --shadow-xl: var(--shadow-xl);
+    --shadow-lg: var(--shadow-lg);
+    --shadow-md: var(--shadow-md);
+    --shadow: var(--shadow);
+    --shadow-sm: var(--shadow-sm);
+    --shadow-xs: var(--shadow-xs);
+    --shadow-2xs: var(--shadow-2xs);
+    --spacing: var(--spacing);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+    }
+  body {
+    @apply bg-background text-foreground;
+    letter-spacing: var(--tracking-normal);
+    }
+}
+
+html,
+body {
+    overscroll-behavior: none;
+    touch-action: none;
+}

+ 76 - 0
resources/js/Components/AppSidebar.vue

@@ -0,0 +1,76 @@
+<script setup lang="ts">
+import {Plus, MoreHorizontal, Trash, MessageSquareOff} from "lucide-vue-next"
+
+import {
+    Sidebar,
+    SidebarContent,
+    SidebarGroup,
+    SidebarGroupContent,
+    SidebarGroupLabel,
+    SidebarMenu,
+    SidebarMenuButton,
+    SidebarMenuItem,
+    SidebarGroupAction, SidebarMenuAction
+} from '@/Packages/Shadcn/Components/ui/sidebar'
+
+import {
+    DropdownMenu,
+    DropdownMenuTrigger,
+    DropdownMenuContent,
+    DropdownMenuItem
+} from "@/Packages/Shadcn/Components/ui/dropdown-menu";
+
+import {
+    Empty,
+    EmptyDescription,
+    EmptyHeader,
+    EmptyMedia,
+    EmptyTitle,
+} from '@/Packages/Shadcn/Components/ui/empty'
+
+const items = [{title: "Нет чатов", url: "#"}];
+</script>
+
+<template>
+    <Sidebar variant="floating">
+        <SidebarContent>
+            <SidebarGroup>
+                <SidebarGroupLabel>Application</SidebarGroupLabel>
+                <SidebarGroupAction title="New Chat" class="rounded-full cursor-pointer">
+                    <Plus/>
+                    <span class="sr-only">New Chat</span>
+                </SidebarGroupAction>
+                <SidebarGroupContent>
+                    <Empty class="border border-dashed">
+                        <EmptyHeader>
+                            <EmptyMedia variant="icon">
+                                <MessageSquareOff/>
+                            </EmptyMedia>
+                            <EmptyTitle>Нет чатов</EmptyTitle>
+                            <EmptyDescription>Список чатов пока пуст</EmptyDescription>
+                        </EmptyHeader>
+                    </Empty>
+                    <!--                    <SidebarMenu>-->
+                    <!--                        <SidebarMenuItem v-for="item in items" :key="item.title">-->
+                    <!--                            <SidebarMenuButton asChild class="cursor-pointer">-->
+                    <!--                                <span>{{ item.title }}</span>-->
+                    <!--                            </SidebarMenuButton>-->
+                    <!--                            <DropdownMenu>-->
+                    <!--                                <DropdownMenuTrigger asChild>-->
+                    <!--                                    <SidebarMenuAction class="rounded-full cursor-pointer">-->
+                    <!--                                        <MoreHorizontal/>-->
+                    <!--                                    </SidebarMenuAction>-->
+                    <!--                                </DropdownMenuTrigger>-->
+                    <!--                                <DropdownMenuContent side="top" align="start">-->
+                    <!--                                    <DropdownMenuItem class="text-primary cursor-pointer">-->
+                    <!--                                        <Trash /> <span>Удалить чат</span>-->
+                    <!--                                    </DropdownMenuItem>-->
+                    <!--                                </DropdownMenuContent>-->
+                    <!--                            </DropdownMenu>-->
+                    <!--                        </SidebarMenuItem>-->
+                    <!--                    </SidebarMenu>-->
+                </SidebarGroupContent>
+            </SidebarGroup>
+        </SidebarContent>
+    </Sidebar>
+</template>

+ 17 - 8
resources/js/Composables/useAppearance.js → resources/js/Composables/useAppearance.ts

@@ -1,21 +1,28 @@
 import { onMounted, ref } from 'vue';
 
-export function updateTheme(value) {
+type Appearance = 'light' | 'dark' | 'system';
+
+export function updateTheme(value: Appearance) {
     if (typeof window === 'undefined') {
         return;
     }
 
     if (value === 'system') {
-        const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
+        const mediaQueryList = window.matchMedia(
+            '(prefers-color-scheme: dark)',
+        );
         const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
 
-        document.documentElement.classList.toggle('dark', systemTheme === 'dark');
+        document.documentElement.classList.toggle(
+            'dark',
+            systemTheme === 'dark',
+        );
     } else {
         document.documentElement.classList.toggle('dark', value === 'dark');
     }
 }
 
-const setCookie = (name, value, days = 365) => {
+const setCookie = (name: string, value: string, days = 365) => {
     if (typeof document === 'undefined') {
         return;
     }
@@ -38,7 +45,7 @@ const getStoredAppearance = () => {
         return null;
     }
 
-    return localStorage.getItem('appearance') | null;
+    return localStorage.getItem('appearance') as Appearance | null;
 };
 
 const handleSystemThemeChange = () => {
@@ -60,18 +67,20 @@ export function initializeTheme() {
     mediaQuery()?.addEventListener('change', handleSystemThemeChange);
 }
 
-const appearance = ref('system');
+const appearance = ref<Appearance>('system');
 
 export function useAppearance() {
     onMounted(() => {
-        const savedAppearance = localStorage.getItem('appearance') | null;
+        const savedAppearance = localStorage.getItem(
+            'appearance',
+        ) as Appearance | null;
 
         if (savedAppearance) {
             appearance.value = savedAppearance;
         }
     });
 
-    function updateAppearance(value) {
+    function updateAppearance(value: Appearance) {
         appearance.value = value;
 
         // Store in localStorage for client-side persistence...

+ 21 - 2
resources/js/Layouts/AppLayout.vue

@@ -1,9 +1,28 @@
-<script setup>
+<script setup lang="ts">
+import AppSidebar from '@/Components/AppSidebar.vue'
+import {SidebarProvider, SidebarTrigger} from '@/Packages/Shadcn/Components/ui/sidebar'
+import {Head} from "@inertiajs/vue3";
+
+defineOptions({ inheritAttrs: false })
+
+defineProps({
+    pageClass: {
+        default: () => null,
+    }
+})
 
 </script>
 
 <template>
-    <slot/>
+    <Head/>
+
+    <SidebarProvider>
+        <AppSidebar/>
+
+        <main class="container mx-auto px-2 md:px-0 md:pr-2 py-2" :class="pageClass">
+            <slot/>
+        </main>
+    </SidebarProvider>
 </template>
 
 <style scoped>

+ 29 - 0
resources/js/Packages/Shadcn/Components/ui/button/Button.vue

@@ -0,0 +1,29 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import type { ButtonVariants } from "."
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { buttonVariants } from "."
+
+interface Props extends PrimitiveProps {
+  variant?: ButtonVariants["variant"]
+  size?: ButtonVariants["size"]
+  class?: HTMLAttributes["class"]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  as: "button",
+})
+</script>
+
+<template>
+  <Primitive
+    data-slot="button"
+    :as="as"
+    :as-child="asChild"
+    :class="cn(buttonVariants({ variant, size }), props.class)"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 36 - 0
resources/js/Packages/Shadcn/Components/ui/button/index.js

@@ -0,0 +1,36 @@
+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-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white shadow-xs 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 shadow-xs 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",
+        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-sm": "size-8",
+        "icon-lg": "size-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  },
+);

+ 39 - 0
resources/js/Packages/Shadcn/Components/ui/button/index.ts

@@ -0,0 +1,39 @@
+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-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white shadow-xs 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 shadow-xs 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",
+        "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-sm": "size-8",
+        "icon-lg": "size-10",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  },
+)
+
+export type ButtonVariants = VariantProps<typeof buttonVariants>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenu.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
+import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
+
+const props = defineProps<DropdownMenuRootProps>()
+const emits = defineEmits<DropdownMenuRootEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <DropdownMenuRoot
+    data-slot="dropdown-menu"
+    v-bind="forwarded"
+  >
+    <slot />
+  </DropdownMenuRoot>
+</template>

+ 38 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { Check } from "lucide-vue-next"
+import {
+  DropdownMenuCheckboxItem,
+
+  DropdownMenuItemIndicator,
+  useForwardPropsEmits,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
+const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
+
+const delegatedProps = reactiveOmit(props, "class")
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DropdownMenuCheckboxItem
+    data-slot="dropdown-menu-checkbox-item"
+    v-bind="forwarded"
+    :class=" cn(
+      'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
+      props.class,
+    )"
+  >
+    <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+      <DropdownMenuItemIndicator>
+        <Check class="size-4" />
+      </DropdownMenuItemIndicator>
+    </span>
+    <slot />
+  </DropdownMenuCheckboxItem>
+</template>

+ 36 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuContent.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import {
+  DropdownMenuContent,
+
+  DropdownMenuPortal,
+  useForwardPropsEmits,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = withDefaults(
+  defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
+  {
+    sideOffset: 4,
+  },
+)
+const emits = defineEmits<DropdownMenuContentEmits>()
+
+const delegatedProps = reactiveOmit(props, "class")
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DropdownMenuPortal>
+    <DropdownMenuContent
+      data-slot="dropdown-menu-content"
+      v-bind="forwarded"
+      :class="cn('bg-popover text-popover-foreground 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-[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 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
+    >
+      <slot />
+    </DropdownMenuContent>
+  </DropdownMenuPortal>
+</template>

+ 15 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuGroup.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import type { DropdownMenuGroupProps } from "reka-ui"
+import { DropdownMenuGroup } from "reka-ui"
+
+const props = defineProps<DropdownMenuGroupProps>()
+</script>
+
+<template>
+  <DropdownMenuGroup
+    data-slot="dropdown-menu-group"
+    v-bind="props"
+  >
+    <slot />
+  </DropdownMenuGroup>
+</template>

+ 31 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuItem.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+import type { DropdownMenuItemProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { DropdownMenuItem, useForwardProps } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = withDefaults(defineProps<DropdownMenuItemProps & {
+  class?: HTMLAttributes["class"]
+  inset?: boolean
+  variant?: "default" | "destructive"
+}>(), {
+  variant: "default",
+})
+
+const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
+
+const forwardedProps = useForwardProps(delegatedProps)
+</script>
+
+<template>
+  <DropdownMenuItem
+    data-slot="dropdown-menu-item"
+    :data-inset="inset ? '' : undefined"
+    :data-variant="variant"
+    v-bind="forwardedProps"
+    :class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
+  >
+    <slot />
+  </DropdownMenuItem>
+</template>

+ 23 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuLabel.vue

@@ -0,0 +1,23 @@
+<script setup lang="ts">
+import type { DropdownMenuLabelProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { DropdownMenuLabel, useForwardProps } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
+
+const delegatedProps = reactiveOmit(props, "class", "inset")
+const forwardedProps = useForwardProps(delegatedProps)
+</script>
+
+<template>
+  <DropdownMenuLabel
+    data-slot="dropdown-menu-label"
+    :data-inset="inset ? '' : undefined"
+    v-bind="forwardedProps"
+    :class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
+  >
+    <slot />
+  </DropdownMenuLabel>
+</template>

+ 22 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuRadioGroup.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
+import {
+  DropdownMenuRadioGroup,
+
+  useForwardPropsEmits,
+} from "reka-ui"
+
+const props = defineProps<DropdownMenuRadioGroupProps>()
+const emits = defineEmits<DropdownMenuRadioGroupEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <DropdownMenuRadioGroup
+    data-slot="dropdown-menu-radio-group"
+    v-bind="forwarded"
+  >
+    <slot />
+  </DropdownMenuRadioGroup>
+</template>

+ 39 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuRadioItem.vue

@@ -0,0 +1,39 @@
+<script setup lang="ts">
+import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { Circle } from "lucide-vue-next"
+import {
+  DropdownMenuItemIndicator,
+  DropdownMenuRadioItem,
+
+  useForwardPropsEmits,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
+
+const emits = defineEmits<DropdownMenuRadioItemEmits>()
+
+const delegatedProps = reactiveOmit(props, "class")
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DropdownMenuRadioItem
+    data-slot="dropdown-menu-radio-item"
+    v-bind="forwarded"
+    :class="cn(
+      'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
+      props.class,
+    )"
+  >
+    <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
+      <DropdownMenuItemIndicator>
+        <Circle class="size-2 fill-current" />
+      </DropdownMenuItemIndicator>
+    </span>
+    <slot />
+  </DropdownMenuRadioItem>
+</template>

+ 24 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSeparator.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import type { DropdownMenuSeparatorProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import {
+  DropdownMenuSeparator,
+
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuSeparatorProps & {
+  class?: HTMLAttributes["class"]
+}>()
+
+const delegatedProps = reactiveOmit(props, "class")
+</script>
+
+<template>
+  <DropdownMenuSeparator
+    data-slot="dropdown-menu-separator"
+    v-bind="delegatedProps"
+    :class="cn('bg-border -mx-1 my-1 h-px', props.class)"
+  />
+</template>

+ 17 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuShortcut.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <span
+    data-slot="dropdown-menu-shortcut"
+    :class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
+  >
+    <slot />
+  </span>
+</template>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSub.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
+import {
+  DropdownMenuSub,
+
+  useForwardPropsEmits,
+} from "reka-ui"
+
+const props = defineProps<DropdownMenuSubProps>()
+const emits = defineEmits<DropdownMenuSubEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
+    <slot />
+  </DropdownMenuSub>
+</template>

+ 28 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSubContent.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import {
+  DropdownMenuSubContent,
+
+  useForwardPropsEmits,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
+const emits = defineEmits<DropdownMenuSubContentEmits>()
+
+const delegatedProps = reactiveOmit(props, "class")
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DropdownMenuSubContent
+    data-slot="dropdown-menu-sub-content"
+    v-bind="forwarded"
+    :class="cn('bg-popover text-popover-foreground 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-[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 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
+  >
+    <slot />
+  </DropdownMenuSubContent>
+</template>

+ 31 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuSubTrigger.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+import type { DropdownMenuSubTriggerProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { ChevronRight } from "lucide-vue-next"
+import {
+  DropdownMenuSubTrigger,
+
+  useForwardProps,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
+
+const delegatedProps = reactiveOmit(props, "class", "inset")
+const forwardedProps = useForwardProps(delegatedProps)
+</script>
+
+<template>
+  <DropdownMenuSubTrigger
+    data-slot="dropdown-menu-sub-trigger"
+    v-bind="forwardedProps"
+    :class="cn(
+      'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
+      props.class,
+    )"
+  >
+    <slot />
+    <ChevronRight class="ml-auto size-4" />
+  </DropdownMenuSubTrigger>
+</template>

+ 17 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/DropdownMenuTrigger.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import type { DropdownMenuTriggerProps } from "reka-ui"
+import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
+
+const props = defineProps<DropdownMenuTriggerProps>()
+
+const forwardedProps = useForwardProps(props)
+</script>
+
+<template>
+  <DropdownMenuTrigger
+    data-slot="dropdown-menu-trigger"
+    v-bind="forwardedProps"
+  >
+    <slot />
+  </DropdownMenuTrigger>
+</template>

+ 16 - 0
resources/js/Packages/Shadcn/Components/ui/dropdown-menu/index.ts

@@ -0,0 +1,16 @@
+export { default as DropdownMenu } from "./DropdownMenu.vue"
+
+export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
+export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
+export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
+export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
+export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
+export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
+export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
+export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
+export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
+export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
+export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
+export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
+export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
+export { DropdownMenuPortal } from "reka-ui"

+ 20 - 0
resources/js/Packages/Shadcn/Components/ui/empty/Empty.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="empty"
+    :class="cn(
+      'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12',
+      props.class,
+    )"
+  >
+    <slot />
+  </div>
+</template>

+ 20 - 0
resources/js/Packages/Shadcn/Components/ui/empty/EmptyContent.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="empty-content"
+    :class="cn(
+      'flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm',
+      props.class,
+    )"
+  >
+    <slot />
+  </div>
+</template>

+ 20 - 0
resources/js/Packages/Shadcn/Components/ui/empty/EmptyDescription.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <p
+    data-slot="empty-description"
+    :class="cn(
+      'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
+      $attrs.class ?? '',
+    )"
+  >
+    <slot />
+  </p>
+</template>

+ 20 - 0
resources/js/Packages/Shadcn/Components/ui/empty/EmptyHeader.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="empty-header"
+    :class="cn(
+      'flex max-w-sm flex-col items-center gap-2 text-center',
+      $attrs.class ?? '',
+    )"
+  >
+    <slot />
+  </div>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/empty/EmptyMedia.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import type { EmptyMediaVariants } from "."
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { emptyMediaVariants } from "."
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+  variant?: EmptyMediaVariants["variant"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="empty-icon"
+    :data-variant="variant"
+    :class="cn(emptyMediaVariants({ variant }), props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/empty/EmptyTitle.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import type { EmptyMediaVariants } from "."
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { emptyMediaVariants } from "."
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+  variant?: EmptyMediaVariants["variant"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="empty-icon"
+    :data-variant="variant"
+    :class="cn(emptyMediaVariants({ variant }), props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 26 - 0
resources/js/Packages/Shadcn/Components/ui/empty/index.ts

@@ -0,0 +1,26 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Empty } from "./Empty.vue"
+export { default as EmptyContent } from "./EmptyContent.vue"
+export { default as EmptyDescription } from "./EmptyDescription.vue"
+export { default as EmptyHeader } from "./EmptyHeader.vue"
+export { default as EmptyMedia } from "./EmptyMedia.vue"
+export { default as EmptyTitle } from "./EmptyTitle.vue"
+
+export const emptyMediaVariants = cva(
+  "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "bg-transparent",
+        icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  },
+)
+
+export type EmptyMediaVariants = VariantProps<typeof emptyMediaVariants>

+ 35 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroup.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="input-group"
+    role="group"
+    :class="cn(
+      'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
+      'h-9 min-w-0 has-[>textarea]:h-auto',
+
+      // Variants based on alignment.
+      'has-[>[data-align=inline-start]]:[&>input]:pl-2',
+      'has-[>[data-align=inline-end]]:[&>input]:pr-2',
+      'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
+      'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
+
+      // Focus state.
+      'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
+
+      // Error state.
+      'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
+
+      props.class,
+    )"
+  >
+    <slot />
+  </div>
+</template>

+ 36 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupAddon.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import type { InputGroupVariants } from "."
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { inputGroupAddonVariants } from "."
+
+const props = withDefaults(defineProps<{
+  align?: InputGroupVariants["align"]
+  class?: HTMLAttributes["class"]
+}>(), {
+  align: "inline-start",
+})
+
+function handleInputGroupAddonClick(e: MouseEvent) {
+  const currentTarget = e.currentTarget as HTMLElement | null
+  const target = e.target as HTMLElement | null
+  if (target && target.closest("button")) {
+    return
+  }
+  if (currentTarget && currentTarget?.parentElement) {
+    currentTarget.parentElement?.querySelector("input")?.focus()
+  }
+}
+</script>
+
+<template>
+  <div
+    role="group"
+    data-slot="input-group-addon"
+    :data-align="props.align"
+    :class="cn(inputGroupAddonVariants({ align: props.align }), props.class)"
+    @click="handleInputGroupAddonClick"
+  >
+    <slot />
+  </div>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupButton.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { InputGroupButtonProps } from "."
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Button } from '@/Packages/Shadcn/Components/ui/button'
+import { inputGroupButtonVariants } from "."
+
+const props = withDefaults(defineProps<InputGroupButtonProps>(), {
+  size: "xs",
+  variant: "ghost",
+})
+</script>
+
+<template>
+  <Button
+    :data-size="props.size"
+    :variant="props.variant"
+    :class="cn(inputGroupButtonVariants({ size: props.size }), props.class)"
+  >
+    <slot />
+  </Button>
+</template>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupInput.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Input } from '@/Packages/Shadcn/Components/ui/input'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Input
+    data-slot="input-group-control"
+    :class="cn(
+      'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
+      props.class,
+    )"
+  />
+</template>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupText.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <span
+    :class="cn(
+      'text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4',
+      props.class,
+    )"
+  >
+    <slot />
+  </span>
+</template>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/InputGroupTextarea.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Textarea } from '@/Packages/Shadcn/Components/ui/textarea'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Textarea
+    data-slot="input-group-control"
+    :class="cn(
+      'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
+      props.class,
+    )"
+  />
+</template>

+ 47 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/index.js

@@ -0,0 +1,47 @@
+import { cva } from "class-variance-authority";
+
+export { default as InputGroup } from "./InputGroup.vue";
+export { default as InputGroupAddon } from "./InputGroupAddon.vue";
+export { default as InputGroupButton } from "./InputGroupButton.vue";
+export { default as InputGroupInput } from "./InputGroupInput.vue";
+export { default as InputGroupText } from "./InputGroupText.vue";
+export { default as InputGroupTextarea } from "./InputGroupTextarea.vue";
+
+export const inputGroupAddonVariants = cva(
+  "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
+  {
+    variants: {
+      align: {
+        "inline-start":
+          "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
+        "inline-end":
+          "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
+        "block-start":
+          "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
+        "block-end":
+          "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
+      },
+    },
+    defaultVariants: {
+      align: "inline-start",
+    },
+  },
+);
+
+export const inputGroupButtonVariants = cva(
+  "text-sm shadow-none flex gap-2 items-center",
+  {
+    variants: {
+      size: {
+        xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
+        sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
+        "icon-xs":
+          "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
+        "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+      },
+    },
+    defaultVariants: {
+      size: "xs",
+    },
+  },
+);

+ 59 - 0
resources/js/Packages/Shadcn/Components/ui/input-group/index.ts

@@ -0,0 +1,59 @@
+import type { VariantProps } from "class-variance-authority"
+import type { HTMLAttributes } from "vue"
+import type { ButtonVariants } from '@/Packages/Shadcn/Components/ui/button'
+import { cva } from "class-variance-authority"
+
+export { default as InputGroup } from "./InputGroup.vue"
+export { default as InputGroupAddon } from "./InputGroupAddon.vue"
+export { default as InputGroupButton } from "./InputGroupButton.vue"
+export { default as InputGroupInput } from "./InputGroupInput.vue"
+export { default as InputGroupText } from "./InputGroupText.vue"
+export { default as InputGroupTextarea } from "./InputGroupTextarea.vue"
+
+export const inputGroupAddonVariants = cva(
+  "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
+  {
+    variants: {
+      align: {
+        "inline-start":
+          "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
+        "inline-end":
+          "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
+        "block-start":
+          "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
+        "block-end":
+          "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
+      },
+    },
+    defaultVariants: {
+      align: "inline-start",
+    },
+  },
+)
+
+export type InputGroupVariants = VariantProps<typeof inputGroupAddonVariants>
+
+export const inputGroupButtonVariants = cva(
+  "text-sm shadow-none flex gap-2 items-center",
+  {
+    variants: {
+      size: {
+        "xs": "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
+        "sm": "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
+        "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
+        "icon-sm": "size-8 p-0 has-[>svg]:p-0",
+      },
+    },
+    defaultVariants: {
+      size: "xs",
+    },
+  },
+)
+
+export type InputGroupButtonVariants = VariantProps<typeof inputGroupButtonVariants>
+
+export interface InputGroupButtonProps {
+  variant?: ButtonVariants["variant"]
+  size?: InputGroupButtonVariants["size"]
+  class?: HTMLAttributes["class"]
+}

+ 33 - 0
resources/js/Packages/Shadcn/Components/ui/input/Input.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { useVModel } from "@vueuse/core"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  defaultValue?: string | number
+  modelValue?: string | number
+  class?: HTMLAttributes["class"]
+}>()
+
+const emits = defineEmits<{
+  (e: "update:modelValue", payload: string | number): void
+}>()
+
+const modelValue = useVModel(props, "modelValue", emits, {
+  passive: true,
+  defaultValue: props.defaultValue,
+})
+</script>
+
+<template>
+  <input
+    v-model="modelValue"
+    data-slot="input"
+    :class="cn(
+      'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+      'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
+      'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
+      props.class,
+    )"
+  >
+</template>

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/input/index.js

@@ -0,0 +1 @@
+export { default as Input } from "./Input.vue";

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/input/index.ts

@@ -0,0 +1 @@
+export { default as Input } from "./Input.vue"

+ 29 - 0
resources/js/Packages/Shadcn/Components/ui/separator/Separator.vue

@@ -0,0 +1,29 @@
+<script setup lang="ts">
+import type { SeparatorProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { Separator } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = withDefaults(defineProps<
+  SeparatorProps & { class?: HTMLAttributes["class"] }
+>(), {
+  orientation: "horizontal",
+  decorative: true,
+})
+
+const delegatedProps = reactiveOmit(props, "class")
+</script>
+
+<template>
+  <Separator
+    data-slot="separator-root"
+    v-bind="delegatedProps"
+    :class="
+      cn(
+        'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
+        props.class,
+      )
+    "
+  />
+</template>

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/separator/index.ts

@@ -0,0 +1 @@
+export { default as Separator } from "./Separator.vue"

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/Sheet.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { DialogRootEmits, DialogRootProps } from "reka-ui"
+import { DialogRoot, useForwardPropsEmits } from "reka-ui"
+
+const props = defineProps<DialogRootProps>()
+const emits = defineEmits<DialogRootEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <DialogRoot
+    data-slot="sheet"
+    v-bind="forwarded"
+  >
+    <slot />
+  </DialogRoot>
+</template>

+ 15 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetClose.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import type { DialogCloseProps } from "reka-ui"
+import { DialogClose } from "reka-ui"
+
+const props = defineProps<DialogCloseProps>()
+</script>
+
+<template>
+  <DialogClose
+    data-slot="sheet-close"
+    v-bind="props"
+  >
+    <slot />
+  </DialogClose>
+</template>

+ 63 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetContent.vue

@@ -0,0 +1,63 @@
+<script setup lang="ts">
+import type { DialogContentEmits, DialogContentProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { X } from "lucide-vue-next"
+import {
+  DialogClose,
+  DialogContent,
+
+  DialogPortal,
+  useForwardPropsEmits,
+} from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import SheetOverlay from "./SheetOverlay.vue"
+
+interface SheetContentProps extends DialogContentProps {
+  class?: HTMLAttributes["class"]
+  side?: "top" | "right" | "bottom" | "left"
+}
+
+defineOptions({
+  inheritAttrs: false,
+})
+
+const props = withDefaults(defineProps<SheetContentProps>(), {
+  side: "right",
+})
+const emits = defineEmits<DialogContentEmits>()
+
+const delegatedProps = reactiveOmit(props, "class", "side")
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DialogPortal>
+    <SheetOverlay />
+    <DialogContent
+      data-slot="sheet-content"
+      :class="cn(
+        'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
+        side === 'right'
+          && 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
+        side === 'left'
+          && 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
+        side === 'top'
+          && 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
+        side === 'bottom'
+          && 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
+        props.class)"
+      v-bind="{ ...forwarded, ...$attrs }"
+    >
+      <slot />
+
+      <DialogClose
+        class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
+      >
+        <X class="size-4" />
+        <span class="sr-only">Close</span>
+      </DialogClose>
+    </DialogContent>
+  </DialogPortal>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetDescription.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { DialogDescriptionProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { DialogDescription } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
+
+const delegatedProps = reactiveOmit(props, "class")
+</script>
+
+<template>
+  <DialogDescription
+    data-slot="sheet-description"
+    :class="cn('text-muted-foreground text-sm', props.class)"
+    v-bind="delegatedProps"
+  >
+    <slot />
+  </DialogDescription>
+</template>

+ 16 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetFooter.vue

@@ -0,0 +1,16 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{ class?: HTMLAttributes["class"] }>()
+</script>
+
+<template>
+  <div
+    data-slot="sheet-footer"
+    :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
+    "
+  >
+    <slot />
+  </div>
+</template>

+ 15 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetHeader.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{ class?: HTMLAttributes["class"] }>()
+</script>
+
+<template>
+  <div
+    data-slot="sheet-header"
+    :class="cn('flex flex-col gap-1.5 p-4', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetOverlay.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { DialogOverlayProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { DialogOverlay } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
+
+const delegatedProps = reactiveOmit(props, "class")
+</script>
+
+<template>
+  <DialogOverlay
+    data-slot="sheet-overlay"
+    :class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
+    v-bind="delegatedProps"
+  >
+    <slot />
+  </DialogOverlay>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetTitle.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { DialogTitleProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { DialogTitle } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
+
+const delegatedProps = reactiveOmit(props, "class")
+</script>
+
+<template>
+  <DialogTitle
+    data-slot="sheet-title"
+    :class="cn('text-foreground font-semibold', props.class)"
+    v-bind="delegatedProps"
+  >
+    <slot />
+  </DialogTitle>
+</template>

+ 15 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/SheetTrigger.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import type { DialogTriggerProps } from "reka-ui"
+import { DialogTrigger } from "reka-ui"
+
+const props = defineProps<DialogTriggerProps>()
+</script>
+
+<template>
+  <DialogTrigger
+    data-slot="sheet-trigger"
+    v-bind="props"
+  >
+    <slot />
+  </DialogTrigger>
+</template>

+ 8 - 0
resources/js/Packages/Shadcn/Components/ui/sheet/index.ts

@@ -0,0 +1,8 @@
+export { default as Sheet } from "./Sheet.vue"
+export { default as SheetClose } from "./SheetClose.vue"
+export { default as SheetContent } from "./SheetContent.vue"
+export { default as SheetDescription } from "./SheetDescription.vue"
+export { default as SheetFooter } from "./SheetFooter.vue"
+export { default as SheetHeader } from "./SheetHeader.vue"
+export { default as SheetTitle } from "./SheetTitle.vue"
+export { default as SheetTrigger } from "./SheetTrigger.vue"

+ 96 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/Sidebar.vue

@@ -0,0 +1,96 @@
+<script setup lang="ts">
+import type { SidebarProps } from "."
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Sheet, SheetContent } from '@/Packages/Shadcn/Components/ui/sheet'
+import SheetDescription from '@/Packages/Shadcn/Components/ui/sheet/SheetDescription.vue'
+import SheetHeader from '@/Packages/Shadcn/Components/ui/sheet/SheetHeader.vue'
+import SheetTitle from '@/Packages/Shadcn/Components/ui/sheet/SheetTitle.vue'
+import { SIDEBAR_WIDTH_MOBILE, useSidebar } from "./utils"
+
+defineOptions({
+  inheritAttrs: false,
+})
+
+const props = withDefaults(defineProps<SidebarProps>(), {
+  side: "left",
+  variant: "sidebar",
+  collapsible: "offcanvas",
+})
+
+const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+</script>
+
+<template>
+  <div
+    v-if="collapsible === 'none'"
+    data-slot="sidebar"
+    :class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
+    v-bind="$attrs"
+  >
+    <slot />
+  </div>
+
+  <Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
+    <SheetContent
+      data-sidebar="sidebar"
+      data-slot="sidebar"
+      data-mobile="true"
+      :side="side"
+      class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
+      :style="{
+        '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
+      }"
+    >
+      <SheetHeader class="sr-only">
+        <SheetTitle>Sidebar</SheetTitle>
+        <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+      </SheetHeader>
+      <div class="flex h-full w-full flex-col">
+        <slot />
+      </div>
+    </SheetContent>
+  </Sheet>
+
+  <div
+    v-else
+    class="group peer text-sidebar-foreground hidden md:block"
+    data-slot="sidebar"
+    :data-state="state"
+    :data-collapsible="state === 'collapsed' ? collapsible : ''"
+    :data-variant="variant"
+    :data-side="side"
+  >
+    <!-- This is what handles the sidebar gap on desktop  -->
+    <div
+      :class="cn(
+        'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
+        'group-data-[collapsible=offcanvas]:w-0',
+        'group-data-[side=right]:rotate-180',
+        variant === 'floating' || variant === 'inset'
+          ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
+          : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
+      )"
+    />
+    <div
+      :class="cn(
+        'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
+        side === 'left'
+          ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
+          : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
+        // Adjust the padding for floating and inset variants.
+        variant === 'floating' || variant === 'inset'
+          ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
+          : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
+        props.class,
+      )"
+      v-bind="$attrs"
+    >
+      <div
+        data-sidebar="sidebar"
+        class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
+      >
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarContent.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-content"
+    data-sidebar="content"
+    :class="cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarFooter.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-footer"
+    data-sidebar="footer"
+    :class="cn('flex flex-col gap-2 p-2', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroup.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-group"
+    data-sidebar="group"
+    :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 27 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupAction.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<PrimitiveProps & {
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Primitive
+    data-slot="sidebar-group-action"
+    data-sidebar="group-action"
+    :as="as"
+    :as-child="asChild"
+    :class="cn(
+      'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
+      'after:absolute after:-inset-2 md:after:hidden',
+      'group-data-[collapsible=icon]:hidden',
+      props.class,
+    )"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupContent.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-group-content"
+    data-sidebar="group-content"
+    :class="cn('w-full text-sm', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 25 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarGroupLabel.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<PrimitiveProps & {
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Primitive
+    data-slot="sidebar-group-label"
+    data-sidebar="group-label"
+    :as="as"
+    :as-child="asChild"
+    :class="cn(
+      'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
+      'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
+      props.class)"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarHeader.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-header"
+    data-sidebar="header"
+    :class="cn('flex flex-col gap-2 p-2', props.class)"
+  >
+    <slot />
+  </div>
+</template>

+ 22 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarInput.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Input } from '@/Packages/Shadcn/Components/ui/input'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Input
+    data-slot="sidebar-input"
+    data-sidebar="input"
+    :class="cn(
+      'bg-background h-8 w-full shadow-none',
+      props.class,
+    )"
+  >
+    <slot />
+  </Input>
+</template>

+ 21 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarInset.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <main
+    data-slot="sidebar-inset"
+    :class="cn(
+      'bg-background relative flex w-full flex-1 flex-col',
+      'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
+      props.class,
+    )"
+  >
+    <slot />
+  </main>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenu.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <ul
+    data-slot="sidebar-menu"
+    data-sidebar="menu"
+    :class="cn('flex w-full min-w-0 flex-col gap-1', props.class)"
+  >
+    <slot />
+  </ul>
+</template>

+ 35 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuAction.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = withDefaults(defineProps<PrimitiveProps & {
+  showOnHover?: boolean
+  class?: HTMLAttributes["class"]
+}>(), {
+  as: "button",
+})
+</script>
+
+<template>
+  <Primitive
+    data-slot="sidebar-menu-action"
+    data-sidebar="menu-action"
+    :class="cn(
+      'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
+      'after:absolute after:-inset-2 md:after:hidden',
+      'peer-data-[size=sm]/menu-button:top-1',
+      'peer-data-[size=default]/menu-button:top-1.5',
+      'peer-data-[size=lg]/menu-button:top-2.5',
+      'group-data-[collapsible=icon]:hidden',
+      showOnHover
+        && 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
+      props.class,
+    )"
+    :as="as"
+    :as-child="asChild"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 26 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuBadge.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-menu-badge"
+    data-sidebar="menu-badge"
+    :class="cn(
+      'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
+      'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
+      'peer-data-[size=sm]/menu-button:top-1',
+      'peer-data-[size=default]/menu-button:top-1.5',
+      'peer-data-[size=lg]/menu-button:top-2.5',
+      'group-data-[collapsible=icon]:hidden',
+      props.class,
+    )"
+  >
+    <slot />
+  </div>
+</template>

+ 48 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuButton.vue

@@ -0,0 +1,48 @@
+<script setup lang="ts">
+import type { Component } from "vue"
+import type { SidebarMenuButtonProps } from "./SidebarMenuButtonChild.vue"
+import { reactiveOmit } from "@vueuse/core"
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/Packages/Shadcn/Components/ui/tooltip'
+import SidebarMenuButtonChild from "./SidebarMenuButtonChild.vue"
+import { useSidebar } from "./utils"
+
+defineOptions({
+  inheritAttrs: false,
+})
+
+const props = withDefaults(defineProps<SidebarMenuButtonProps & {
+  tooltip?: string | Component
+}>(), {
+  as: "button",
+  variant: "default",
+  size: "default",
+})
+
+const { isMobile, state } = useSidebar()
+
+const delegatedProps = reactiveOmit(props, "tooltip")
+</script>
+
+<template>
+  <SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
+    <slot />
+  </SidebarMenuButtonChild>
+
+  <Tooltip v-else>
+    <TooltipTrigger as-child>
+      <SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
+        <slot />
+      </SidebarMenuButtonChild>
+    </TooltipTrigger>
+    <TooltipContent
+      side="right"
+      align="center"
+      :hidden="state !== 'collapsed' || isMobile"
+    >
+      <template v-if="typeof tooltip === 'string'">
+        {{ tooltip }}
+      </template>
+      <component :is="tooltip" v-else />
+    </TooltipContent>
+  </Tooltip>
+</template>

+ 36 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuButtonChild.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import type { SidebarMenuButtonVariants } from "."
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { sidebarMenuButtonVariants } from "."
+
+export interface SidebarMenuButtonProps extends PrimitiveProps {
+  variant?: SidebarMenuButtonVariants["variant"]
+  size?: SidebarMenuButtonVariants["size"]
+  isActive?: boolean
+  class?: HTMLAttributes["class"]
+}
+
+const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
+  as: "button",
+  variant: "default",
+  size: "default",
+})
+</script>
+
+<template>
+  <Primitive
+    data-slot="sidebar-menu-button"
+    data-sidebar="menu-button"
+    :data-size="size"
+    :data-active="isActive"
+    :class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
+    :as="as"
+    :as-child="asChild"
+    v-bind="$attrs"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuItem.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <li
+    data-slot="sidebar-menu-item"
+    data-sidebar="menu-item"
+    :class="cn('group/menu-item relative', props.class)"
+  >
+    <slot />
+  </li>
+</template>

+ 35 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSkeleton.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { computed } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Skeleton } from '@/Packages/Shadcn/Components/ui/skeleton'
+
+const props = defineProps<{
+  showIcon?: boolean
+  class?: HTMLAttributes["class"]
+}>()
+
+const width = computed(() => {
+  return `${Math.floor(Math.random() * 40) + 50}%`
+})
+</script>
+
+<template>
+  <div
+    data-slot="sidebar-menu-skeleton"
+    data-sidebar="menu-skeleton"
+    :class="cn('flex h-8 items-center gap-2 rounded-md px-2', props.class)"
+  >
+    <Skeleton
+      v-if="showIcon"
+      class="size-4 rounded-md"
+      data-sidebar="menu-skeleton-icon"
+    />
+
+    <Skeleton
+      class="h-4 max-w-(--skeleton-width) flex-1"
+      data-sidebar="menu-skeleton-text"
+      :style="{ '--skeleton-width': width }"
+    />
+  </div>
+</template>

+ 22 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSub.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <ul
+    data-slot="sidebar-menu-sub"
+    data-sidebar="menu-badge"
+    :class="cn(
+      'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
+      'group-data-[collapsible=icon]:hidden',
+      props.class,
+    )"
+  >
+    <slot />
+  </ul>
+</template>

+ 36 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSubButton.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import type { PrimitiveProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { Primitive } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = withDefaults(defineProps<PrimitiveProps & {
+  size?: "sm" | "md"
+  isActive?: boolean
+  class?: HTMLAttributes["class"]
+}>(), {
+  as: "a",
+  size: "md",
+})
+</script>
+
+<template>
+  <Primitive
+    data-slot="sidebar-menu-sub-button"
+    data-sidebar="menu-sub-button"
+    :as="as"
+    :as-child="asChild"
+    :data-size="size"
+    :data-active="isActive"
+    :class="cn(
+      'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
+      'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
+      size === 'sm' && 'text-xs',
+      size === 'md' && 'text-sm',
+      'group-data-[collapsible=icon]:hidden',
+      props.class,
+    )"
+  >
+    <slot />
+  </Primitive>
+</template>

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarMenuSubItem.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <li
+    data-slot="sidebar-menu-sub-item"
+    data-sidebar="menu-sub-item"
+    :class="cn('group/menu-sub-item relative', props.class)"
+  >
+    <slot />
+  </li>
+</template>

+ 82 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarProvider.vue

@@ -0,0 +1,82 @@
+<script setup lang="ts">
+import type { HTMLAttributes, Ref } from "vue"
+import { defaultDocument, useEventListener, useMediaQuery, useVModel } from "@vueuse/core"
+import { TooltipProvider } from "reka-ui"
+import { computed, ref } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from "./utils"
+
+const props = withDefaults(defineProps<{
+  defaultOpen?: boolean
+  open?: boolean
+  class?: HTMLAttributes["class"]
+}>(), {
+  defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
+  open: undefined,
+})
+
+const emits = defineEmits<{
+  "update:open": [open: boolean]
+}>()
+
+const isMobile = useMediaQuery("(max-width: 768px)")
+const openMobile = ref(false)
+
+const open = useVModel(props, "open", emits, {
+  defaultValue: props.defaultOpen ?? false,
+  passive: (props.open === undefined) as false,
+}) as Ref<boolean>
+
+function setOpen(value: boolean) {
+  open.value = value // emits('update:open', value)
+
+  // This sets the cookie to keep the sidebar state.
+  document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+}
+
+function setOpenMobile(value: boolean) {
+  openMobile.value = value
+}
+
+// Helper to toggle the sidebar.
+function toggleSidebar() {
+  return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
+}
+
+useEventListener("keydown", (event: KeyboardEvent) => {
+  if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
+    event.preventDefault()
+    toggleSidebar()
+  }
+})
+
+// We add a state so that we can do data-state="expanded" or "collapsed".
+// This makes it easier to style the sidebar with Tailwind classes.
+const state = computed(() => open.value ? "expanded" : "collapsed")
+
+provideSidebarContext({
+  state,
+  open,
+  setOpen,
+  isMobile,
+  openMobile,
+  setOpenMobile,
+  toggleSidebar,
+})
+</script>
+
+<template>
+  <TooltipProvider :delay-duration="0">
+    <div
+      data-slot="sidebar-wrapper"
+      :style="{
+        '--sidebar-width': SIDEBAR_WIDTH,
+        '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
+      }"
+      :class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
+      v-bind="$attrs"
+    >
+      <slot />
+    </div>
+  </TooltipProvider>
+</template>

+ 33 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarRail.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { useSidebar } from "./utils"
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+
+const { toggleSidebar } = useSidebar()
+</script>
+
+<template>
+  <button
+    data-sidebar="rail"
+    data-slot="sidebar-rail"
+    aria-label="Toggle Sidebar"
+    :tabindex="-1"
+    title="Toggle Sidebar"
+    :class="cn(
+      'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
+      'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
+      '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
+      'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
+      '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
+      '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
+      props.class,
+    )"
+    @click="toggleSidebar"
+  >
+    <slot />
+  </button>
+</template>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarSeparator.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Separator } from '@/Packages/Shadcn/Components/ui/separator'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+</script>
+
+<template>
+  <Separator
+    data-slot="sidebar-separator"
+    data-sidebar="separator"
+    :class="cn('bg-sidebar-border mx-2 w-auto', props.class)"
+  >
+    <slot />
+  </Separator>
+</template>

+ 27 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/SidebarTrigger.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { PanelLeft } from "lucide-vue-next"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+import { Button } from '@/Packages/Shadcn/Components/ui/button'
+import { useSidebar } from "./utils"
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+}>()
+
+const { toggleSidebar } = useSidebar()
+</script>
+
+<template>
+  <Button
+    data-sidebar="trigger"
+    data-slot="sidebar-trigger"
+    variant="ghost"
+    size="icon"
+    :class="cn('h-7 w-7', props.class)"
+    @click="toggleSidebar"
+  >
+    <PanelLeft />
+    <span class="sr-only">Toggle Sidebar</span>
+  </Button>
+</template>

+ 60 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/index.ts

@@ -0,0 +1,60 @@
+import type { VariantProps } from "class-variance-authority"
+import type { HTMLAttributes } from "vue"
+import { cva } from "class-variance-authority"
+
+export interface SidebarProps {
+  side?: "left" | "right"
+  variant?: "sidebar" | "floating" | "inset"
+  collapsible?: "offcanvas" | "icon" | "none"
+  class?: HTMLAttributes["class"]
+}
+
+export { default as Sidebar } from "./Sidebar.vue"
+export { default as SidebarContent } from "./SidebarContent.vue"
+export { default as SidebarFooter } from "./SidebarFooter.vue"
+export { default as SidebarGroup } from "./SidebarGroup.vue"
+export { default as SidebarGroupAction } from "./SidebarGroupAction.vue"
+export { default as SidebarGroupContent } from "./SidebarGroupContent.vue"
+export { default as SidebarGroupLabel } from "./SidebarGroupLabel.vue"
+export { default as SidebarHeader } from "./SidebarHeader.vue"
+export { default as SidebarInput } from "./SidebarInput.vue"
+export { default as SidebarInset } from "./SidebarInset.vue"
+export { default as SidebarMenu } from "./SidebarMenu.vue"
+export { default as SidebarMenuAction } from "./SidebarMenuAction.vue"
+export { default as SidebarMenuBadge } from "./SidebarMenuBadge.vue"
+export { default as SidebarMenuButton } from "./SidebarMenuButton.vue"
+export { default as SidebarMenuItem } from "./SidebarMenuItem.vue"
+export { default as SidebarMenuSkeleton } from "./SidebarMenuSkeleton.vue"
+export { default as SidebarMenuSub } from "./SidebarMenuSub.vue"
+export { default as SidebarMenuSubButton } from "./SidebarMenuSubButton.vue"
+export { default as SidebarMenuSubItem } from "./SidebarMenuSubItem.vue"
+export { default as SidebarProvider } from "./SidebarProvider.vue"
+export { default as SidebarRail } from "./SidebarRail.vue"
+export { default as SidebarSeparator } from "./SidebarSeparator.vue"
+export { default as SidebarTrigger } from "./SidebarTrigger.vue"
+
+export { useSidebar } from "./utils"
+
+export const sidebarMenuButtonVariants = cva(
+  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+        outline:
+          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+      },
+      size: {
+        default: "h-8 text-sm",
+        sm: "h-7 text-xs",
+        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  },
+)
+
+export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>

+ 19 - 0
resources/js/Packages/Shadcn/Components/ui/sidebar/utils.ts

@@ -0,0 +1,19 @@
+import type { ComputedRef, Ref } from "vue"
+import { createContext } from "reka-ui"
+
+export const SIDEBAR_COOKIE_NAME = "sidebar_state"
+export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+export const SIDEBAR_WIDTH = "16rem"
+export const SIDEBAR_WIDTH_MOBILE = "18rem"
+export const SIDEBAR_WIDTH_ICON = "3rem"
+export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+export const [useSidebar, provideSidebarContext] = createContext<{
+  state: ComputedRef<"expanded" | "collapsed">
+  open: Ref<boolean>
+  setOpen: (value: boolean) => void
+  isMobile: Ref<boolean>
+  openMobile: Ref<boolean>
+  setOpenMobile: (value: boolean) => void
+  toggleSidebar: () => void
+}>("Sidebar")

+ 17 - 0
resources/js/Packages/Shadcn/Components/ui/skeleton/Skeleton.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+interface SkeletonProps {
+  class?: HTMLAttributes["class"]
+}
+
+const props = defineProps<SkeletonProps>()
+</script>
+
+<template>
+  <div
+    data-slot="skeleton"
+    :class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
+  />
+</template>

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/skeleton/index.ts

@@ -0,0 +1 @@
+export { default as Skeleton } from "./Skeleton.vue"

+ 28 - 0
resources/js/Packages/Shadcn/Components/ui/textarea/Textarea.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from "vue"
+import { useVModel } from "@vueuse/core"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes["class"]
+  defaultValue?: string | number
+  modelValue?: string | number
+}>()
+
+const emits = defineEmits<{
+  (e: "update:modelValue", payload: string | number): void
+}>()
+
+const modelValue = useVModel(props, "modelValue", emits, {
+  passive: true,
+  defaultValue: props.defaultValue,
+})
+</script>
+
+<template>
+  <textarea
+    v-model="modelValue"
+    data-slot="textarea"
+    :class="cn('border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', props.class)"
+  />
+</template>

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/textarea/index.js

@@ -0,0 +1 @@
+export { default as Textarea } from "./Textarea.vue";

+ 1 - 0
resources/js/Packages/Shadcn/Components/ui/textarea/index.ts

@@ -0,0 +1 @@
+export { default as Textarea } from "./Textarea.vue"

+ 18 - 0
resources/js/Packages/Shadcn/Components/ui/tooltip/Tooltip.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type { TooltipRootEmits, TooltipRootProps } from "reka-ui"
+import { TooltipRoot, useForwardPropsEmits } from "reka-ui"
+
+const props = defineProps<TooltipRootProps>()
+const emits = defineEmits<TooltipRootEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <TooltipRoot
+    data-slot="tooltip"
+    v-bind="forwarded"
+  >
+    <slot />
+  </TooltipRoot>
+</template>

+ 34 - 0
resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipContent.vue

@@ -0,0 +1,34 @@
+<script setup lang="ts">
+import type { TooltipContentEmits, TooltipContentProps } from "reka-ui"
+import type { HTMLAttributes } from "vue"
+import { reactiveOmit } from "@vueuse/core"
+import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui"
+import { cn } from '@/Packages/Shadcn/Lib/utils'
+
+defineOptions({
+  inheritAttrs: false,
+})
+
+const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), {
+  sideOffset: 4,
+})
+
+const emits = defineEmits<TooltipContentEmits>()
+
+const delegatedProps = reactiveOmit(props, "class")
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <TooltipPortal>
+    <TooltipContent
+      data-slot="tooltip-content"
+      v-bind="{ ...forwarded, ...$attrs }"
+      :class="cn('bg-primary text-primary-foreground 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 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)"
+    >
+      <slot />
+
+      <TooltipArrow class="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+    </TooltipContent>
+  </TooltipPortal>
+</template>

+ 14 - 0
resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipProvider.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import type { TooltipProviderProps } from "reka-ui"
+import { TooltipProvider } from "reka-ui"
+
+const props = withDefaults(defineProps<TooltipProviderProps>(), {
+  delayDuration: 0,
+})
+</script>
+
+<template>
+  <TooltipProvider v-bind="props">
+    <slot />
+  </TooltipProvider>
+</template>

+ 15 - 0
resources/js/Packages/Shadcn/Components/ui/tooltip/TooltipTrigger.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import type { TooltipTriggerProps } from "reka-ui"
+import { TooltipTrigger } from "reka-ui"
+
+const props = defineProps<TooltipTriggerProps>()
+</script>
+
+<template>
+  <TooltipTrigger
+    data-slot="tooltip-trigger"
+    v-bind="props"
+  >
+    <slot />
+  </TooltipTrigger>
+</template>

+ 4 - 0
resources/js/Packages/Shadcn/Components/ui/tooltip/index.ts

@@ -0,0 +1,4 @@
+export { default as Tooltip } from "./Tooltip.vue"
+export { default as TooltipContent } from "./TooltipContent.vue"
+export { default as TooltipProvider } from "./TooltipProvider.vue"
+export { default as TooltipTrigger } from "./TooltipTrigger.vue"

+ 6 - 0
resources/js/Packages/Shadcn/Lib/utils.ts

@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+    return twMerge(clsx(inputs))
+}

+ 93 - 4
resources/js/Pages/Chat.vue

@@ -1,10 +1,99 @@
-<script setup>
-import AppLayout from "@/Layouts/AppLayout.vue";
+<script setup lang="ts">
+import AppLayout from "@/Layouts/AppLayout.vue"
+import {ArrowUpIcon} from "lucide-vue-next"
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger
+} from "@/Packages/Shadcn/Components/ui/dropdown-menu"
+import {
+    InputGroup,
+    InputGroupAddon,
+    InputGroupButton,
+    InputGroupText,
+    InputGroupTextarea
+} from "@/Packages/Shadcn/Components/ui/input-group"
+import {Separator} from "@/Packages/Shadcn/Components/ui/separator"
+import {nextTick, reactive, ref} from "vue";
+import {useForm} from "@inertiajs/vue3";
+
+const messages = reactive<Array<string>>([])
+const form = useForm({
+    message: null
+})
+
+const submit = function () {
+    messages.push({from: 'user', text: form.message})
+    messages.push({from: 'ai', text: form.message})
+    form.reset()
+}
+
+const handleEnter = function (e) {
+    if (e.metaKey || e.ctrlKey) {
+        const {selectionStart, selectionEnd, value} = e.target
+        form.message = value.slice(0, selectionStart) + '\n' + value.slice(selectionEnd)
+        nextTick(() => {
+            const pos = selectionStart + 1
+            e.target.setSelectionRange(pos, pos)
+        })
+    } else submit()
+}
+
 </script>
 
 <template>
-    <AppLayout>
-        sdfsdfffff
+    <AppLayout page-class="!py-0">
+        <div class="max-w-4xl mx-auto flex flex-col h-full">
+            <div class="overflow-y-auto duration-500" :class="{'grow mt-2': messages.length !== 0}">
+                <div v-for="message in messages">
+                    <div class="flex" v-if="message.from === 'user'">
+                        <div class="ml-auto border bg-card text-card-foreground p-2 rounded-lg max-w-[75%]">
+                            {{ message.text }}
+                        </div>
+                    </div>
+                    <div v-else>{{ message.text }}</div>
+                </div>
+                <!--                <div class=""></div>-->
+            </div>
+
+            <div class="my-auto pb-2" :class="{'sticky bottom-0': messages.length !== 0}">
+                <InputGroup class="!bg-card">
+                    <InputGroupTextarea placeholder="Ask, Search or Chat..." v-model="form.message"
+                                        @keydown.enter.prevent="handleEnter"/>
+                    <InputGroupAddon align="block-end">
+                        <!--                <InputGroupButton-->
+                        <!--                    variant="outline"-->
+                        <!--                    class="rounded-full"-->
+                        <!--                    size="icon-xs"-->
+                        <!--                >-->
+                        <!--                    <PlusIcon class="size-4"/>-->
+                        <!--                </InputGroupButton>-->
+                        <DropdownMenu>
+                            <DropdownMenuTrigger as-child>
+                                <InputGroupButton variant="ghost">
+                                    Модель 1
+                                </InputGroupButton>
+                            </DropdownMenuTrigger>
+                            <DropdownMenuContent side="top" align="start">
+                                <DropdownMenuItem>Модель 1</DropdownMenuItem>
+                                <DropdownMenuItem>Модель 2</DropdownMenuItem>
+                                <DropdownMenuItem>Модель 3</DropdownMenuItem>
+                            </DropdownMenuContent>
+                        </DropdownMenu>
+                        <InputGroupText class="ml-auto">
+                            123 токенов
+                        </InputGroupText>
+                        <Separator orientation="vertical" class="!h-4"/>
+                        <InputGroupButton class="cursor-pointer" variant="default" size="icon-xs"
+                                          @click="() => submit()">
+                            <ArrowUpIcon class="size-4"/>
+                            <span class="sr-only">Отправить</span>
+                        </InputGroupButton>
+                    </InputGroupAddon>
+                </InputGroup>
+            </div>
+        </div>
     </AppLayout>
 </template>
 

+ 1 - 2
resources/js/app.js

@@ -2,7 +2,7 @@ import './bootstrap';
 import { createApp, h } from 'vue'
 import { createInertiaApp } from '@inertiajs/vue3'
 import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
-import { initializeTheme } from './Composables/useAppearance';
+import { initializeTheme } from './Composables/useAppearance.js';
 import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist';
 
 const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
@@ -21,5 +21,4 @@ createInertiaApp({
     },
 })
 
-// This will set light / dark mode on page load...
 initializeTheme();

+ 6 - 0
vite.config.js

@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
 import laravel from 'laravel-vite-plugin';
 import tailwindcss from '@tailwindcss/vite';
 import vue from '@vitejs/plugin-vue';
+import path from 'node:path'
 
 export default defineConfig({
     server: {
@@ -25,4 +26,9 @@ export default defineConfig({
         }),
         tailwindcss(),
     ],
+    resolve: {
+        alias: {
+            '@': path.resolve(__dirname, 'resources/js'),
+        },
+    }
 });