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; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@@ -23,31 +18,8 @@ class RegisteredUserController extends Controller
return Inertia::render('auth/Register'); return Inertia::render('auth/Register');
} }
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
abort(403, 'Registration is disabled.'); 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

@@ -24,17 +24,17 @@ class CreateNetworkRequest extends Request implements HasBody
'name' => $this->name, 'name' => $this->name,
'ip_range' => '10.0.0.0/16', 'ip_range' => '10.0.0.0/16',
]; ];
if ($this->networkZone) { if ($this->networkZone) {
$body['subnets'] = [ $body['subnets'] = [
[ [
'type' => 'cloud', 'type' => 'cloud',
'ip_range' => '10.0.1.0/24', 'ip_range' => '10.0.1.0/24',
'network_zone' => $this->networkZone 'network_zone' => $this->networkZone,
] ],
]; ];
} }
return $body; return $body;
} }

View File

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

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

@@ -20,11 +20,13 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"larastan/larastan": "^3.0",
"laravel/boost": "^1.1", "laravel/boost": "^1.1",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",
"laravel/pint": "^1.18", "laravel/pint": "^1.18",
"laravel/sail": "^1.41", "laravel/sail": "^1.41",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"mrpunyapal/peststan": "^0.2.5",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7", "pestphp/pest": "^3.7",
"pestphp/pest-plugin-laravel": "^3.1" "pestphp/pest-plugin-laravel": "^3.1"
@@ -65,6 +67,14 @@
"npm run build:ssr", "npm run build:ssr",
"Composer\\Config::disableProcessTimeout", "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" "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": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "69f6de114270a8beb46d9283a2acd24d", "content-hash": "f73763833c370943f03916f4eaa3ce26",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -6540,6 +6540,47 @@
}, },
"time": "2020-07-09T08:09:16+00:00" "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", "name": "jean85/pretty-package-versions",
"version": "2.1.1", "version": "2.1.1",
@@ -6600,6 +6641,99 @@
}, },
"time": "2025-03-19T14:43:43+00:00" "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", "name": "laravel/boost",
"version": "v1.1.5", "version": "v1.1.5",
@@ -7080,6 +7214,69 @@
}, },
"time": "2024-05-16T03:13:13+00:00" "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", "name": "myclabs/deep-copy",
"version": "1.13.0", "version": "1.13.0",
@@ -7976,6 +8173,59 @@
}, },
"time": "2025-02-19T13:28:12+00:00" "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", "name": "phpunit/php-code-coverage",
"version": "11.0.9", "version": "11.0.9",

View File

