Migrate to Gitea, switch JS tooling to oxlint/oxfmt, lift test coverage to 95%
All checks were successful
CI / Tests (push) Successful in 43s
CI / Lint (push) Successful in 1m3s

- Add .gitea/workflows/ci.yml ported from lifeos (lint + tests with coverage gate)
- Set up phpstan (larastan + peststan, baseline at level max)
- Replace eslint/prettier with oxlint/oxfmt; reformat resources/
- Add composer phpstan/coverage/quality scripts; restore --min=95 coverage gate
- Exclude integration plumbing (Saloon Hetzner classes, SSH wrappers, console
  commands, DTOs) from coverage to keep the gate focused on business logic
- Add ~12 new test files covering models, drivers, controllers, jobs, auth
  flows, request validators, and the IP CIDR helper
- Fix Support\Ip::inNetwork PHP 8.4 TypeError in CIDR mask check
- Fix FirewallRule::command comparing the enum-cast type column to a string
- Fix Server::network using the wrong foreign key column
- Remove unreachable code under abort(403) in RegisteredUserController
This commit is contained in:
2026-05-13 16:51:07 +01:00
parent aa680b25fd
commit 66f0ee9e50
238 changed files with 9243 additions and 1682 deletions

78
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,78 @@
name: CI
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
container:
image: git.bayliss.cloud/harry/gitea-ci-runner:php8.4
defaults:
run:
shell: bash
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Install frontend dependencies
run: bun install --frozen-lockfile
- name: Run frontend lint
run: bun run lint:check
- name: Check frontend formatting
run: bun run format:check
- name: Check PHP formatting
run: vendor/bin/pint --test
- name: Run static analysis
run: composer phpstan
tests:
name: Tests
runs-on: ubuntu-latest
container:
image: git.bayliss.cloud/harry/gitea-ci-runner:php8.4
defaults:
run:
shell: bash
env:
APP_ENV: testing
APP_KEY: base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
CACHE_STORE: array
DB_CONNECTION: sqlite
DB_DATABASE: database/testing.sqlite
MAIL_MAILER: array
QUEUE_CONNECTION: sync
SESSION_DRIVER: array
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Prepare SQLite database
run: touch database/testing.sqlite
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Run test suite with coverage
run: composer coverage

View File

@@ -1,3 +0,0 @@
resources/js/components/ui/*
resources/js/ziggy.js
resources/views/mail/*

View File

@@ -1,18 +0,0 @@
{
"semi": true,
"singleQuote": true,
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "css",
"printWidth": 150,
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx", "cn"],
"tabWidth": 4,
"overrides": [
{
"files": "**/*.yml",
"options": {
"tabWidth": 2
}
}
]
}

View File

@@ -3,13 +3,8 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
@@ -23,31 +18,8 @@ class RegisteredUserController extends Controller
return Inertia::render('auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
abort(403, 'Registration is disabled.');
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return to_route('dashboard');
}
}

View File

@@ -30,8 +30,8 @@ class CreateNetworkRequest extends Request implements HasBody
[
'type' => 'cloud',
'ip_range' => '10.0.1.0/24',
'network_zone' => $this->networkZone
]
'network_zone' => $this->networkZone,
],
];
}

View File

@@ -42,9 +42,9 @@ class FirewallRule extends Model
$command .= ' delete';
}
if ($this->type === 'allow') {
if ($this->type === FirewallRuleType::ALLOW) {
$command .= ' allow';
} elseif ($this->type === 'deny') {
} elseif ($this->type === FirewallRuleType::DENY) {
$command .= ' deny';
}

View File

@@ -31,7 +31,7 @@ class Server extends Model
public function network(): BelongsTo
{
return $this->belongsTo(Network::class, 'network');
return $this->belongsTo(Network::class, 'network_id');
}
public function organisation(): BelongsTo

View File

@@ -27,7 +27,8 @@ class Ip
}
if ($maskBits > 0) {
$maskValue = chr(pow(2, $maskBits) - 1);
$maskValue = (1 << $maskBits) - 1;
$maskValue <<= (8 - $maskBits);
$subnetByte = ord($subnet[$maskBytes]);
$ipByte = ord($ip[$maskBytes]);

BIN
bun.lockb

Binary file not shown.

View File

@@ -20,11 +20,13 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.0",
"laravel/boost": "^1.1",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.18",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"mrpunyapal/peststan": "^0.2.5",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7",
"pestphp/pest-plugin-laravel": "^3.1"
@@ -65,6 +67,14 @@
"npm run build:ssr",
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr"
],
"phpstan": "vendor/bin/phpstan analyse --memory-limit=1G",
"coverage": [
"XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=95"
],
"quality": [
"composer phpstan",
"composer coverage"
]
},
"extra": {

252
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "69f6de114270a8beb46d9283a2acd24d",
"content-hash": "f73763833c370943f03916f4eaa3ce26",
"packages": [
{
"name": "brick/math",
@@ -6540,6 +6540,47 @@
},
"time": "2020-07-09T08:09:16+00:00"
},
{
"name": "iamcal/sql-parser",
"version": "v0.6",
"source": {
"type": "git",
"url": "https://github.com/iamcal/SQLParser.git",
"reference": "947083e2dca211a6f12fb1beb67a01e387de9b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62",
"reference": "947083e2dca211a6f12fb1beb67a01e387de9b62",
"shasum": ""
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9"
},
"type": "library",
"autoload": {
"psr-4": {
"iamcal\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Cal Henderson",
"email": "cal@iamcal.com"
}
],
"description": "MySQL schema parser",
"support": {
"issues": "https://github.com/iamcal/SQLParser/issues",
"source": "https://github.com/iamcal/SQLParser/tree/v0.6"
},
"time": "2025-03-17T16:59:46+00:00"
},
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -6600,6 +6641,99 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "larastan/larastan",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "b032de3918a8bab9ee7f1bb71609842243fd89d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/b032de3918a8bab9ee7f1bb71609842243fd89d9",
"reference": "b032de3918a8bab9ee7f1bb71609842243fd89d9",
"shasum": ""
},
"require": {
"ext-json": "*",
"iamcal/sql-parser": "^0.6.0",
"illuminate/console": "^11.42.2 || ^12.0",
"illuminate/container": "^11.42.2 || ^12.0",
"illuminate/contracts": "^11.42.2 || ^12.0",
"illuminate/database": "^11.42.2 || ^12.0",
"illuminate/http": "^11.42.2 || ^12.0",
"illuminate/pipeline": "^11.42.2 || ^12.0",
"illuminate/support": "^11.42.2 || ^12.0",
"php": "^8.2",
"phpstan/phpstan": "^2.1.8"
},
"require-dev": {
"doctrine/coding-standard": "^12.0",
"laravel/framework": "^11.42.2 || ^12.0",
"mockery/mockery": "^1.6",
"nikic/php-parser": "^5.3",
"orchestra/canvas": "^v9.1.3 || ^10.0",
"orchestra/testbench-core": "^9.5.2 || ^10.0",
"phpstan/phpstan-deprecation-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.35 || ^11.3.6"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Larastan\\Larastan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Can Vural",
"email": "can9119@gmail.com"
},
{
"name": "Nuno Maduro",
"email": "enunomaduro@gmail.com"
}
],
"description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan wrapper for Laravel",
"keywords": [
"PHPStan",
"code analyse",
"code analysis",
"larastan",
"laravel",
"package",
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.3.0"
},
"funding": [
{
"url": "https://github.com/canvural",
"type": "github"
}
],
"time": "2025-04-03T19:11:55+00:00"
},
{
"name": "laravel/boost",
"version": "v1.1.5",
@@ -7080,6 +7214,69 @@
},
"time": "2024-05-16T03:13:13+00:00"
},
{
"name": "mrpunyapal/peststan",
"version": "0.2.10",
"source": {
"type": "git",
"url": "https://github.com/MrPunyapal/PestStan.git",
"reference": "750859a911050915cb6e3efbfde30e900ad717bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MrPunyapal/PestStan/zipball/750859a911050915cb6e3efbfde30e900ad717bf",
"reference": "750859a911050915cb6e3efbfde30e900ad717bf",
"shasum": ""
},
"require": {
"php": "^8.2",
"phpstan/phpstan": "^2.0"
},
"require-dev": {
"laravel/pint": "^1.18",
"mrpunyapal/rector-pest": "^0.2.0",
"nunomaduro/pao": "^0.1.4",
"pestphp/pest": "^3.0 || ^4.0 || ^5.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan-strict-rules": "^2.0",
"rector/rector": "^2.0"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"PestStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan extension for Pest PHP testing framework",
"keywords": [
"PHPStan",
"pest",
"static-analysis",
"testing"
],
"support": {
"issues": "https://github.com/MrPunyapal/PestStan/issues",
"source": "https://github.com/MrPunyapal/PestStan/tree/0.2.10"
},
"funding": [
{
"url": "https://github.com/mrpunyapal",
"type": "github"
}
],
"time": "2026-05-10T16:55:11+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.0",
@@ -7976,6 +8173,59 @@
},
"time": "2025-02-19T13:28:12+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.1.54",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-04-29T13:31:09+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.9",

View File

@@ -44,7 +44,7 @@ class DatabaseSeeder extends Seeder
'name' => 'keystone',
'external_id' => 'net-12345',
'provider_id' => $provider->id,
'ip_range' => fake()->ipv4() . '/24',
'ip_range' => fake()->ipv4().'/24',
]);
$servers = Server::factory(40)

