web: complete migration to latest shadcn, fix gin SPA routing

This commit is contained in:
hayzamjs
2025-07-02 23:04:22 +04:00
parent 851f5ed876
commit 159b27a5cf
639 changed files with 6340 additions and 39460 deletions
+8 -7
View File
@@ -7,7 +7,7 @@ require (
github.com/digitalocean/go-libvirt v0.0.0-20250417173424-a6a66ef779d6
github.com/gin-contrib/gzip v1.2.2
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.24.0
github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
@@ -28,15 +28,16 @@ require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/cenkalti/log v1.0.0 // indirect
github.com/cenkalti/rain v1.13.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gin-contrib/static v1.1.5 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -44,7 +45,7 @@ require (
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/btree v1.1.3 // indirect
@@ -84,12 +85,12 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/bencode v1.0.0 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.2 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+17
View File
@@ -6,9 +6,13 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
@@ -19,6 +23,8 @@ github.com/cenkalti/rain v1.13.0 h1:Hxy0/KobLTrc4+287O3I8trNt3bmjdoYw25HxLdngmI=
github.com/cenkalti/rain v1.13.0/go.mod h1:1yClO1IkcP8Vofr6MZ4AxAhe4wfQrsy6Juo2IUvATQk=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -35,6 +41,8 @@ github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEli
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4=
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -60,8 +68,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -106,6 +118,7 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -207,6 +220,8 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -230,6 +245,8 @@ golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+10 -8
View File
@@ -10,9 +10,10 @@ package handlers
import (
"log"
"net/http"
static "github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
static "github.com/soulteary/gin-static"
"sylve/internal/assets"
diskHandlers "sylve/internal/handlers/disk"
@@ -217,19 +218,20 @@ func RegisterRoutes(r *gin.Engine,
ReverseProxy(c, "http://127.0.0.1:5173")
})
} else {
staticFiles, err := static.EmbedFolder(assets.SvelteKitFiles, "web-files")
files, err := static.EmbedFolder(assets.SvelteKitFiles, "web-files")
if err != nil {
log.Fatalln("Initialization of embed folder failed:", err)
}
r.Use(static.Serve("/", staticFiles))
r.Use(static.Serve("/", files))
r.NoRoute(func(c *gin.Context) {
c.FileFromFS("index.html", staticFiles)
})
indexFile, err := assets.SvelteKitFiles.ReadFile("web-files/index.html")
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
r.GET("/", func(c *gin.Context) {
c.FileFromFS("index.html", staticFiles)
c.Data(http.StatusOK, "text/html", indexFile)
})
}
}
-26
View File
@@ -1,26 +0,0 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
src/lib/locales/*.json
src/lib/locales/*.js
-1
View File
@@ -1 +0,0 @@
engine-strict=true
-6
View File
@@ -1,6 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
-15
View File
@@ -1,15 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
-16
View File
@@ -1,16 +0,0 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
-36
View File
@@ -1,36 +0,0 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: { 'no-undef': 'off' }
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);
-6428
View File
File diff suppressed because it is too large Load Diff
-78
View File
@@ -1,78 +0,0 @@
{
"name": "new-web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@iconify/svelte": "^5.0.0",
"@internationalized/date": "^3.8.2",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.7",
"@types/tabulator-tables": "^6.2.6",
"@types/validator": "^13.15.2",
"bits-ui": "^2.8.0",
"clsx": "^2.1.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"layerchart": "^2.0.0-next.18",
"mode-watcher": "^1.0.8",
"paneforge": "^1.0.0-next.5",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.4",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6"
},
"dependencies": {
"@battlefieldduck/xterm-svelte": "^2.1.0",
"@fontsource/noto-sans": "^5.2.7",
"@layerstack/svelte-stores": "^1.0.2",
"@lucide/svelte": "^0.516.0",
"@svelte-put/shortcut": "^4.1.0",
"@sveltestack/svelte-query": "^1.6.0",
"adze": "^2.2.4",
"axios": "^1.10.0",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.2.0",
"d3-shape": "^3.2.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"human-format": "^1.2.1",
"is-cidr": "^5.1.1",
"is-ip": "^5.0.1",
"lucide-svelte": "^0.516.0",
"tabulator-tables": "^6.3.1",
"validator": "^13.15.15",
"wuchale": "0.5.5",
"zod": "^3.25.67"
}
}
-13
View File
@@ -1,13 +0,0 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
-15
View File
@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="" id="favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
-233
View File
@@ -1,233 +0,0 @@
@use './tabulator.scss';
.tabulator {
@apply bg-primary border-none;
.tabulator-header {
@apply border-border bg-background;
.tabulator-header-contents {
@apply bg-background;
}
.tabulator-col-content {
@apply bg-background hover:bg-muted;
}
.tabulator-calcs-holder {
@apply !border-border !bg-background !border;
.tabulator-row {
@apply !bg-background;
}
}
.tabulator-col {
@apply bg-background;
&.tabulator-sortable {
@media (hover: hover) and (pointer: fine) {
@apply border-border;
&.tabulator-col-sorter-element:hover {
@apply bg-background cursor-pointer;
}
}
}
}
.tabulator-col-title-holder {
@apply text-black dark:text-white;
.tabulator-col-title {
@apply text-black dark:text-white;
}
}
.tabulator-col-title input {
@apply border-border border;
}
}
.tabulator-row {
@apply border-border bg-background border-b text-black dark:text-white;
@media (hover: hover) and (pointer: fine) {
&.tabulator-selectable:hover {
@apply bg-background;
cursor: pointer;
}
}
.tabulator-cell {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:last-of-type {
border-right: none;
}
&.tabulator-row-header {
border-bottom: none;
}
.tabulator-data-tree-control {
@apply border-primary bg-primary;
.tabulator-data-tree-control-collapse {
background: transparent;
&:after {
@apply bg-primary-foreground;
}
}
.tabulator-data-tree-control-expand {
@apply bg-primary-foreground;
&:after {
@apply bg-primary-foreground;
}
}
}
}
&.tabulator-group {
span {
color: #666;
}
}
.tabulator-frozen input {
@apply opacity-40;
background-color: rgb(63 63 70) !important;
@apply border-border border;
}
}
.tabulator-tableholder {
.tabulator-cell {
.tabulator-data-tree-control {
@apply bg-primary;
}
}
@apply bg-background;
.tabulator-placeholder {
span {
@apply text-secondary;
}
.tabulator-placeholder-contents {
@apply text-secondary;
}
}
}
.tabulator-footer {
@apply border-border bg-background;
.tabulator-footer-contents {
@apply text-black dark:text-white;
.tabulator-paginator {
label {
@apply text-black dark:text-white;
}
select {
@apply border-border bg-primary text-black dark:text-white;
}
.tabulator-page {
@apply border-border bg-primary-foreground hover:border-primary/10 hover:bg-primary/10 dark:bg-muted hover:dark:border-muted hover:dark:bg-muted/10 text-black dark:text-white;
&.active {
@apply !bg-primary !text-secondary;
}
&:disabled {
opacity: 0.4;
}
}
}
}
.tabulator-calcs-holder {
.tabulator-row {
@apply !bg-primary;
}
}
.tabulator-spreadsheet-tabs {
.tabulator-spreadsheet-tab {
font-weight: normal;
&.tabulator-spreadsheet-tab-active {
color: tabulator.$footerActiveColor;
font-weight: bold;
}
}
}
}
}
.tabulator-table .tabulator-row-odd {
@apply border-border bg-background border-b border-none text-black dark:text-white;
.tabulator-frozen {
@apply bg-secondary;
}
@media (hover: hover) and (pointer: fine) {
&.tabulator-selectable:hover {
// @apply bg-background hover:bg-muted;
cursor: pointer;
}
}
}
.tabulator-table .tabulator-row-even {
@apply border-border bg-background border-b border-none text-black dark:text-white;
.tabulator-frozen {
@apply bg-secondary;
}
.tabulator-frozen input {
@apply border-border border;
}
@media (hover: hover) and (pointer: fine) {
&.tabulator-selectable:hover {
// @apply bg-background hover:bg-muted;
cursor: pointer;
}
}
}
.tabulator-table .tabulator-selected {
@apply !bg-muted;
}
.tabulator-table .tabulator-tree-level-0 .tabulator-cell:first-of-type {
padding-left: 9px;
}
.tabulator-table .tabulator-tree-level-1 .tabulator-cell:first-of-type {
padding-left: 5px;
}
.tabulator-table .tabulator-tree-level-2 .tabulator-cell:first-of-type {
padding-left: 12px;
}
.tabulator-table .tabulator-tree-level-3 .tabulator-cell:first-of-type {
padding-left: 12px;
}
.tabulator-print-table {
.tabulator-print-table-group {
span {
margin-left: 10px;
color: #666;
}
}
}
-145
View File
@@ -1,145 +0,0 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { oldStore, store } from '$lib/stores/auth';
import { hostname, language as langStore } from '$lib/stores/basic';
import type { APIResponse } from '$lib/types/common';
import adze from 'adze';
import axios, { AxiosError } from 'axios';
import { toast } from 'svelte-sonner';
import { get } from 'svelte/store';
export async function login(
username: string,
password: string,
authType: string,
remember: boolean,
language: string
) {
try {
if (username === '' || password === '') {
toast.error('Credentials are required', {
position: 'bottom-center'
});
return;
}
if (authType === '') {
toast.error('Authentication type is required', {
position: 'bottom-center'
});
return;
}
const response = await axios.post('/api/auth/login', {
username,
password,
authType,
remember
});
if (response.status === 200 && response.data) {
if (response.data.data?.hostname && response.data.data?.token) {
langStore.set(language);
hostname.set(response.data.data.hostname);
store.set(response.data.data.token);
return true;
} else {
toast.error('Invalid response received', {
position: 'bottom-center'
});
}
} else {
return false;
}
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
const data = axiosError.response?.data as APIResponse;
if (data.error) {
toast.error('Authentication failed', {
position: 'bottom-center'
});
}
} else {
toast.error('Fatal error logging in, check logs!', {
position: 'bottom-center'
});
}
return false;
}
}
export function getToken(): string | null {
if (browser) {
try {
const parsed = JSON.parse(localStorage.getItem('token') || '');
return parsed.value;
} catch (_e: unknown) {
return null;
}
}
return null;
}
export async function isTokenValid(): Promise<boolean> {
try {
const response = await axios.get('/api/health/basic', {
headers: {
Authorization: `Bearer ${getToken()}`
}
});
if (response.status < 400) {
if (response.data?.hostname) {
hostname.set(response.data.hostname);
}
return true;
}
} catch (_e: unknown) {
return false;
}
return false;
}
export async function logOut() {
const token = getToken();
if (token) {
oldStore.set(token);
}
store.set('');
hostname.set('');
if (browser) {
localStorage.removeItem('token');
localStorage.removeItem('hostname');
}
goto('/', {
replaceState: true,
state: {
loggedOut: true
}
});
}
export async function revokeJWT() {
try {
const oldToken = get(oldStore);
if (oldToken) {
await axios.get('/api/auth/logout', {
headers: {
Authorization: `Bearer ${oldToken}`
}
});
oldStore.set('');
}
} catch (_e: unknown) {
adze.error('Failed to revoke JWT');
}
}
-113
View File
@@ -1,113 +0,0 @@
/**
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2025 The FreeBSD Foundation.
*
* This software was developed by Hayzam Sherif <hayzam@alchemilla.io>
* of Alchemilla Ventures Pvt. Ltd. <hello@alchemilla.io>,
* under sponsorship from the FreeBSD Foundation.
*/
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { store as token } from '$lib/stores/auth';
import type { APIResponse } from '$lib/types/common';
import adze from 'adze';
import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios';
import { toast } from 'svelte-sonner';
import { get } from 'svelte/store';
export let ENDPOINT: string;
export let API_ENDPOINT: string;
if (browser) {
ENDPOINT = window.location.origin;
API_ENDPOINT = `${window.location.origin}/api`;
} else {
ENDPOINT = '';
API_ENDPOINT = '';
}
export const api: AxiosInstance = axios.create({
baseURL: API_ENDPOINT
});
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (browser) {
if (get(token)) {
config.headers['Authorization'] = `Bearer ${get(token)}`;
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && browser) {
toast.error('Session expired, please login again', {
position: 'bottom-center'
});
goto('/login');
return;
}
handleAxiosError(error);
return Promise.reject(error);
}
);
export function handleAxiosError(error: unknown): void {
if (!browser) return;
if (!axios.isAxiosError(error)) {
toast.error('An unexpected error occurred', {
position: 'bottom-center'
});
adze.withEmoji.error('An unexpected error occurred');
return;
}
const axiosError = error as AxiosError<{ message?: string }>;
if (axiosError.response) {
const errorMessage =
axiosError.response.data?.message || axiosError.message || 'An error occurred';
adze.withEmoji.error(
JSON.stringify({
status: axiosError.response.status,
data: axiosError.response.data,
message: errorMessage
})
);
} else if (axiosError.request) {
adze.withEmoji.error('No response:', axiosError.request);
}
}
export function handleAPIResponse(
response: APIResponse,
messages: {
success?: string;
error?: string;
info?: string;
warn?: string;
}
): void {
// console.log('API Response:', response);
if (response.status === 'error') {
adze.withEmoji.error(response);
toast.error(messages.error || 'Operation failed', {
position: 'bottom-center'
});
}
if (response.status === 'success') {
toast.success(messages.success || 'Operation successful', {
position: 'bottom-center'
});
}
}
-33
View File
@@ -1,33 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import { DiskSchema, type Disk } from '$lib/types/disk/disk';
import { apiRequest } from '$lib/utils/http';
import { z } from 'zod/v4';
export async function listDisks(): Promise<Disk[]> {
return await apiRequest('/disk/list', z.array(DiskSchema), 'GET');
}
export async function destroyDisk(disk: string): Promise<APIResponse> {
return await apiRequest(`/disk/wipe`, APIResponseSchema, 'POST', {
device: disk
});
}
export async function destroyPartition(partition: string): Promise<APIResponse> {
return await apiRequest(`/disk/delete-partition`, APIResponseSchema, 'POST', {
device: partition
});
}
export async function initializeGPT(disk: string): Promise<APIResponse> {
return await apiRequest(`/disk/initialize-gpt`, APIResponseSchema, 'POST', {
device: disk
});
}
export async function createPartitions(disk: string, sizes: number[]): Promise<APIResponse> {
return await apiRequest(`/disk/create-partitions`, APIResponseSchema, 'POST', {
device: disk,
sizes
});
}
-27
View File
@@ -1,27 +0,0 @@
import { AuditLogSchema, type AuditLog } from '$lib/types/info/audit';
import { apiRequest } from '$lib/utils/http';
export async function getAuditLogs(): Promise<AuditLog> {
return await apiRequest('/info/audit-logs', AuditLogSchema, 'GET');
}
export function formatAction(action: string): string {
return action;
}
export function formatStatus(status: string): string {
switch (status) {
case 'started':
return 'Started';
case 'success':
return 'OK';
case 'failure':
return 'Failed';
case 'failed':
return 'Failed';
case 'progress':
return 'In Progress';
default:
return status;
}
}
-6
View File
@@ -1,6 +0,0 @@
import { BasicInfoSchema, type BasicInfo } from '$lib/types/info/basic';
import { apiRequest } from '$lib/utils/http';
export async function getBasicInfo(): Promise<BasicInfo> {
return await apiRequest('/info/basic', BasicInfoSchema, 'GET');
}
-20
View File
@@ -1,20 +0,0 @@
import {
CPUInfoHistoricalSchema,
CPUInfoSchema,
type CPUInfo,
type CPUInfoHistorical
} from '$lib/types/info/cpu';
import { apiRequest } from '$lib/utils/http';
import type { QueryFunctionContext } from '@sveltestack/svelte-query';
export async function getCPUInfo(
queryObj?: QueryFunctionContext
): Promise<CPUInfo | CPUInfoHistorical> {
if (queryObj) {
if (queryObj.queryKey.includes('cpuInfoHistorical')) {
return await apiRequest('/info/cpu/historical', CPUInfoHistoricalSchema, 'GET');
}
}
return await apiRequest('/info/cpu', CPUInfoSchema, 'GET');
}
View File
-41
View File
@@ -1,41 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import { NoteSchema, NotesSchema, type Note, type Notes } from '$lib/types/info/notes';
import { apiRequest } from '$lib/utils/http';
import { z } from 'zod/v4';
async function notesRequest(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
body?: object
): Promise<Notes | Note | APIResponse> {
let schema;
if (method === 'GET') {
schema = z.array(NoteSchema);
} else if (method === 'POST') {
schema = NoteSchema;
} else {
schema = APIResponseSchema;
}
return await apiRequest(endpoint, schema, method, body);
}
export const getNotes = () => notesRequest('/info/notes', 'GET');
export const deleteNote = (id: number) => notesRequest(`/info/notes/${id}`, 'DELETE');
export const createNote = async (title: string, content: string): Promise<Note | APIResponse> => {
return (await notesRequest('/info/notes', 'POST', { title, content })) as Note | APIResponse;
};
export const updateNote = async (
id: number,
title: string,
content: string
): Promise<APIResponse> => {
return (await notesRequest(`/info/notes/${id}`, 'PUT', { title, content })) as APIResponse;
};
export const deleteNotes = async (ids: number[]): Promise<APIResponse> => {
return (await notesRequest('/info/notes/bulk-delete', 'POST', { ids })) as APIResponse;
};
-32
View File
@@ -1,32 +0,0 @@
import {
RAMInfoHistoricalSchema,
RAMInfoSchema,
type RAMInfo,
type RAMInfoHistorical
} from '$lib/types/info/ram';
import { apiRequest } from '$lib/utils/http';
import type { QueryFunctionContext } from '@sveltestack/svelte-query';
export async function getRAMInfo(
queryObj?: QueryFunctionContext
): Promise<RAMInfo | RAMInfoHistorical> {
if (queryObj) {
if (queryObj.queryKey.includes('ramInfoHistorical')) {
return await apiRequest('/info/ram/historical', RAMInfoHistoricalSchema, 'GET');
}
}
return await apiRequest('/info/ram', RAMInfoSchema, 'GET');
}
export async function getSwapInfo(
queryObj?: QueryFunctionContext
): Promise<RAMInfo | RAMInfoHistorical> {
if (queryObj) {
if (queryObj.queryKey.includes('swapInfoHistorical')) {
return await apiRequest('/info/swap/historical', RAMInfoHistoricalSchema, 'GET');
}
}
return await apiRequest('/info/swap', RAMInfoSchema, 'GET');
}
-6
View File
@@ -1,6 +0,0 @@
import { IfaceSchema, type Iface } from '$lib/types/network/iface';
import { apiRequest } from '$lib/utils/http';
export async function getInterfaces(): Promise<Iface[]> {
return await apiRequest('/network/interface', IfaceSchema.array(), 'GET');
}
-67
View File
@@ -1,67 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import { SwitchListSchema, type SwitchList } from '$lib/types/network/switch';
import { apiRequest } from '$lib/utils/http';
export async function getSwitches(): Promise<SwitchList> {
return await apiRequest('/network/switch', SwitchListSchema, 'GET');
}
export async function createSwitch(
name: string,
mtu: number,
vlan: number,
address: string,
address6: string,
privateSw: boolean,
dhcp: boolean,
ports: string[],
disableIPv6: boolean,
slaac: boolean
): Promise<APIResponse> {
const body = {
name,
mtu,
vlan,
address,
address6,
private: privateSw,
ports,
dhcp,
disableIPv6,
slaac
};
return await apiRequest('/network/switch/standard', APIResponseSchema, 'POST', body);
}
export async function deleteSwitch(id: number): Promise<APIResponse> {
return await apiRequest(`/network/switch/standard/${id}`, APIResponseSchema, 'DELETE');
}
export async function updateSwitch(
id: number,
mtu: number,
vlan: number,
address: string,
address6: string,
privateSw: boolean,
ports: string[],
disableIPv6: boolean,
slaac: boolean,
dhcp: boolean
): Promise<APIResponse> {
const body = {
id,
mtu,
vlan,
address,
address6,
private: privateSw,
ports,
disableIPv6,
slaac,
dhcp
};
return await apiRequest('/network/switch/standard', APIResponseSchema, 'PUT', body);
}
-24
View File
@@ -1,24 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import {
PCIDeviceSchema,
PPTDeviceSchema,
type PCIDevice,
type PPTDevice
} from '$lib/types/system/pci';
import { apiRequest } from '$lib/utils/http';
export async function getPCIDevices(): Promise<PCIDevice[]> {
return await apiRequest('/system/pci-devices', PCIDeviceSchema.array(), 'GET');
}
export async function getPPTDevices(): Promise<PPTDevice[]> {
return await apiRequest('/system/ppt-devices', PPTDeviceSchema.array(), 'GET');
}
export async function addPPTDevice(domain: string, deviceID: string): Promise<APIResponse> {
return await apiRequest('/system/ppt-devices', APIResponseSchema, 'POST', { domain, deviceID });
}
export async function removePPTDevice(deviceID: string): Promise<APIResponse> {
return await apiRequest(`/system/ppt-devices/${deviceID}`, APIResponseSchema, 'DELETE');
}
@@ -1,30 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import { DownloadSchema, type Download } from '$lib/types/utilities/downloader';
import { apiRequest } from '$lib/utils/http';
export async function getDownloads(): Promise<Download[]> {
return await apiRequest('/utilities/downloads', DownloadSchema.array(), 'GET');
}
export async function startDownload(url: string): Promise<APIResponse> {
return await apiRequest('/utilities/downloads', APIResponseSchema, 'POST', {
url
});
}
export async function deleteDownload(id: number): Promise<APIResponse> {
return await apiRequest(`/utilities/downloads/${id}`, APIResponseSchema, 'DELETE');
}
export async function bulkDeleteDownloads(ids: number[]): Promise<APIResponse> {
return await apiRequest('/utilities/downloads/bulk-delete', APIResponseSchema, 'POST', {
ids
});
}
export async function getSignedURL(name: string, parentUUID: string): Promise<APIResponse> {
return await apiRequest('/utilities/downloads/signed-url', APIResponseSchema, 'POST', {
name,
parentUUID
});
}
-9
View File
@@ -1,9 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import { apiRequest } from '$lib/utils/http';
export async function storageDetach(vmId: number, storageId: number): Promise<APIResponse> {
return await apiRequest(`/vm/storage/detach`, APIResponseSchema, 'POST', {
vmId,
storageId
});
}
-70
View File
@@ -1,70 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import {
VMDomainSchema,
VMSchema,
VMStatSchema,
type CreateData,
type VM,
type VMDomain,
type VMStat
} from '$lib/types/vm/vm';
import { apiRequest } from '$lib/utils/http';
import { z } from 'zod/v4';
export async function getVMs(): Promise<VM[]> {
return await apiRequest('/vm', z.array(VMSchema), 'GET');
}
export async function newVM(data: CreateData): Promise<APIResponse> {
return await apiRequest('/vm', APIResponseSchema, 'POST', {
name: data.name,
vmId: parseInt(data.id.toString(), 10),
iso: data.storage.iso,
storageType: data.storage.type,
storageDataset: data.storage.guid,
storageSize: data.storage.size,
storageEmulationType: data.storage.emulation,
switchId: data.network.switch,
switchEmulationType: data.network.emulation,
macAddress: data.network.mac,
cpuSockets: parseInt(data.hardware.sockets.toString(), 10),
cpuCores: parseInt(data.hardware.cores.toString(), 10),
cpuThreads: parseInt(data.hardware.threads.toString(), 10),
ram: parseInt(data.hardware.memory.toString(), 10),
cpuPinning: data.hardware.pinnedCPUs,
vncPort: data.advanced.vncPort,
vncPassword: data.advanced.vncPassword,
vncWait: data.advanced.vncWait,
vncResolution: data.advanced.vncResolution,
startAtBoot: data.advanced.startAtBoot,
bootOrder: parseInt(data.advanced.bootOrder.toString(), 10),
pciDevices: data.hardware.passthroughIds,
description: data.description
});
}
export async function deleteVM(id: number): Promise<APIResponse> {
return await apiRequest(`/vm/${id}`, APIResponseSchema, 'DELETE');
}
export async function getVMDomain(id: number | string): Promise<VMDomain> {
return await apiRequest(`/vm/domain/${id}`, VMDomainSchema, 'GET');
}
export async function actionVm(id: number | string, action: string): Promise<APIResponse> {
return await apiRequest(`/vm/${id}/${action}`, APIResponseSchema, 'POST');
}
export async function getStats(vmId: number, limit: number): Promise<VMStat[]> {
return await apiRequest(`/vm/stats`, z.array(VMStatSchema), 'POST', {
vmId,
limit
});
}
export async function updateDescription(id: number, description: string): Promise<APIResponse> {
return await apiRequest(`/vm/description`, APIResponseSchema, 'PUT', {
id,
description
});
}
-140
View File
@@ -1,140 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import {
DatasetSchema,
PeriodicSnapshotSchema,
type Dataset,
type PeriodicSnapshot
} from '$lib/types/zfs/dataset';
import { apiRequest } from '$lib/utils/http';
export async function getDatasets(): Promise<Dataset[]> {
return await apiRequest('/zfs/datasets', DatasetSchema.array(), 'GET');
}
export async function deleteSnapshot(
snapshot: Dataset,
recursive: boolean = false
): Promise<APIResponse> {
const param = recursive ? '?recursive=true' : '';
return await apiRequest(
`/zfs/datasets/snapshot/${snapshot.properties.guid}${param}`,
APIResponseSchema,
'DELETE'
);
}
export async function createSnapshot(
dataset: Dataset,
name: string,
recursive: boolean
): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/snapshot', APIResponseSchema, 'POST', {
name: name,
recursive: recursive,
guid: dataset.properties.guid
});
}
export async function getPeriodicSnapshots(): Promise<PeriodicSnapshot[]> {
return await apiRequest('/zfs/datasets/snapshot/periodic', PeriodicSnapshotSchema.array(), 'GET');
}
export async function createPeriodicSnapshot(
dataset: Dataset,
prefix: string,
recursive: boolean,
interval: number
): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/snapshot/periodic', APIResponseSchema, 'POST', {
guid: dataset.properties.guid,
prefix: prefix,
recursive: recursive,
interval: interval
});
}
export async function deletePeriodicSnapshot(guid: string): Promise<APIResponse> {
return await apiRequest(`/zfs/datasets/snapshot/periodic/${guid}`, APIResponseSchema, 'DELETE');
}
export async function createFileSystem(
name: string,
parent: string,
properties: Record<string, string>
): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/filesystem', APIResponseSchema, 'POST', {
name: name,
parent: parent,
properties: properties
});
}
export async function editFileSystem(
guid: string,
properties: Record<string, string>
): Promise<APIResponse> {
return await apiRequest(`/zfs/datasets/filesystem`, APIResponseSchema, 'PATCH', {
guid: guid,
properties: properties
});
}
export async function deleteFileSystem(dataset: Dataset): Promise<APIResponse> {
return await apiRequest(
`/zfs/datasets/filesystem/${dataset.properties.guid}`,
APIResponseSchema,
'DELETE'
);
}
export async function rollbackSnapshot(guid: string): Promise<APIResponse> {
return await apiRequest(`/zfs/datasets/snapshot/rollback`, APIResponseSchema, 'POST', {
guid: guid,
destroyMoreRecent: true
});
}
export async function createVolume(
name: string,
parent: string,
props: Record<string, string>
): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/volume', APIResponseSchema, 'POST', {
name: name,
parent: parent,
properties: props
});
}
export async function editVolume(
dataset: Dataset,
properties: Record<string, string>
): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/volume', APIResponseSchema, 'PATCH', {
name: dataset.name,
properties: properties
});
}
export async function deleteVolume(dataset: Dataset): Promise<APIResponse> {
return await apiRequest(
`/zfs/datasets/volume/${dataset.properties.guid}`,
APIResponseSchema,
'DELETE'
);
}
export async function bulkDelete(datasets: Dataset[]): Promise<APIResponse> {
const guids = datasets.map((dataset) => dataset.properties.guid);
return await apiRequest('/zfs/datasets/bulk-delete', APIResponseSchema, 'POST', {
guids: guids
});
}
export async function flashVolume(guid: string, uuid: string): Promise<APIResponse> {
return await apiRequest('/zfs/datasets/volume/flash', APIResponseSchema, 'POST', {
guid: guid,
uuid: uuid
});
}
-79
View File
@@ -1,79 +0,0 @@
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import {
IODelayHistoricalSchema,
IODelaySchema,
PoolStatPointsResponseSchema,
ZpoolSchema,
type CreateZpool,
type IODelay,
type IODelayHistorical,
type PoolStatPointsResponse,
type ReplaceDevice,
type Zpool
} from '$lib/types/zfs/pool';
import { apiRequest } from '$lib/utils/http';
import type { QueryFunctionContext } from '@sveltestack/svelte-query';
export async function getIODelay(
queryObj: QueryFunctionContext | undefined
): Promise<IODelay | IODelayHistorical> {
if (queryObj) {
if (queryObj.queryKey.includes('ioDelayHistorical')) {
const data = await apiRequest(
'/zfs/pool/io-delay/historical',
IODelayHistoricalSchema,
'GET'
);
return IODelayHistoricalSchema.parse(data);
}
}
return await apiRequest('/zfs/pool/io-delay', IODelaySchema, 'GET');
}
export async function getPools(): Promise<Zpool[]> {
return await apiRequest('/zfs/pools', ZpoolSchema.array(), 'GET');
}
export async function createPool(data: CreateZpool) {
return await apiRequest('/zfs/pools', APIResponseSchema, 'POST', {
...data
});
}
export async function replaceDevice(data: ReplaceDevice) {
return await apiRequest(`/zfs/pools/${data.guid}/replace-device`, APIResponseSchema, 'POST', {
...data
});
}
export async function deletePool(guid: string) {
return await apiRequest(`/zfs/pools/${guid}`, APIResponseSchema, 'DELETE');
}
export async function scrubPool(guid: string) {
return await apiRequest(`/zfs/pools/${guid}/scrub`, APIResponseSchema, 'POST');
}
export async function getPoolStats(
interval: number,
limit: number
): Promise<PoolStatPointsResponse> {
return await apiRequest(
`/zfs/pool/stats/${interval}/${limit}`,
PoolStatPointsResponseSchema,
'GET'
);
}
export async function editPool(
name: string,
properties: Record<string, string>,
spares: string[] = []
): Promise<APIResponse> {
return await apiRequest(`/zfs/pools`, APIResponseSchema, 'PATCH', {
name,
properties,
spares
});
}
@@ -1,123 +0,0 @@
<script>
import { logOut } from '$lib/api/auth';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Sheet from '$lib/components/ui/sheet/index.js';
import { openTerminal, terminalStore } from '$lib/stores/terminal.svelte';
import Icon from '@iconify/svelte';
import { mode, toggleMode } from 'mode-watcher';
import CreateVM from './VM/Create/CreateVM.svelte';
let menuData = $state({
createVM: {
open: false
},
menuItems: [
{ icon: 'ic:baseline-settings', label: 'My Settings', shortcut: '⌘S' },
{ icon: 'solar:key-bold', label: 'Password', shortcut: '⇧⌘P' },
{ icon: 'ic:round-lock', label: 'TFA', shortcut: '⌘K' },
{ icon: 'mdi:palette', label: 'Color Theme', shortcut: '⌘⇧T' },
{ icon: 'meteor-icons:language', label: 'Language', shortcut: '⌘K' }
]
});
</script>
<header class="sticky top-0 flex h-[5vh] items-center gap-4 border-x border-b px-2 md:h-[4vh]">
<nav
class="hidden flex-col gap-2 text-lg font-medium md:items-center md:gap-2 md:text-sm lg:flex lg:flex-row lg:gap-4"
>
<div class="flex items-center space-x-2">
{#if mode.current === 'dark'}
<img src="/logo/white.svg" alt="Sylve Logo" class="h-6 w-auto max-w-[100px]" />
{:else}
<img src="/logo/black.svg" alt="Sylve Logo" class="h-6 w-auto max-w-[100px]" />
{/if}
<p class="font-normal tracking-[.45em]">SYLVE</p>
</div>
</nav>
<Sheet.Root>
<Sheet.Trigger>
<Button variant="outline" size="icon" class="h-7 shrink-0 lg:hidden">
<Icon icon="material-symbols:menu-rounded" class="h-5 w-5" />
<span class="sr-only">Toggle navigation menu</span>
</Button>
</Sheet.Trigger>
<Sheet.Content side="left">
<!-- mobile view -->
<nav class="flex flex-col text-lg font-medium">
<div class="mt-4 flex items-center space-x-2">
<img src="/logo/white.svg" alt="Sylve Logo" class="h-6 w-auto max-w-[100px]" />
<p class="font-normal tracking-[.45em]">SYLVE</p>
</div>
<p class="mt-4 whitespace-nowrap">Virtual Environment 0.0.1</p>
</nav>
</Sheet.Content>
</Sheet.Root>
<div class="flex w-full items-center justify-end gap-2 md:ml-auto">
<!-- desktop view -->
<div class="mr-2 hidden items-center gap-4 lg:inline-flex">
<Button
size="icon"
variant="link"
class="relative z-[9999] flex w-auto items-center justify-center "
onclick={() => openTerminal()}
>
<Icon icon="garden:terminal-cli-stroke-16" class="h-6 w-6" />
{#if $terminalStore.tabs.length > 0}
<span
class="absolute -right-1 top-0.5 flex h-4 min-w-[8px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold text-white"
>
{$terminalStore.tabs.length}
</span>
{/if}
</Button>
<Button
class="h-6"
size="sm"
onclick={() => (menuData.createVM.open = !menuData.createVM.open)}
>
<Icon icon="material-symbols:monitor-outline-rounded" class="mr-1.5 h-5 w-5" />
Create VM
</Button>
{#if menuData.createVM.open}
<CreateVM bind:open={menuData.createVM.open} />
{/if}
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline" size="sm" class="h-6.5"
><Icon icon="mdi:user" class="h-4 w-4" /> Root <Icon
icon="famicons:chevron-down"
class="h-4 w-4"
/>
<span class="sr-only">Toggle user menu</span></Button
>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56">
<DropdownMenu.Group>
{#each menuData.menuItems as { icon, label, shortcut }}
<DropdownMenu.Item
class="cursor-pointer"
onclick={() => label === 'Color Theme' && toggleMode()}
>
<Icon {icon} class="mr-2 h-4 w-4" />
<span>{label}</span>
{#if shortcut}
<DropdownMenu.Shortcut>{shortcut}</DropdownMenu.Shortcut>
{/if}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item class="cursor-pointer" onclick={() => logOut()}>
<Icon icon="ic:twotone-logout" class="mr-2 h-4 w-4" />
<span>Log out</span>
<DropdownMenu.Shortcut>⌘⇧Q</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</header>
@@ -1,133 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import Icon from '@iconify/svelte';
interface Props {
open: boolean;
titles: {
icon?: string;
main: string;
key: string;
value: string;
};
type: string;
KV:
| Record<string, string | number | Record<string, string | number>>
| Array<Record<string, string | number>>;
actions: {
close: () => void;
};
}
let { open, titles, type, KV, actions }: Props = $props();
let tableHeaders = $derived.by(() => {
if (Array.isArray(KV)) {
return Object.keys(KV[0]);
} else {
return [];
}
});
let expandedObjects: Record<string, boolean> = $state({});
function toggleObjectExpansion(key: string) {
expandedObjects[key] = !expandedObjects[key];
}
</script>
<Dialog.Root bind:open>
<Dialog.Content
class="flex max-h-[80vh] w-[90%] flex-col gap-0 overflow-hidden p-5 lg:max-w-4xl"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeydown={(e) => e.preventDefault()}
>
<div class="flex items-center justify-between">
<div class="flex items-center">
{#if titles.icon}
<Icon icon={titles.icon} class="h-6 w-6" />
{/if}
<h2 class="ml-2 text-lg font-semibold">{titles.main}</h2>
</div>
<Dialog.Close
class="flex h-6 w-6 items-center justify-center rounded-sm opacity-70 transition-opacity hover:opacity-100"
onclick={actions.close}
>
<Icon icon="material-symbols:close-rounded" class="h-6 w-6" />
</Dialog.Close>
</div>
<div class="mt-2 max-h-[60vh] overflow-y-auto">
<Table.Root class="w-full table-auto border-collapse">
<Table.Header class="bg-background sticky top-0 z-[50]">
<Table.Row>
{#if tableHeaders.length > 0}
{#each tableHeaders as header}
<Table.Head class="h-10 px-3 py-2">{header}</Table.Head>
{/each}
{:else}
<Table.Head class="h-10 px-3 py-2">{titles.key}</Table.Head>
<Table.Head class="h-10 px-3 py-2">{titles.value}</Table.Head>
{/if}
</Table.Row>
</Table.Header>
<Table.Body>
{#if tableHeaders.length > 0}
{#each KV as Array<Record<string, string | number>> as row}
<Table.Row>
{#each tableHeaders as header}
<Table.Cell class="h-10 px-3 py-2">{row[header]}</Table.Cell>
{/each}
</Table.Row>
{/each}
{:else}
{#each Object.entries(KV) as [key, value]}
{#if typeof value === 'object' && value !== null && !Array.isArray(value)}
<Table.Row>
<Table.Cell class="h-10 w-1/2 whitespace-nowrap px-1 py-2 font-medium">
<button
class="flex w-full items-center gap-1 text-left"
onclick={() => toggleObjectExpansion(key)}
>
<Icon
icon={expandedObjects[key]
? 'material-symbols:keyboard-arrow-down'
: 'material-symbols:keyboard-arrow-right'}
class="h-4 w-4 opacity-70"
/>
{key}
</button>
</Table.Cell>
<Table.Cell class="h-10 px-3 py-2 italic opacity-50">
Object ({Object.keys(value).length} properties)
</Table.Cell>
</Table.Row>
{#if expandedObjects[key]}
{#each Object.entries(value) as [nestedKey, nestedValue]}
<Table.Row>
<Table.Cell class="h-10 py-2 pl-8 pr-3 opacity-90">
{nestedKey}
</Table.Cell>
<Table.Cell class="h-10 px-3 py-2">
{nestedValue}
</Table.Cell>
</Table.Row>
{/each}
{/if}
{:else}
<Table.Row>
<Table.Cell class="h-10 px-3 py-2">{key}</Table.Cell>
<Table.Cell class="h-10 px-3 py-2">{value}</Table.Cell>
</Table.Row>
{/if}
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -1,155 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import { revokeJWT } from '$lib/api/auth';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import Icon from '@iconify/svelte';
import { mode } from 'mode-watcher';
import { onDestroy, onMount } from 'svelte';
interface Props {
onLogin: (
username: string,
password: string,
type: string,
language: string,
remember: boolean
) => void;
}
let { onLogin }: Props = $props();
let username = $state('');
let password = $state('');
let authType = $state('sylve');
let language = $state('en');
let remember = $state(false);
let loading = $state(false);
$effect(() => {
if (page.url.search.includes('loggedOut')) {
revokeJWT();
}
});
async function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
if (loading) return;
loading = true;
try {
onLogin(username, password, authType, language, remember);
} catch (error) {
console.error('Login error:', error);
} finally {
loading = false;
}
}
}
onMount(() => {
window.addEventListener('keydown', handleKeydown);
});
onDestroy(() => {
window.removeEventListener('keydown', handleKeydown);
});
let languageArr = [
{ value: 'en', label: 'English' },
{ value: 'mal', label: 'മലയാളം' }
];
</script>
<div class="fixed inset-0 flex items-center justify-center px-3">
<Card.Root class="w-full max-w-lg rounded-lg shadow-lg">
<Card.Header class="flex flex-row items-center justify-center gap-2">
{#if mode.current === 'dark'}
<img src="/logo/white.svg" alt="Sylve Logo" class="mt-2 h-8 w-auto" />
{:else}
<img src="/logo/black.svg" alt="Sylve Logo" class="h-8 w-auto" />
{/if}
<p class="ml-2 text-xl font-medium tracking-[.45em] text-gray-800 dark:text-white">SYLVE</p>
</Card.Header>
<Card.Content class="space-y-4 p-6">
<div class="flex items-center gap-2">
<Label for="username" class="w-44">Username</Label>
<Input
id="username"
class="h-8 w-full"
type="text"
placeholder="Enter your username"
bind:value={username}
required
/>
</div>
<div class="flex items-center gap-2">
<Label for="password" class="w-44">Password</Label>
<Input
id="password"
type="password"
placeholder="●●●●●●●●"
class="h-8 w-full"
bind:value={password}
required
/>
</div>
<div class="flex items-center gap-2">
<Label for="realm" class="w-44">Realm</Label>
<Select.Root type="single" bind:value={authType}>
<Select.Trigger class="h-8 w-full">
{#if authType === 'pam'}
PAM
{:else if authType === 'sylve'}
Sylve
{/if}
</Select.Trigger>
<Select.Content>
<Select.Item value="pam">PAM</Select.Item>
<Select.Item value="sylve">Sylve</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="flex items-center gap-2" title="Language selection is disabled for now">
<Label for="language" class="w-44">Language</Label>
<Select.Root type="single" bind:value={language} disabled>
<Select.Trigger class="h-8 w-full">
{languageArr.find((lang) => lang.value === language)?.label || 'Select Language'}
</Select.Trigger>
<Select.Content>
<Select.Item value="en">English</Select.Item>
<Select.Item value="mal">Malayalam</Select.Item>
</Select.Content>
</Select.Root>
</div>
</Card.Content>
<Card.Footer class="flex items-center justify-between px-6 py-4">
<div class="flex items-center space-x-2">
<Checkbox id="remember" bind:checked={remember} />
<Label for="remember" class="text-sm font-medium">Remember Me</Label>
</div>
<Button
onclick={() => {
onLogin(username, password, authType, language, remember);
}}
size="sm"
class="w-20 rounded-md bg-blue-700 text-white hover:bg-blue-600"
>
{#if loading}
<Icon icon="line-md:loading-loop" width="24" height="24" />
{:else}
Login
{/if}
</Button>
</Card.Footer>
</Card.Root>
</div>
@@ -1,274 +0,0 @@
<script lang="ts">
import { store } from '$lib/stores/auth';
import { getDefaultTitle, terminalStore } from '$lib/stores/terminal.svelte';
import {
Xterm,
XtermAddon,
type FitAddon,
type ITerminalInitOnlyOptions,
type ITerminalOptions,
type Terminal
} from '@battlefieldduck/xterm-svelte';
import Icon from '@iconify/svelte';
import adze from 'adze';
import { nanoid } from 'nanoid';
import { untrack } from 'svelte';
import { fade, scale } from 'svelte/transition';
let terminal = $state<Terminal>();
let ws = $state<WebSocket>();
let fitAddonGlobal = $state<FitAddon>();
let options: ITerminalOptions & ITerminalInitOnlyOptions = {
cursorBlink: true
};
let tabsCount = $derived.by(() => {
return $terminalStore.tabs.length;
});
let currentTab = $derived.by(() => {
return $terminalStore.tabs.find((tab) => tab.id === $terminalStore.activeTabId);
});
async function killSession(sessionId: string): Promise<boolean> {
return new Promise((resolve) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
resolve(false);
return;
}
const onMessage = (event: MessageEvent) => {
if (event.data) {
if (typeof event.data === 'string') {
if (event.data.includes(`Session killed: ${sessionId}`)) {
ws?.removeEventListener('message', onMessage);
resolve(true);
}
}
}
};
ws.addEventListener('message', onMessage);
ws.send(new TextEncoder().encode('\x02' + JSON.stringify({ kill: sessionId })));
setTimeout(() => {
ws?.removeEventListener('message', onMessage);
resolve(false);
}, 2000);
});
}
async function onLoad() {
try {
if (!currentTab) return;
ws?.close();
terminal?.clear();
terminal?.reset();
const fitAddon = new (await XtermAddon.FitAddon()).FitAddon();
terminal?.loadAddon(fitAddon);
fitAddon.fit();
ws = new WebSocket(`/api/info/terminal?id=${currentTab?.id}`, ['Bearer', $store]);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
if (!currentTab) return;
adze.info(`Terminal WebSocket connected for tab ${currentTab?.id}`);
if (terminal) {
const dimensions = fitAddon.proposeDimensions();
(ws as WebSocket).send(
new TextEncoder().encode(
'\x01' + JSON.stringify({ rows: dimensions?.rows, cols: dimensions?.cols })
)
);
fitAddonGlobal = fitAddon;
}
};
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
if (terminal) {
terminal.write(new Uint8Array(event.data));
}
}
};
ws.onclose = () => {
if (!currentTab) return;
adze.info(`Terminal WebSocket disconnected for tab ${currentTab?.id}`);
};
} catch (e) {
adze.error('Failed to connect to terminal WebSocket', { error: e });
}
}
function onData(data: string) {
ws?.send(new TextEncoder().encode('\x00' + data));
}
async function visiblityAction(t: string, e?: MouseEvent | string) {
if (t === 'window-minimize') {
$terminalStore.isMinimized = true;
return;
}
if (t === 'window-close') {
const tabsToKill = [...$terminalStore.tabs];
for (const tab of tabsToKill) {
await killSession(tab.id);
}
$terminalStore.tabs = [];
$terminalStore.isOpen = false;
ws?.close();
}
if (t === 'tab-close') {
const event = e as MouseEvent;
if (event) {
const target = event.target as HTMLElement;
const parent = target.closest('button');
if (parent) {
const tabId = parent.getAttribute('data-id');
if (tabId) {
await killSession(tabId);
$terminalStore.tabs = $terminalStore.tabs.filter((tab) => tab.id !== tabId);
if ($terminalStore.tabs.length > 0) {
$terminalStore.activeTabId = $terminalStore.tabs[0].id;
}
}
}
}
}
if (t === 'tab-select') {
const tabId = e as string;
$terminalStore.activeTabId = tabId;
}
}
function addTab() {
const newTab = {
id: nanoid(5),
title: getDefaultTitle()
};
$terminalStore.tabs = [...$terminalStore.tabs, newTab];
$terminalStore.activeTabId = newTab.id;
}
let innerWidth = $state(0);
$effect(() => {
if (innerWidth) {
untrack(() => {
fitAddonGlobal?.fit();
const dimensions = fitAddonGlobal?.proposeDimensions();
ws?.send(
new TextEncoder().encode(
'\x01' + JSON.stringify({ rows: dimensions?.rows, cols: dimensions?.cols })
)
);
});
}
});
</script>
<svelte:window bind:innerWidth />
{#if $terminalStore.isOpen && !$terminalStore.isMinimized}
<div
class="fixed inset-0 z-[9998] bg-black/30 backdrop-blur-sm transition-all duration-150"
></div>
<div
class="fixed inset-0 z-[9999] flex items-center justify-center transition-all duration-150"
in:scale={{ start: 0.9, duration: 150 }}
out:scale={{ start: 0.9, duration: 150 }}
>
<div
class="border-muted bg-muted-foreground/10 relative flex w-[60%] flex-col rounded-lg border-4"
>
<div class="bg-primary-foreground flex items-center justify-between p-2">
<!-- Add Tab Button -->
<div class="flex items-center gap-2">
<span>{$terminalStore.title}</span>
</div>
<!-- Minimize / Close -->
<div class="flex space-x-3">
<button
class="rounded-full transition-colors duration-300 ease-in-out hover:bg-yellow-600 hover:text-white"
onclick={() => visiblityAction('window-minimize')}
title="Minimize"
>
<Icon icon="mdi:window-minimize" class="h-5 w-5" />
</button>
<button
class="rounded-full transition-colors duration-300 ease-in-out hover:bg-red-500 hover:text-white"
onclick={() => visiblityAction('window-close')}
title="Close"
>
<Icon icon="mdi:close" class="h-5 w-5" />
</button>
</div>
</div>
<!-- Available Tabs -->
<div class="dark:bg-muted/30 flex overflow-x-auto bg-white">
{#each $terminalStore.tabs as tab}
<div
class="border-muted-foreground/40 flex cursor-pointer items-center px-3.5 py-2 {tab.id ===
$terminalStore.activeTabId
? 'bg-muted-foreground/40 dark:bg-muted-foreground/25 '
: 'border-muted-foreground/25 hover:bg-muted-foreground/25 border-x border-t'}"
onclick={() => visiblityAction('tab-select', tab.id)}
onkeydown={(e) =>
(e.key === 'Enter' || e.key === ' ') && visiblityAction('tab-select', tab.id)}
role="button"
tabindex="0"
>
<span class="mr-2 whitespace-nowrap text-sm">{tab.title}</span>
{#if tabsCount > 1}
<button
class="rounded-full transition-colors duration-300 ease-in-out hover:bg-red-500 hover:text-white"
data-id={tab.id}
onclick={(e) => {
e.stopPropagation();
visiblityAction('tab-close', e);
}}
>
<Icon icon="mdi:close" class="h-4 w-4" />
</button>
{/if}
</div>
{/each}
<div
class="hover:border-muted-foreground/30 hover:bg-muted-foreground/30 flex items-center justify-center border px-1"
>
<button
class="dark:hover-bg-muted flex h-6 w-6 items-center justify-center rounded"
onclick={() => addTab()}
title="Add new tab"
>
<Icon icon="ic:sharp-plus" class="h-5 w-5" />
</button>
</div>
</div>
<!-- Terminal Body -->
<div
id="terminal-container"
class="relative min-h-0 w-full flex-grow overflow-hidden bg-black"
>
{#each $terminalStore.tabs as tab}
{#if tab.id === $terminalStore.activeTabId}
<div in:fade={{ duration: 150 }}>
<Xterm bind:terminal {options} {onLoad} {onData} />
</div>
{/if}
{/each}
</div>
</div>
</div>
{/if}
@@ -1,195 +0,0 @@
<script>
import { onMount } from 'svelte';
let darkMode = $state(false);
onMount(() => {
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
});
</script>
<div class="flex h-screen flex-col items-center justify-center gap-3 overflow-hidden">
<svg
version="1.1"
id="thrb"
width="120"
height="136"
viewBox="0 0 624.98667 707.78264"
xmlns="http://www.w3.org/2000/svg"
>
<defs id="defs1">
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath2">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-901.62842,-925.7178)"
id="path2"
/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath4">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-966.65875,-884.12602)"
id="path4"
/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath6">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-847.32865,-944.4102)"
id="path6"
/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath8">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-637.24852,-944.4102)"
id="path8"
/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath10">
<path
d="M 0,2000 H 2000 V 0 H 0 Z"
transform="translate(-742.28862,-1129.9302)"
id="path10"
/>
</clipPath>
</defs>
<g id="layer-MC0" transform="translate(-1048.7193,-975.76448)">
<path
id="thrbP1"
d="m 0,0 c 0,-7.946 -4.231,-15.291 -11.105,-19.276 l -104.371,-60.512 c -7.887,-4.573 -17.764,1.117 -17.764,10.234 V 58.619 c 0,7.944 4.229,15.287 11.1,19.273 l 104.376,60.52 C -9.877,142.985 0,137.294 0,128.178 Z m -145.311,108.103 c -8.948,-5.189 -19.99,-5.189 -28.939,0 l -103.426,59.971 c -6.451,3.74 -6.451,13.056 10e-4,16.797 l 103.988,60.293 c 8.947,5.188 19.987,5.188 28.935,0 L -41.324,185.2 c 6.451,-3.74 6.451,-13.055 0,-16.797 z M -185.44,-66.169 c 0,-10.557 -11.438,-17.146 -20.57,-11.851 l -100.67,58.374 c -7.428,4.307 -12,12.244 -12,20.83 v 123.232 c 0,10.556 11.437,17.145 20.57,11.85 l 100.68,-58.374 c 7.422,-4.308 11.99,-12.242 11.99,-20.824 z m 210.08,256.781 -13.84,8.02 -10.8,6.26 -57.79,33.51 -63.92,36.9 -26.38,15.23 c -6.96,4.02 -15.54,4.02 -22.5,0 l -26.26,-15.159 -6.43,-3.711 -115.4,-66.91 -11.13,-6.45 -14.22,-8.25 -0.03,-0.02 c -5.14,-4.24 -8.19,-10.579 -8.19,-17.36 v -196.77 c 0,-7.12 3.36,-13.759 8.97,-17.98 l 24.6,-14.26 52.9,-30.68 69.07,-39.87 11.27,-6.509 14.85,-8.571 c 6.96,-4.019 15.54,-4.019 22.5,0 l 14.85,8.571 11.39,6.58 121.85,70.339 5.42,3.14 17.97,10.41 0.04,0.031 c 6.3,4.139 10.14,11.2 10.14,18.799 v 196.77 c 0,7.09 -3.35,13.72 -8.93,17.94"
style={darkMode
? 'fill:#4c4847;fill-opacity:1;fill-rule:nonzero;stroke:none'
: 'fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none'}
transform="matrix(1.3333333,0,0,-1.3333333,1573.6655,1428.6981)"
clip-path="url(#clipPath2)"
/>
<path
id="thrbP3"
d="m 0,0 c 0,-8.456 -4.512,-16.271 -11.835,-20.5 l -200.7,-115.873 c -3.664,-2.115 -7.746,-3.171 -11.835,-3.172 -4.089,10e-4 -8.172,1.057 -11.836,3.172 L -436.905,-20.5 c -7.324,4.229 -11.836,12.044 -11.835,20.5 v 231.748 c -10e-4,8.456 4.511,16.272 11.835,20.5 l 200.699,115.873 c 3.664,2.115 7.747,3.171 11.836,3.171 4.089,0 8.171,-1.056 11.835,-3.171 l 200.7,-115.873 C -4.512,248.02 0,240.204 0,231.748 Z m -6.835,260.908 -200.7,115.873 c -5.207,3.007 -11.024,4.512 -16.835,4.511 -5.811,0.001 -11.629,-1.504 -16.836,-4.511 L -441.905,260.908 c -10.417,-6.015 -16.835,-17.131 -16.835,-29.16 L -458.74,0 c 0,-12.029 6.418,-23.145 16.835,-29.16 l 200.699,-115.873 c 5.207,-3.007 11.025,-4.512 16.836,-4.512 5.811,0 11.629,1.505 16.835,4.512 L -6.835,-29.16 C 3.582,-23.145 10,-12.029 10,0 v 231.748 c 0,12.029 -6.417,23.145 -16.835,29.16"
style="fill:#aaaaaa;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1660.3726,1484.1538)"
clip-path="url(#clipPath4)"
/>
<path
id="thrbP5"
d="M 0,0 C 0,-3.13 -1.67,-6.021 -4.38,-7.58 L -63.94,-41.97 V -53.521 L 0.62,-16.24 C 6.42,-12.89 10,-6.7 10,0 V 86.99 L 0,81.19 Z"
style="fill:#aaaaaa;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1501.2658,1403.7749)"
clip-path="url(#clipPath6)"
/>
<path
id="thrbP7"
d="m 0,0 v 80.68 l -10,5.79 V 0 c 0,-6.7 3.58,-12.89 9.38,-16.24 L 63.94,-53.521 V -41.97 L 4.38,-7.58 C 1.67,-6.021 0,-3.13 0,0"
style="fill:#aaaaaa;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1221.1589,1403.7749)"
clip-path="url(#clipPath8)"
/>
<path
id="thrbP9"
d="m 0,0 c -3.24,0 -6.48,-0.84 -9.38,-2.51 l -68.67,-39.65 9.98,-5.78 63.69,36.77 c 1.36,0.78 2.87,1.17 4.38,1.17 1.51,0 3.02,-0.39 4.38,-1.17 L 67.63,-47.69 77.61,-41.9 9.38,-2.51 C 6.48,-0.84 3.24,0 0,0"
style="fill:#aaaaaa;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1361.2124,1156.4149)"
clip-path="url(#clipPath10)"
/>
</g>
</svg>
<p
class="animate-blurFadePerfect ml-3 flex w-full justify-center text-center font-[500] tracking-[.45em] opacity-0"
>
SYLVE
</p>
</div>
<style>
@keyframes blurFadePerfect {
0% {
opacity: 0;
filter: blur(12px);
transform: scale(1.05);
}
60% {
opacity: 1;
filter: blur(4px);
transform: scale(1);
}
100% {
opacity: 1;
filter: blur(0);
transform: scale(1);
}
}
.animate-blurFadePerfect {
animation: blurFadePerfect 1s ease-out forwards;
}
#thrb {
transform-origin: center;
animation: thrbApp 1s ease-out forwards;
}
#thrbP1,
#thrbP3,
#thrbP5,
#thrbP7,
#thrbP9 {
opacity: 0;
animation: thrbFadeIn 0.5s ease-out forwards;
}
#thrbP1 {
animation-delay: 0.2s;
}
#thrbP3 {
animation-delay: 0.4s;
}
#thrbP5 {
animation:
thrbFadeIn 0.5s ease-out forwards 0.6s,
thrbPulseSeq 3s ease-in-out infinite 2s;
}
#thrbP7 {
animation:
thrbFadeIn 0.5s ease-out forwards 0.8s,
thrbPulseSeq 3s ease-in-out infinite 3s;
}
#thrbP9 {
animation:
thrbFadeIn 0.5s ease-out forwards 1s,
thrbPulseSeq 3s ease-in-out infinite 4s;
}
@keyframes thrbApp {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes thrbFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes thrbPulseSeq {
0%,
15%,
100% {
opacity: 1;
}
5%,
10% {
opacity: 0.3;
}
}
</style>
@@ -1,158 +0,0 @@
<script lang="ts">
import type { Column, Row } from '$lib/types/components/tree-table';
import { hasRowsChanged, matchAny } from '$lib/utils/table';
import { findRow, getAllRows, pruneEmptyChildren } from '$lib/utils/tree-table';
import { onMount, untrack } from 'svelte';
import {
TabulatorFull as Tabulator,
type ColumnDefinition,
type RowComponent
} from 'tabulator-tables';
let tableComponent: HTMLDivElement | null = null;
let table: Tabulator | null = $state(null);
interface Props {
data: {
rows: Row[];
columns: Column[];
};
name: string;
parentActiveRow?: Row[] | null;
query?: string;
multipleSelect?: boolean;
}
let {
data,
name,
parentActiveRow = $bindable([]),
query = $bindable(),
multipleSelect = true
}: Props = $props();
let tableInitialized = $state(false);
let scroll = $state([0, 0]);
let aboutToClick = $state(false);
function updateParentActiveRows() {
if (tableInitialized) {
parentActiveRow = table?.getSelectedRows().map((r) => r.getData() as Row) || [];
}
}
$effect(() => {
if (data.rows) {
untrack(async () => {
if (query && query !== '') return;
if (data.rows.length === 0) {
table?.clearData();
return;
}
const selectedIds = table?.getSelectedRows().map((row) => row.getData().id) || [];
const treeExpands = getAllRows(table?.getRows() || []).map((row) => ({
id: row.getData().id,
expanded: row.isTreeExpanded()
}));
if (hasRowsChanged(table, data.rows) && !aboutToClick) {
await table?.replaceData(pruneEmptyChildren(data.rows));
}
selectedIds.forEach((id) => {
const row = findRow(table?.getRows() || [], id);
if (row) row.select();
});
treeExpands.forEach((treeExpand) => {
const row = findRow(table?.getRows() || [], treeExpand.id);
if (row) {
treeExpand.expanded ? row.treeExpand() : row.treeCollapse();
}
});
updateParentActiveRows();
});
}
});
onMount(() => {
if (tableComponent) {
table = new Tabulator(tableComponent, {
data: pruneEmptyChildren(data.rows),
reactiveData: true,
columns: data.columns as ColumnDefinition[],
layout: 'fitColumns',
selectableRows: multipleSelect ? true : 1,
dataTreeChildIndent: 16,
dataTree: true,
dataTreeChildField: 'children',
dataTreeStartExpanded: true,
persistenceID: name,
paginationMode: 'local',
persistence: {
sort: true,
page: true,
filter: true
},
placeholder: 'No data available',
pagination: true,
paginationSize: 25,
paginationCounter: 'pages'
});
}
table?.on('rowSelected', updateParentActiveRows);
table?.on('rowDeselected', updateParentActiveRows);
table?.on('rowDblClick', (_event: UIEvent, row: RowComponent) => {
row.toggleSelect();
});
table?.on('tableBuilt', () => {
tableInitialized = true;
document.querySelector('.tabulator-footer')?.addEventListener('mouseover', () => {
aboutToClick = true;
});
document.querySelector('.tabulator-footer')?.addEventListener('mouseout', () => {
aboutToClick = false;
});
});
table?.on('scrollVertical', (top) => {
scroll = [top, scroll[1]];
});
table?.on('scrollHorizontal', (left) => {
scroll = [scroll[0], left];
});
table?.on('renderComplete', () => {
const container = document.querySelector('.tabulator-tableholder') as HTMLDivElement;
if (container) {
container.scrollTop = scroll[0];
container.scrollLeft = scroll[1];
}
});
});
function tableFilter(query: string) {
if (table && tableInitialized) {
if (query === '') {
table.clearFilter(true);
return;
}
table.setFilter(matchAny, { query });
}
}
$effect(() => {
tableFilter(query || '');
});
</script>
<div bind:this={tableComponent} class="flex-1 cursor-pointer" id={name}></div>
@@ -1,62 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { onMount } from 'svelte';
import { cubicOut } from 'svelte/easing';
import { fade, slide } from 'svelte/transition';
let expanded = $state(false);
interface Props {
query: string;
}
let { query = $bindable() }: Props = $props();
function toggleSearch() {
expanded = !expanded;
if (expanded) {
requestAnimationFrame(() => {
const input = document.getElementById('search-input');
input?.focus();
});
}
}
onMount(() => {
if (query !== '') {
expanded = true;
}
});
</script>
<div class="relative">
<div
class="bg-primary text-primary-foreground flex h-6 items-center overflow-hidden rounded-lg transition-[width] duration-300 ease-in-out"
style="width: {expanded ? '16rem' : '1.5rem'}"
>
<button
class="flex h-6 w-6 min-w-[1.5rem] shrink-0 items-center justify-center"
onclick={toggleSearch}
>
<Icon icon="mdi:magnify" class="h-5 w-5" />
</button>
{#if expanded}
<input
id="search-input"
bind:value={query}
type="text"
placeholder={'Search...'}
class="bg-primary ml-1 w-full text-sm leading-4 focus:outline-none"
in:slide={{ duration: 250, easing: cubicOut, axis: 'x' }}
out:fade={{ duration: 150 }}
onkeydown={(e) => {
if (e.key === 'Escape') {
query = '';
expanded = false;
}
}}
/>
{/if}
</div>
</div>
@@ -1,98 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Icon from '@iconify/svelte';
import { slide } from 'svelte/transition';
import SidebarElement from './TreeView.svelte';
interface SidebarProps {
label: string;
icon: string;
href?: string;
children?: SidebarProps[];
}
interface Props {
item: SidebarProps;
onToggle: (label: string) => void;
}
let { item, onToggle }: Props = $props();
let isOpen = $state(false);
const toggle = (e: MouseEvent) => {
e.preventDefault();
if (item.children) {
isOpen = !isOpen;
onToggle(item.label);
}
if (item.href) {
goto(item.href, { replaceState: false, noScroll: false });
}
};
const sidebarActive = 'rounded-md bg-muted dark:bg-muted font-inter font-medium';
function isItemActive(menuItem: SidebarProps, currentUrl: string): boolean {
if (menuItem.href && currentUrl.startsWith(menuItem.href)) {
return true;
}
if (menuItem.children) {
return menuItem.children.some((child) => isItemActive(child, currentUrl));
}
return false;
}
let activeUrl = $derived(page.url.pathname);
let isActive = $derived(isItemActive(item, activeUrl));
let lastActiveUrl = $derived.by(() => {
const segments = activeUrl.split('/');
return segments[segments.length - 1];
});
function isItemOpen(menuItem: SidebarProps, currentUrl: string): boolean {
if (menuItem.href && currentUrl.startsWith(menuItem.href)) {
return true;
}
if (menuItem.children) {
return menuItem.children.some((child) => isItemOpen(child, currentUrl));
}
return false;
}
$effect(() => {
isOpen = isItemOpen(item, activeUrl);
});
</script>
<li class={`w-full`}>
<a
class={`my-0.5 flex w-full items-center justify-between px-1.5 py-0.5 ${isActive ? sidebarActive : 'hover:bg-muted dark:hover:bg-muted rounded-md'}${lastActiveUrl === item.label ? '!text-primary' : ' '}`}
href={item.href}
onclick={toggle}
>
<div class="flex items-center space-x-1 text-sm">
<Icon icon={item.icon} width="18" />
<p class="font-inter cursor-pointer whitespace-nowrap">
{item.label}
</p>
</div>
{#if item.children}
<Icon
icon={isOpen ? 'teenyicons:down-solid' : 'teenyicons:right-solid'}
class="h-3.5 w-3.5"
/>
{/if}
</a>
</li>
{#if isOpen && item.children}
<ul class="pl-5" transition:slide={{ duration: 200, easing: (t) => t }} style="overflow: hidden;">
{#each item.children as child}
<SidebarElement item={child} {onToggle} />
{/each}
</ul>
{/if}
@@ -1,258 +0,0 @@
<script lang="ts">
import { createPartitions } from '$lib/api/disk/disk';
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Slider } from '$lib/components/ui/slider/index.js';
import * as Table from '$lib/components/ui/table';
import type { Disk } from '$lib/types/disk/disk';
import Icon from '@iconify/svelte';
import humanFormat from 'human-format';
import { tick } from 'svelte';
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
interface Data {
open: boolean;
disk: Disk | null;
onCancel: () => void;
}
let { open, disk, onCancel }: Data = $props();
let newPartitions: { name: string; size: number }[] = $state([]);
let currentPartitionInput = $state('0 B');
let currentPartition = $derived.by(() => {
try {
const parsed = humanFormat.parse.raw(currentPartitionInput);
return parsed.factor * parsed.value;
} catch (e) {
return 0;
}
});
function removePartition(index: number) {
const removedPartition = newPartitions.splice(index, 1)[0];
remainingSpace += removedPartition.size;
}
async function savePartitions() {
if (disk) {
const sizes = newPartitions.map((partition) => Math.floor(partition.size));
const result = await createPartitions(`/dev/${disk.device}`, sizes);
let message = '';
if (result.status === 'success') {
message = `Partition${sizes.length > 1 ? 's' : ''} created`;
} else {
message = `Error creating ${sizes.length > 1 ? 'partitions' : 'partition'}`;
}
toast.success(message, {
position: 'bottom-center'
});
newPartitions = [];
}
onCancel();
}
async function addPartition() {
if (currentPartition > 0) {
newPartitions.push({
name: `New Partition ${newPartitions.length + 1}`,
size: currentPartition
});
remainingSpace -= currentPartition;
currentPartition = 0;
currentPartitionInput = '0B';
await tick();
const table = document.getElementById('table-body');
if (table) {
table.scroll({
top: table.scrollHeight,
behavior: 'smooth'
});
}
}
}
function close() {
newPartitions = [];
remainingSpace = 0;
currentPartition = 0;
onCancel();
}
function calculateRemainingSpace(disk: Disk) {
if (!disk) return 0;
const usedSpace =
disk.partitions && disk.partitions.length > 0
? disk.partitions.reduce((total, partition) => total + partition.size, 0)
: 0;
let actual = disk.size - usedSpace;
if (actual > 128 * 1024 * 1024) {
actual = actual - 128 * 1024 * 1024;
}
return actual;
}
let remainingSpace = $derived.by(() => (disk ? calculateRemainingSpace(disk) : 0));
</script>
<Dialog.Root bind:open>
<Dialog.Content
class="fixed left-1/2 top-1/2 w-[80%] -translate-x-1/2 -translate-y-1/2 transform gap-4 overflow-hidden p-5 lg:max-w-3xl"
>
<div class="flex items-center justify-between">
<Dialog.Header class="p-0">
<Dialog.Title>Create Partitions</Dialog.Title>
<Dialog.Description></Dialog.Description>
</Dialog.Header>
<div class="flex items-center gap-0.5">
<Button
size="sm"
variant="link"
class="h-4 cursor-pointer"
title={'Reset'}
onclick={() => {
newPartitions = [];
remainingSpace = disk ? calculateRemainingSpace(disk) : 0;
currentPartition = 0;
currentPartitionInput = '';
}}
>
<Icon icon="radix-icons:reset" class="pointer-events-none h-4 w-4" />
<span class="sr-only">Reset</span>
</Button>
<Button
size="sm"
variant="link"
class="h-4 cursor-pointer"
title={'Close'}
onclick={() => close()}
>
<Icon icon="material-symbols:close-rounded" class="pointer-events-none h-4 w-4" />
<span class="sr-only">Close</span>
</Button>
</div>
</div>
<div class="max-h-[300px] overflow-y-auto" id="table-body">
<Table.Root>
<Table.Header class="bg-background sticky top-0 z-10">
<Table.Row>
<Table.Head class="w-[200px]">Name</Table.Head>
<Table.Head class="w-[150px] text-right">Size</Table.Head>
<Table.Head class="w-[150px] text-right">Usage</Table.Head>
<Table.Head class="w-[100px] text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if disk && disk.partitions && disk.partitions.length > 0}
{#each disk.partitions as partition}
<Table.Row>
<Table.Cell>{partition.name}</Table.Cell>
<Table.Cell class="text-right">{humanFormat(partition.size)}</Table.Cell>
<Table.Cell class="text-right">{partition.usage}</Table.Cell>
<Table.Cell class="text-right">
<span class="text-muted-foreground text-xs italic">Existing</span>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if newPartitions.length > 0}
{#each newPartitions as partition, index}
<Table.Row>
<Table.Cell>{partition.name}</Table.Cell>
<Table.Cell class="text-right">{humanFormat(partition.size)}</Table.Cell>
<Table.Cell class="text-right">-</Table.Cell>
<Table.Cell class="text-right">
<Button variant="ghost" class="h-8" onclick={() => removePartition(index)}>
<Icon icon="gg:trash" class="h-4 w-4" />
</Button>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if (!disk || !disk.partitions || disk.partitions.length === 0) && newPartitions.length === 0}
<Table.Row>
<Table.Cell colspan={4} class="text-muted-foreground h-20 text-center">
No partitions created yet
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="space-y-2 border-t pt-4">
<div class="flex items-center gap-6">
<div class="flex-1">
{#if remainingSpace > 0}
<!-- <Slider
type="single"
bind:value={currentPartition}
max={remainingSpace}
step={0.1}
onValueCommit={(value: number) => {
currentPartition = value <= 0 ? 0 : value;
currentPartitionInput = humanFormat(currentPartition);
console.log('Slider value committed:', value);
}}
/> -->
{/if}
</div>
<Input
type="text"
class="h-8 w-24 text-right"
min="0"
max={remainingSpace}
bind:value={currentPartitionInput}
/>
<div class={remainingSpace > 0 ? '' : 'cursor-not-allowed'}>
<Button
class="h-8 whitespace-nowrap"
onclick={addPartition}
disabled={currentPartition <= 0}
>
{#if remainingSpace > 0}
Add Partition
{:else}
No space left
{/if}
</Button>
</div>
</div>
<div class="flex flex-col items-end gap-2">
<p class="text-muted-foreground text-sm">
Size: {humanFormat(currentPartition)}
</p>
<p class="text-muted-foreground text-sm">
Remaining space: {humanFormat(remainingSpace)}
</p>
</div>
</div>
{#if newPartitions.length > 0}
<div in:slide={{ duration: 200 }} out:slide={{ duration: 200 }}>
<Dialog.Footer class="flex justify-between gap-2 border-t px-6 py-4">
<div class="flex gap-2">
<Button size="sm" class="h-8" onclick={savePartitions}>Save Partitions</Button>
</div>
</Dialog.Footer>
</div>
{/if}
</Dialog.Content>
</Dialog.Root>
@@ -1,65 +0,0 @@
<script lang="ts">
import { formatAction, formatStatus, getAuditLogs } from '$lib/api/info/audit';
import * as Table from '$lib/components/ui/table/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import type { AuditLog } from '$lib/types/info/audit';
import { convertDbTime } from '$lib/utils/time';
import { useQueries } from '@sveltestack/svelte-query';
const results = useQueries([
{
queryKey: ['auditLog'],
queryFn: async () => {
return await getAuditLogs();
},
refetchInterval: 1000,
keepPreviousData: true
}
]);
let logs = $derived($results[0].data as AuditLog);
</script>
<Tabs.Root value="cluster" class="flex h-full w-full flex-col">
<Tabs.Content value="cluster" class="flex h-full flex-col border-x border-b">
<div class="flex h-full flex-col overflow-hidden">
<Table.Root class="w-full table-fixed border-collapse">
<Table.Header class="bg-background sticky top-0 z-[50] ">
<Table.Row class="dark:hover:bg-background ">
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>Start Time</Table.Head
>
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>End Time</Table.Head
>
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>Node</Table.Head
>
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>User</Table.Head
>
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>Action</Table.Head
>
<Table.Head class="h-10 px-4 py-2 font-semibold text-black dark:text-white"
>Status</Table.Head
>
</Table.Row>
</Table.Header>
<Table.Body class="flex-grow overflow-auto pb-32">
{#each logs as log, i (i)}
<Table.Row>
<Table.Cell class="h-10 px-4 py-2">{convertDbTime(log.started)}</Table.Cell>
<Table.Cell class="h-10 px-4 py-2">{convertDbTime(log.ended)}</Table.Cell>
<Table.Cell class="h-10 px-4 py-2">{log.node}</Table.Cell>
<Table.Cell class="h-10 px-4 py-2">{`${log.user}@${log.authType}`}</Table.Cell>
<Table.Cell class="h-10 px-4 py-2">{formatAction(log.action)}</Table.Cell>
<Table.Cell class="h-10 px-4 py-2">{formatStatus(log.status)}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
</Tabs.Content>
</Tabs.Root>
@@ -1,60 +0,0 @@
<script lang="ts">
import { getVMs } from '$lib/api/vm/vm';
import { default as TreeView } from '$lib/components/custom/TreeView.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { hostname } from '$lib/stores/basic';
import type { VM } from '$lib/types/vm/vm';
import { useQueries } from '@sveltestack/svelte-query';
let openCategories: { [key: string]: boolean } = $state({});
let node = $hostname;
const toggleCategory = (label: string) => {
openCategories[label] = !openCategories[label];
};
const results = useQueries([
{
queryKey: ['vms-list'],
queryFn: async () => {
return await getVMs();
},
refetchInterval: 1000,
keepPreviousData: true,
initialData: [] as VM[],
refetchOnMount: 'always'
}
]);
const vms = $derived($results[0].data || []);
const tree = $derived([
{
label: 'Data Center',
icon: 'fa-solid:server',
children: [
{
label: node,
icon: 'mdi:dns',
href: `/${node}`,
children: vms.map((vm) => ({
label: `${vm.name} (${vm.vmId})`,
icon: 'material-symbols:monitor-outline',
href: `/${node}/vm/${vm.vmId}`
}))
}
]
}
]);
</script>
<div class="h-full overflow-y-auto px-1.5 pt-1">
<nav aria-label="Difuse-sidebar" class="menu thin-scrollbar w-full">
<ul>
<ScrollArea orientation="both" class="h-full w-full">
{#each tree as item}
<TreeView {item} onToggle={toggleCategory} bind:this={openCategories} />
{/each}
</ScrollArea>
</ul>
</nav>
</div>
@@ -1,52 +0,0 @@
<script lang="ts">
import Header from '$lib/components/custom/Header.svelte';
import * as Resizable from '$lib/components/ui/resizable';
import Terminal from '$lib/components/custom/Terminal.svelte';
import BottomPanel from '$lib/components/skeleton/BottomPanel.svelte';
import LeftPanel from '$lib/components/skeleton/LeftPanel.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="flex min-h-screen w-full flex-col">
<Header />
<main class="flex flex-1 flex-col">
<div class="h-[95vh] w-full md:h-[96vh]">
<Resizable.PaneGroup
direction="vertical"
id="child-pane-auto"
autoSaveId="child-pane-auto-save"
>
<Resizable.Pane>
<Resizable.PaneGroup
direction="horizontal"
id="child-left-pane-auto"
autoSaveId="child-left-pane-auto-save"
>
<Resizable.Pane defaultSize={12} class="border-l">
<LeftPanel />
</Resizable.Pane>
<Resizable.Handle withHandle />
<Resizable.Pane class="border-r">
{@render children?.()}
</Resizable.Pane>
</Resizable.PaneGroup>
</Resizable.Pane>
<Resizable.Handle withHandle />
<Resizable.Pane class="h-full min-h-20" defaultSize={10}>
<BottomPanel />
</Resizable.Pane>
</Resizable.PaneGroup>
</div>
<Terminal />
</main>
</div>
@@ -1,18 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.ActionProps = $props();
</script>
<AlertDialogPrimitive.Action
bind:ref
data-slot="alert-dialog-action"
class={cn(buttonVariants(), className)}
{...restProps}
/>
@@ -1,18 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.CancelProps = $props();
</script>
<AlertDialogPrimitive.Cancel
bind:ref
data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant: "outline" }), className)}
{...restProps}
/>
@@ -1,27 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
} = $props();
</script>
<AlertDialogPrimitive.Portal {...portalProps}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
bind:ref
data-slot="alert-dialog-content"
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...restProps}
/>
</AlertDialogPrimitive.Portal>
@@ -1,17 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.DescriptionProps = $props();
</script>
<AlertDialogPrimitive.Description
bind:ref
data-slot="alert-dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.OverlayProps = $props();
</script>
<AlertDialogPrimitive.Overlay
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>
@@ -1,17 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.TitleProps = $props();
</script>
<AlertDialogPrimitive.Title
bind:ref
data-slot="alert-dialog-title"
class={cn("text-lg font-semibold", className)}
{...restProps}
/>
@@ -1,39 +0,0 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Portal = AlertDialogPrimitive.Portal;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};
@@ -1,50 +0,0 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>
@@ -1,2 +0,0 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
@@ -1,80 +0,0 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { tv, type VariantProps } from 'tailwind-variants';
export const buttonVariants = tv({
base: "cursor-pointer focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
outline:
'bg-background shadow-xs hover:bg-muted hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-muted border',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline hover:text-white'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}
@@ -1,17 +0,0 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
@@ -1,15 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,23 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,23 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,25 +0,0 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
@@ -1,36 +0,0 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="text-current transition-none">
{#if checked}
<CheckIcon class="size-3.5" />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
@@ -1,6 +0,0 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};
@@ -1,40 +0,0 @@
<script lang="ts">
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
import type { Snippet } from "svelte";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(""),
title = "Command Palette",
description = "Search for a command to run",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
title?: string;
description?: string;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Header class="sr-only">
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
<Command
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
</Dialog.Content>
</Dialog.Root>
@@ -1,17 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty
bind:ref
data-slot="command-empty"
class={cn("py-6 text-center text-sm", className)}
{...restProps}
/>
@@ -1,32 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive, useId } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn("text-foreground overflow-hidden p-1", className)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
>
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>
@@ -1,26 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import SearchIcon from "@lucide/svelte/icons/search";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
<SearchIcon class="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
class={cn(
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:ref
{...restProps}
bind:value
/>
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>
@@ -1,17 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
{...restProps}
/>
@@ -1,17 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn("bg-border -mx-1 h-px", className)}
{...restProps}
/>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -1,22 +0,0 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: CommandPrimitive.RootProps = $props();
</script>
<CommandPrimitive.Root
bind:value
bind:ref
data-slot="command"
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...restProps}
/>
@@ -1,40 +0,0 @@
import { Command as CommandPrimitive } from "bits-ui";
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import LinkItem from "./command-link-item.svelte";
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};
@@ -1,30 +0,0 @@
<script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { generateNanoId } from '$lib/utils/string';
interface Props {
label: string;
checked: boolean;
classes: string;
}
let {
label = '',
checked = $bindable(false),
classes = 'flex items-center gap-2'
}: Props = $props();
let nanoId = $state(generateNanoId(label));
</script>
<div class={classes}>
<Checkbox id={nanoId} bind:checked aria-labelledby={label} />
<Label
id={nanoId}
for={nanoId}
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{label}
</Label>
</div>
@@ -1,151 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Command from '$lib/components/ui/command/index.js';
import Label from '$lib/components/ui/label/label.svelte';
import * as Popover from '$lib/components/ui/popover/index.js';
import { cn } from '$lib/utils.js';
import Icon from '@iconify/svelte';
interface Props {
open: boolean;
label: string;
value: string | string[];
data: { value: string; label: string }[];
onValueChange?: (value: string | string[]) => void;
placeholder?: string;
disabled?: boolean;
classes?: string;
triggerWidth?: string;
width?: string;
disallowEmpty?: boolean;
multiple?: boolean;
}
let {
open = $bindable(false),
label = '',
data = [],
onValueChange = () => {},
placeholder = '',
disabled = false,
classes = 'space-y-1',
triggerWidth = 'w-full',
width = 'w-1/2',
disallowEmpty = false,
multiple = false,
value = $bindable(multiple ? [] : '')
}: Props = $props();
let search = $state('');
const filteredData = $derived.by(() => {
if (!search) return data;
const q = search.toLowerCase();
return data.filter(
({ label, value }) => label.toLowerCase().includes(q) || value.toLowerCase().includes(q)
);
});
function selectItem(val: string) {
if (multiple) {
// start with a fresh array copy
const arr = Array.isArray(value) ? [...value] : [];
const idx = arr.indexOf(val);
if (idx >= 0) {
arr.splice(idx, 1);
} else {
arr.push(val);
}
value = arr;
onValueChange(arr);
// keep open=true so you can pick more
} else {
if (value === val && !disallowEmpty) {
value = '';
onValueChange('');
} else {
value = val;
onValueChange(val);
}
open = false;
}
search = '';
}
const selectedLabels = $derived.by(() => {
const vals = multiple ? (Array.isArray(value) ? value : []) : value ? [value] : [];
return data.filter((d) => vals.includes(d.value)).map((d) => d.label);
});
</script>
<div class={classes}>
<Label class="w-full whitespace-nowrap text-sm" for={label.toLowerCase()}>
{label}
</Label>
<Popover.Root bind:open>
<Popover.Trigger class={triggerWidth}>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
class="w-full flex-nowrap justify-between gap-1 overflow-hidden"
{disabled}
>
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
{#if selectedLabels.length > 0}
{#each selectedLabels as lbl, i}
<span
class={multiple
? 'bg-secondary/100 truncate rounded px-2 py-0.5 text-sm'
: 'truncate rounded px-2 text-sm'}
title={lbl}
>
{lbl}
</span>
{/each}
{:else}
<span class="truncate opacity-50">{placeholder}</span>
{/if}
</div>
<Icon icon="lucide:chevrons-up-down" class="ml-auto h-4 w-4 shrink-0 opacity-50" />
</Button>
</Popover.Trigger>
<Popover.Content class="{width} mx-auto p-0">
<Command.Root shouldFilter={false}>
<Command.Input bind:value={search} placeholder={placeholder || 'Search...'} />
<Command.Empty>No data</Command.Empty>
<div class="max-h-64 overflow-y-auto">
<Command.Group>
{#each filteredData as element}
<Command.Item
value={element.value}
onSelect={() => selectItem(element.value)}
onkeydown={(e) => {
if (e.key === 'Enter') selectItem(element.value);
}}
>
<Icon
icon="lucide:check"
class={cn(
'mr-2 h-4 w-4',
multiple
? Array.isArray(value) && value.includes(element.value)
? 'opacity-100'
: 'opacity-0'
: value === element.value
? 'opacity-100'
: 'opacity-0'
)}
/>
{element.label}
</Command.Item>
{/each}
</Command.Group>
</div>
</Command.Root>
</Popover.Content>
</Popover.Root>
</div>
@@ -1,66 +0,0 @@
<script lang="ts">
import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
import { generateNanoId } from '$lib/utils/string';
import type { FullAutoFill } from 'svelte/elements';
interface Props {
label: string;
value: string | number;
placeholder: string;
autocomplete?: FullAutoFill | null | undefined;
classes: string;
type?: string;
textAreaCLasses?: string;
disabled?: boolean;
onChange?: (value: string | number) => void;
}
let {
value = $bindable(''),
label = '',
placeholder = '',
autocomplete = 'off',
classes = 'space-y-1.5',
type = 'text',
textAreaCLasses = 'min-h-56',
disabled = false,
onChange
}: Props = $props();
let nanoId = $state(generateNanoId(label));
</script>
<div class={`${classes}`}>
{#if label}
<Label class="w-full whitespace-nowrap text-sm" for={nanoId}>{label}</Label>
{/if}
{#if type === 'textarea'}
<Textarea
class={textAreaCLasses}
id={nanoId}
{placeholder}
{autocomplete}
bind:value
{disabled}
oninput={(e) => {
value = e.target?.value;
if (onChange) onChange(value);
}}
/>
{:else}
<Input
{type}
id={nanoId}
{placeholder}
{autocomplete}
bind:value
{disabled}
oninput={(e) => {
value = e.target?.value;
if (onChange) onChange(value);
}}
/>
{/if}
</div>
@@ -1,43 +0,0 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
// import XIcon from '@lucide/svelte/icons/x';
import { Dialog as DialogPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
>
<!-- <XIcon /> -->
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
@@ -1,17 +0,0 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
@@ -1,20 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,20 +0,0 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
data-slot="dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>
@@ -1,17 +0,0 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn("text-lg font-semibold leading-none", className)}
{...restProps}
/>
@@ -1,37 +0,0 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};
@@ -1,41 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,27 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
@@ -1,27 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>
@@ -1,24 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,16 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
@@ -1,31 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<CircleIcon class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
@@ -1,17 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
@@ -1,20 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -1,20 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...restProps}
/>
@@ -1,29 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
@@ -1,49 +0,0 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};
@@ -1,7 +0,0 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};
@@ -1,69 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined }) & {
showPasswordOnFocus?: boolean;
}
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
showPasswordOnFocus = false,
...restProps
}: Props = $props();
let showPassword: boolean = $state(false);
function handleFocus() {
if (type === 'password') {
showPassword = true;
}
}
function handleBlur() {
if (type === 'password') {
showPassword = false;
}
}
</script>
{#if type === 'file'}
<input
bind:this={ref}
data-slot="input"
class={cn(
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot="input"
onfocus={handleFocus}
onblur={handleBlur}
class={cn(
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
type={type === 'password' && showPassword && showPasswordOnFocus ? 'text' : type}
bind:value
{...restProps}
/>
{/if}
@@ -1,7 +0,0 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

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