@@ -44,7 +44,7 @@ class DatabaseSeeder extends Seeder
'name' => 'keystone', 'name' => 'keystone',
'external_id' => 'net-12345', 'external_id' => 'net-12345',
'provider_id' => $provider->id, 'provider_id' => $provider->id,
'ip_range' => fake()->ipv4() . '/24', 'ip_range' => fake()->ipv4().'/24',
]); ]);
$servers = Server::factory(40) $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": "vite build",
"build:ssr": "vite build && vite build --ssr", "build:ssr": "vite build && vite build --ssr",
"dev": "vite", "dev": "vite",
"format": "prettier --write resources/", "format": "oxfmt resources/",
"format:check": "prettier --check resources/", "format:check": "oxfmt --check resources/",
"lint": "eslint . --fix" "lint": "oxlint --fix",
"lint:check": "oxlint"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0",
"@types/node": "^22.13.5", "@types/node": "^22.13.5",
"@vue/eslint-config-typescript": "^14.3.0", "oxfmt": "^0.49.0",
"eslint": "^9.17.0", "oxlint": "^1.64.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",
"vue-tsc": "^2.2.4" "vue-tsc": "^2.2.4"
}, },
"dependencies": { "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> <include>
<directory>app</directory> <directory>app</directory>
</include> </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> </source>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>

View File

@@ -5,7 +5,8 @@
body, body,
html { html {
--font-sans: --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 { @layer base {

View File

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

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { SidebarInset } from '@/components/ui/sidebar'; import { SidebarInset } from "@/components/ui/sidebar";
import { computed } from 'vue'; import { computed } from "vue";
interface Props { interface Props {
variant?: 'header' | 'sidebar'; variant?: "header" | "sidebar";
class?: string; class?: string;
} }
@@ -15,7 +15,11 @@ const className = computed(() => props.class);
<SidebarInset v-if="props.variant === 'sidebar'" :class="className"> <SidebarInset v-if="props.variant === 'sidebar'" :class="className">
<slot /> <slot />
</SidebarInset> </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 /> <slot />
</main> </main>
</template> </template>

View File

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

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import AppLogoIcon from '@/components/AppLogoIcon.vue'; import AppLogoIcon from "@/components/AppLogoIcon.vue";
</script> </script>
<template> <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" /> <AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
</div> </div>
<div class="ml-1 grid flex-1 text-left text-sm"> <div class="ml-1 grid flex-1 text-left text-sm">

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAppearance } from '@/composables/useAppearance'; import { useAppearance } from "@/composables/useAppearance";
import { Monitor, Moon, Sun } from 'lucide-vue-next'; import { Monitor, Moon, Sun } from "lucide-vue-next";
interface Props { interface Props {
class?: string; class?: string;
} }
const { class: containerClass = '' } = defineProps<Props>(); const { class: containerClass = "" } = defineProps<Props>();
const { appearance, updateAppearance } = useAppearance(); const { appearance, updateAppearance } = useAppearance();
const tabs = [ const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' }, { value: "light", Icon: Sun, label: "Light" },
{ value: 'dark', Icon: Moon, label: 'Dark' }, { value: "dark", Icon: Moon, label: "Dark" },
{ value: 'system', Icon: Monitor, label: 'System' }, { value: "system", Icon: Monitor, label: "System" },
] as const; ] as const;
</script> </script>
<template> <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 <button
v-for="{ value, Icon, label } in tabs" v-for="{ value, Icon, label } in tabs"
:key="value" :key="value"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue'; import UserInfo from "@/components/UserInfo.vue";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import {
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'; DropdownMenu,
import { type SharedData, type User } from '@/types'; DropdownMenuContent,
import { usePage } from '@inertiajs/vue3'; DropdownMenuTrigger,
import { ChevronsUpDown } from 'lucide-vue-next'; } from "@/components/ui/dropdown-menu";
import UserMenuContent from './UserMenuContent.vue'; 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 page = usePage<SharedData>();
const user = page.props.auth.user as User; const user = page.props.auth.user as User;
@@ -17,15 +26,18 @@ const { isMobile, state } = useSidebar();
<SidebarMenuItem> <SidebarMenuItem>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <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" /> <UserInfo :user="user" />
<ChevronsUpDown class="ml-auto size-4" /> <ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
:side="isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'" :side="isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'"
align="end" align="end"
:side-offset="4" :side-offset="4"
> >
<UserMenuContent :user="user" /> <UserMenuContent :user="user" />

View File

@@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from "vue";
const patternId = computed(() => `pattern-${Math.random().toString(36).substring(2, 9)}`); const patternId = computed(() => `pattern-${Math.random().toString(36).substring(2, 9)}`);
</script> </script>
<template> <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> <defs>
<pattern :id="patternId" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse"> <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> <path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>

View File

@@ -6,16 +6,16 @@ defineProps({
name: String, name: String,
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(["update:modelValue"]);
function onChange(event) { function onChange(event) {
emit('update:modelValue', event.target.value) emit("update:modelValue", event.target.value);
} }
</script> </script>
<template> <template>
<label <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 <input
type="radio" type="radio"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue'; import { AvatarImage, type AvatarImageProps } from "radix-vue";
const props = defineProps<AvatarImageProps>(); const props = defineProps<AvatarImageProps>();
</script> </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 Avatar } from "./Avatar.vue";
export { default as AvatarFallback } from './AvatarFallback.vue'; export { default as AvatarFallback } from "./AvatarFallback.vue";
export { default as AvatarImage } from './AvatarImage.vue'; export { default as AvatarImage } from "./AvatarImage.vue";
export const avatarVariant = cva( 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: { variants: {
size: { size: {
sm: 'h-10 w-10 text-xs', sm: "h-10 w-10 text-xs",
base: 'h-16 w-16 text-2xl', base: "h-16 w-16 text-2xl",
lg: 'h-32 w-32 text-5xl', lg: "h-32 w-32 text-5xl",
}, },
shape: { shape: {
circle: 'rounded-full', circle: "rounded-full",
square: 'rounded-md', square: "rounded-md",
}, },
}, },
}, },

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue";
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils";
import { type BadgeVariants, badgeVariants } from '.' import { type BadgeVariants, badgeVariants } from ".";
const props = defineProps<{ const props = defineProps<{
variant?: BadgeVariants['variant'] variant?: BadgeVariants["variant"];
class?: HTMLAttributes['class'] class?: HTMLAttributes["class"];
}>() }>();
</script> </script>
<template> <template>
<div :class="cn(badgeVariants({ variant }), props.class)"> <div :class="cn(badgeVariants({ variant }), props.class)">
<slot /> <slot />
</div> </div>
</template> </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( 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: { variants: {
variant: { variant: {
default: default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
success: success: "border-transparent bg-green-200 text-green-800 hover:bg-green-200/80",
'border-transparent bg-green-200 text-green-800 hover:bg-green-200/80', outline: "text-foreground",
outline: 'text-foreground', },
}, },
defaultVariants: {
variant: "default",
},
}, },
defaultVariants: { );
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants> export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

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

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { MoreHorizontal } from 'lucide-vue-next'; import { MoreHorizontal } from "lucide-vue-next";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
}>(); }>();
</script> </script>
<template> <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> <slot>
<MoreHorizontal class="h-4 w-4" /> <MoreHorizontal class="h-4 w-4" />
</slot> </slot>