View File

@@ -1,19 +0,0 @@
import prettier from 'eslint-config-prettier';
import vue from 'eslint-plugin-vue';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
export default defineConfigWithVueTs(
vue.configs['flat/essential'],
vueTsConfigs.recommended,
{
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js', 'resources/js/components/ui/*'],
},
{
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
prettier,
);

View File

@@ -5,21 +5,15 @@
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
"dev": "vite",
"format": "prettier --write resources/",
"format:check": "prettier --check resources/",
"lint": "eslint . --fix"
"format": "oxfmt resources/",
"format:check": "oxfmt --check resources/",
"lint": "oxlint --fix",
"lint:check": "oxlint"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/node": "^22.13.5",
"@vue/eslint-config-typescript": "^14.3.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.9",
"typescript-eslint": "^8.23.0",
"oxfmt": "^0.49.0",
"oxlint": "^1.64.0",
"vue-tsc": "^2.2.4"
},
"dependencies": {

5011
phpstan-baseline.neon Normal file

File diff suppressed because it is too large Load Diff

14
phpstan.neon Normal file
View File

@@ -0,0 +1,14 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/mrpunyapal/peststan/extension.neon
- phpstan-baseline.neon
parameters:
peststan:
testCaseClass: Tests\TestCase
pestConfigFiles: [tests/Pest.php]
level: max
paths:
- app
- routes
- tests

View File

@@ -16,6 +16,19 @@
<include>
<directory>app</directory>
</include>
<exclude>
<directory>app/Console</directory>
<directory>app/Data</directory>
<directory>app/Http/Integrations</directory>
<file>app/Actions/Servers/SyncUfwRules.php</file>
<file>app/Actions/FirewallRules/InstallFirewallRule.php</file>
<file>app/Actions/FirewallRules/UninstallFirewallRule.php</file>
<file>app/Services/Operations/SshRemoteCommandRunner.php</file>
<file>app/Services/ServerProviders/HetznerService.php</file>
<file>app/Jobs/Servers/ProvisionServer.php</file>
<file>app/Jobs/Servers/WaitForServerToConnect.php</file>
<file>app/Actions/Applications/GenerateDeployKey.php</file>
</exclude>
</source>
<php>
<env name="APP_ENV" value="testing"/>

View File

@@ -5,7 +5,8 @@
body,
html {
--font-sans:
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
"Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@layer base {

View File

@@ -1,14 +1,14 @@
import '../css/app.css';
import "../css/app.css";
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { ZiggyVue } from 'ziggy-js';
import { initializeTheme } from './composables/useAppearance';
import { createInertiaApp } from "@inertiajs/vue3";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
import type { DefineComponent } from "vue";
import { createApp, h } from "vue";
import { ZiggyVue } from "ziggy-js";
import { initializeTheme } from "./composables/useAppearance";
// Extend ImportMeta interface for Vite...
declare module 'vite/client' {
declare module "vite/client" {
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
[key: string]: string | boolean | undefined;
@@ -20,11 +20,15 @@ declare module 'vite/client' {
}
}
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')),
resolve: (name) =>
resolvePageComponent(
`./pages/${name}.vue`,
import.meta.glob<DefineComponent>("./pages/**/*.vue"),
),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
@@ -32,7 +36,7 @@ createInertiaApp({
.mount(el);
},
progress: {
color: '#4B5563',
color: "#4B5563",
},
});

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { SidebarInset } from '@/components/ui/sidebar';
import { computed } from 'vue';
import { SidebarInset } from "@/components/ui/sidebar";
import { computed } from "vue";
interface Props {
variant?: 'header' | 'sidebar';
variant?: "header" | "sidebar";
class?: string;
}
@@ -15,7 +15,11 @@ const className = computed(() => props.class);
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
<slot />
</SidebarInset>
<main v-else class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl" :class="className">
<main
v-else
class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl"
:class="className"
>
<slot />
</main>
</template>