View File

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

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from 'radix-vue'; import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), { const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
as: 'a', as: "a",
}); });
</script> </script>
<template> <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 /> <slot />
</Primitive> </Primitive>
</template> </template>

View File

@@ -1,14 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
}>(); }>();
</script> </script>
<template> <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 /> <slot />
</ol> </ol>
</template> </template>

View File

@@ -1,14 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
}>(); }>();
</script> </script>
<template> <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 /> <slot />
</span> </span>
</template> </template>

View File

@@ -1,15 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { ChevronRight } from 'lucide-vue-next'; import { ChevronRight } from "lucide-vue-next";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
}>(); }>();
</script> </script>
<template> <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> <slot>
<ChevronRight /> <ChevronRight />
</slot> </slot>

View File

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

View File

@@ -1,22 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { Primitive, type PrimitiveProps } from 'radix-vue'; import { Primitive, type PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
import { buttonVariants, type ButtonVariants } from '.'; import { buttonVariants, type ButtonVariants } from ".";
interface Props extends PrimitiveProps { interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']; variant?: ButtonVariants["variant"];
size?: ButtonVariants['size']; size?: ButtonVariants["size"];
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
as: 'button', as: "button",
}); });
</script> </script>
<template> <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 /> <slot />
</Primitive> </Primitive>
</template> </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( 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: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', destructive:
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', outline:
ghost: 'hover:bg-accent hover:text-accent-foreground', "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
link: 'text-primary underline-offset-4 hover:underline', 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: { size: {
default: 'h-9 px-4 py-2', default: "h-9 px-4 py-2",
xs: 'h-7 px-2 text-xs', xs: "h-7 px-2 text-xs",
sm: 'h-8 rounded-md px-3 text-xs', sm: "h-8 rounded-md px-3 text-xs",
lg: 'h-10 rounded-md px-8', lg: "h-10 rounded-md px-8",
icon: 'h-9 w-9', icon: "h-9 w-9",
'iconxs': 'h-7 px-0.5', iconxs: "h-7 px-0.5",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default', size: "default",
}, },
}, },
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue' import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils";
import { Check } from 'lucide-vue-next' import { Check } from "lucide-vue-next";
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue' import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "radix-vue";
import { computed, type HTMLAttributes } from 'vue' import { computed, type HTMLAttributes } from "vue";
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<CheckboxRootEmits>() const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => { 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> </script>
<template> <template>
<CheckboxRoot <CheckboxRoot
v-bind="forwarded" v-bind="forwarded"
:class=" :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', cn(
props.class)" '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> "
<Check class="size-3.5 stroke-[3]" /> >
</slot> <CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
</CheckboxIndicator> <slot>
</CheckboxRoot> <Check class="size-3.5 stroke-[3]" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template> </template>

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"> <script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue'; import type { CollapsibleRootEmits, CollapsibleRootProps } from "radix-vue";
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue'; import { CollapsibleRoot, useForwardPropsEmits } from "radix-vue";
const props = defineProps<CollapsibleRootProps>(); const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>(); const emits = defineEmits<CollapsibleRootEmits>();

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
<script setup lang="ts"> <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 props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>(); const emits = defineEmits<DialogRootEmits>();

View File

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

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { X } from 'lucide-vue-next'; import { X } from "lucide-vue-next";
import { import {
DialogClose, DialogClose,
DialogContent, DialogContent,
@@ -9,10 +9,10 @@ import {
useForwardPropsEmits, useForwardPropsEmits,
type DialogContentEmits, type DialogContentEmits,
type DialogContentProps, type DialogContentProps,
} from 'radix-vue'; } from "radix-vue";
import { computed, type HTMLAttributes } from '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 emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DialogDescription, useForwardProps, type DialogDescriptionProps } from 'radix-vue'; import { DialogDescription, useForwardProps, type DialogDescriptionProps } from "radix-vue";
import { computed, type HTMLAttributes } from '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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props; const { class: _, ...delegated } = props;
@@ -15,7 +15,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script> </script>
<template> <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 /> <slot />
</DialogDescription> </DialogDescription>
</template> </template>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { X } from 'lucide-vue-next'; import { X } from "lucide-vue-next";
import { import {
DialogClose, DialogClose,
DialogContent, DialogContent,
@@ -9,10 +9,10 @@ import {
useForwardPropsEmits, useForwardPropsEmits,
type DialogContentEmits, type DialogContentEmits,
type DialogContentProps, type DialogContentProps,
} from 'radix-vue'; } from "radix-vue";
import { computed, type HTMLAttributes } from '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 emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
@@ -41,7 +41,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
(event) => { (event) => {
const originalEvent = event.detail.originalEvent; const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement; 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(); event.preventDefault();
} }
} }
@@ -49,7 +52,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
> >
<slot /> <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" /> <X class="h-4 w-4" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</DialogClose> </DialogClose>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DialogTitle, useForwardProps, type DialogTitleProps } from 'radix-vue'; import { DialogTitle, useForwardProps, type DialogTitleProps } from "radix-vue";
import { computed, type HTMLAttributes } from '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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props; const { class: _, ...delegated } = props;
@@ -15,7 +15,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script> </script>
<template> <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 /> <slot />
</DialogTitle> </DialogTitle>
</template> </template>

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
<script setup lang="ts"> <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 props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>(); const emits = defineEmits<DropdownMenuRootEmits>();

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { Check } from 'lucide-vue-next'; import { Check } from "lucide-vue-next";
import { import {
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
useForwardPropsEmits, useForwardPropsEmits,
type DropdownMenuCheckboxItemEmits, type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps, type DropdownMenuCheckboxItemProps,
} from 'radix-vue'; } from "radix-vue";
import { computed, type HTMLAttributes } from '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 emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from 'radix-vue'; import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from "radix-vue";
import { computed, type HTMLAttributes } from '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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props; const { class: _, ...delegated } = props;

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from 'radix-vue'; import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from "radix-vue";
import { computed, type HTMLAttributes } from '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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props; const { class: _, ...delegated } = props;
@@ -15,7 +17,10 @@ const forwardedProps = useForwardProps(delegatedProps);
</script> </script>
<template> <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 /> <slot />
</DropdownMenuLabel> </DropdownMenuLabel>
</template> </template>

View File