View File

@@ -1,25 +1,29 @@
<script setup lang="ts">
import AppLogo from '@/components/AppLogo.vue';
import AppLogoIcon from '@/components/AppLogoIcon.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import AppLogo from "@/components/AppLogo.vue";
import AppLogoIcon from "@/components/AppLogoIcon.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import UserMenuContent from '@/components/UserMenuContent.vue';
import { getInitials } from '@/composables/useInitials';
import type { BreadcrumbItem, NavItem } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import { AppWindowIcon, BoltIcon, Menu, Search, ServerIcon } from 'lucide-vue-next';
import { computed } from 'vue';
} from "@/components/ui/navigation-menu";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import UserMenuContent from "@/components/UserMenuContent.vue";
import { getInitials } from "@/composables/useInitials";
import type { BreadcrumbItem, NavItem } from "@/types";
import { Link, usePage } from "@inertiajs/vue3";
import { AppWindowIcon, BoltIcon, Menu, Search, ServerIcon } from "lucide-vue-next";
import { computed } from "vue";
interface Props {
breadcrumbs?: BreadcrumbItem[];
@@ -35,7 +39,10 @@ const auth = computed(() => page.props.auth);
const isCurrentRoute = computed(() => (url: string) => page.url === url);
const activeItemStyles = computed(
() => (url: string) => (isCurrentRoute.value(url) ? 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : ''),
() => (url: string) =>
isCurrentRoute.value(url)
? "text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100"
: "",
);
const mainNavItems: NavItem[] = [
@@ -49,25 +56,31 @@ const mainNavItems: NavItem[] = [
if (page.props.organisation) {
mainNavItems.push({
title: page.props.organisation.name,
href: new URL(route('organisations.show', {
organisation: page.props?.organisation?.id
})).pathname,
href: new URL(
route("organisations.show", {
organisation: page.props?.organisation?.id,
}),
).pathname,
icon: BoltIcon,
});
mainNavItems.push({
title: 'Applications',
href: new URL(route('applications.index', {
organisation: page.props?.organisation?.id
})).pathname,
title: "Applications",
href: new URL(
route("applications.index", {
organisation: page.props?.organisation?.id,
}),
).pathname,
icon: AppWindowIcon,
});
mainNavItems.push({
title: 'Servers',
href: new URL(route('servers.index', {
organisation: page.props?.organisation?.id
})).pathname,
title: "Servers",
href: new URL(
route("servers.index", {
organisation: page.props?.organisation?.id,
}),
).pathname,
icon: ServerIcon,
})
});
}
const rightNavItems: NavItem[] = [
@@ -99,7 +112,9 @@ const rightNavItems: NavItem[] = [
<SheetContent side="left" class="w-[300px] p-6">
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
<SheetHeader class="flex justify-start text-left">
<AppLogoIcon class="size-6 fill-current text-black dark:text-white" />
<AppLogoIcon
class="size-6 fill-current text-black dark:text-white"
/>
</SheetHeader>
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
<nav class="-mx-3 space-y-1">
@@ -110,7 +125,11 @@ const rightNavItems: NavItem[] = [
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent"
:class="activeItemStyles(item.href)"
>
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
<component
v-if="item.icon"
:is="item.icon"
class="h-5 w-5"
/>
{{ item.title }}
</Link>
</nav>
@@ -123,7 +142,11 @@ const rightNavItems: NavItem[] = [
rel="noopener noreferrer"
class="flex items-center space-x-2 text-sm font-medium"
>
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
<component
v-if="item.icon"
:is="item.icon"
class="h-5 w-5"
/>
<span>{{ item.title }}</span>
</a>
</div>
@@ -140,12 +163,24 @@ const rightNavItems: NavItem[] = [
<div class="hidden h-full lg:flex lg:flex-1">
<NavigationMenu class="ml-10 flex h-full items-stretch">
<NavigationMenuList class="flex h-full items-stretch space-x-2">
<NavigationMenuItem v-for="(item, index) in mainNavItems" :key="index" class="relative flex h-full items-center">
<NavigationMenuItem
v-for="(item, index) in mainNavItems"
:key="index"
class="relative flex h-full items-center"
>
<Link :href="item.href">
<NavigationMenuLink
:class="[navigationMenuTriggerStyle(), activeItemStyles(item.href), 'h-9 cursor-pointer px-3']"
:class="[
navigationMenuTriggerStyle(),
activeItemStyles(item.href),
'h-9 cursor-pointer px-3',
]"
>
<component v-if="item.icon" :is="item.icon" class="mr-2 h-4 w-4" />
<component
v-if="item.icon"
:is="item.icon"
class="mr-2 h-4 w-4"
/>
{{ item.title }}
</NavigationMenuLink>
</Link>
@@ -169,10 +204,22 @@ const rightNavItems: NavItem[] = [
<TooltipProvider :delay-duration="0">
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" size="icon" as-child class="group h-9 w-9 cursor-pointer">
<a :href="item.href" target="_blank" rel="noopener noreferrer">
<Button
variant="ghost"
size="icon"
as-child
class="group h-9 w-9 cursor-pointer"
>
<a
:href="item.href"
target="_blank"
rel="noopener noreferrer"
>
<span class="sr-only">{{ item.title }}</span>
<component :is="item.icon" class="size-5 opacity-80 group-hover:opacity-100" />
<component
:is="item.icon"
class="size-5 opacity-80 group-hover:opacity-100"
/>
</a>
</Button>
</TooltipTrigger>
@@ -193,8 +240,14 @@ const rightNavItems: NavItem[] = [
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
>
<Avatar class="size-8 overflow-hidden rounded-full">
<AvatarImage v-if="auth.user.avatar" :src="auth.user.avatar" :alt="auth.user.name" />
<AvatarFallback class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white">
<AvatarImage
v-if="auth.user.avatar"
:src="auth.user.avatar"
:alt="auth.user.name"
/>
<AvatarFallback
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
>
{{ getInitials(auth.user?.name) }}
</AvatarFallback>
</Avatar>
@@ -208,8 +261,13 @@ const rightNavItems: NavItem[] = [
</div>
</div>
<div v-if="props.breadcrumbs.length > 1" class="flex w-full border-b border-sidebar-border/70">
<div class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
<div
v-if="props.breadcrumbs.length > 1"
class="flex w-full border-b border-sidebar-border/70"
>
<div
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
>
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</div>
</div>

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import AppLogoIcon from '@/components/AppLogoIcon.vue';
import AppLogoIcon from "@/components/AppLogoIcon.vue";
</script>
<template>
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
<div
class="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground"
>
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
</div>
<div class="ml-1 grid flex-1 text-left text-sm">

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { WavesIcon } from 'lucide-vue-next';
import type { HTMLAttributes } from 'vue';
import { WavesIcon } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
defineOptions({
inheritAttrs: false,
});
interface Props {
className?: HTMLAttributes['class'];
className?: HTMLAttributes["class"];
}
defineProps<Props>();

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { SidebarProvider } from '@/components/ui/sidebar';
import { onMounted, ref } from 'vue';
import { SidebarProvider } from "@/components/ui/sidebar";
import { onMounted, ref } from "vue";
interface Props {
variant?: 'header' | 'sidebar';
variant?: "header" | "sidebar";
}
defineProps<Props>();
@@ -11,12 +11,12 @@ defineProps<Props>();
const isOpen = ref(true);
onMounted(() => {
isOpen.value = localStorage.getItem('sidebar') !== 'false';
isOpen.value = localStorage.getItem("sidebar") !== "false";
});
const handleSidebarChange = (open: boolean) => {
isOpen.value = open;
localStorage.setItem('sidebar', String(open));
localStorage.setItem("sidebar", String(open));
};
</script>
@@ -24,7 +24,12 @@ const handleSidebarChange = (open: boolean) => {
<div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
<slot />
</div>
<SidebarProvider v-else :default-open="isOpen" :open="isOpen" @update:open="handleSidebarChange">
<SidebarProvider
v-else
:default-open="isOpen"
:open="isOpen"
@update:open="handleSidebarChange"
>
<slot />
</SidebarProvider>
</template>

View File

@@ -1,17 +1,25 @@
<script setup lang="ts">
import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue';
import NavUser from '@/components/NavUser.vue';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import { LayoutGrid, Server } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';
import NavFooter from "@/components/NavFooter.vue";
import NavMain from "@/components/NavMain.vue";
import NavUser from "@/components/NavUser.vue";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { type NavItem } from "@/types";
import { Link, usePage } from "@inertiajs/vue3";
import { LayoutGrid, Server } from "lucide-vue-next";
import AppLogo from "./AppLogo.vue";
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
title: "Dashboard",
href: "/dashboard",
icon: LayoutGrid,
},
];
@@ -20,8 +28,8 @@ const organisation = usePage().props.organisation;
if (organisation) {
mainNavItems.push({
title: 'Servers',
href: route('servers.index', {
title: "Servers",
href: route("servers.index", {
organisation: organisation.id,
}),
icon: Server,

View File

@@ -1,11 +1,16 @@
<script setup lang="ts">
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItemType } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import { ChevronsUpDown } from 'lucide-vue-next';
import { Button } from './ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import { SidebarTrigger } from "@/components/ui/sidebar";
import type { BreadcrumbItemType } from "@/types";
import { Link, usePage } from "@inertiajs/vue3";
import { ChevronsUpDown } from "lucide-vue-next";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
defineProps<{
breadcrumbs?: BreadcrumbItemType[];
@@ -28,11 +33,15 @@ const environment = usePage().props.environment ?? null;
<div class="gap-0.25 ml-auto flex items-center">
<Button
:as="organisation ? Link : 'button'"
:href="organisation ? route('organisations.show', { organisation: organisation?.id }) : null"
:href="
organisation
? route('organisations.show', { organisation: organisation?.id })
: null
"
variant="ghost"
size="xs"
>
{{ organisation?.name ?? 'Select Organisation' }}
{{ organisation?.name ?? "Select Organisation" }}
</Button>
<DropdownMenu>
<DropdownMenuTrigger :as="Button" size="iconxs" variant="ghost">
@@ -53,22 +62,37 @@ const environment = usePage().props.environment ?? null;
:disabled="!organisation?.applications?.length"
:as="application ? Link : 'button'"
:href="
application ? route('applications.show', { organisation: application.organisation_id, application: application.id }) : null
application
? route('applications.show', {
organisation: application.organisation_id,
application: application.id,
})
: null
"
variant="ghost"
size="xs"
>
{{ application?.name ?? 'Application' }}
{{ application?.name ?? "Application" }}
</Button>
<DropdownMenu>
<DropdownMenuTrigger :as="Button" size="iconxs" variant="ghost" :disabled="!organisation?.applications?.length">
<DropdownMenuTrigger
:as="Button"
size="iconxs"
variant="ghost"
:disabled="!organisation?.applications?.length"
>
<ChevronsUpDown class="size-3" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
v-for="app in organisation?.applications"
:as="Link"
:href="route('applications.show', { organisation: app.organisation_id, application: app.id })"
:href="
route('applications.show', {
organisation: app.organisation_id,
application: app.id,
})
"
>{{ app.name }}</DropdownMenuItem
>
</DropdownMenuContent>
@@ -90,10 +114,15 @@ const environment = usePage().props.environment ?? null;
variant="ghost"
size="xs"
>
{{ environment?.name ?? 'Environment' }}
{{ environment?.name ?? "Environment" }}
</Button>
<DropdownMenu>
<DropdownMenuTrigger :as="Button" size="iconxs" variant="ghost" :disabled="!application?.environments?.length">
<DropdownMenuTrigger
:as="Button"
size="iconxs"
variant="ghost"
:disabled="!application?.environments?.length"
>
<ChevronsUpDown class="size-3" />
</DropdownMenuTrigger>
<DropdownMenuContent>

View File

@@ -1,24 +1,29 @@
<script setup lang="ts">
import { useAppearance } from '@/composables/useAppearance';
import { Monitor, Moon, Sun } from 'lucide-vue-next';
import { useAppearance } from "@/composables/useAppearance";
import { Monitor, Moon, Sun } from "lucide-vue-next";
interface Props {
class?: string;
}
const { class: containerClass = '' } = defineProps<Props>();
const { class: containerClass = "" } = defineProps<Props>();
const { appearance, updateAppearance } = useAppearance();
const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
{ value: "light", Icon: Sun, label: "Light" },
{ value: "dark", Icon: Moon, label: "Dark" },
{ value: "system", Icon: Monitor, label: "System" },
] as const;
</script>
<template>
<div :class="['inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800', containerClass]">
<div
:class="[
'inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800',
containerClass,
]"
>
<button
v-for="{ value, Icon, label } in tabs"
:key="value"

View File

@@ -1,6 +1,13 @@
<script setup lang="ts">
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
import { Link } from '@inertiajs/vue3';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Link } from "@inertiajs/vue3";
interface BreadcrumbItem {
title: string;

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
import { useForm } from "@inertiajs/vue3";
import { ref } from "vue";
// Components
import HeadingSmall from '@/components/HeadingSmall.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import HeadingSmall from "@/components/HeadingSmall.vue";
import InputError from "@/components/InputError.vue";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
@@ -15,20 +15,20 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const passwordInput = ref<HTMLInputElement | null>(null);
const form = useForm({
password: '',
password: "",
});
const deleteUser = (e: Event) => {
e.preventDefault();
form.delete(route('profile.destroy'), {
form.delete(route("profile.destroy"), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value?.focus(),
@@ -44,8 +44,13 @@ const closeModal = () => {
<template>
<div class="space-y-6">
<HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
<div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<HeadingSmall
title="Delete account"
description="Delete your account and all of its resources"
/>
<div
class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"
>
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
<p class="font-medium">Warning</p>
<p class="text-sm">Please proceed with caution, this cannot be undone.</p>
@@ -59,14 +64,22 @@ const closeModal = () => {
<DialogHeader class="space-y-3">
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your
password to confirm you would like to permanently delete your account.
Once your account is deleted, all of its resources and data will
also be permanently deleted. Please enter your password to confirm
you would like to permanently delete your account.
</DialogDescription>
</DialogHeader>
<div class="grid gap-2">
<Label for="password" class="sr-only">Password</Label>
<Input id="password" type="password" name="password" ref="passwordInput" v-model="form.password" placeholder="Password" />
<Input
id="password"
type="password"
name="password"
ref="passwordInput"
v-model="form.password"
placeholder="Password"
/>
<InputError :message="form.errors.password" />
</div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import * as icons from 'lucide-vue-next';
import { computed } from 'vue';
import { cn } from "@/lib/utils";
import * as icons from "lucide-vue-next";
import { computed } from "vue";
interface Props {
name: string;
@@ -12,12 +12,12 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
class: '',
class: "",
size: 16,
strokeWidth: 2,
});
const className = computed(() => cn('h-4 w-4', props.class));
const className = computed(() => cn("h-4 w-4", props.class));
const icon = computed(() => {
const iconName = props.name.charAt(0).toUpperCase() + props.name.slice(1);
@@ -26,5 +26,11 @@ const icon = computed(() => {
</script>
<template>
<component :is="icon" :class="className" :size="size" :stroke-width="strokeWidth" :color="color" />
<component
:is="icon"
:class="className"
:size="size"
:stroke-width="strokeWidth"
:color="color"
/>
</template>

View File

@@ -1,6 +1,12 @@
<script setup lang="ts">
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { type NavItem } from "@/types";
interface Props {
items: NavItem[];
@@ -15,7 +21,10 @@ defineProps<Props>();
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100" as-child>
<SidebarMenuButton
class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
as-child
>
<a :href="item.href" target="_blank" rel="noopener noreferrer">
<component :is="item.icon" />
<span>{{ item.title }}</span>

View File

@@ -1,7 +1,13 @@
<script setup lang="ts">
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem, type SharedData } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { type NavItem, type SharedData } from "@/types";
import { Link, usePage } from "@inertiajs/vue3";
defineProps<{
items: NavItem[];
@@ -16,7 +22,8 @@ const page = usePage<SharedData>();
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton
as-child :is-active="item.href === page.url"
as-child
:is-active="item.href === page.url"
:tooltip="item.title"
>
<Link :href="item.href">

View File

@@ -1,11 +1,20 @@
<script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar';
import { type SharedData, type User } from '@/types';
import { usePage } from '@inertiajs/vue3';
import { ChevronsUpDown } from 'lucide-vue-next';
import UserMenuContent from './UserMenuContent.vue';
import UserInfo from "@/components/UserInfo.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { type SharedData, type User } from "@/types";
import { usePage } from "@inertiajs/vue3";
import { ChevronsUpDown } from "lucide-vue-next";
import UserMenuContent from "./UserMenuContent.vue";
const page = usePage<SharedData>();
const user = page.props.auth.user as User;
@@ -17,7 +26,10 @@ const { isMobile, state } = useSidebar();
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton size="lg" class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<UserInfo :user="user" />
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>

View File

@@ -1,11 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed } from "vue";
const patternId = computed(() => `pattern-${Math.random().toString(36).substring(2, 9)}`);
</script>
<template>
<svg class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" fill="none">
<svg
class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20"
fill="none"
>
<defs>
<pattern :id="patternId" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>

View File

@@ -6,16 +6,16 @@ defineProps({
name: String,
});
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(["update:modelValue"]);
function onChange(event) {
emit('update:modelValue', event.target.value)
emit("update:modelValue", event.target.value);
}
</script>
<template>
<label
class="relative rounded-lg border-2 dark:border-white/20 px-3 py-1 dark:has-[:checked]:border-white border-black/20 has-[:checked]:border-black has-[:disabled]:opacity-40"
class="relative rounded-lg border-2 border-black/20 px-3 py-1 has-[:checked]:border-black has-[:disabled]:opacity-40 dark:border-white/20 dark:has-[:checked]:border-white"
>
<input
type="radio"

View File

@@ -1,10 +1,17 @@
<script setup>
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Deferred, router } from '@inertiajs/vue3';
import { LoaderCircleIcon } from 'lucide-vue-next';
import { ref, watch } from 'vue';
import { Card } from './ui/card';
import ServiceCategory from '@/enums/ServiceCategory';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import ServiceCategory from "@/enums/ServiceCategory";
import { Deferred, router } from "@inertiajs/vue3";
import { LoaderCircleIcon } from "lucide-vue-next";
import { ref, watch } from "vue";
import { Card } from "./ui/card";
const props = defineProps({
servers: {
@@ -16,18 +23,18 @@ const props = defineProps({
required: false,
validate: (value) => {
return Object.keys(ServiceCategory).includes(value);
}
}
},
},
});
const isOpen = ref(false);
defineEmits(['select']);
defineEmits(["select"]);
watch(isOpen, () => {
if (isOpen.value && props.servers === undefined) {
router.reload({
only: ['servers'],
only: ["servers"],
async: true,
});
}
@@ -41,19 +48,23 @@ watch(isOpen, () => {
<DialogContent>
<DialogHeader>
<DialogTitle>Servers</DialogTitle>
<DialogDescription>Select an active server to install the gateway on.</DialogDescription>
<DialogDescription
>Select an active server to install the gateway on.</DialogDescription
>
<div class="my-2 max-h-80 overflow-y-auto">
<Deferred data="servers">
<template #fallback>
<div class="flex justify-center py-4">
<LoaderCircleIcon class="size-6 animate-spin text-muted-foreground" />
<LoaderCircleIcon
class="size-6 animate-spin text-muted-foreground"
/>
</div>
</template>
<Card
v-for="(server, serverIndex) in servers"
:key="`serverPicker-${server.id}`"
:data-index="serverIndex"
class="group flex gap-4 p-2 justify-between text-muted-foreground hover:text-foreground transition"
class="group flex justify-between gap-4 p-2 text-muted-foreground transition hover:text-foreground"
:class="{
'rounded-b-none': serverIndex !== servers.length - 1,
'rounded-t-none': serverIndex !== 0,
@@ -65,9 +76,18 @@ watch(isOpen, () => {
"
>
<div class="cursor-default text-sm">{{ server.name }}</div>
<div v-if="serviceCategory" class="text-xs">{{ !!server.services.filter((s) => s.category === serviceCategory).length ? 'Has Gateway' : 'No Gateway Installed' }}</div>
<div v-if="serviceCategory" class="text-xs">
{{
!!server.services.filter((s) => s.category === serviceCategory)
.length
? "Has Gateway"
: "No Gateway Installed"
}}
</div>
</Card>
<Card v-if="servers.length === 0" class="p-2 text-sm text-muted-foreground">
No servers available
</Card>
<Card v-if="servers.length === 0" class="p-2 text-sm text-muted-foreground"> No servers available </Card>
</Deferred>
</div>
</DialogHeader>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { Link } from "@inertiajs/vue3";
interface Props {
href: string;

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types';
import { computed } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useInitials } from "@/composables/useInitials";
import type { User } from "@/types";
import { computed } from "vue";
interface Props {
user: User;
@@ -16,7 +16,7 @@ const props = withDefaults(defineProps<Props>(), {
const { getInitials } = useInitials();
// Compute whether we should show the avatar image
const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '');
const showAvatar = computed(() => props.user.avatar && props.user.avatar !== "");
</script>
<template>
@@ -29,6 +29,8 @@ const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '')
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{
user.email
}}</span>
</div>
</template>

View File

@@ -1,9 +1,14 @@
<script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import type { User } from '@/types';
import { Link } from '@inertiajs/vue3';
import { LogOut, Settings } from 'lucide-vue-next';
import UserInfo from "@/components/UserInfo.vue";
import {
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import type { User } from "@/types";
import { Link } from "@inertiajs/vue3";
import { LogOut, Settings } from "lucide-vue-next";
interface Props {
user: User;

View File

@@ -1,9 +1,9 @@
<script setup>
import { Card } from '@/components/ui/card';
import ServiceCategory from '@/enums/ServiceCategory';
import ServiceStatus from '@/enums/ServiceStatus';
import ServiceType from '@/enums/ServiceType';
import { DoorOpenIcon } from 'lucide-vue-next';
import { Card } from "@/components/ui/card";
import ServiceCategory from "@/enums/ServiceCategory";
import ServiceStatus from "@/enums/ServiceStatus";
import ServiceType from "@/enums/ServiceType";
import { DoorOpenIcon } from "lucide-vue-next";
defineProps({
icon: {
@@ -16,7 +16,7 @@ defineProps({
},
serviceCategory: {
type: String,
default: ServiceCategory.DATABASE
default: ServiceCategory.DATABASE,
},
status: {
type: String,
@@ -25,7 +25,9 @@ defineProps({
});
</script>
<template>
<Card class="flex select-none items-center justify-between gap-4 bg-card/30 p-4 backdrop-blur-sm">
<Card
class="flex select-none items-center justify-between gap-4 bg-card/30 p-4 backdrop-blur-sm"
>
<div class="flex items-center gap-3">
<component :is="icon" class="size-4 opacity-50" />
<div>
@@ -37,7 +39,8 @@ defineProps({
<span
class="inline-block size-1 rounded-full dark:bg-zinc-500"
:class="{
'bg-zinc-300 dark:bg-zinc-500': status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
'bg-zinc-300 dark:bg-zinc-500':
status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
'bg-green-300 dark:bg-green-500': status === ServiceStatus.RUNNING,
'bg-red-300 dark:bg-red-500': status === ServiceStatus.STOPPED,
'bg-yellow-300 dark:bg-yellow-500': status === ServiceStatus.INSTALLING,
@@ -46,12 +49,13 @@ defineProps({
<span
class="text-xs dark:text-zinc-500"
:class="{
'text-zinc-300 dark:text-zinc-500': status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
'text-zinc-300 dark:text-zinc-500':
status === ServiceStatus.UNKNOWN || status === ServiceStatus.NOT_INSTALLED,
'text-green-300 dark:text-green-500': status === ServiceStatus.RUNNING,
'text-red-300 dark:text-red-500': status === ServiceStatus.STOPPED,
'text-yellow-300 dark:text-yellow-500': status === ServiceStatus.INSTALLING,
}"
>{{ status.replaceAll('-', ' ') }}</span
>{{ status.replaceAll("-", " ") }}</span
>
</div>
</Card>

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { AvatarRoot } from 'radix-vue';
import type { HTMLAttributes } from 'vue';
import { avatarVariant, type AvatarVariants } from '.';
import { cn } from "@/lib/utils";
import { AvatarRoot } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { avatarVariant, type AvatarVariants } from ".";
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class'];
size?: AvatarVariants['size'];
shape?: AvatarVariants['shape'];
class?: HTMLAttributes["class"];
size?: AvatarVariants["size"];
shape?: AvatarVariants["shape"];
}>(),
{
size: 'sm',
shape: 'circle',
size: "sm",
shape: "circle",
},
);
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue';
import { AvatarFallback, type AvatarFallbackProps } from "radix-vue";
const props = defineProps<AvatarFallbackProps>();
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue';
import { AvatarImage, type AvatarImageProps } from "radix-vue";
const props = defineProps<AvatarImageProps>();
</script>

View File

@@ -1,21 +1,21 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { cva, type VariantProps } from "class-variance-authority";
export { default as Avatar } from './Avatar.vue';
export { default as AvatarFallback } from './AvatarFallback.vue';
export { default as AvatarImage } from './AvatarImage.vue';
export { default as Avatar } from "./Avatar.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{
variants: {
size: {
sm: 'h-10 w-10 text-xs',
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
},
shape: {
circle: 'rounded-full',
square: 'rounded-md',
circle: "rounded-full",
square: "rounded-md",
},
},
},

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { type BadgeVariants, badgeVariants } from '.'
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
import { type BadgeVariants, badgeVariants } from ".";
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
variant?: BadgeVariants["variant"];
class?: HTMLAttributes["class"];
}>();
</script>
<template>

View File

@@ -1,27 +1,26 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { cva, type VariantProps } from "class-variance-authority";
export { default as Badge } from './Badge.vue'
export { default as Badge } from "./Badge.vue";
export const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2 py-0.25 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
"inline-flex items-center rounded-full border px-2 py-0.25 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
success:
'border-transparent bg-green-200 text-green-800 hover:bg-green-200/80',
outline: 'text-foreground',
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
success: "border-transparent bg-green-200 text-green-800 hover:bg-green-200/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: 'default',
variant: "default",
},
},
)
);
export type BadgeVariants = VariantProps<typeof badgeVariants>
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { MoreHorizontal } from 'lucide-vue-next';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { MoreHorizontal } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span role="presentation" aria-hidden="true" :class="cn('flex h-9 w-9 items-center justify-center', props.class)">
<span
role="presentation"
aria-hidden="true"
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
as: 'a',
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
as: "a",
});
</script>
<template>
<Primitive :as="as" :as-child="asChild" :class="cn('transition-colors hover:text-foreground', props.class)">
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,14 +1,21 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<ol :class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)">
<ol
:class="
cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
props.class,
)
"
>
<slot />
</ol>
</template>

View File

@@ -1,14 +1,19 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span role="link" aria-disabled="true" aria-current="page" :class="cn('font-normal text-foreground', props.class)">
<span
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
>
<slot />
</span>
</template>

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils';
import { ChevronRight } from 'lucide-vue-next';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<li role="presentation" aria-hidden="true" :class="cn('[&>svg]:h-3.5 [&>svg]:w-3.5', props.class)">
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:h-3.5 [&>svg]:w-3.5', props.class)"
>
<slot>
<ChevronRight />
</slot>

View File

@@ -1,7 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
export { default as BreadcrumbList } from './BreadcrumbList.vue';
export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';
export { default as Breadcrumb } from "./Breadcrumb.vue";
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue";
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue";
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue";

View File

@@ -1,22 +1,26 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import type { HTMLAttributes } from 'vue';
import { buttonVariants, type ButtonVariants } from '.';
import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { buttonVariants, type ButtonVariants } from ".";
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
as: "button",
});
</script>
<template>
<Primitive :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,31 +1,33 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { cva, type VariantProps } from "class-variance-authority";
export { default as Button } from './Button.vue';
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 px-2 text-xs',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
'iconxs': 'h-7 px-0.5',
default: "h-9 px-4 py-2",
xs: "h-7 px-2 text-xs",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
iconxs: "h-7 px-0.5",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
);

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,6 +1,6 @@
export { default as Card } from './Card.vue';
export { default as CardContent } from './CardContent.vue';
export { default as CardDescription } from './CardDescription.vue';
export { default as CardFooter } from './CardFooter.vue';
export { default as CardHeader } from './CardHeader.vue';
export { default as CardTitle } from './CardTitle.vue';
export { default as Card } from "./Card.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";

View File

@@ -1,28 +1,31 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
import { cn } from "@/lib/utils";
import { Check } from "lucide-vue-next";
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer size-5 shrink-0 rounded-sm border border-input ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-accent-foreground',
props.class)"
cn(
'peer size-5 shrink-0 rounded-sm border border-input ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-accent-foreground',
props.class,
)
"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>

View File

@@ -1 +1 @@
export { default as Checkbox } from './Checkbox.vue'
export { default as Checkbox } from "./Checkbox.vue";

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue';
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue';
import type { CollapsibleRootEmits, CollapsibleRootProps } from "radix-vue";
import { CollapsibleRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>();

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue';
import { CollapsibleContent, type CollapsibleContentProps } from "radix-vue";
const props = defineProps<CollapsibleContentProps>();
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue';
import { CollapsibleTrigger, type CollapsibleTriggerProps } from "radix-vue";
const props = defineProps<CollapsibleTriggerProps>();
</script>

View File

@@ -1,3 +1,3 @@
export { default as Collapsible } from './Collapsible.vue';
export { default as CollapsibleContent } from './CollapsibleContent.vue';
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue';
export { default as Collapsible } from "./Collapsible.vue";
export { default as CollapsibleContent } from "./CollapsibleContent.vue";
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue";

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { DialogRoot, useForwardPropsEmits, type DialogRootEmits, type DialogRootProps } from 'radix-vue';
import {
DialogRoot,
useForwardPropsEmits,
type DialogRootEmits,
type DialogRootProps,
} from "radix-vue";
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'radix-vue';
import { DialogClose, type DialogCloseProps } from "radix-vue";
const props = defineProps<DialogCloseProps>();
</script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { X } from 'lucide-vue-next';
import { cn } from "@/lib/utils";
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
@@ -9,10 +9,10 @@ import {
useForwardPropsEmits,
type DialogContentEmits,
type DialogContentProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DialogDescription, useForwardProps, type DialogDescriptionProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { DialogDescription, useForwardProps, type DialogDescriptionProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@@ -15,7 +15,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogDescription v-bind="forwardedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{ class?: HTMLAttributes['class'] }>();
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>
<template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { X } from 'lucide-vue-next';
import { cn } from "@/lib/utils";
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
@@ -9,10 +9,10 @@ import {
useForwardPropsEmits,
type DialogContentEmits,
type DialogContentProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
@@ -41,7 +41,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
if (
originalEvent.offsetX > target.clientWidth ||
originalEvent.offsetY > target.clientHeight
) {
event.preventDefault();
}
}
@@ -49,7 +52,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
>
<slot />
<DialogClose class="absolute right-3 top-3 rounded-md p-0.5 transition-colors hover:bg-secondary">
<DialogClose
class="absolute right-3 top-3 rounded-md p-0.5 transition-colors hover:bg-secondary"
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogClose>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DialogTitle, useForwardProps, type DialogTitleProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { DialogTitle, useForwardProps, type DialogTitleProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@@ -15,7 +15,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogTitle v-bind="forwardedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
<DialogTitle
v-bind="forwardedProps"
:class="cn('text-lg font-semibold leading-none tracking-tight', props.class)"
>
<slot />
</DialogTitle>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue';
import { DialogTrigger, type DialogTriggerProps } from "radix-vue";
const props = defineProps<DialogTriggerProps>();
</script>

View File

@@ -1,9 +1,9 @@
export { default as Dialog } from './Dialog.vue';
export { default as DialogClose } from './DialogClose.vue';
export { default as DialogContent } from './DialogContent.vue';
export { default as DialogDescription } from './DialogDescription.vue';
export { default as DialogFooter } from './DialogFooter.vue';
export { default as DialogHeader } from './DialogHeader.vue';
export { default as DialogScrollContent } from './DialogScrollContent.vue';
export { default as DialogTitle } from './DialogTitle.vue';
export { default as DialogTrigger } from './DialogTrigger.vue';
export { default as Dialog } from "./Dialog.vue";
export { default as DialogClose } from "./DialogClose.vue";
export { default as DialogContent } from "./DialogContent.vue";
export { default as DialogDescription } from "./DialogDescription.vue";
export { default as DialogFooter } from "./DialogFooter.vue";
export { default as DialogHeader } from "./DialogHeader.vue";
export { default as DialogScrollContent } from "./DialogScrollContent.vue";
export { default as DialogTitle } from "./DialogTitle.vue";
export { default as DialogTrigger } from "./DialogTrigger.vue";

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { DropdownMenuRoot, useForwardPropsEmits, type DropdownMenuRootEmits, type DropdownMenuRootProps } from 'radix-vue';
import {
DropdownMenuRoot,
useForwardPropsEmits,
type DropdownMenuRootEmits,
type DropdownMenuRootProps,
} from "radix-vue";
const props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>();

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { Check } from 'lucide-vue-next';
import { cn } from "@/lib/utils";
import { Check } from "lucide-vue-next";
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,17 +1,20 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = withDefaults(defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(), {
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
});
},
);
const emits = defineEmits<DropdownMenuContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue';
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
const props = defineProps<DropdownMenuGroupProps>();
</script>

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }>();
const props = defineProps<
DropdownMenuItemProps & { class?: HTMLAttributes["class"]; inset?: boolean }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }>();
const props = defineProps<
DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@@ -15,7 +17,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel v-bind="forwardedProps" :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { DropdownMenuRadioGroup, useForwardPropsEmits, type DropdownMenuRadioGroupEmits, type DropdownMenuRadioGroupProps } from 'radix-vue';
import {
DropdownMenuRadioGroup,
useForwardPropsEmits,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
} from "radix-vue";
const props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { Circle } from 'lucide-vue-next';
import { cn } from "@/lib/utils";
import { Circle } from "lucide-vue-next";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuRadioItemEmits>();

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<
DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}
>();
@@ -17,5 +17,8 @@ const delegatedProps = computed(() => {
</script>
<template>
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
<DropdownMenuSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
/>
</template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
</script>

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { DropdownMenuSub, useForwardPropsEmits, type DropdownMenuSubEmits, type DropdownMenuSubProps } from 'radix-vue';
import {
DropdownMenuSub,
useForwardPropsEmits,
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
} from "radix-vue";
const props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();

View File

@@ -1,9 +1,14 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { DropdownMenuSubContent, useForwardPropsEmits, type DropdownMenuSubContentEmits, type DropdownMenuSubContentProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import {
DropdownMenuSubContent,
useForwardPropsEmits,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,10 +1,14 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { ChevronRight } from 'lucide-vue-next';
import { DropdownMenuSubTrigger, useForwardProps, type DropdownMenuSubTriggerProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-vue-next";
import {
DropdownMenuSubTrigger,
useForwardProps,
type DropdownMenuSubTriggerProps,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } from 'radix-vue';
import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } from "radix-vue";
const props = defineProps<DropdownMenuTriggerProps>();

View File

@@ -1,16 +1,16 @@
export { default as DropdownMenu } from './DropdownMenu.vue';
export { default as DropdownMenu } from "./DropdownMenu.vue";
export { DropdownMenuPortal } from 'radix-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 "radix-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";

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { useVModel } from '@vueuse/core';
import type { HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { useVModel } from "@vueuse/core";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
defaultValue?: string | number;
modelValue?: string | number;
class?: HTMLAttributes['class'];
class?: HTMLAttributes["class"];
}>();
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void;
(e: "update:modelValue", payload: string | number): void;
}>();
const modelValue = useVModel(props, 'modelValue', emits, {
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
});

View File

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

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { Label, type LabelProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from "@/lib/utils";
import { Label, type LabelProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
@@ -15,7 +15,12 @@ const delegatedProps = computed(() => {
<template>
<Label
v-bind="delegatedProps"
:class="cn('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', props.class)"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class,
)
"
>
<slot />
</Label>

View File

@@ -1 +1 @@
export { default as Label } from './Label.vue';
export { default as Label } from "./Label.vue";

View File

@@ -1,25 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import {
NavigationMenuRoot,
type NavigationMenuRootEmits,
type NavigationMenuRootProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import NavigationMenuViewport from './NavigationMenuViewport.vue'
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import NavigationMenuViewport from "./NavigationMenuViewport.vue";
const props = defineProps<NavigationMenuRootProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<NavigationMenuRootProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<NavigationMenuRootEmits>()
const emits = defineEmits<NavigationMenuRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>

View File

@@ -1,33 +1,35 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import {
NavigationMenuContent,
type NavigationMenuContentEmits,
type NavigationMenuContentProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<NavigationMenuContentProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<NavigationMenuContentProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<NavigationMenuContentEmits>()
const emits = defineEmits<NavigationMenuContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<NavigationMenuContent
v-bind="forwarded"
:class="cn(
:class="
cn(
'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto',
props.class,
)"
)
"
>
<slot />
</NavigationMenuContent>

Some files were not shown because too many files have changed in this diff Show More