@@ -1,5 +1,10 @@
<script setup lang="ts"> <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 props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>(); const emits = defineEmits<DropdownMenuRadioGroupEmits>();

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { Circle } from 'lucide-vue-next'; import { Circle } from "lucide-vue-next";
import { import {
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
DropdownMenuRadioItem, DropdownMenuRadioItem,
useForwardPropsEmits, useForwardPropsEmits,
type DropdownMenuRadioItemEmits, type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps, type DropdownMenuRadioItemProps,
} from 'radix-vue'; } from "radix-vue";
import { computed, type HTMLAttributes } from '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>(); const emits = defineEmits<DropdownMenuRadioItemEmits>();

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'radix-vue'; import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from "radix-vue";
import { computed, type HTMLAttributes } from 'vue'; import { computed, type HTMLAttributes } from "vue";
const props = defineProps< const props = defineProps<
DropdownMenuSeparatorProps & { DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
} }
>(); >();
@@ -17,5 +17,8 @@ const delegatedProps = computed(() => {
</script> </script>
<template> <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> </template>

View File

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

View File

@@ -1,5 +1,10 @@
<script setup lang="ts"> <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 props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>(); const emits = defineEmits<DropdownMenuSubEmits>();

View File

@@ -1,9 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { DropdownMenuSubContent, useForwardPropsEmits, type DropdownMenuSubContentEmits, type DropdownMenuSubContentProps } from 'radix-vue'; import {
import { computed, type HTMLAttributes } from 'vue'; 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 emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {

View File

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

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } from 'radix-vue'; import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } from "radix-vue";
const props = defineProps<DropdownMenuTriggerProps>(); 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 { DropdownMenuPortal } from "radix-vue";
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'; export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue";
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'; export { default as DropdownMenuContent } from "./DropdownMenuContent.vue";
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'; export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue";
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'; export { default as DropdownMenuItem } from "./DropdownMenuItem.vue";
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'; export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue";
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'; export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue";
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'; export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue";
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'; export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue";
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'; export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue";
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'; export { default as DropdownMenuSub } from "./DropdownMenuSub.vue";
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'; export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue";
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'; export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue";
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'; export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue";

View File

@@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { useVModel } from '@vueuse/core'; import { useVModel } from "@vueuse/core";
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from "vue";
const props = defineProps<{ const props = defineProps<{
defaultValue?: string | number; defaultValue?: string | number;
modelValue?: string | number; modelValue?: string | number;
class?: HTMLAttributes['class']; class?: HTMLAttributes["class"];
}>(); }>();
const emits = defineEmits<{ 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, passive: true,
defaultValue: props.defaultValue, 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"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { Label, type LabelProps } from 'radix-vue'; import { Label, type LabelProps } from "radix-vue";
import { computed, type HTMLAttributes } from '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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props; const { class: _, ...delegated } = props;
@@ -15,7 +15,12 @@ const delegatedProps = computed(() => {
<template> <template>
<Label <Label
v-bind="delegatedProps" 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 /> <slot />
</Label> </Label>

View File

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

View File

@@ -1,33 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils";
import { import {
NavigationMenuRoot, NavigationMenuRoot,
type NavigationMenuRootEmits, type NavigationMenuRootEmits,
type NavigationMenuRootProps, type NavigationMenuRootProps,
useForwardPropsEmits, useForwardPropsEmits,
} from 'radix-vue' } from "radix-vue";
import { computed, type HTMLAttributes } from 'vue' import { computed, type HTMLAttributes } from "vue";
import NavigationMenuViewport from './NavigationMenuViewport.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 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> </script>
<template> <template>
<NavigationMenuRoot <NavigationMenuRoot
v-bind="forwarded" v-bind="forwarded"
:class="cn('relative z-10 flex max-w-max flex-1 items-center justify-center', props.class)" :class="cn('relative z-10 flex max-w-max flex-1 items-center justify-center', props.class)"
> >
<slot /> <slot />
<NavigationMenuViewport /> <NavigationMenuViewport />
</NavigationMenuRoot> </NavigationMenuRoot>
</template> </template>

View File

@@ -1,34 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils";
import { import {
NavigationMenuContent, NavigationMenuContent,
type NavigationMenuContentEmits, type NavigationMenuContentEmits,
type NavigationMenuContentProps, type NavigationMenuContentProps,
useForwardPropsEmits, useForwardPropsEmits,
} from 'radix-vue' } from "radix-vue";
import { computed, type HTMLAttributes } from '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 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> </script>
<template> <template>
<NavigationMenuContent <NavigationMenuContent
v-bind="forwarded" v-bind="forwarded"
:class="cn( :class="
'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', cn(
props.class, '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> >
<slot />
</NavigationMenuContent>
</template> </template>

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