mirror of
https://github.com/plexusorg/Module-HTTPD.git
synced 2026-06-04 17:16:54 +00:00
reformat
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "neutral"
|
"baseColor": "neutral"
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "$lib/components",
|
"components": "$lib/components",
|
||||||
"utils": "$lib/utils",
|
"utils": "$lib/utils",
|
||||||
"ui": "$lib/components/ui",
|
"ui": "$lib/components/ui",
|
||||||
"hooks": "$lib/hooks",
|
"hooks": "$lib/hooks",
|
||||||
"lib": "$lib"
|
"lib": "$lib"
|
||||||
},
|
},
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"registry": "https://shadcn-svelte.com/registry",
|
"registry": "https://shadcn-svelte.com/registry",
|
||||||
"style": "maia",
|
"style": "maia",
|
||||||
"iconLibrary": "hugeicons",
|
"iconLibrary": "hugeicons",
|
||||||
"menuColor": "default",
|
"menuColor": "default",
|
||||||
"menuAccent": "subtle"
|
"menuAccent": "subtle"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Plex HTTPD</title>
|
<title>Plex HTTPD</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
|
import StaffRequired from '$lib/components/auth/StaffRequired.svelte';
|
||||||
import AppShell from '$lib/components/layout/AppShell.svelte';
|
import AppShell from '$lib/components/layout/AppShell.svelte';
|
||||||
import {getAuth} from '$lib/api';
|
import {getAuth} from '$lib/api';
|
||||||
import {isInternalAppLink, navigate, parseRoute} from '$lib/router';
|
import {isInternalAppLink, navigate, parseRoute} from '$lib/router';
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
let route = $state(parseRoute(window.location.pathname));
|
let route = $state(parseRoute(window.location.pathname));
|
||||||
let auth: AuthState | null = $state(null);
|
let auth: AuthState | null = $state(null);
|
||||||
let dark = $state(false);
|
let dark = $state(false);
|
||||||
|
const staff = $derived((auth as AuthState | null)?.is_staff === true);
|
||||||
|
|
||||||
function syncRoute() {
|
function syncRoute() {
|
||||||
route = parseRoute(window.location.pathname);
|
route = parseRoute(window.location.pathname);
|
||||||
@@ -48,13 +50,21 @@
|
|||||||
<HomePage/>
|
<HomePage/>
|
||||||
{/await}
|
{/await}
|
||||||
{:else if route.path === 'players'}
|
{:else if route.path === 'players'}
|
||||||
{#await import('$lib/pages/PlayersPage.svelte') then {default: PlayersPage}}
|
{#if auth === null}
|
||||||
<PlayersPage staff={Boolean(auth?.is_staff)}/>
|
<p class="rise text-sm text-muted-foreground">Loading players...</p>
|
||||||
{/await}
|
{:else}
|
||||||
|
{#await import('$lib/pages/PlayersPage.svelte') then {default: PlayersPage}}
|
||||||
|
<PlayersPage {staff}/>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
{:else if route.path === 'player'}
|
{:else if route.path === 'player'}
|
||||||
{#await import('$lib/pages/PlayerPage.svelte') then {default: PlayerPage}}
|
{#if staff}
|
||||||
<PlayerPage id={route.params.id} staff={Boolean(auth?.is_staff)}/>
|
{#await import('$lib/pages/PlayerPage.svelte') then {default: PlayerPage}}
|
||||||
{/await}
|
<PlayerPage id={route.params.id} {staff}/>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<StaffRequired {auth} action="access player admin tools"/>
|
||||||
|
{/if}
|
||||||
{:else if route.path === 'commands'}
|
{:else if route.path === 'commands'}
|
||||||
{#await import('$lib/pages/CommandsPage.svelte') then {default: CommandsPage}}
|
{#await import('$lib/pages/CommandsPage.svelte') then {default: CommandsPage}}
|
||||||
<CommandsPage/>
|
<CommandsPage/>
|
||||||
@@ -68,17 +78,25 @@
|
|||||||
<PunishmentsDetailPage id={route.params.id}/>
|
<PunishmentsDetailPage id={route.params.id}/>
|
||||||
{/await}
|
{/await}
|
||||||
{:else if route.path === 'indefbans'}
|
{:else if route.path === 'indefbans'}
|
||||||
{#await import('$lib/pages/IndefBansPage.svelte') then {default: IndefBansPage}}
|
{#if staff}
|
||||||
<IndefBansPage/>
|
{#await import('$lib/pages/IndefBansPage.svelte') then {default: IndefBansPage}}
|
||||||
{/await}
|
<IndefBansPage/>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<StaffRequired {auth} action="view indefinite bans"/>
|
||||||
|
{/if}
|
||||||
{:else if route.path === 'schematics'}
|
{:else if route.path === 'schematics'}
|
||||||
{#await import('$lib/pages/SchematicsPage.svelte') then {default: SchematicsPage}}
|
{#await import('$lib/pages/SchematicsPage.svelte') then {default: SchematicsPage}}
|
||||||
<SchematicsPage/>
|
<SchematicsPage {staff}/>
|
||||||
{/await}
|
{/await}
|
||||||
{:else if route.path === 'schematics-upload'}
|
{:else if route.path === 'schematics-upload'}
|
||||||
{#await import('$lib/pages/SchematicUploadPage.svelte') then {default: SchematicUploadPage}}
|
{#if staff}
|
||||||
<SchematicUploadPage/>
|
{#await import('$lib/pages/SchematicUploadPage.svelte') then {default: SchematicUploadPage}}
|
||||||
{/await}
|
<SchematicUploadPage/>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<StaffRequired {auth} action="upload schematics"/>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<section class="rise">
|
<section class="rise">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Not found</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Not found</h1>
|
||||||
|
|||||||
+206
-206
@@ -6,238 +6,238 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Figtree Variable', sans-serif;
|
--font-sans: 'Figtree Variable', sans-serif;
|
||||||
--font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", monospace;
|
--font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", monospace;
|
||||||
|
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--radius-2xl: calc(var(--radius) + 8px);
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
|
||||||
--color-background: oklch(1 0 0);
|
--color-background: oklch(1 0 0);
|
||||||
--color-foreground: oklch(0.145 0 0);
|
--color-foreground: oklch(0.145 0 0);
|
||||||
--color-card: oklch(1 0 0);
|
--color-card: oklch(1 0 0);
|
||||||
--color-card-foreground: oklch(0.145 0 0);
|
--color-card-foreground: oklch(0.145 0 0);
|
||||||
--color-popover: oklch(1 0 0);
|
--color-popover: oklch(1 0 0);
|
||||||
--color-popover-foreground: oklch(0.145 0 0);
|
--color-popover-foreground: oklch(0.145 0 0);
|
||||||
--color-primary: oklch(0.555 0.265 264);
|
--color-primary: oklch(0.555 0.265 264);
|
||||||
--color-primary-foreground: oklch(0.985 0 0);
|
--color-primary-foreground: oklch(0.985 0 0);
|
||||||
--color-secondary: oklch(0.97 0 0);
|
--color-secondary: oklch(0.97 0 0);
|
||||||
--color-secondary-foreground: oklch(0.205 0 0);
|
--color-secondary-foreground: oklch(0.205 0 0);
|
||||||
--color-muted: oklch(0.97 0 0);
|
--color-muted: oklch(0.97 0 0);
|
||||||
--color-muted-foreground: oklch(0.556 0 0);
|
--color-muted-foreground: oklch(0.556 0 0);
|
||||||
--color-accent: oklch(0.97 0 0);
|
--color-accent: oklch(0.97 0 0);
|
||||||
--color-accent-foreground: oklch(0.205 0 0);
|
--color-accent-foreground: oklch(0.205 0 0);
|
||||||
--color-destructive: oklch(0.58 0.22 27);
|
--color-destructive: oklch(0.58 0.22 27);
|
||||||
--color-destructive-foreground: oklch(0.985 0 0);
|
--color-destructive-foreground: oklch(0.985 0 0);
|
||||||
--color-success: oklch(0.62 0.18 145);
|
--color-success: oklch(0.62 0.18 145);
|
||||||
--color-success-foreground: oklch(0.985 0 0);
|
--color-success-foreground: oklch(0.985 0 0);
|
||||||
--color-warning: oklch(0.74 0.16 75);
|
--color-warning: oklch(0.74 0.16 75);
|
||||||
--color-warning-foreground: oklch(0.145 0 0);
|
--color-warning-foreground: oklch(0.145 0 0);
|
||||||
--color-border: oklch(0.922 0 0);
|
--color-border: oklch(0.922 0 0);
|
||||||
--color-input: oklch(0.922 0 0);
|
--color-input: oklch(0.922 0 0);
|
||||||
--color-ring: oklch(0.555 0.265 264);
|
--color-ring: oklch(0.555 0.265 264);
|
||||||
--color-surface: oklch(0.98 0 0);
|
--color-surface: oklch(0.98 0 0);
|
||||||
--color-surface-foreground: oklch(0.145 0 0);
|
--color-surface-foreground: oklch(0.145 0 0);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.205 0 0);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.97 0 0);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--chart-5: oklch(0.269 0 0);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--color-background: oklch(0.145 0 0);
|
--color-background: oklch(0.145 0 0);
|
||||||
--color-foreground: oklch(0.985 0 0);
|
--color-foreground: oklch(0.985 0 0);
|
||||||
--color-card: oklch(0.205 0 0);
|
--color-card: oklch(0.205 0 0);
|
||||||
--color-card-foreground: oklch(0.985 0 0);
|
--color-card-foreground: oklch(0.985 0 0);
|
||||||
--color-popover: oklch(0.205 0 0);
|
--color-popover: oklch(0.205 0 0);
|
||||||
--color-popover-foreground: oklch(0.985 0 0);
|
--color-popover-foreground: oklch(0.985 0 0);
|
||||||
--color-primary: oklch(0.62 0.235 264);
|
--color-primary: oklch(0.62 0.235 264);
|
||||||
--color-primary-foreground: oklch(0.985 0 0);
|
--color-primary-foreground: oklch(0.985 0 0);
|
||||||
--color-secondary: oklch(0.269 0 0);
|
--color-secondary: oklch(0.269 0 0);
|
||||||
--color-secondary-foreground: oklch(0.985 0 0);
|
--color-secondary-foreground: oklch(0.985 0 0);
|
||||||
--color-muted: oklch(0.269 0 0);
|
--color-muted: oklch(0.269 0 0);
|
||||||
--color-muted-foreground: oklch(0.708 0 0);
|
--color-muted-foreground: oklch(0.708 0 0);
|
||||||
--color-accent: oklch(0.371 0 0);
|
--color-accent: oklch(0.371 0 0);
|
||||||
--color-accent-foreground: oklch(0.985 0 0);
|
--color-accent-foreground: oklch(0.985 0 0);
|
||||||
--color-destructive: oklch(0.704 0.191 22);
|
--color-destructive: oklch(0.704 0.191 22);
|
||||||
--color-destructive-foreground: oklch(0.985 0 0);
|
--color-destructive-foreground: oklch(0.985 0 0);
|
||||||
--color-success: oklch(0.74 0.18 145);
|
--color-success: oklch(0.74 0.18 145);
|
||||||
--color-success-foreground: oklch(0.145 0 0);
|
--color-success-foreground: oklch(0.145 0 0);
|
||||||
--color-warning: oklch(0.82 0.16 75);
|
--color-warning: oklch(0.82 0.16 75);
|
||||||
--color-warning-foreground: oklch(0.145 0 0);
|
--color-warning-foreground: oklch(0.145 0 0);
|
||||||
--color-border: oklch(1 0 0 / 10%);
|
--color-border: oklch(1 0 0 / 10%);
|
||||||
--color-input: oklch(1 0 0 / 15%);
|
--color-input: oklch(1 0 0 / 15%);
|
||||||
--color-ring: oklch(0.62 0.235 264);
|
--color-ring: oklch(0.62 0.235 264);
|
||||||
--color-surface: oklch(0.2 0 0);
|
--color-surface: oklch(0.2 0 0);
|
||||||
--color-surface-foreground: oklch(0.708 0 0);
|
--color-surface-foreground: oklch(0.708 0 0);
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.205 0 0);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.205 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.922 0 0);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.269 0 0);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--chart-5: oklch(0.269 0 0);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
border-color: var(--color-border);
|
border-color: var(--color-border);
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-feature-settings: "cv11", "ss01";
|
font-feature-settings: "cv11", "ss01";
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-image: radial-gradient(circle at 1px 1px, oklch(from var(--color-foreground) l c h / 0.05) 1px, transparent 0);
|
background-image: radial-gradient(circle at 1px 1px, oklch(from var(--color-foreground) l c h / 0.05) 1px, transparent 0);
|
||||||
background-size: 28px 28px;
|
background-size: 28px 28px;
|
||||||
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::after {
|
body::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset-inline: 0;
|
inset-inline: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
height: 45vh;
|
height: 45vh;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: linear-gradient(to top, oklch(from var(--color-primary) calc(l - 0.05) c h / 0.12), transparent);
|
background: linear-gradient(to top, oklch(from var(--color-primary) calc(l - 0.05) c h / 0.12), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.layer-content {
|
.layer-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ring-card {
|
.ring-card {
|
||||||
box-shadow: inset 0 0 0 1px oklch(from var(--color-foreground) l c h / 0.08);
|
box-shadow: inset 0 0 0 1px oklch(from var(--color-foreground) l c h / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabular {
|
.tabular {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-pixelated {
|
.inventory-pixelated {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rise {
|
@keyframes rise {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rise {
|
.rise {
|
||||||
animation: rise 0.35s cubic-bezier(0.16, 0.84, 0.32, 1) backwards;
|
animation: rise 0.35s cubic-bezier(0.16, 0.84, 0.32, 1) backwards;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,29 @@ import type {
|
|||||||
Schematic
|
Schematic
|
||||||
} from '$lib/types/api';
|
} from '$lib/types/api';
|
||||||
|
|
||||||
export async function getJson<T>(url: string): Promise<T> {
|
export async function getJson<T>(url: string, timeoutMs = 15_000): Promise<T> {
|
||||||
const response = await fetch(url, {
|
const controller = new AbortController();
|
||||||
credentials: 'same-origin',
|
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||||
headers: {Accept: 'application/json'}
|
try {
|
||||||
});
|
const response = await fetch(url, {
|
||||||
const body = await response.json().catch(() => null);
|
credentials: 'same-origin',
|
||||||
if (!response.ok || (body && typeof body === 'object' && 'error' in body)) {
|
headers: {Accept: 'application/json'},
|
||||||
const message = body && typeof body === 'object' && 'error' in body ? String(body.error) : `${response.status} ${response.statusText}`;
|
signal: controller.signal
|
||||||
throw new Error(message);
|
});
|
||||||
|
const body = await response.json().catch(() => null);
|
||||||
|
if (!response.ok || (body && typeof body === 'object' && 'error' in body)) {
|
||||||
|
const message = body && typeof body === 'object' && 'error' in body ? String(body.error) : `${response.status} ${response.statusText}`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return body as T;
|
||||||
|
} catch (cause) {
|
||||||
|
if (cause instanceof DOMException && cause.name === 'AbortError') {
|
||||||
|
throw new Error('Request timed out.');
|
||||||
|
}
|
||||||
|
throw cause;
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
return body as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAuth(): Promise<AuthState> {
|
export async function getAuth(): Promise<AuthState> {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Button} from '$lib/components/ui/button';
|
||||||
|
import {Card} from '$lib/components/ui/card';
|
||||||
|
import type {AuthState} from '$lib/types/api';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
auth: AuthState | null;
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {auth, action}: Props = $props();
|
||||||
|
const loginHref = $derived(`/oauth2/login?return_to=${encodeURIComponent(window.location.pathname + window.location.search)}`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if auth === null}
|
||||||
|
<p class="rise text-sm text-muted-foreground">Checking access...</p>
|
||||||
|
{:else}
|
||||||
|
<Card class="rise max-w-xl p-5">
|
||||||
|
<h1 class="text-xl font-medium">Staff access required</h1>
|
||||||
|
<p class="mt-2 text-sm text-muted-foreground">You must sign in as staff to {action}.</p>
|
||||||
|
{#if auth.reason !== 'disabled'}
|
||||||
|
<Button href={loginHref} class="mt-4">Sign in</Button>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
@@ -1,128 +1,139 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type {Snippet} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import {
|
import {
|
||||||
Cancel01Icon,
|
Cancel01Icon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
DashboardSquare01Icon,
|
DashboardSquare01Icon,
|
||||||
JusticeScale01Icon,
|
JusticeScale01Icon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
Login01Icon,
|
Login01Icon,
|
||||||
Logout01Icon,
|
Logout01Icon,
|
||||||
Menu01Icon,
|
Menu01Icon,
|
||||||
Moon02Icon,
|
Moon02Icon,
|
||||||
PackageIcon,
|
PackageIcon,
|
||||||
Sun02Icon,
|
Sun02Icon,
|
||||||
UserGroupIcon
|
UserGroupIcon
|
||||||
} from '@hugeicons/core-free-icons';
|
} from '@hugeicons/core-free-icons';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import type { AuthState } from '$lib/types/api';
|
import type {AuthState} from '$lib/types/api';
|
||||||
import plexLogo from '$lib/assets/plexlogo.webp';
|
import plexLogo from '$lib/assets/plexlogo.webp';
|
||||||
import { navigate } from '$lib/router';
|
import {navigate} from '$lib/router';
|
||||||
import { cn } from '$lib/utils';
|
import {cn} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
route: string;
|
route: string;
|
||||||
auth: AuthState | null;
|
auth: AuthState | null;
|
||||||
dark: boolean;
|
dark: boolean;
|
||||||
onToggleDark: () => void;
|
onToggleDark: () => void;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { route, auth, dark, onToggleDark, children }: Props = $props();
|
let {route, auth, dark, onToggleDark, children}: Props = $props();
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ href: '/', label: 'Overview', icon: DashboardSquare01Icon, match: ['home'] },
|
{href: '/', label: 'Overview', icon: DashboardSquare01Icon, match: ['home']},
|
||||||
{ href: '/players/', label: 'Players', icon: UserGroupIcon, match: ['players', 'player'] },
|
{href: '/players/', label: 'Players', icon: UserGroupIcon, match: ['players', 'player']},
|
||||||
{ href: '/commands/', label: 'Commands', icon: CodeIcon, match: ['commands'] },
|
{href: '/commands/', label: 'Commands', icon: CodeIcon, match: ['commands']},
|
||||||
{ href: '/punishments/', label: 'Punishments', icon: JusticeScale01Icon, match: ['punishments', 'punishments-detail'] },
|
{
|
||||||
{ href: '/indefbans/', label: 'Indef Bans', icon: LockIcon, match: ['indefbans'] },
|
href: '/punishments/',
|
||||||
{ href: '/schematics/', label: 'Schematics', icon: PackageIcon, match: ['schematics', 'schematics-upload'] }
|
label: 'Punishments',
|
||||||
];
|
icon: JusticeScale01Icon,
|
||||||
|
match: ['punishments', 'punishments-detail']
|
||||||
|
},
|
||||||
|
{href: '/indefbans/', label: 'Indef Bans', icon: LockIcon, match: ['indefbans']},
|
||||||
|
{href: '/schematics/', label: 'Schematics', icon: PackageIcon, match: ['schematics', 'schematics-upload']}
|
||||||
|
];
|
||||||
|
|
||||||
const loginHref = $derived(`/oauth2/login?return_to=${encodeURIComponent(window.location.pathname + window.location.search)}`);
|
const loginHref = $derived(`/oauth2/login?return_to=${encodeURIComponent(window.location.pathname + window.location.search)}`);
|
||||||
|
|
||||||
function navTo(path: string) {
|
function navTo(path: string) {
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
navigate(path);
|
navigate(path);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="layer-content flex min-h-screen flex-col">
|
<div class="layer-content flex min-h-screen flex-col">
|
||||||
<header class="sticky top-0 z-50 border-b border-border/60 bg-background/75 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
|
<header class="sticky top-0 z-50 border-b border-border/60 bg-background/75 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
|
||||||
<div class="mx-auto flex h-14 max-w-7xl items-center gap-4 px-4 sm:px-6">
|
<div class="mx-auto flex h-14 max-w-7xl items-center gap-4 px-4 sm:px-6">
|
||||||
<button type="button" class="flex items-center gap-2.5 text-foreground transition-opacity hover:opacity-80" onclick={() => navTo('/')}>
|
<button type="button" class="flex items-center gap-2.5 text-foreground transition-opacity hover:opacity-80"
|
||||||
<img src={plexLogo} alt="" class="size-7 rounded-md" width="28" height="28" />
|
onclick={() => navTo('/')}>
|
||||||
<span class="text-sm font-semibold tracking-tight">Plex HTTPD</span>
|
<img src={plexLogo} alt="" class="size-7 rounded-md" width="28" height="28"/>
|
||||||
</button>
|
<span class="text-sm font-semibold tracking-tight">Plex HTTPD</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<nav class="hidden flex-1 items-center gap-1 md:flex">
|
<nav class="hidden flex-1 items-center gap-1 md:flex">
|
||||||
{#each nav as item (item.href)}
|
{#each nav as item (item.href)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={cn(
|
class={cn(
|
||||||
'group inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
|
'group inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
|
||||||
item.match.includes(route) && 'bg-muted text-foreground'
|
item.match.includes(route) && 'bg-muted text-foreground'
|
||||||
)}
|
)}
|
||||||
onclick={() => navTo(item.href)}
|
onclick={() => navTo(item.href)}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={item.icon} class={cn('size-3.5 opacity-70 group-hover:opacity-100', item.match.includes(route) && 'text-primary opacity-100')} aria-hidden="true" />
|
<HugeiconsIcon icon={item.icon}
|
||||||
{item.label}
|
class={cn('size-3.5 opacity-70 group-hover:opacity-100', item.match.includes(route) && 'text-primary opacity-100')}
|
||||||
</button>
|
aria-hidden="true"/>
|
||||||
{/each}
|
{item.label}
|
||||||
</nav>
|
</button>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-2">
|
<div class="ml-auto flex items-center gap-2">
|
||||||
{#if auth?.authenticated}
|
{#if auth?.authenticated}
|
||||||
<span class="hidden text-xs text-muted-foreground sm:inline">{auth.username}</span>
|
<span class="hidden text-xs text-muted-foreground sm:inline">{auth.username}</span>
|
||||||
<Button href="/oauth2/logout" variant="outline" size="sm">
|
<Button href="/oauth2/logout" variant="outline" size="sm">
|
||||||
<HugeiconsIcon icon={Logout01Icon} class="size-3.5" />
|
<HugeiconsIcon icon={Logout01Icon} class="size-3.5"/>
|
||||||
<span class="hidden sm:inline">Sign out</span>
|
<span class="hidden sm:inline">Sign out</span>
|
||||||
</Button>
|
</Button>
|
||||||
{:else if auth?.reason !== 'disabled'}
|
{:else if auth?.reason !== 'disabled'}
|
||||||
<Button href={loginHref} variant="outline" size="sm">
|
<Button href={loginHref} variant="outline" size="sm">
|
||||||
<HugeiconsIcon icon={Login01Icon} class="size-3.5" />
|
<HugeiconsIcon icon={Login01Icon} class="size-3.5"/>
|
||||||
<span class="hidden sm:inline">Sign in</span>
|
<span class="hidden sm:inline">Sign in</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button variant="ghost" size="icon" aria-label="Toggle theme" onclick={onToggleDark}>
|
<Button variant="ghost" size="icon" aria-label="Toggle theme" onclick={onToggleDark}>
|
||||||
{#if dark}
|
{#if dark}
|
||||||
<HugeiconsIcon icon={Sun02Icon} class="size-4" />
|
<HugeiconsIcon icon={Sun02Icon} class="size-4"/>
|
||||||
{:else}
|
{:else}
|
||||||
<HugeiconsIcon icon={Moon02Icon} class="size-4" />
|
<HugeiconsIcon icon={Moon02Icon} class="size-4"/>
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon" class="md:hidden" aria-label="Toggle menu" aria-expanded={menuOpen} onclick={() => (menuOpen = !menuOpen)}>
|
<Button variant="outline" size="icon" class="md:hidden" aria-label="Toggle menu"
|
||||||
{#if menuOpen}
|
aria-expanded={menuOpen} onclick={() => (menuOpen = !menuOpen)}>
|
||||||
<HugeiconsIcon icon={Cancel01Icon} class="size-4" />
|
{#if menuOpen}
|
||||||
{:else}
|
<HugeiconsIcon icon={Cancel01Icon} class="size-4"/>
|
||||||
<HugeiconsIcon icon={Menu01Icon} class="size-4" />
|
{:else}
|
||||||
{/if}
|
<HugeiconsIcon icon={Menu01Icon} class="size-4"/>
|
||||||
</Button>
|
{/if}
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
<nav class="border-t border-border/60 px-4 py-3 md:hidden">
|
<nav class="border-t border-border/60 px-4 py-3 md:hidden">
|
||||||
{#each nav as item (item.href)}
|
{#each nav as item (item.href)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
|
'flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
|
||||||
item.match.includes(route) && 'bg-muted text-foreground'
|
item.match.includes(route) && 'bg-muted text-foreground'
|
||||||
)}
|
)}
|
||||||
onclick={() => navTo(item.href)}
|
onclick={() => navTo(item.href)}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={item.icon} class={cn('size-4 opacity-70', item.match.includes(route) && 'text-primary opacity-100')} aria-hidden="true" />
|
<HugeiconsIcon icon={item.icon}
|
||||||
{item.label}
|
class={cn('size-4 opacity-70', item.match.includes(route) && 'text-primary opacity-100')}
|
||||||
</button>
|
aria-hidden="true"/>
|
||||||
{/each}
|
{item.label}
|
||||||
</nav>
|
</button>
|
||||||
{/if}
|
{/each}
|
||||||
</header>
|
</nav>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
<main class="mx-auto w-full max-w-7xl flex-1 px-4 py-8 sm:px-6 md:py-10">
|
<main class="mx-auto w-full max-w-7xl flex-1 px-4 py-8 sm:px-6 md:py-10">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,166 +1,168 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ItemIcon from '$lib/components/ui/ItemIcon.svelte';
|
import ItemIcon from '$lib/components/ui/ItemIcon.svelte';
|
||||||
import type { InventoryItem, InventoryPayload } from '$lib/types/api';
|
import type {InventoryItem, InventoryPayload} from '$lib/types/api';
|
||||||
import { cn, titleCase } from '$lib/utils';
|
import {cn, titleCase} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inventory: InventoryPayload | null;
|
inventory: InventoryPayload | null;
|
||||||
selectedKey: string | null;
|
selectedKey: string | null;
|
||||||
onSelect: (slot: string | null) => void;
|
onSelect: (slot: string | null) => void;
|
||||||
}
|
|
||||||
|
|
||||||
let { inventory, selectedKey, onSelect }: Props = $props();
|
|
||||||
|
|
||||||
const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'];
|
|
||||||
|
|
||||||
function itemAt(slot: string | null) {
|
|
||||||
if (!inventory?.online || !slot) return null;
|
|
||||||
if (slot === 'offhand') return inventory.offhand ?? null;
|
|
||||||
if (slot.startsWith('storage-')) return inventory.storage?.[Number(slot.substring(8))] ?? null;
|
|
||||||
if (slot.startsWith('hotbar-')) return inventory.hotbar?.[Number(slot.substring(7))] ?? null;
|
|
||||||
if (slot.startsWith('armor-')) return inventory.armor?.[slot.substring(6)] ?? null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedItem = $derived(itemAt(selectedKey));
|
|
||||||
|
|
||||||
function tooltip(item: InventoryItem) {
|
|
||||||
const parts = [item.name || titleCase(item.type)];
|
|
||||||
if (item.amount > 1) parts[0] += ` x${item.amount}`;
|
|
||||||
if (item.enchants) {
|
|
||||||
for (const [key, value] of Object.entries(item.enchants)) parts.push(`${titleCase(key)} ${ROMAN[value] || value}`);
|
|
||||||
}
|
}
|
||||||
if (item.maxDamage) parts.push(`Durability: ${item.maxDamage - (item.damage || 0)} / ${item.maxDamage}`);
|
|
||||||
return parts.join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function durabilityPercent(item: InventoryItem) {
|
let {inventory, selectedKey, onSelect}: Props = $props();
|
||||||
if (!item.maxDamage) return null;
|
|
||||||
return Math.max(0, Math.min(100, ((item.maxDamage - (item.damage || 0)) / item.maxDamage) * 100));
|
const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'];
|
||||||
}
|
|
||||||
|
function itemAt(slot: string | null) {
|
||||||
|
if (!inventory?.online || !slot) return null;
|
||||||
|
if (slot === 'offhand') return inventory.offhand ?? null;
|
||||||
|
if (slot.startsWith('storage-')) return inventory.storage?.[Number(slot.substring(8))] ?? null;
|
||||||
|
if (slot.startsWith('hotbar-')) return inventory.hotbar?.[Number(slot.substring(7))] ?? null;
|
||||||
|
if (slot.startsWith('armor-')) return inventory.armor?.[slot.substring(6)] ?? null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedItem = $derived(itemAt(selectedKey));
|
||||||
|
|
||||||
|
function tooltip(item: InventoryItem) {
|
||||||
|
const parts = [item.name || titleCase(item.type)];
|
||||||
|
if (item.amount > 1) parts[0] += ` x${item.amount}`;
|
||||||
|
if (item.enchants) {
|
||||||
|
for (const [key, value] of Object.entries(item.enchants)) parts.push(`${titleCase(key)} ${ROMAN[value] || value}`);
|
||||||
|
}
|
||||||
|
if (item.maxDamage) parts.push(`Durability: ${item.maxDamage - (item.damage || 0)} / ${item.maxDamage}`);
|
||||||
|
return parts.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function durabilityPercent(item: InventoryItem) {
|
||||||
|
if (!item.maxDamage) return null;
|
||||||
|
return Math.max(0, Math.min(100, ((item.maxDamage - (item.damage || 0)) / item.maxDamage) * 100));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet slot(item: InventoryItem | null | undefined, key: string)}
|
{#snippet slot(item: InventoryItem | null | undefined, key: string)}
|
||||||
{#if item}
|
{#if item}
|
||||||
{@const durability = durabilityPercent(item)}
|
{@const durability = durabilityPercent(item)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title={tooltip(item)}
|
title={tooltip(item)}
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative size-12 rounded-md bg-muted/40 transition-colors hover:bg-muted',
|
'relative size-12 rounded-md bg-muted/40 transition-colors hover:bg-muted',
|
||||||
selectedKey === key ? 'ring-2 ring-primary' : 'ring-card'
|
selectedKey === key ? 'ring-2 ring-primary' : 'ring-card'
|
||||||
)}
|
)}
|
||||||
onclick={() => onSelect(key)}
|
onclick={() => onSelect(key)}
|
||||||
>
|
>
|
||||||
<ItemIcon type={item.type} />
|
<ItemIcon type={item.type}/>
|
||||||
{#if item.enchants}
|
{#if item.enchants}
|
||||||
<span class="pointer-events-none absolute inset-0 rounded-md bg-primary/5 ring-1 ring-inset ring-primary/40"></span>
|
<span class="pointer-events-none absolute inset-0 rounded-md bg-primary/5 ring-1 ring-inset ring-primary/40"></span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.amount > 1}
|
{#if item.amount > 1}
|
||||||
<span class="pointer-events-none absolute bottom-0.5 right-1 font-mono text-xs font-medium [text-shadow:0_1px_2px_rgba(0,0,0,0.7)]">{item.amount}</span>
|
<span class="pointer-events-none absolute bottom-0.5 right-1 font-mono text-xs font-medium [text-shadow:0_1px_2px_rgba(0,0,0,0.7)]">{item.amount}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if durability != null && durability < 99.9}
|
{#if durability != null && durability < 99.9}
|
||||||
<span class="absolute inset-x-1 bottom-0.5 h-0.5 rounded-full bg-foreground/15">
|
<span class="absolute inset-x-1 bottom-0.5 h-0.5 rounded-full bg-foreground/15">
|
||||||
<span class={cn('block h-full rounded-full', durability > 50 ? 'bg-success' : durability > 25 ? 'bg-warning' : 'bg-destructive')} style:width={`${durability}%`}></span>
|
<span class={cn('block h-full rounded-full', durability > 50 ? 'bg-success' : durability > 25 ? 'bg-warning' : 'bg-destructive')}
|
||||||
|
style:width={`${durability}%`}></span>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="ring-card size-12 rounded-md bg-muted/40"></div>
|
<div class="ring-card size-12 rounded-md bg-muted/40"></div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if !inventory}
|
{#if !inventory}
|
||||||
<p class="py-6 text-center text-sm text-muted-foreground">Waiting for data...</p>
|
<p class="py-6 text-center text-sm text-muted-foreground">Waiting for data...</p>
|
||||||
{:else if !inventory.online}
|
{:else if !inventory.online}
|
||||||
<p class="py-6 text-center text-sm text-muted-foreground">Player is offline.</p>
|
<p class="py-6 text-center text-sm text-muted-foreground">Player is offline.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid gap-6 lg:grid-cols-[auto_1fr]">
|
<div class="grid gap-6 lg:grid-cols-[auto_1fr]">
|
||||||
<div class="-mx-2 overflow-x-auto px-2 pb-2 sm:mx-0 sm:px-0">
|
<div class="-mx-2 overflow-x-auto px-2 pb-2 sm:mx-0 sm:px-0">
|
||||||
<div class="flex min-w-max flex-wrap gap-4 lg:flex-nowrap">
|
<div class="flex min-w-max flex-wrap gap-4 lg:flex-nowrap">
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Main</p>
|
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Main</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="grid grid-cols-9 gap-1">
|
<div class="grid grid-cols-9 gap-1">
|
||||||
{#each inventory.storage ?? [] as item, index (index)}
|
{#each inventory.storage ?? [] as item, index (index)}
|
||||||
{@render slot(item, `storage-${index}`)}
|
{@render slot(item, `storage-${index}`)}
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-9 gap-1 border-t border-border/40 pt-2">
|
||||||
|
{#each inventory.hotbar ?? [] as item, index (index)}
|
||||||
|
{@render slot(item, `hotbar-${index}`)}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Armor</p>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
{@render slot(inventory.armor?.helmet, 'armor-helmet')}
|
||||||
|
{@render slot(inventory.armor?.chest, 'armor-chest')}
|
||||||
|
{@render slot(inventory.armor?.legs, 'armor-legs')}
|
||||||
|
{@render slot(inventory.armor?.boots, 'armor-boots')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Offhand</p>
|
||||||
|
{@render slot(inventory.offhand, 'offhand')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-9 gap-1 border-t border-border/40 pt-2">
|
|
||||||
{#each inventory.hotbar ?? [] as item, index (index)}
|
|
||||||
{@render slot(item, `hotbar-${index}`)}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
|
||||||
<div>
|
<div class="min-w-0 rounded-xl border border-border/40 bg-background/40 p-4">
|
||||||
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Armor</p>
|
{#if selectedItem}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="space-y-4">
|
||||||
{@render slot(inventory.armor?.helmet, 'armor-helmet')}
|
<div class="flex items-start gap-3">
|
||||||
{@render slot(inventory.armor?.chest, 'armor-chest')}
|
<div class="ring-card relative size-16 shrink-0 rounded-md bg-muted/40">
|
||||||
{@render slot(inventory.armor?.legs, 'armor-legs')}
|
<ItemIcon type={selectedItem.type}/>
|
||||||
{@render slot(inventory.armor?.boots, 'armor-boots')}
|
</div>
|
||||||
</div>
|
<div class="min-w-0">
|
||||||
</div>
|
{#if selectedItem.name}
|
||||||
<div>
|
<p class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-base font-medium italic">{selectedItem.name}</p>
|
||||||
<p class="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">Offhand</p>
|
{/if}
|
||||||
{@render slot(inventory.offhand, 'offhand')}
|
<p class="break-all font-mono text-xs text-muted-foreground">{selectedItem.type}</p>
|
||||||
</div>
|
<p class="mt-0.5 text-xs text-muted-foreground">Count: {selectedItem.amount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedItem.lore?.length}
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Lore</p>
|
||||||
|
<ul class="mt-1 space-y-0.5 text-xs italic text-foreground/80">
|
||||||
|
{#each selectedItem.lore as line, index (index)}
|
||||||
|
<li class="break-all">{line}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedItem.enchants}
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Enchantments</p>
|
||||||
|
<ul class="mt-1 space-y-0.5 text-xs">
|
||||||
|
{#each Object.entries(selectedItem.enchants) as [key, value] (key)}
|
||||||
|
<li class="flex justify-between gap-3"><span>{titleCase(key)}</span><span
|
||||||
|
class="font-mono text-muted-foreground">{ROMAN[value] || value}</span></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedItem.nbt}
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">NBT</p>
|
||||||
|
<pre class="mt-1 max-h-48 max-w-full overflow-auto rounded-md bg-muted/40 p-2 font-mono text-[10px] leading-snug whitespace-pre-wrap break-all">{selectedItem.nbt}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full min-h-56 items-center justify-center text-center text-sm text-muted-foreground">
|
||||||
|
Select an occupied slot to inspect the item.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 rounded-xl border border-border/40 bg-background/40 p-4">
|
|
||||||
{#if selectedItem}
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<div class="ring-card relative size-16 shrink-0 rounded-md bg-muted/40">
|
|
||||||
<ItemIcon type={selectedItem.type} />
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
{#if selectedItem.name}
|
|
||||||
<p class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-base font-medium italic">{selectedItem.name}</p>
|
|
||||||
{/if}
|
|
||||||
<p class="break-all font-mono text-xs text-muted-foreground">{selectedItem.type}</p>
|
|
||||||
<p class="mt-0.5 text-xs text-muted-foreground">Count: {selectedItem.amount}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if selectedItem.lore?.length}
|
|
||||||
<div>
|
|
||||||
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Lore</p>
|
|
||||||
<ul class="mt-1 space-y-0.5 text-xs italic text-foreground/80">
|
|
||||||
{#each selectedItem.lore as line, index (index)}
|
|
||||||
<li class="break-all">{line}</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedItem.enchants}
|
|
||||||
<div>
|
|
||||||
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">Enchantments</p>
|
|
||||||
<ul class="mt-1 space-y-0.5 text-xs">
|
|
||||||
{#each Object.entries(selectedItem.enchants) as [key, value] (key)}
|
|
||||||
<li class="flex justify-between gap-3"><span>{titleCase(key)}</span><span class="font-mono text-muted-foreground">{ROMAN[value] || value}</span></li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedItem.nbt}
|
|
||||||
<div>
|
|
||||||
<p class="text-[10px] uppercase tracking-wide text-muted-foreground">NBT</p>
|
|
||||||
<pre class="mt-1 max-h-48 max-w-full overflow-auto rounded-md bg-muted/40 p-2 font-mono text-[10px] leading-snug whitespace-pre-wrap break-all">{selectedItem.nbt}</pre>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-full min-h-56 items-center justify-center text-center text-sm text-muted-foreground">
|
|
||||||
Select an occupied slot to inspect the item.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import { titleCase } from '$lib/utils';
|
import {titleCase} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: string;
|
type: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { type, class: className = '' }: Props = $props();
|
let {type, class: className = ''}: Props = $props();
|
||||||
let url: string | null = $state(null);
|
let url: string | null = $state(null);
|
||||||
const normalized = $derived(type.toLowerCase());
|
const normalized = $derived(type.toLowerCase());
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
import('$lib/rendering/itemRenderer')
|
import('$lib/rendering/itemRenderer')
|
||||||
.then(({ renderItem }) => renderItem(normalized))
|
.then(({renderItem}) => renderItem(normalized))
|
||||||
.then((next) => {
|
.then((next) => {
|
||||||
if (alive) url = next;
|
if (alive) url = next;
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url}
|
{#if url}
|
||||||
<img class="size-full object-contain inventory-pixelated {className}" src={url} alt={titleCase(type)} />
|
<img class="size-full object-contain inventory-pixelated {className}" src={url} alt={titleCase(type)}/>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="grid size-full place-items-center px-0.5 text-center font-mono text-[8px] leading-tight text-muted-foreground {className}">
|
<span class="grid size-full place-items-center px-0.5 text-center font-mono text-[8px] leading-tight text-muted-foreground {className}">
|
||||||
{normalized.replace(/_/g, ' ')}
|
{normalized.replace(/_/g, ' ')}
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import {type VariantProps, tv} from "tailwind-variants";
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
export const badgeVariants = tv({
|
||||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/30",
|
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/30",
|
||||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
import type {HTMLAnchorAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
href,
|
href,
|
||||||
class: className,
|
class: className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={href ? "a" : "span"}
|
this={href ? "a" : "span"}
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="badge"
|
data-slot="badge"
|
||||||
{href}
|
{href}
|
||||||
class={cn(badgeVariants({ variant }), className)}
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default as Badge } from "./badge.svelte";
|
export {default as Badge} from "./badge.svelte";
|
||||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
export {badgeVariants, type BadgeVariant} from "./badge.svelte";
|
||||||
|
|||||||
@@ -1,82 +1,82 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from "svelte/elements";
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import {type VariantProps, tv} from "tailwind-variants";
|
||||||
|
|
||||||
export const buttonVariants = tv({
|
export const buttonVariants = 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 dark:aria-invalid:border-destructive/50 rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
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 dark:aria-invalid:border-destructive/50 rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
outline: "border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
|
outline: "border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
default: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
||||||
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
||||||
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||||
"icon-sm": "size-8",
|
"icon-sm": "size-8",
|
||||||
"icon-lg": "size-10",
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
WithElementRef<HTMLAnchorAttributes> & {
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let {
|
let {
|
||||||
class: className,
|
class: className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "default",
|
size = "default",
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
href = undefined,
|
href = undefined,
|
||||||
type = "button",
|
type = "button",
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: ButtonProps = $props();
|
}: ButtonProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
href={disabled ? undefined : href}
|
href={disabled ? undefined : href}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
role={disabled ? "link" : undefined}
|
role={disabled ? "link" : undefined}
|
||||||
tabindex={disabled ? -1 : undefined}
|
tabindex={disabled ? -1 : undefined}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
{type}
|
{type}
|
||||||
{disabled}
|
{disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import Root, {
|
import Root, {
|
||||||
type ButtonProps,
|
type ButtonProps,
|
||||||
type ButtonSize,
|
type ButtonSize,
|
||||||
type ButtonVariant,
|
type ButtonVariant,
|
||||||
buttonVariants,
|
buttonVariants,
|
||||||
} from "./button.svelte";
|
} from "./button.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
type ButtonProps as Props,
|
type ButtonProps as Props,
|
||||||
//
|
//
|
||||||
Root as Button,
|
Root as Button,
|
||||||
buttonVariants,
|
buttonVariants,
|
||||||
type ButtonProps,
|
type ButtonProps,
|
||||||
type ButtonSize,
|
type ButtonSize,
|
||||||
type ButtonVariant,
|
type ButtonVariant,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
class={cn(
|
class={cn(
|
||||||
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-content"
|
data-slot="card-content"
|
||||||
class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
|
class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
class={cn(
|
class={cn(
|
||||||
"gap-2 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
"gap-2 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
class={cn("text-base font-medium", className)}
|
class={cn("text-base font-medium", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
size = "default",
|
size = "default",
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-2xl py-6 text-sm ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-2xl py-6 text-sm ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ import Title from "./card-title.svelte";
|
|||||||
import Action from "./card-action.svelte";
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Content,
|
Content,
|
||||||
Description,
|
Description,
|
||||||
Footer,
|
Footer,
|
||||||
Header,
|
Header,
|
||||||
Title,
|
Title,
|
||||||
Action,
|
Action,
|
||||||
//
|
//
|
||||||
Root as Card,
|
Root as Card,
|
||||||
Content as CardContent,
|
Content as CardContent,
|
||||||
Description as CardDescription,
|
Description as CardDescription,
|
||||||
Footer as CardFooter,
|
Footer as CardFooter,
|
||||||
Header as CardHeader,
|
Header as CardHeader,
|
||||||
Title as CardTitle,
|
Title as CardTitle,
|
||||||
Action as CardAction,
|
Action as CardAction,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
type = "button",
|
type = "button",
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.CloseProps = $props();
|
}: DialogPrimitive.CloseProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps}/>
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
import DialogPortal from "./dialog-portal.svelte";
|
import DialogPortal from "./dialog-portal.svelte";
|
||||||
import type { Snippet } from "svelte";
|
import type {Snippet} from "svelte";
|
||||||
import * as Dialog from "./index.js";
|
import * as Dialog from "./index.js";
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import {Button} from "$lib/components/ui/button/index.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { Cancel01Icon } from '@hugeicons/core-free-icons';
|
import {Cancel01Icon} from '@hugeicons/core-free-icons';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
portalProps,
|
portalProps,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPortal {...portalProps}>
|
<DialogPortal {...portalProps}>
|
||||||
<Dialog.Overlay />
|
<Dialog.Overlay/>
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<DialogPrimitive.Close data-slot="dialog-close">
|
<DialogPrimitive.Close data-slot="dialog-close">
|
||||||
{#snippet child({ props })}
|
{#snippet child({props})}
|
||||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2}/>
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
{/if}
|
{/if}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.DescriptionProps = $props();
|
}: DialogPrimitive.DescriptionProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import {Button} from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<DialogPrimitive.Close>
|
<DialogPrimitive.Close>
|
||||||
{#snippet child({ props })}
|
{#snippet child({props})}
|
||||||
<Button variant="outline" {...props}>Close</Button>
|
<Button variant="outline" {...props}>Close</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
class={cn("gap-2 flex flex-col", className)}
|
class={cn("gap-2 flex flex-col", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.OverlayProps = $props();
|
}: DialogPrimitive.OverlayProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
let {...restProps}: DialogPrimitive.PortalProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Portal {...restProps} />
|
<DialogPrimitive.Portal {...restProps}/>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.TitleProps = $props();
|
}: DialogPrimitive.TitleProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
class={cn("text-base leading-none font-medium", className)}
|
class={cn("text-base leading-none font-medium", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
type = "button",
|
type = "button",
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.TriggerProps = $props();
|
}: DialogPrimitive.TriggerProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import {Dialog as DialogPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
let {open = $bindable(false), ...restProps}: DialogPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Root bind:open {...restProps} />
|
<DialogPrimitive.Root bind:open {...restProps}/>
|
||||||
|
|||||||
@@ -10,25 +10,25 @@ import Trigger from "./dialog-trigger.svelte";
|
|||||||
import Close from "./dialog-close.svelte";
|
import Close from "./dialog-close.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Title,
|
Title,
|
||||||
Portal,
|
Portal,
|
||||||
Footer,
|
Footer,
|
||||||
Header,
|
Header,
|
||||||
Trigger,
|
Trigger,
|
||||||
Overlay,
|
Overlay,
|
||||||
Content,
|
Content,
|
||||||
Description,
|
Description,
|
||||||
Close,
|
Close,
|
||||||
//
|
//
|
||||||
Root as Dialog,
|
Root as Dialog,
|
||||||
Title as DialogTitle,
|
Title as DialogTitle,
|
||||||
Portal as DialogPortal,
|
Portal as DialogPortal,
|
||||||
Footer as DialogFooter,
|
Footer as DialogFooter,
|
||||||
Header as DialogHeader,
|
Header as DialogHeader,
|
||||||
Trigger as DialogTrigger,
|
Trigger as DialogTrigger,
|
||||||
Overlay as DialogOverlay,
|
Overlay as DialogOverlay,
|
||||||
Content as DialogContent,
|
Content as DialogContent,
|
||||||
Description as DialogDescription,
|
Description as DialogDescription,
|
||||||
Close as DialogClose,
|
Close as DialogClose,
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-10
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable([]),
|
value = $bindable([]),
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxGroup
|
<DropdownMenuPrimitive.CheckboxGroup
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
data-slot="dropdown-menu-checkbox-group"
|
data-slot="dropdown-menu-checkbox-group"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+30
-30
@@ -1,45 +1,45 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { MinusSignIcon } from '@hugeicons/core-free-icons';
|
import {MinusSignIcon} from '@hugeicons/core-free-icons';
|
||||||
import { Tick02Icon } from '@hugeicons/core-free-icons';
|
import {Tick02Icon} from '@hugeicons/core-free-icons';
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import type { Snippet } from "svelte";
|
import type {Snippet} from "svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
checked = $bindable(false),
|
checked = $bindable(false),
|
||||||
indeterminate = $bindable(false),
|
indeterminate = $bindable(false),
|
||||||
class: className,
|
class: className,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:checked
|
bind:checked
|
||||||
bind:indeterminate
|
bind:indeterminate
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked, indeterminate })}
|
{#snippet children({checked, indeterminate})}
|
||||||
<span
|
<span
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
>
|
>
|
||||||
{#if indeterminate}
|
{#if indeterminate}
|
||||||
<HugeiconsIcon icon={MinusSignIcon} strokeWidth={2} />
|
<HugeiconsIcon icon={MinusSignIcon} strokeWidth={2}/>
|
||||||
{:else if checked}
|
{:else if checked}
|
||||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2}/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.()}
|
{@render childrenProp?.()}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
|||||||
+22
-22
@@ -1,31 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
align = "start",
|
align = "start",
|
||||||
portalProps,
|
portalProps,
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.ContentProps & {
|
}: DropdownMenuPrimitive.ContentProps & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPortal {...portalProps}>
|
<DropdownMenuPortal {...portalProps}>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
{align}
|
{align}
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 bg-popover text-popover-foreground dark:ring-foreground/10 min-w-48 rounded-2xl p-1 shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 bg-popover text-popover-foreground dark:ring-foreground/10 min-w-48 rounded-2xl p-1 shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
|
|||||||
+16
-16
@@ -1,22 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
...restProps
|
...restProps
|
||||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.GroupHeading
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-group-heading"
|
data-slot="dropdown-menu-group-heading"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
let {ref = $bindable(null), ...restProps}: DropdownMenuPrimitive.GroupProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps}/>
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.ItemProps & {
|
}: DropdownMenuPrimitive.ItemProps & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive";
|
variant?: "default" | "destructive";
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-item"
|
data-slot="dropdown-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("text-muted-foreground px-3 py-2.5 text-xs data-inset:pl-9.5 data-[inset]:pl-8", className)}
|
class={cn("text-muted-foreground px-3 py-2.5 text-xs data-inset:pl-9.5 data-[inset]:pl-8", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
let {...restProps}: DropdownMenuPrimitive.PortalProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
<DropdownMenuPrimitive.Portal {...restProps}/>
|
||||||
|
|||||||
+10
-10
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+21
-21
@@ -1,35 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { Tick02Icon } from '@hugeicons/core-free-icons';
|
import {Tick02Icon} from '@hugeicons/core-free-icons';
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import {cn, type WithoutChild} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked })}
|
{#snippet children({checked})}
|
||||||
<span
|
<span
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
data-slot="dropdown-menu-radio-item-indicator"
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
>
|
>
|
||||||
{#if checked}
|
{#if checked}
|
||||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2}/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.({ checked })}
|
{@render childrenProp?.({checked})}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
|||||||
+11
-11
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
class={cn("bg-border/50 -mx-1 my-1 h-px", className)}
|
class={cn("bg-border/50 -mx-1 my-1 h-px", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+12
-12
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+11
-11
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 bg-popover text-popover-foreground min-w-36 rounded-2xl p-1 shadow-2xl ring-1 duration-100 w-auto", className)}
|
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 bg-popover text-popover-foreground min-w-36 rounded-2xl p-1 shadow-2xl ring-1 duration-100 w-auto", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+20
-20
@@ -1,30 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
|
import {ArrowRight01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-xl px-3 py-2 text-sm data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
<HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} class="ml-auto" />
|
<HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} class="ml-auto"/>
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
let {open = $bindable(false), ...restProps}: DropdownMenuPrimitive.SubProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
<DropdownMenuPrimitive.Sub bind:open {...restProps}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
let {ref = $bindable(null), ...restProps}: DropdownMenuPrimitive.TriggerProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
let {open = $bindable(false), ...restProps}: DropdownMenuPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
<DropdownMenuPrimitive.Root bind:open {...restProps}/>
|
||||||
|
|||||||
@@ -17,38 +17,38 @@ import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
|||||||
import Portal from "./dropdown-menu-portal.svelte";
|
import Portal from "./dropdown-menu-portal.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
CheckboxItem,
|
CheckboxItem,
|
||||||
Content,
|
Content,
|
||||||
Portal,
|
Portal,
|
||||||
Root as DropdownMenu,
|
Root as DropdownMenu,
|
||||||
CheckboxGroup as DropdownMenuCheckboxGroup,
|
CheckboxGroup as DropdownMenuCheckboxGroup,
|
||||||
CheckboxItem as DropdownMenuCheckboxItem,
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
Content as DropdownMenuContent,
|
Content as DropdownMenuContent,
|
||||||
Portal as DropdownMenuPortal,
|
Portal as DropdownMenuPortal,
|
||||||
Group as DropdownMenuGroup,
|
Group as DropdownMenuGroup,
|
||||||
Item as DropdownMenuItem,
|
Item as DropdownMenuItem,
|
||||||
Label as DropdownMenuLabel,
|
Label as DropdownMenuLabel,
|
||||||
RadioGroup as DropdownMenuRadioGroup,
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
RadioItem as DropdownMenuRadioItem,
|
RadioItem as DropdownMenuRadioItem,
|
||||||
Separator as DropdownMenuSeparator,
|
Separator as DropdownMenuSeparator,
|
||||||
Shortcut as DropdownMenuShortcut,
|
Shortcut as DropdownMenuShortcut,
|
||||||
Sub as DropdownMenuSub,
|
Sub as DropdownMenuSub,
|
||||||
SubContent as DropdownMenuSubContent,
|
SubContent as DropdownMenuSubContent,
|
||||||
SubTrigger as DropdownMenuSubTrigger,
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
Trigger as DropdownMenuTrigger,
|
Trigger as DropdownMenuTrigger,
|
||||||
GroupHeading as DropdownMenuGroupHeading,
|
GroupHeading as DropdownMenuGroupHeading,
|
||||||
Group,
|
Group,
|
||||||
GroupHeading,
|
GroupHeading,
|
||||||
Item,
|
Item,
|
||||||
Label,
|
Label,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioItem,
|
RadioItem,
|
||||||
Root,
|
Root,
|
||||||
Separator,
|
Separator,
|
||||||
Shortcut,
|
Shortcut,
|
||||||
Sub,
|
Sub,
|
||||||
SubContent,
|
SubContent,
|
||||||
SubTrigger,
|
SubTrigger,
|
||||||
Trigger,
|
Trigger,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Root from "./input.svelte";
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Input,
|
Root as Input,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
import type {HTMLInputAttributes, HTMLInputTypeAttribute} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
|
|
||||||
type Props = WithElementRef<
|
type Props = WithElementRef<
|
||||||
Omit<HTMLInputAttributes, "type"> &
|
Omit<HTMLInputAttributes, "type"> &
|
||||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
>;
|
>;
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
type,
|
type,
|
||||||
files = $bindable(),
|
files = $bindable(),
|
||||||
class: className,
|
class: className,
|
||||||
"data-slot": dataSlot = "input",
|
"data-slot": dataSlot = "input",
|
||||||
...restProps
|
...restProps
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === "file"}
|
{#if type === "file"}
|
||||||
<input
|
<input
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot={dataSlot}
|
data-slot={dataSlot}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border px-3 py-1 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
"bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border px-3 py-1 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
type="file"
|
type="file"
|
||||||
bind:files
|
bind:files
|
||||||
bind:value
|
bind:value
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot={dataSlot}
|
data-slot={dataSlot}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border px-3 py-1 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
"bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border px-3 py-1 text-base transition-colors file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{type}
|
{type}
|
||||||
bind:value
|
bind:value
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Root from "./label.svelte";
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Label,
|
Root as Label,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label as LabelPrimitive } from "bits-ui";
|
import {Label as LabelPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: LabelPrimitive.RootProps = $props();
|
}: LabelPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
class={cn(
|
class={cn(
|
||||||
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,27 +11,27 @@ import GroupHeading from "./select-group-heading.svelte";
|
|||||||
import Portal from "./select-portal.svelte";
|
import Portal from "./select-portal.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Group,
|
Group,
|
||||||
Label,
|
Label,
|
||||||
Item,
|
Item,
|
||||||
Content,
|
Content,
|
||||||
Trigger,
|
Trigger,
|
||||||
Separator,
|
Separator,
|
||||||
ScrollDownButton,
|
ScrollDownButton,
|
||||||
ScrollUpButton,
|
ScrollUpButton,
|
||||||
GroupHeading,
|
GroupHeading,
|
||||||
Portal,
|
Portal,
|
||||||
//
|
//
|
||||||
Root as Select,
|
Root as Select,
|
||||||
Group as SelectGroup,
|
Group as SelectGroup,
|
||||||
Label as SelectLabel,
|
Label as SelectLabel,
|
||||||
Item as SelectItem,
|
Item as SelectItem,
|
||||||
Content as SelectContent,
|
Content as SelectContent,
|
||||||
Trigger as SelectTrigger,
|
Trigger as SelectTrigger,
|
||||||
Separator as SelectSeparator,
|
Separator as SelectSeparator,
|
||||||
ScrollDownButton as SelectScrollDownButton,
|
ScrollDownButton as SelectScrollDownButton,
|
||||||
ScrollUpButton as SelectScrollUpButton,
|
ScrollUpButton as SelectScrollUpButton,
|
||||||
GroupHeading as SelectGroupHeading,
|
GroupHeading as SelectGroupHeading,
|
||||||
Portal as SelectPortal,
|
Portal as SelectPortal,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import SelectPortal from "./select-portal.svelte";
|
import SelectPortal from "./select-portal.svelte";
|
||||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import {cn, type WithoutChild} from "$lib/utils.js";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
import type {WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
portalProps,
|
portalProps,
|
||||||
children,
|
children,
|
||||||
preventScroll = true,
|
preventScroll = true,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPortal {...portalProps}>
|
<SelectPortal {...portalProps}>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
{preventScroll}
|
{preventScroll}
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 min-w-36 rounded-2xl shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto",
|
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/5 min-w-36 rounded-2xl shadow-2xl ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton/>
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
class={cn(
|
class={cn(
|
||||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton/>
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPortal>
|
</SelectPortal>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.GroupHeading
|
<SelectPrimitive.GroupHeading
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-group-heading"
|
data-slot="select-group-heading"
|
||||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</SelectPrimitive.GroupHeading>
|
</SelectPrimitive.GroupHeading>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectPrimitive.GroupProps = $props();
|
}: SelectPrimitive.GroupProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.Group
|
<SelectPrimitive.Group
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-group"
|
data-slot="select-group"
|
||||||
class={cn("scroll-my-1 p-1", className)}
|
class={cn("scroll-my-1 p-1", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import {cn, type WithoutChild} from "$lib/utils.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { Tick02Icon } from '@hugeicons/core-free-icons';
|
import {Tick02Icon} from '@hugeicons/core-free-icons';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
bind:ref
|
bind:ref
|
||||||
{value}
|
{value}
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 focus:bg-accent data-highlighted:bg-accent data-highlighted:text-accent-foreground focus:text-accent-foreground relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 focus:bg-accent data-highlighted:bg-accent data-highlighted:text-accent-foreground focus:text-accent-foreground relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ selected, highlighted })}
|
{#snippet children({selected, highlighted})}
|
||||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||||
{#if selected}
|
{#if selected}
|
||||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} class="cn-select-item-indicator-icon" />
|
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} class="cn-select-item-indicator-icon"/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if childrenProp}
|
{#if childrenProp}
|
||||||
{@render childrenProp({ selected, highlighted })}
|
{@render childrenProp({selected, highlighted})}
|
||||||
{:else}
|
{:else}
|
||||||
{label || value}
|
{label || value}
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
class={cn("text-muted-foreground px-3 py-2.5 text-xs", className)}
|
class={cn("text-muted-foreground px-3 py-2.5 text-xs", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
let {...restProps}: SelectPrimitive.PortalProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.Portal {...restProps} />
|
<SelectPrimitive.Portal {...restProps}/>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
|
import {ArrowDown01Icon} from '@hugeicons/core-free-icons';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2}/>
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { ArrowUp01Icon } from '@hugeicons/core-free-icons';
|
import {ArrowUp01Icon} from '@hugeicons/core-free-icons';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className)}
|
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2}/>
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
import type {Separator as SeparatorPrimitive} from "bits-ui";
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
import {Separator} from "$lib/components/ui/separator/index.js";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SeparatorPrimitive.RootProps = $props();
|
}: SeparatorPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Separator
|
<Separator
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
class={cn("bg-border/50 -mx-1 my-1 h-px pointer-events-none", className)}
|
class={cn("bg-border/50 -mx-1 my-1 h-px pointer-events-none", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import {cn, type WithoutChild} from "$lib/utils.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { UnfoldMoreIcon } from '@hugeicons/core-free-icons';
|
import {UnfoldMoreIcon} from '@hugeicons/core-free-icons';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
size = "default",
|
size = "default",
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||||
size?: "sm" | "default";
|
size?: "sm" | "default";
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
class={cn(
|
class={cn(
|
||||||
"border-input data-placeholder:text-muted-foreground bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-4xl border px-3 py-2 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"border-input data-placeholder:text-muted-foreground bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-4xl border px-3 py-2 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
<HugeiconsIcon icon={UnfoldMoreIcon} strokeWidth={2} class="text-muted-foreground size-4 pointer-events-none" />
|
<HugeiconsIcon icon={UnfoldMoreIcon} strokeWidth={2} class="text-muted-foreground size-4 pointer-events-none"/>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
import {Select as SelectPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = $bindable(false),
|
open = $bindable(false),
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectPrimitive.RootProps = $props();
|
}: SelectPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Root from "./separator.svelte";
|
import Root from "./separator.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Separator,
|
Root as Separator,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
import {Separator as SeparatorPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
"data-slot": dataSlot = "separator",
|
"data-slot": dataSlot = "separator",
|
||||||
...restProps
|
...restProps
|
||||||
}: SeparatorPrimitive.RootProps = $props();
|
}: SeparatorPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot={dataSlot}
|
data-slot={dataSlot}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||||
// this is different in shadcn/ui but self-stretch breaks things for us
|
// this is different in shadcn/ui but self-stretch breaks things for us
|
||||||
"data-[orientation=vertical]:h-full",
|
"data-[orientation=vertical]:h-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,25 +10,25 @@ import Title from "./sheet-title.svelte";
|
|||||||
import Description from "./sheet-description.svelte";
|
import Description from "./sheet-description.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Close,
|
Close,
|
||||||
Trigger,
|
Trigger,
|
||||||
Portal,
|
Portal,
|
||||||
Overlay,
|
Overlay,
|
||||||
Content,
|
Content,
|
||||||
Header,
|
Header,
|
||||||
Footer,
|
Footer,
|
||||||
Title,
|
Title,
|
||||||
Description,
|
Description,
|
||||||
//
|
//
|
||||||
Root as Sheet,
|
Root as Sheet,
|
||||||
Close as SheetClose,
|
Close as SheetClose,
|
||||||
Trigger as SheetTrigger,
|
Trigger as SheetTrigger,
|
||||||
Portal as SheetPortal,
|
Portal as SheetPortal,
|
||||||
Overlay as SheetOverlay,
|
Overlay as SheetOverlay,
|
||||||
Content as SheetContent,
|
Content as SheetContent,
|
||||||
Header as SheetHeader,
|
Header as SheetHeader,
|
||||||
Footer as SheetFooter,
|
Footer as SheetFooter,
|
||||||
Title as SheetTitle,
|
Title as SheetTitle,
|
||||||
Description as SheetDescription,
|
Description as SheetDescription,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
|
let {ref = $bindable(null), ...restProps}: SheetPrimitive.CloseProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps}/>
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
export type Side = "top" | "right" | "bottom" | "left";
|
export type Side = "top" | "right" | "bottom" | "left";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
import type { Snippet } from "svelte";
|
import type {Snippet} from "svelte";
|
||||||
import SheetPortal from "./sheet-portal.svelte";
|
import SheetPortal from "./sheet-portal.svelte";
|
||||||
import SheetOverlay from "./sheet-overlay.svelte";
|
import SheetOverlay from "./sheet-overlay.svelte";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import {Button} from "$lib/components/ui/button/index.js";
|
||||||
import { HugeiconsIcon } from "@hugeicons/svelte"
|
import {HugeiconsIcon} from "@hugeicons/svelte"
|
||||||
import { Cancel01Icon } from '@hugeicons/core-free-icons';
|
import {Cancel01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import {cn, type WithoutChildrenOrChild} from "$lib/utils.js";
|
||||||
import type { ComponentProps } from "svelte";
|
import type {ComponentProps} from "svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
side = "right",
|
side = "right",
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
portalProps,
|
portalProps,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
||||||
side?: Side;
|
side?: Side;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPortal {...portalProps}>
|
<SheetPortal {...portalProps}>
|
||||||
<SheetOverlay />
|
<SheetOverlay/>
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
data-side={side}
|
data-side={side}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground fixed z-50 flex flex-col bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
"bg-popover text-popover-foreground fixed z-50 flex flex-col bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<SheetPrimitive.Close data-slot="sheet-close">
|
<SheetPrimitive.Close data-slot="sheet-close">
|
||||||
{#snippet child({ props })}
|
{#snippet child({props})}
|
||||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||||
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2}/>
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
{/if}
|
{/if}
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SheetPrimitive.DescriptionProps = $props();
|
}: SheetPrimitive.DescriptionProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="sheet-description"
|
data-slot="sheet-description"
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="sheet-footer"
|
data-slot="sheet-footer"
|
||||||
class={cn("gap-2 p-6 mt-auto flex flex-col", className)}
|
class={cn("gap-2 p-6 mt-auto flex flex-col", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="sheet-header"
|
data-slot="sheet-header"
|
||||||
class={cn("gap-1.5 p-6 flex flex-col", className)}
|
class={cn("gap-1.5 p-6 flex flex-col", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SheetPrimitive.OverlayProps = $props();
|
}: SheetPrimitive.OverlayProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
class={cn("bg-black/80 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
class={cn("bg-black/80 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ...restProps }: SheetPrimitive.PortalProps = $props();
|
let {...restProps}: SheetPrimitive.PortalProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Portal {...restProps} />
|
<SheetPrimitive.Portal {...restProps}/>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
import { cn } from "$lib/utils.js";
|
import {cn} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SheetPrimitive.TitleProps = $props();
|
}: SheetPrimitive.TitleProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="sheet-title"
|
data-slot="sheet-title"
|
||||||
class={cn("text-foreground text-base font-medium", className)}
|
class={cn("text-foreground text-base font-medium", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps = $props();
|
let {ref = $bindable(null), ...restProps}: SheetPrimitive.TriggerProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
|
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import {Dialog as SheetPrimitive} from "bits-ui";
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: SheetPrimitive.RootProps = $props();
|
let {open = $bindable(false), ...restProps}: SheetPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Root bind:open {...restProps} />
|
<SheetPrimitive.Root bind:open {...restProps}/>
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import Header from "./table-header.svelte";
|
|||||||
import Row from "./table-row.svelte";
|
import Row from "./table-row.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Body,
|
Body,
|
||||||
Caption,
|
Caption,
|
||||||
Cell,
|
Cell,
|
||||||
Footer,
|
Footer,
|
||||||
Head,
|
Head,
|
||||||
Header,
|
Header,
|
||||||
Row,
|
Row,
|
||||||
//
|
//
|
||||||
Root as Table,
|
Root as Table,
|
||||||
Body as TableBody,
|
Body as TableBody,
|
||||||
Caption as TableCaption,
|
Caption as TableCaption,
|
||||||
Cell as TableCell,
|
Cell as TableCell,
|
||||||
Footer as TableFooter,
|
Footer as TableFooter,
|
||||||
Head as TableHead,
|
Head as TableHead,
|
||||||
Header as TableHeader,
|
Header as TableHeader,
|
||||||
Row as TableRow,
|
Row as TableRow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tbody bind:this={ref} data-slot="table-body" class={cn("[&_tr:last-child]:border-0", className)} {...restProps}>
|
<tbody bind:this={ref} data-slot="table-body" class={cn("[&_tr:last-child]:border-0", className)} {...restProps}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<caption
|
<caption
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="table-caption"
|
data-slot="table-caption"
|
||||||
class={cn("text-muted-foreground mt-4 text-sm", className)}
|
class={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</caption>
|
</caption>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLTdAttributes } from "svelte/elements";
|
import type {HTMLTdAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLTdAttributes> = $props();
|
}: WithElementRef<HTMLTdAttributes> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td bind:this={ref} data-slot="table-cell" class={cn("p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} {...restProps}>
|
<td bind:this={ref} data-slot="table-cell"
|
||||||
{@render children?.()}
|
class={cn("p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tfoot
|
<tfoot
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="table-footer"
|
data-slot="table-footer"
|
||||||
class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLThAttributes } from "svelte/elements";
|
import type {HTMLThAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLThAttributes> = $props();
|
}: WithElementRef<HTMLThAttributes> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<th bind:this={ref} data-slot="table-head" class={cn("text-foreground h-12 px-3 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} {...restProps}>
|
<th bind:this={ref} data-slot="table-head"
|
||||||
{@render children?.()}
|
class={cn("text-foreground h-12 px-3 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<thead
|
<thead
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="table-header"
|
data-slot="table-header"
|
||||||
class={cn("[&_tr]:border-b", className)}
|
class={cn("[&_tr]:border-b", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type {HTMLAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr bind:this={ref} data-slot="table-row" class={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)} {...restProps}>
|
<tr bind:this={ref} data-slot="table-row"
|
||||||
{@render children?.()}
|
class={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLTableAttributes } from "svelte/elements";
|
import type {HTMLTableAttributes} from "svelte/elements";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import {cn, type WithElementRef} from "$lib/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLTableAttributes> = $props();
|
}: WithElementRef<HTMLTableAttributes> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
||||||
<table bind:this={ref} data-slot="table" class={cn("w-full caption-bottom text-sm", className)} {...restProps}>
|
<table bind:this={ref} data-slot="table" class={cn("w-full caption-bottom text-sm", className)} {...restProps}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Root from "./textarea.svelte";
|
import Root from "./textarea.svelte";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Textarea,
|
Root as Textarea,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
import {cn, type WithElementRef, type WithoutChildren} from "$lib/utils.js";
|
||||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
import type {HTMLTextareaAttributes} from "svelte/elements";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
class: className,
|
class: className,
|
||||||
"data-slot": dataSlot = "textarea",
|
"data-slot": dataSlot = "textarea",
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot={dataSlot}
|
data-slot={dataSlot}
|
||||||
class={cn(
|
class={cn(
|
||||||
"border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-xl border px-3 py-3 text-base transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-xl border px-3 py-3 text-base transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:value
|
bind:value
|
||||||
{...restProps}
|
{...restProps}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@@ -1,91 +1,97 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { ArrowDown01Icon, Search01Icon } from '@hugeicons/core-free-icons';
|
import {ArrowDown01Icon, Search01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { api } from '$lib/api';
|
import {api} from '$lib/api';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import type { CommandGroup } from '$lib/types/api';
|
import type {CommandGroup} from '$lib/types/api';
|
||||||
import { lowerSearch } from '$lib/utils';
|
import {lowerSearch} from '$lib/utils';
|
||||||
|
|
||||||
let groups: CommandGroup[] = $state([]);
|
let groups: CommandGroup[] = $state([]);
|
||||||
let filter = $state('');
|
let filter = $state('');
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let collapsed = $state(false);
|
let collapsed = $state(false);
|
||||||
|
|
||||||
const visibleGroups = $derived.by(() => {
|
const visibleGroups = $derived.by(() => {
|
||||||
const q = filter.toLowerCase().trim();
|
const q = filter.toLowerCase().trim();
|
||||||
return groups
|
return groups
|
||||||
.map((group) => ({
|
.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
commands: group.commands.filter((command) => !q || lowerSearch(command).includes(q) || group.plugin.toLowerCase().includes(q))
|
commands: group.commands.filter((command) => !q || lowerSearch(command).includes(q) || group.plugin.toLowerCase().includes(q))
|
||||||
}))
|
}))
|
||||||
.filter((group) => group.commands.length > 0);
|
.filter((group) => group.commands.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
groups = (await api.commands()).groups ?? [];
|
groups = (await api.commands()).groups ?? [];
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
error = cause instanceof Error ? cause.message : 'Unable to load commands.';
|
error = cause instanceof Error ? cause.message : 'Unable to load commands.';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise">
|
<section class="rise">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Commands</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Commands</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-6 flex flex-wrap items-center gap-3">
|
<section class="rise mt-6 flex flex-wrap items-center gap-3">
|
||||||
<div class="relative w-full sm:max-w-md">
|
<div class="relative w-full sm:max-w-md">
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
<Input bind:value={filter} placeholder="Filter commands, aliases, permissions..." autocomplete="off" class="pl-9" />
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
</div>
|
<Input bind:value={filter} placeholder="Filter commands, aliases, permissions..." autocomplete="off"
|
||||||
<Button variant="outline" onclick={() => (collapsed = !collapsed)}>{collapsed ? 'Expand all' : 'Collapse all'}</Button>
|
class="pl-9"/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline"
|
||||||
|
onclick={() => (collapsed = !collapsed)}>{collapsed ? 'Expand all' : 'Collapse all'}</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">Loading commands...</p>
|
<p class="mt-4 text-sm text-muted-foreground">Loading commands...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="mt-4 text-sm text-destructive">{error}</p>
|
<p class="mt-4 text-sm text-destructive">{error}</p>
|
||||||
{:else if visibleGroups.length === 0}
|
{:else if visibleGroups.length === 0}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">No commands match that filter.</p>
|
<p class="mt-4 text-sm text-muted-foreground">No commands match that filter.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<section class="rise mt-4 space-y-3">
|
<section class="rise mt-4 space-y-3">
|
||||||
{#each visibleGroups as group (group.plugin)}
|
{#each visibleGroups as group (group.plugin)}
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<details open={!collapsed}>
|
<details open={!collapsed}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-4 py-3">
|
<summary
|
||||||
<span class="text-sm font-medium">{group.plugin}</span>
|
class="flex cursor-pointer list-none items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-4 py-3">
|
||||||
<span class="flex items-center gap-2 text-xs text-muted-foreground"><span>{group.commands.length} commands</span><HugeiconsIcon icon={ArrowDown01Icon} class="size-4" /></span>
|
<span class="text-sm font-medium">{group.plugin}</span>
|
||||||
</summary>
|
<span class="flex items-center gap-2 text-xs text-muted-foreground"><span>{group.commands.length}
|
||||||
<div class="divide-y divide-border/60">
|
commands</span><HugeiconsIcon icon={ArrowDown01Icon} class="size-4"/></span>
|
||||||
{#each group.commands as command (command.name)}
|
</summary>
|
||||||
<article class="grid gap-2 px-4 py-3 md:grid-cols-[14rem_1fr]">
|
<div class="divide-y divide-border/60">
|
||||||
<div class="min-w-0">
|
{#each group.commands as command (command.name)}
|
||||||
<p class="break-all font-mono text-sm font-medium text-foreground">/{command.name}</p>
|
<article class="grid gap-2 px-4 py-3 md:grid-cols-[14rem_1fr]">
|
||||||
{#if command.aliases?.length}
|
<div class="min-w-0">
|
||||||
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{command.aliases.map((alias) => `/${alias}`).join(', ')}</p>
|
<p class="break-all font-mono text-sm font-medium text-foreground">
|
||||||
{/if}
|
/{command.name}</p>
|
||||||
</div>
|
{#if command.aliases?.length}
|
||||||
<div class="min-w-0 text-sm">
|
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{command.aliases.map((alias) => `/${alias}`).join(', ')}</p>
|
||||||
<p class="text-foreground/90">{command.description || 'No description.'}</p>
|
{/if}
|
||||||
{#if command.usage}
|
</div>
|
||||||
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{command.usage}</p>
|
<div class="min-w-0 text-sm">
|
||||||
{/if}
|
<p class="text-foreground/90">{command.description || 'No description.'}</p>
|
||||||
{#if command.permission}
|
{#if command.usage}
|
||||||
<p class="mt-1 break-all font-mono text-xs text-primary">{command.permission}</p>
|
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{command.usage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{#if command.permission}
|
||||||
</article>
|
<p class="mt-1 break-all font-mono text-xs text-primary">{command.permission}</p>
|
||||||
{/each}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</article>
|
||||||
</Card>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</section>
|
</details>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,197 +1,203 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import {onDestroy, onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import {
|
import {
|
||||||
ChartNoAxesColumnIncreasingIcon,
|
ChartNoAxesColumnIncreasingIcon,
|
||||||
Clock03Icon,
|
Clock03Icon,
|
||||||
CpuIcon,
|
CpuIcon,
|
||||||
CubeIcon,
|
CubeIcon,
|
||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
ServerStack01Icon,
|
ServerStack01Icon,
|
||||||
UserGroupIcon
|
UserGroupIcon
|
||||||
} from '@hugeicons/core-free-icons';
|
} from '@hugeicons/core-free-icons';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {formatBytes, formatDuration} from '$lib/utils';
|
||||||
import { cn, formatBytes, formatDuration } from '$lib/utils';
|
import type {StatsPayload} from '$lib/types/api';
|
||||||
import type { StatsPayload } from '$lib/types/api';
|
|
||||||
|
|
||||||
const SPARK_MAX = 60;
|
const SPARK_MAX = 60;
|
||||||
let stats = $state<StatsPayload | null>(null);
|
let stats = $state<StatsPayload | null>(null);
|
||||||
let connected = $state(false);
|
let now = $state(Date.now());
|
||||||
let now = $state(Date.now());
|
let tpsHistory: number[] = $state([]);
|
||||||
let tpsHistory: number[] = $state([]);
|
let es: EventSource | null = null;
|
||||||
let es: EventSource | null = null;
|
let timer: number | null = null;
|
||||||
let timer: number | null = null;
|
|
||||||
|
|
||||||
const uptime = $derived(stats?.server.startTime ? formatDuration(now - stats.server.startTime) : '-');
|
const uptime = $derived(stats?.server.startTime ? formatDuration(now - stats.server.startTime) : '-');
|
||||||
const memoryPercent = $derived(stats ? Math.max(0, Math.min(100, (stats.memory.used / stats.memory.max) * 100)) : 0);
|
const memoryPercent = $derived(stats ? Math.max(0, Math.min(100, (stats.memory.used / stats.memory.max) * 100)) : 0);
|
||||||
const cpuPercent = $derived(stats ? Math.max(0, Math.min(100, stats.cpu.process * 100)) : 0);
|
const cpuPercent = $derived(stats ? Math.max(0, Math.min(100, stats.cpu.process * 100)) : 0);
|
||||||
const playersPercent = $derived(stats && stats.players.max > 0 ? Math.max(0, Math.min(100, (stats.players.online / stats.players.max) * 100)) : 0);
|
const playersPercent = $derived(stats && stats.players.max > 0 ? Math.max(0, Math.min(100, (stats.players.online / stats.players.max) * 100)) : 0);
|
||||||
const tps = $derived(stats?.server.tps ?? []);
|
const tps = $derived(stats?.server.tps ?? []);
|
||||||
const tpsColor = $derived((tps[0] ?? 20) >= 19.5 ? 'text-success' : (tps[0] ?? 20) >= 18 ? 'text-warning' : 'text-destructive');
|
const tpsColor = $derived((tps[0] ?? 20) >= 19.5 ? 'text-success' : (tps[0] ?? 20) >= 18 ? 'text-warning' : 'text-destructive');
|
||||||
const sparkPoints = $derived.by(() => {
|
const sparkPoints = $derived.by(() => {
|
||||||
if (tpsHistory.length < 2) return '';
|
if (tpsHistory.length < 2) return '';
|
||||||
const width = 600;
|
const width = 600;
|
||||||
const height = 60;
|
const height = 60;
|
||||||
const pad = 4;
|
const pad = 4;
|
||||||
const values = tpsHistory.slice(-SPARK_MAX);
|
const values = tpsHistory.slice(-SPARK_MAX);
|
||||||
const step = (width - pad * 2) / (SPARK_MAX - 1);
|
const step = (width - pad * 2) / (SPARK_MAX - 1);
|
||||||
const offset = SPARK_MAX - values.length;
|
const offset = SPARK_MAX - values.length;
|
||||||
return values
|
return values
|
||||||
.map((value, index) => {
|
.map((value, index) => {
|
||||||
const x = pad + (index + offset) * step;
|
const x = pad + (index + offset) * step;
|
||||||
const clamped = Math.max(15, Math.min(20, value));
|
const clamped = Math.max(15, Math.min(20, value));
|
||||||
const y = pad + (height - pad * 2) * (1 - (clamped - 15) / 5);
|
const y = pad + (height - pad * 2) * (1 - (clamped - 15) / 5);
|
||||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||||
})
|
})
|
||||||
.join(' ');
|
.join(' ');
|
||||||
});
|
|
||||||
|
|
||||||
function pct(value: number | null | undefined) {
|
|
||||||
if (!Number.isFinite(value ?? NaN)) return '-';
|
|
||||||
return `${((value as number) * 100).toFixed(1)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tpsText(value: number | undefined) {
|
|
||||||
if (!Number.isFinite(value ?? NaN)) return '-';
|
|
||||||
return Math.min(value as number, 20).toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
timer = window.setInterval(() => (now = Date.now()), 1000);
|
|
||||||
es = new EventSource('/api/stats/stream');
|
|
||||||
es.addEventListener('open', () => (connected = true));
|
|
||||||
es.addEventListener('error', () => (connected = false));
|
|
||||||
es.addEventListener('message', (event) => {
|
|
||||||
try {
|
|
||||||
stats = JSON.parse(event.data) as StatsPayload;
|
|
||||||
connected = true;
|
|
||||||
const currentTps = stats.server.tps[0];
|
|
||||||
if (Number.isFinite(currentTps)) tpsHistory = [...tpsHistory.slice(-(SPARK_MAX - 1)), currentTps];
|
|
||||||
} catch {
|
|
||||||
connected = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
function pct(value: number | null | undefined) {
|
||||||
es?.close();
|
if (!Number.isFinite(value ?? NaN)) return '-';
|
||||||
if (timer) window.clearInterval(timer);
|
return `${((value as number) * 100).toFixed(1)}%`;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function tpsText(value: number | undefined) {
|
||||||
|
if (!Number.isFinite(value ?? NaN)) return '-';
|
||||||
|
return Math.min(value as number, 20).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
timer = window.setInterval(() => (now = Date.now()), 1000);
|
||||||
|
es = new EventSource('/api/stats/stream');
|
||||||
|
es.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
stats = JSON.parse(event.data) as StatsPayload;
|
||||||
|
const currentTps = stats.server.tps[0];
|
||||||
|
if (Number.isFinite(currentTps)) tpsHistory = [...tpsHistory.slice(-(SPARK_MAX - 1)), currentTps];
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
es?.close();
|
||||||
|
if (timer) window.clearInterval(timer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Overview</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Overview</h1>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">Minecraft version <span class="text-foreground">{stats?.server.version ?? '-'}</span></p>
|
<p class="mt-1 text-sm text-muted-foreground">Minecraft version <span
|
||||||
</div>
|
class="text-foreground">{stats?.server.version ?? '-'}</span></p>
|
||||||
<Badge variant={connected ? 'secondary' : 'destructive'} class={cn('gap-2', connected && 'bg-success/10 text-success')}>
|
</div>
|
||||||
<span class={cn('size-2 rounded-full', connected ? 'bg-success' : 'bg-destructive')}></span>
|
|
||||||
{connected ? 'streaming' : 'disconnected'}
|
|
||||||
</Badge>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<section class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<Card class="rise flex min-h-40 flex-col p-5">
|
<Card class="rise flex min-h-32 flex-col p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">Players</span>
|
<span class="text-sm text-muted-foreground">Players</span>
|
||||||
<HugeiconsIcon icon={UserGroupIcon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={UserGroupIcon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-baseline gap-2">
|
<div class="mt-3 flex items-baseline gap-2">
|
||||||
<span class="tabular text-4xl font-medium tracking-tight">{stats?.players.online ?? '-'}</span>
|
<span class="tabular text-3xl font-medium tracking-tight">{stats?.players.online ?? '-'}</span>
|
||||||
<span class="text-sm text-muted-foreground">/ {stats?.players.max ?? '-'}</span>
|
<span class="text-sm text-muted-foreground">/ {stats?.players.max ?? '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
|
<div class="mt-3 h-1 overflow-hidden rounded-full bg-muted">
|
||||||
<div class="h-full rounded-full bg-primary transition-[width] duration-500" style:width={`${playersPercent}%`}></div>
|
<div class="h-full rounded-full bg-primary transition-[width] duration-500"
|
||||||
</div>
|
style:width={`${playersPercent}%`}></div>
|
||||||
<a href="/players/" class="mt-auto pt-3 text-xs text-primary hover:underline">view list</a>
|
</div>
|
||||||
</Card>
|
<a href="/players/" class="mt-auto pt-2 text-xs text-primary hover:underline">view list</a>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="rise flex min-h-40 flex-col p-5">
|
<Card class="rise flex min-h-32 flex-col p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">CPU</span>
|
<span class="text-sm text-muted-foreground">CPU</span>
|
||||||
<HugeiconsIcon icon={CpuIcon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={CpuIcon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 tabular text-4xl font-medium tracking-tight">{pct(stats?.cpu.process)}</div>
|
<div class="mt-3 tabular text-3xl font-medium tracking-tight">{pct(stats?.cpu.process)}</div>
|
||||||
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
|
<div class="mt-3 h-1 overflow-hidden rounded-full bg-muted">
|
||||||
<div class="h-full rounded-full transition-[width] duration-500 {cpuPercent < 70 ? 'bg-primary' : cpuPercent < 90 ? 'bg-warning' : 'bg-destructive'}" style:width={`${cpuPercent}%`}></div>
|
<div class="h-full rounded-full transition-[width] duration-500 {cpuPercent < 70 ? 'bg-primary' : cpuPercent < 90 ? 'bg-warning' : 'bg-destructive'}"
|
||||||
</div>
|
style:width={`${cpuPercent}%`}></div>
|
||||||
<div class="mt-auto flex justify-between pt-3 text-xs text-muted-foreground">
|
</div>
|
||||||
<span>{stats?.cpu.cores ?? '-'} cores</span>
|
<div class="mt-auto flex justify-between pt-2 text-xs text-muted-foreground">
|
||||||
<span>system {pct(stats?.cpu.system)}</span>
|
<span>{stats?.cpu.cores ?? '-'} cores</span>
|
||||||
</div>
|
<span>system {pct(stats?.cpu.system)}</span>
|
||||||
</Card>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="rise flex min-h-40 flex-col p-5">
|
<Card class="rise flex min-h-32 flex-col p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">Memory</span>
|
<span class="text-sm text-muted-foreground">Memory</span>
|
||||||
<HugeiconsIcon icon={DatabaseIcon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={DatabaseIcon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-baseline gap-2">
|
<div class="mt-3 flex items-baseline gap-2">
|
||||||
<span class="tabular text-4xl font-medium tracking-tight">{formatBytes(stats?.memory.used).split(' ')[0]}</span>
|
<span class="tabular text-3xl font-medium tracking-tight">{formatBytes(stats?.memory.used).split(' ')[0]}</span>
|
||||||
<span class="text-sm text-muted-foreground">{formatBytes(stats?.memory.used).split(' ')[1] ?? ''}</span>
|
<span class="text-sm text-muted-foreground">{formatBytes(stats?.memory.used).split(' ')[1] ?? ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
|
<div class="mt-3 h-1 overflow-hidden rounded-full bg-muted">
|
||||||
<div class="h-full rounded-full transition-[width] duration-500 {memoryPercent < 70 ? 'bg-primary' : memoryPercent < 90 ? 'bg-warning' : 'bg-destructive'}" style:width={`${memoryPercent}%`}></div>
|
<div class="h-full rounded-full transition-[width] duration-500 {memoryPercent < 70 ? 'bg-primary' : memoryPercent < 90 ? 'bg-warning' : 'bg-destructive'}"
|
||||||
</div>
|
style:width={`${memoryPercent}%`}></div>
|
||||||
<div class="mt-auto flex justify-between pt-3 text-xs text-muted-foreground">
|
</div>
|
||||||
<span>{memoryPercent ? memoryPercent.toFixed(1) : '-'}%</span>
|
<div class="mt-auto flex justify-between pt-2 text-xs text-muted-foreground">
|
||||||
<span>max {formatBytes(stats?.memory.max)}</span>
|
<span>{memoryPercent ? memoryPercent.toFixed(1) : '-'}%</span>
|
||||||
</div>
|
<span>max {formatBytes(stats?.memory.max)}</span>
|
||||||
</Card>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="rise flex min-h-40 flex-col p-5">
|
<Card class="rise flex min-h-32 flex-col p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">Ticks per second</span>
|
<span class="text-sm text-muted-foreground">Ticks per second</span>
|
||||||
<HugeiconsIcon icon={ChartNoAxesColumnIncreasingIcon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={ChartNoAxesColumnIncreasingIcon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-baseline gap-2">
|
<div class="mt-3 flex items-baseline gap-2">
|
||||||
<span class="tabular text-4xl font-medium tracking-tight {tpsColor}">{tpsText(tps[0])}</span>
|
<span class="tabular text-3xl font-medium tracking-tight {tpsColor}">{tpsText(tps[0])}</span>
|
||||||
<span class="text-sm text-muted-foreground">/ 20.00</span>
|
<span class="text-sm text-muted-foreground">/ 20.00</span>
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 600 60" preserveAspectRatio="none" class="mt-3 h-12 w-full overflow-visible text-primary">
|
<svg viewBox="0 0 600 60" preserveAspectRatio="none" class="mt-2 h-9 w-full overflow-visible text-primary">
|
||||||
<polyline fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" points={sparkPoints} />
|
<polyline fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"
|
||||||
</svg>
|
stroke-linecap="round" points={sparkPoints}/>
|
||||||
<div class="mt-auto flex justify-between text-xs text-muted-foreground">
|
</svg>
|
||||||
<span>5m {tpsText(tps[1])}</span>
|
<div class="mt-auto flex justify-between text-xs text-muted-foreground">
|
||||||
<span>15m {tpsText(tps[2])}</span>
|
<span>5m {tpsText(tps[1])}</span>
|
||||||
</div>
|
<span>15m {tpsText(tps[2])}</span>
|
||||||
</Card>
|
</div>
|
||||||
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<section class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<Card class="rise p-5">
|
<Card class="rise p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">Uptime</span>
|
<span class="text-sm text-muted-foreground">Uptime</span>
|
||||||
<HugeiconsIcon icon={Clock03Icon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={Clock03Icon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 font-mono text-4xl font-medium tracking-tight md:text-5xl">{uptime}</div>
|
<div class="mt-2 font-mono text-3xl font-medium tracking-tight md:text-4xl">{uptime}</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="rise p-5">
|
<Card class="rise p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">World</span>
|
<span class="text-sm text-muted-foreground">World</span>
|
||||||
<HugeiconsIcon icon={CubeIcon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={CubeIcon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<dl class="mt-3 grid grid-cols-3 gap-3 text-center">
|
<dl class="mt-2 grid grid-cols-3 gap-2 text-center">
|
||||||
<div><dt class="text-xs text-muted-foreground">Worlds</dt><dd class="tabular text-4xl font-medium">{stats?.world.worlds ?? '-'}</dd></div>
|
<div>
|
||||||
<div><dt class="text-xs text-muted-foreground">Chunks</dt><dd class="tabular text-4xl font-medium">{stats?.world.loadedChunks ?? '-'}</dd></div>
|
<dt class="text-xs text-muted-foreground">Worlds</dt>
|
||||||
<div><dt class="text-xs text-muted-foreground">Entities</dt><dd class="tabular text-4xl font-medium">{stats?.world.entities ?? '-'}</dd></div>
|
<dd class="tabular text-3xl font-medium">{stats?.world.worlds ?? '-'}</dd>
|
||||||
</dl>
|
</div>
|
||||||
</Card>
|
<div>
|
||||||
|
<dt class="text-xs text-muted-foreground">Chunks</dt>
|
||||||
|
<dd class="tabular text-3xl font-medium">{stats?.world.loadedChunks ?? '-'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs text-muted-foreground">Entities</dt>
|
||||||
|
<dd class="tabular text-3xl font-medium">{stats?.world.entities ?? '-'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="rise p-5">
|
<Card class="rise p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-muted-foreground">Plugins</span>
|
<span class="text-sm text-muted-foreground">Plugins</span>
|
||||||
<HugeiconsIcon icon={ServerStack01Icon} class="size-4 text-muted-foreground" />
|
<HugeiconsIcon icon={ServerStack01Icon} class="size-4 text-muted-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-baseline gap-2">
|
<div class="mt-2 flex items-baseline gap-2">
|
||||||
<span class="tabular text-3xl font-medium">{stats?.plugins.active ?? '-'}</span>
|
<span class="tabular text-3xl font-medium">{stats?.plugins.active ?? '-'}</span>
|
||||||
<span class="text-sm text-muted-foreground">active</span>
|
<span class="text-sm text-muted-foreground">active</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 flex gap-2">
|
<div class="mt-3 flex gap-2">
|
||||||
<a href="/commands/" class="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground hover:text-foreground">commands</a>
|
<a href="/commands/"
|
||||||
<a href="/schematics/" class="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground hover:text-foreground">schematics</a>
|
class="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground hover:text-foreground">commands</a>
|
||||||
</div>
|
<a href="/schematics/"
|
||||||
</Card>
|
class="rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground hover:text-foreground">schematics</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,81 +1,82 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { Search01Icon } from '@hugeicons/core-free-icons';
|
import {Search01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { api } from '$lib/api';
|
import {api} from '$lib/api';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import { lowerSearch, titleCase } from '$lib/utils';
|
import {lowerSearch, titleCase} from '$lib/utils';
|
||||||
|
|
||||||
let bans: Array<Record<string, unknown>> = $state([]);
|
let bans: Array<Record<string, unknown>> = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let filter = $state('');
|
let filter = $state('');
|
||||||
|
|
||||||
const visible = $derived(bans.filter((ban) => !filter.trim() || lowerSearch(ban).includes(filter.toLowerCase().trim())));
|
const visible = $derived(bans.filter((ban) => !filter.trim() || lowerSearch(ban).includes(filter.toLowerCase().trim())));
|
||||||
const totals = $derived.by(() => {
|
const totals = $derived.by(() => {
|
||||||
const text = JSON.stringify(bans);
|
const text = JSON.stringify(bans);
|
||||||
return {
|
return {
|
||||||
groups: bans.length,
|
groups: bans.length,
|
||||||
users: (text.match(/user(name)?/gi) ?? []).length,
|
users: (text.match(/user(name)?/gi) ?? []).length,
|
||||||
uuids: (text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? []).length,
|
uuids: (text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? []).length,
|
||||||
ips: (text.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g) ?? []).length
|
ips: (text.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g) ?? []).length
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function display(value: unknown): string {
|
function display(value: unknown): string {
|
||||||
if (value == null || value === '') return '-';
|
if (value == null || value === '') return '-';
|
||||||
if (Array.isArray(value)) return value.map(display).join(', ');
|
if (Array.isArray(value)) return value.map(display).join(', ');
|
||||||
if (typeof value === 'object') return JSON.stringify(value);
|
if (typeof value === 'object') return JSON.stringify(value);
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
bans = await api.indefiniteBans();
|
|
||||||
} catch (cause) {
|
|
||||||
error = cause instanceof Error ? cause.message : 'Unable to load indefinite bans.';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
bans = await api.indefiniteBans();
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Unable to load indefinite bans.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Indefinite bans</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Indefinite bans</h1>
|
||||||
<div class="flex flex-wrap items-center gap-4 text-sm text-muted-foreground tabular">
|
<div class="flex flex-wrap items-center gap-4 text-sm text-muted-foreground tabular">
|
||||||
<span><span class="text-foreground">{totals.groups}</span> groups</span>
|
<span><span class="text-foreground">{totals.groups}</span> groups</span>
|
||||||
<span><span class="text-foreground">{totals.users}</span> user keys</span>
|
<span><span class="text-foreground">{totals.users}</span> user keys</span>
|
||||||
<span><span class="text-foreground">{totals.uuids}</span> uuids</span>
|
<span><span class="text-foreground">{totals.uuids}</span> uuids</span>
|
||||||
<span><span class="text-foreground">{totals.ips}</span> ips</span>
|
<span><span class="text-foreground">{totals.ips}</span> ips</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-6">
|
<section class="rise mt-6">
|
||||||
<div class="relative w-full sm:max-w-md">
|
<div class="relative w-full sm:max-w-md">
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
<Input bind:value={filter} placeholder="Filter by name, UUID, or IP..." autocomplete="off" class="pl-9" />
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
</div>
|
<Input bind:value={filter} placeholder="Filter by name, UUID, or IP..." autocomplete="off" class="pl-9"/>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">Loading bans...</p>
|
<p class="mt-4 text-sm text-muted-foreground">Loading bans...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="mt-4 text-sm text-destructive">{error}</p>
|
<p class="mt-4 text-sm text-destructive">{error}</p>
|
||||||
{:else if visible.length === 0}
|
{:else if visible.length === 0}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">No indefinite bans match that filter.</p>
|
<p class="mt-4 text-sm text-muted-foreground">No indefinite bans match that filter.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<section class="rise mt-4 grid gap-3 md:grid-cols-2">
|
<section class="rise mt-4 grid gap-3 md:grid-cols-2">
|
||||||
{#each visible as ban, index (index)}
|
{#each visible as ban, index (index)}
|
||||||
<Card class="p-4">
|
<Card class="p-4">
|
||||||
<h2 class="text-sm font-medium">Group {index + 1}</h2>
|
<h2 class="text-sm font-medium">Group {index + 1}</h2>
|
||||||
<dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-xs">
|
<dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-xs">
|
||||||
{#each Object.entries(ban) as [key, value] (key)}
|
{#each Object.entries(ban) as [key, value] (key)}
|
||||||
<dt class="text-muted-foreground">{titleCase(key)}</dt>
|
<dt class="text-muted-foreground">{titleCase(key)}</dt>
|
||||||
<dd class="break-all font-mono text-foreground/80">{display(value)}</dd>
|
<dd class="break-all font-mono text-foreground/80">{display(value)}</dd>
|
||||||
{/each}
|
{/each}
|
||||||
</dl>
|
</dl>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,248 +1,273 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import {onDestroy, onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { ArrowLeft01Icon, ArrowUpRight03Icon } from '@hugeicons/core-free-icons';
|
import {ArrowLeft01Icon, ArrowUpRight03Icon} from '@hugeicons/core-free-icons';
|
||||||
import { api, postForm } from '$lib/api';
|
import {api, postForm} from '$lib/api';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import {Badge} from '$lib/components/ui/badge';
|
||||||
import { Button, type ButtonVariant } from '$lib/components/ui/button';
|
import {Button, type ButtonVariant} from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import {Label} from '$lib/components/ui/label';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import {Textarea} from '$lib/components/ui/textarea';
|
||||||
import InventoryGrid from '$lib/components/ui/InventoryGrid.svelte';
|
import InventoryGrid from '$lib/components/ui/InventoryGrid.svelte';
|
||||||
import type { InventoryPayload, PlayerDetails, PlayerSummary, PlayersPayload } from '$lib/types/api';
|
import type {InventoryPayload, PlayerDetails, PlayerSummary, PlayersPayload} from '$lib/types/api';
|
||||||
import { navigate } from '$lib/router';
|
import {navigate} from '$lib/router';
|
||||||
import { cn, pingClass, titleCase } from '$lib/utils';
|
import {cn, pingClass, titleCase} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
staff: boolean;
|
staff: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
let { id, staff }: Props = $props();
|
|
||||||
let player = $state<PlayerDetails | null>(null);
|
|
||||||
let online = $state<PlayerSummary | null>(null);
|
|
||||||
let inventory = $state<InventoryPayload | null>(null);
|
|
||||||
let selectedSlot: string | null = $state(null);
|
|
||||||
let loading = $state(true);
|
|
||||||
let error = $state<string | null>(null);
|
|
||||||
let actionError = $state<string | null>(null);
|
|
||||||
let actionMessage = $state<string | null>(null);
|
|
||||||
let dialogAction: string | null = $state(null);
|
|
||||||
let actionDialogOpen = $state(false);
|
|
||||||
let reason = $state('');
|
|
||||||
let duration = $state('24h');
|
|
||||||
let playersStream: EventSource | null = null;
|
|
||||||
let inventoryStream: EventSource | null = null;
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{ action: 'ban', label: 'Ban', tone: 'destructive', temporary: false, reason: true },
|
|
||||||
{ action: 'tempban', label: 'Tempban', tone: 'warning', temporary: true, reason: true },
|
|
||||||
{ action: 'mute', label: 'Mute', tone: 'warning', temporary: false, reason: true },
|
|
||||||
{ action: 'tempmute', label: 'Tempmute', tone: 'warning', temporary: true, reason: true },
|
|
||||||
{ action: 'freeze', label: 'Freeze', tone: 'default', temporary: true, reason: true },
|
|
||||||
{ action: 'clear-inventory', label: 'Clear inventory', tone: 'destructive', temporary: false, reason: false },
|
|
||||||
{ action: 'clear-selected', label: 'Clear selected', tone: 'destructive', temporary: false, reason: false, selected: true }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const activeAction = $derived(actions.find((item) => item.action === dialogAction));
|
|
||||||
const selectedItem = $derived(Boolean(selectedSlot && inventory?.online));
|
|
||||||
|
|
||||||
function openAction(action: string) {
|
|
||||||
dialogAction = action;
|
|
||||||
actionDialogOpen = true;
|
|
||||||
actionError = null;
|
|
||||||
actionMessage = null;
|
|
||||||
reason = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buttonVariant(tone: string): ButtonVariant {
|
|
||||||
return tone === 'destructive' ? 'destructive' : tone === 'warning' ? 'outline' : 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionButtonClass(item: (typeof actions)[number]) {
|
|
||||||
return cn(
|
|
||||||
item.tone === 'warning' && 'border-warning/30 bg-warning/10 text-warning hover:bg-warning/15 hover:text-warning',
|
|
||||||
item.action === 'freeze' && 'col-span-2'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAction() {
|
|
||||||
if (!player || !activeAction) return;
|
|
||||||
const form = new FormData();
|
|
||||||
form.set('uuid', player.uuid);
|
|
||||||
form.set('action', activeAction.action);
|
|
||||||
form.set('reason', activeAction.reason ? reason : '');
|
|
||||||
form.set('duration', activeAction.temporary ? duration : '');
|
|
||||||
form.set('slot', 'selected' in activeAction && activeAction.selected ? selectedSlot ?? '' : '');
|
|
||||||
try {
|
|
||||||
const result = await postForm<{ ok: boolean; message?: string }>('/api/admin/player-action', form);
|
|
||||||
actionMessage = result.message ?? 'Action completed.';
|
|
||||||
dialogAction = null;
|
|
||||||
actionDialogOpen = false;
|
|
||||||
} catch (cause) {
|
|
||||||
actionError = cause instanceof Error ? cause.message : 'Action failed.';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
let {id, staff}: Props = $props();
|
||||||
loading = true;
|
let player = $state<PlayerDetails | null>(null);
|
||||||
error = null;
|
let online = $state<PlayerSummary | null>(null);
|
||||||
selectedSlot = null;
|
let inventory = $state<InventoryPayload | null>(null);
|
||||||
inventory = null;
|
let selectedSlot: string | null = $state(null);
|
||||||
playersStream?.close();
|
let loading = $state(true);
|
||||||
inventoryStream?.close();
|
let error = $state<string | null>(null);
|
||||||
try {
|
let actionError = $state<string | null>(null);
|
||||||
const response = await api.player(id);
|
let actionMessage = $state<string | null>(null);
|
||||||
player = response.player;
|
let dialogAction: string | null = $state(null);
|
||||||
playersStream = new EventSource('/api/players/stream/staff');
|
let actionDialogOpen = $state(false);
|
||||||
playersStream.addEventListener('message', (event) => {
|
let reason = $state('');
|
||||||
try {
|
let duration = $state('24h');
|
||||||
const payload = JSON.parse(event.data) as PlayersPayload;
|
let playersStream: EventSource | null = null;
|
||||||
online = payload.players.find((item) => item.uuid === player?.uuid) ?? null;
|
let inventoryStream: EventSource | null = null;
|
||||||
} catch {
|
|
||||||
online = null;
|
const actions = [
|
||||||
|
{action: 'ban', label: 'Ban', tone: 'destructive', temporary: false, reason: true},
|
||||||
|
{action: 'tempban', label: 'Tempban', tone: 'warning', temporary: true, reason: true},
|
||||||
|
{action: 'mute', label: 'Mute', tone: 'warning', temporary: false, reason: true},
|
||||||
|
{action: 'tempmute', label: 'Tempmute', tone: 'warning', temporary: true, reason: true},
|
||||||
|
{action: 'freeze', label: 'Freeze', tone: 'default', temporary: true, reason: true},
|
||||||
|
{action: 'clear-inventory', label: 'Clear inventory', tone: 'destructive', temporary: false, reason: false},
|
||||||
|
{
|
||||||
|
action: 'clear-selected',
|
||||||
|
label: 'Clear selected',
|
||||||
|
tone: 'destructive',
|
||||||
|
temporary: false,
|
||||||
|
reason: false,
|
||||||
|
selected: true
|
||||||
}
|
}
|
||||||
});
|
] as const;
|
||||||
if (staff) {
|
|
||||||
inventoryStream = new EventSource(`/api/player/inventory/stream?uuid=${encodeURIComponent(response.player.uuid)}`);
|
|
||||||
inventoryStream.addEventListener('message', (event) => {
|
|
||||||
try {
|
|
||||||
inventory = JSON.parse(event.data) as InventoryPayload;
|
|
||||||
if (!inventory.online) selectedSlot = null;
|
|
||||||
} catch {
|
|
||||||
inventory = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (cause) {
|
|
||||||
error = cause instanceof Error ? cause.message : 'Unable to load player.';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(load);
|
const activeAction = $derived(actions.find((item) => item.action === dialogAction));
|
||||||
onDestroy(() => {
|
const selectedItem = $derived(Boolean(selectedSlot && inventory?.online));
|
||||||
playersStream?.close();
|
|
||||||
inventoryStream?.close();
|
function openAction(action: string) {
|
||||||
});
|
dialogAction = action;
|
||||||
|
actionDialogOpen = true;
|
||||||
|
actionError = null;
|
||||||
|
actionMessage = null;
|
||||||
|
reason = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buttonVariant(tone: string): ButtonVariant {
|
||||||
|
return tone === 'destructive' ? 'destructive' : tone === 'warning' ? 'outline' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionButtonClass(item: (typeof actions)[number]) {
|
||||||
|
return cn(
|
||||||
|
item.tone === 'warning' && 'border-warning/30 bg-warning/10 text-warning hover:bg-warning/15 hover:text-warning',
|
||||||
|
item.action === 'freeze' && 'col-span-2'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAction() {
|
||||||
|
if (!player || !activeAction) return;
|
||||||
|
const form = new FormData();
|
||||||
|
form.set('uuid', player.uuid);
|
||||||
|
form.set('action', activeAction.action);
|
||||||
|
form.set('reason', activeAction.reason ? reason : '');
|
||||||
|
form.set('duration', activeAction.temporary ? duration : '');
|
||||||
|
form.set('slot', 'selected' in activeAction && activeAction.selected ? selectedSlot ?? '' : '');
|
||||||
|
try {
|
||||||
|
const result = await postForm<{ ok: boolean; message?: string }>('/api/admin/player-action', form);
|
||||||
|
actionMessage = result.message ?? 'Action completed.';
|
||||||
|
dialogAction = null;
|
||||||
|
actionDialogOpen = false;
|
||||||
|
} catch (cause) {
|
||||||
|
actionError = cause instanceof Error ? cause.message : 'Action failed.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
selectedSlot = null;
|
||||||
|
inventory = null;
|
||||||
|
playersStream?.close();
|
||||||
|
inventoryStream?.close();
|
||||||
|
try {
|
||||||
|
const response = await api.player(id);
|
||||||
|
player = response.player;
|
||||||
|
playersStream = new EventSource('/api/players/stream/staff');
|
||||||
|
playersStream.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.data) as PlayersPayload;
|
||||||
|
online = payload.players.find((item) => item.uuid === player?.uuid) ?? null;
|
||||||
|
} catch {
|
||||||
|
online = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (staff) {
|
||||||
|
inventoryStream = new EventSource(`/api/player/inventory/stream?uuid=${encodeURIComponent(response.player.uuid)}`);
|
||||||
|
inventoryStream.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
inventory = JSON.parse(event.data) as InventoryPayload;
|
||||||
|
if (!inventory.online) selectedSlot = null;
|
||||||
|
} catch {
|
||||||
|
inventory = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Unable to load player.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
onDestroy(() => {
|
||||||
|
playersStream?.close();
|
||||||
|
inventoryStream?.close();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="rise text-sm text-muted-foreground">Loading player...</p>
|
<p class="rise text-sm text-muted-foreground">Loading player...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<Card class="rise p-5">
|
<Card class="rise p-5">
|
||||||
<h1 class="text-xl font-medium">Player lookup failed</h1>
|
<h1 class="text-xl font-medium">Player lookup failed</h1>
|
||||||
<p class="mt-2 text-sm text-destructive">{error}</p>
|
<p class="mt-2 text-sm text-destructive">{error}</p>
|
||||||
</Card>
|
</Card>
|
||||||
{:else if player}
|
{:else if player}
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
<img class="size-14 rounded-xl bg-muted inventory-pixelated" src={`https://vzge.me/face/512/${encodeURIComponent(player.uuid)}.png`} alt="" loading="lazy" width="56" height="56" />
|
<img class="size-14 rounded-xl bg-muted inventory-pixelated"
|
||||||
<div class="min-w-0">
|
src={`https://vzge.me/face/512/${encodeURIComponent(player.uuid)}.png`} alt="" loading="lazy"
|
||||||
<h1 class="truncate text-3xl font-medium tracking-tight md:text-4xl">{player.name}</h1>
|
width="56" height="56"/>
|
||||||
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{player.uuid}</p>
|
<div class="min-w-0">
|
||||||
</div>
|
<h1 class="truncate text-3xl font-medium tracking-tight md:text-4xl">{player.name}</h1>
|
||||||
</div>
|
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{player.uuid}</p>
|
||||||
<Button variant="secondary" onclick={() => navigate('/players/')}>
|
</div>
|
||||||
<HugeiconsIcon icon={ArrowLeft01Icon} class="size-3.5" />
|
|
||||||
Players
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rise mt-6 grid gap-4 md:grid-cols-2">
|
|
||||||
<Card class="p-5">
|
|
||||||
<h2 class="text-sm font-medium tracking-tight">Info</h2>
|
|
||||||
<dl class="mt-4 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-2 text-sm">
|
|
||||||
<dt class="text-muted-foreground">Status</dt>
|
|
||||||
<dd>{#if online}<Badge variant="secondary" class="bg-success/10 text-success">online</Badge>{:else}<Badge variant="secondary">offline</Badge>{/if}</dd>
|
|
||||||
<dt class="text-muted-foreground">Ping</dt>
|
|
||||||
<dd class="tabular {pingClass(online?.ping)}">{online ? `${online.ping | 0}ms` : '-'}</dd>
|
|
||||||
<dt class="text-muted-foreground">World</dt>
|
|
||||||
<dd class="text-foreground/80">{online?.world ?? '-'}</dd>
|
|
||||||
<dt class="text-muted-foreground">Gamemode</dt>
|
|
||||||
<dd class="text-foreground/80">{online?.gamemode ? titleCase(online.gamemode) : '-'}</dd>
|
|
||||||
<dt class="text-muted-foreground">IP</dt>
|
|
||||||
<dd class="break-all font-mono text-foreground/80">{player.ip ?? '-'}</dd>
|
|
||||||
<dt class="text-muted-foreground">First played</dt>
|
|
||||||
<dd class="text-foreground/80">{player.firstPlayed ?? '-'}</dd>
|
|
||||||
<dt class="text-muted-foreground">Punishments</dt>
|
|
||||||
<dd><a href={`/punishments/${encodeURIComponent(player.uuid)}`} class="inline-flex items-center gap-1 text-primary hover:underline">View history</a></dd>
|
|
||||||
{#if player.nameMcUrl}
|
|
||||||
<dt class="text-muted-foreground">NameMC</dt>
|
|
||||||
<dd><a href={player.nameMcUrl} target="_blank" rel="noopener" class="inline-flex items-center gap-1 text-primary hover:underline">View profile <HugeiconsIcon icon={ArrowUpRight03Icon} class="size-3" /></a></dd>
|
|
||||||
{/if}
|
|
||||||
</dl>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="p-5">
|
|
||||||
<h2 class="text-sm font-medium tracking-tight">Actions</h2>
|
|
||||||
<p class="mt-1 text-xs text-muted-foreground">Issued punishments use the authenticated staff account.</p>
|
|
||||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
|
||||||
{#each actions as item (item.action)}
|
|
||||||
<Button
|
|
||||||
variant={buttonVariant(item.tone)}
|
|
||||||
class={actionButtonClass(item)}
|
|
||||||
disabled={'selected' in item && item.selected && !selectedItem}
|
|
||||||
onclick={() => openAction(item.action)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if actionMessage}
|
|
||||||
<p class="mt-3 text-sm text-success">{actionMessage}</p>
|
|
||||||
{/if}
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if staff}
|
|
||||||
<section class="rise mt-4">
|
|
||||||
<Card class="p-5">
|
|
||||||
<h2 class="text-sm font-medium tracking-tight">Live inventory</h2>
|
|
||||||
<div class="mt-4">
|
|
||||||
<InventoryGrid {inventory} selectedKey={selectedSlot} onSelect={(slot) => (selectedSlot = slot)} />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<Button variant="secondary" onclick={() => navigate('/players/')}>
|
||||||
|
<HugeiconsIcon icon={ArrowLeft01Icon} class="size-3.5"/>
|
||||||
|
Players
|
||||||
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if activeAction}
|
<section class="rise mt-6 grid gap-4 md:grid-cols-2">
|
||||||
<Dialog.Root bind:open={actionDialogOpen}>
|
<Card class="p-5">
|
||||||
<Dialog.Content>
|
<h2 class="text-sm font-medium tracking-tight">Info</h2>
|
||||||
<Dialog.Header>
|
<dl class="mt-4 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-2 text-sm">
|
||||||
<Dialog.Title>Confirm {activeAction.label.toLowerCase()}</Dialog.Title>
|
<dt class="text-muted-foreground">Status</dt>
|
||||||
<Dialog.Description>
|
<dd>
|
||||||
Target: <span class="text-foreground">{player.name}</span>{'selected' in activeAction && activeAction.selected ? ` | Slot: ${selectedSlot}` : ''}
|
{#if online}
|
||||||
</Dialog.Description>
|
<Badge variant="secondary" class="bg-success/10 text-success">online</Badge>
|
||||||
</Dialog.Header>
|
{:else}
|
||||||
{#if activeAction.reason}
|
<Badge variant="secondary">offline</Badge>
|
||||||
<div class="grid gap-2">
|
{/if}
|
||||||
<Label for="actionReason">Reason</Label>
|
</dd>
|
||||||
<Textarea id="actionReason" bind:value={reason} required maxlength={500} />
|
<dt class="text-muted-foreground">Ping</dt>
|
||||||
</div>
|
<dd class="tabular {pingClass(online?.ping)}">{online ? `${online.ping | 0}ms` : '-'}</dd>
|
||||||
{/if}
|
<dt class="text-muted-foreground">World</dt>
|
||||||
{#if activeAction.temporary}
|
<dd class="text-foreground/80">{online?.world ?? '-'}</dd>
|
||||||
<div class="grid gap-2">
|
<dt class="text-muted-foreground">Gamemode</dt>
|
||||||
<Label for="actionDuration">Duration</Label>
|
<dd class="text-foreground/80">{online?.gamemode ? titleCase(online.gamemode) : '-'}</dd>
|
||||||
<select id="actionDuration" bind:value={duration} class="border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 h-9 rounded-4xl border px-3 py-1 text-sm outline-none focus-visible:ring-[3px]">
|
<dt class="text-muted-foreground">IP</dt>
|
||||||
<option value="5m">5 minutes</option>
|
<dd class="break-all font-mono text-foreground/80">{player.ip ?? '-'}</dd>
|
||||||
<option value="1h">1 hour</option>
|
<dt class="text-muted-foreground">First played</dt>
|
||||||
<option value="24h">1 day</option>
|
<dd class="text-foreground/80">{player.firstPlayed ?? '-'}</dd>
|
||||||
<option value="7d">7 days</option>
|
<dt class="text-muted-foreground">Punishments</dt>
|
||||||
<option value="30d">30 days</option>
|
<dd><a href={`/punishments/${encodeURIComponent(player.uuid)}`}
|
||||||
</select>
|
class="inline-flex items-center gap-1 text-primary hover:underline">View history</a></dd>
|
||||||
</div>
|
{#if player.nameMcUrl}
|
||||||
{/if}
|
<dt class="text-muted-foreground">NameMC</dt>
|
||||||
{#if actionError}
|
<dd><a href={player.nameMcUrl} target="_blank" rel="noopener"
|
||||||
<p class="mt-3 text-sm text-destructive">{actionError}</p>
|
class="inline-flex items-center gap-1 text-primary hover:underline">View profile
|
||||||
{/if}
|
<HugeiconsIcon icon={ArrowUpRight03Icon} class="size-3"/>
|
||||||
<Dialog.Footer>
|
</a></dd>
|
||||||
<Button variant="secondary" onclick={() => { actionDialogOpen = false; dialogAction = null; }}>Cancel</Button>
|
{/if}
|
||||||
<Button variant="destructive" disabled={activeAction.reason && !reason.trim()} onclick={submitAction}>Confirm</Button>
|
</dl>
|
||||||
</Dialog.Footer>
|
</Card>
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
<Card class="p-5">
|
||||||
{/if}
|
<h2 class="text-sm font-medium tracking-tight">Actions</h2>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">Issued punishments use the authenticated staff account.</p>
|
||||||
|
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||||
|
{#each actions as item (item.action)}
|
||||||
|
<Button
|
||||||
|
variant={buttonVariant(item.tone)}
|
||||||
|
class={actionButtonClass(item)}
|
||||||
|
disabled={'selected' in item && item.selected && !selectedItem}
|
||||||
|
onclick={() => openAction(item.action)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if actionMessage}
|
||||||
|
<p class="mt-3 text-sm text-success">{actionMessage}</p>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if staff}
|
||||||
|
<section class="rise mt-4">
|
||||||
|
<Card class="p-5">
|
||||||
|
<h2 class="text-sm font-medium tracking-tight">Live inventory</h2>
|
||||||
|
<div class="mt-4">
|
||||||
|
<InventoryGrid {inventory} selectedKey={selectedSlot} onSelect={(slot) => (selectedSlot = slot)}/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeAction}
|
||||||
|
<Dialog.Root bind:open={actionDialogOpen}>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Confirm {activeAction.label.toLowerCase()}</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Target: <span
|
||||||
|
class="text-foreground">{player.name}</span>{'selected' in activeAction && activeAction.selected ? ` | Slot: ${selectedSlot}` : ''}
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
{#if activeAction.reason}
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="actionReason">Reason</Label>
|
||||||
|
<Textarea id="actionReason" bind:value={reason} required maxlength={500}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if activeAction.temporary}
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="actionDuration">Duration</Label>
|
||||||
|
<select id="actionDuration" bind:value={duration}
|
||||||
|
class="border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 h-9 rounded-4xl border px-3 py-1 text-sm outline-none focus-visible:ring-[3px]">
|
||||||
|
<option value="5m">5 minutes</option>
|
||||||
|
<option value="1h">1 hour</option>
|
||||||
|
<option value="24h">1 day</option>
|
||||||
|
<option value="7d">7 days</option>
|
||||||
|
<option value="30d">30 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if actionError}
|
||||||
|
<p class="mt-3 text-sm text-destructive">{actionError}</p>
|
||||||
|
{/if}
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="secondary" onclick={() => { actionDialogOpen = false; dialogAction = null; }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" disabled={activeAction.reason && !reason.trim()}
|
||||||
|
onclick={submitAction}>Confirm
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,98 +1,95 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import {onDestroy, onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { Search01Icon, Shield01Icon, UserGroupIcon } from '@hugeicons/core-free-icons';
|
import {Search01Icon, Shield01Icon, UserGroupIcon} from '@hugeicons/core-free-icons';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import {Badge} from '$lib/components/ui/badge';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import type { PlayerSummary, PlayersPayload } from '$lib/types/api';
|
import type {PlayerSummary, PlayersPayload} from '$lib/types/api';
|
||||||
import { pingClass } from '$lib/utils';
|
import {pingClass} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
staff: boolean;
|
staff: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { staff }: Props = $props();
|
let {staff}: Props = $props();
|
||||||
let players: PlayerSummary[] = $state([]);
|
let players: PlayerSummary[] = $state([]);
|
||||||
let max = $state(0);
|
let max = $state(0);
|
||||||
let filter = $state('');
|
let filter = $state('');
|
||||||
let connected = $state(false);
|
let es: EventSource | null = null;
|
||||||
let es: EventSource | null = null;
|
|
||||||
|
|
||||||
const visiblePlayers = $derived(players.filter((player) => player.name.toLowerCase().includes(filter.toLowerCase().trim())));
|
const visiblePlayers = $derived(players.filter((player) => player.name.toLowerCase().includes(filter.toLowerCase().trim())));
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
es?.close();
|
es?.close();
|
||||||
es = new EventSource(staff ? '/api/players/stream/staff' : '/api/players/stream');
|
es = new EventSource(staff ? '/api/players/stream/staff' : '/api/players/stream');
|
||||||
es.addEventListener('open', () => (connected = true));
|
es.addEventListener('message', (event) => {
|
||||||
es.addEventListener('error', () => (connected = false));
|
try {
|
||||||
es.addEventListener('message', (event) => {
|
const payload = JSON.parse(event.data) as PlayersPayload;
|
||||||
try {
|
players = Array.isArray(payload.players) ? payload.players : [];
|
||||||
const payload = JSON.parse(event.data) as PlayersPayload;
|
max = payload.max ?? 0;
|
||||||
players = Array.isArray(payload.players) ? payload.players : [];
|
} catch {
|
||||||
max = payload.max ?? 0;
|
}
|
||||||
connected = true;
|
});
|
||||||
} catch {
|
}
|
||||||
connected = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(connect);
|
onMount(connect);
|
||||||
onDestroy(() => es?.close());
|
onDestroy(() => es?.close());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Players</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Players</h1>
|
||||||
<span class="tabular text-sm text-muted-foreground">
|
<span class="tabular text-sm text-muted-foreground">
|
||||||
<span class="text-foreground">{players.length}</span> / {max} online
|
<span class="text-foreground">{players.length}</span> / {max} online
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-6 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
<section class="rise mt-6 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||||
<div class="relative w-full sm:max-w-md">
|
<div class="relative w-full sm:max-w-md">
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
<Input bind:value={filter} placeholder="Filter by name..." autocomplete="off" class="pl-9" />
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
</div>
|
<Input bind:value={filter} placeholder="Filter by name..." autocomplete="off" class="pl-9"/>
|
||||||
<span class="text-xs text-muted-foreground">{connected ? 'live' : 'waiting for stream'}</span>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<section class="rise mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{#if visiblePlayers.length === 0}
|
{#if visiblePlayers.length === 0}
|
||||||
<Card class="col-span-full p-10 text-center">
|
<Card class="col-span-full p-10 text-center">
|
||||||
<HugeiconsIcon icon={UserGroupIcon} class="mx-auto size-8 text-muted-foreground/60" />
|
<HugeiconsIcon icon={UserGroupIcon} class="mx-auto size-8 text-muted-foreground/60"/>
|
||||||
<p class="mt-3 text-sm text-muted-foreground">{players.length ? 'No players match that filter.' : 'No players online right now.'}</p>
|
<p class="mt-3 text-sm text-muted-foreground">{players.length ? 'No players match that filter.' : 'No players online right now.'}</p>
|
||||||
</Card>
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
{#each visiblePlayers as player (player.uuid)}
|
{#each visiblePlayers as player (player.uuid)}
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={staff ? 'a' : 'div'}
|
this={staff ? 'a' : 'div'}
|
||||||
href={staff ? `/player/${encodeURIComponent(player.uuid)}` : undefined}
|
href={staff ? `/player/${encodeURIComponent(player.uuid)}` : undefined}
|
||||||
class="ring-card flex items-center gap-3 rounded-xl bg-card p-3 transition-colors hover:bg-secondary/50"
|
class="ring-card flex items-center gap-3 rounded-xl bg-card p-3 transition-colors hover:bg-secondary/50"
|
||||||
>
|
>
|
||||||
<img class="size-10 rounded-lg bg-muted inventory-pixelated" src={`https://vzge.me/face/512/${encodeURIComponent(player.uuid)}.png`} alt="" loading="lazy" width="40" height="40" />
|
<img class="size-10 rounded-lg bg-muted inventory-pixelated"
|
||||||
<div class="min-w-0 flex-1">
|
src={`https://vzge.me/face/512/${encodeURIComponent(player.uuid)}.png`} alt="" loading="lazy"
|
||||||
<div class="flex items-center gap-2">
|
width="40" height="40"/>
|
||||||
<span class="truncate text-sm font-medium">{player.name}</span>
|
<div class="min-w-0 flex-1">
|
||||||
{#if player.op}
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="default">op</Badge>
|
<span class="truncate text-sm font-medium">{player.name}</span>
|
||||||
{/if}
|
{#if player.op}
|
||||||
{#if staff && player.gamemode}
|
<Badge variant="default">op</Badge>
|
||||||
<Badge>{player.gamemode.toLowerCase()}</Badge>
|
{/if}
|
||||||
{/if}
|
{#if staff && player.gamemode}
|
||||||
</div>
|
<Badge>{player.gamemode.toLowerCase()}</Badge>
|
||||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
{/if}
|
||||||
{#if player.world}
|
</div>
|
||||||
<span>In {player.world}</span>
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
||||||
<span class="text-foreground/30">.</span>
|
{#if player.world}
|
||||||
{/if}
|
<span>In {player.world}</span>
|
||||||
<span class="tabular {pingClass(player.ping)}">{player.ping | 0}ms</span>
|
<span class="text-foreground/30">.</span>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
<span class="tabular {pingClass(player.ping)}">{player.ping | 0}ms</span>
|
||||||
{#if staff}
|
</div>
|
||||||
<HugeiconsIcon icon={Shield01Icon} class="size-4 text-muted-foreground" />
|
</div>
|
||||||
{/if}
|
{#if staff}
|
||||||
</svelte:element>
|
<HugeiconsIcon icon={Shield01Icon} class="size-4 text-muted-foreground"/>
|
||||||
{/each}
|
{/if}
|
||||||
{/if}
|
</svelte:element>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,119 +1,129 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { Search01Icon } from '@hugeicons/core-free-icons';
|
import {Search01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { api } from '$lib/api';
|
import {api} from '$lib/api';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import {Badge} from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import type { PunishmentsPayload } from '$lib/types/api';
|
import type {PunishmentsPayload} from '$lib/types/api';
|
||||||
import { lowerSearch, titleCase } from '$lib/utils';
|
import {lowerSearch, titleCase} from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
|
||||||
|
|
||||||
let { id }: Props = $props();
|
|
||||||
let data = $state<PunishmentsPayload | null>(null);
|
|
||||||
let loading = $state(true);
|
|
||||||
let error = $state<string | null>(null);
|
|
||||||
let filter = $state('');
|
|
||||||
let type = $state('all');
|
|
||||||
let status = $state('all');
|
|
||||||
|
|
||||||
const punishments = $derived<Array<Record<string, unknown>>>(data?.punishments ?? []);
|
|
||||||
const types = $derived<string[]>(Array.from(new Set(punishments.map((item) => String(item.type ?? item.kind ?? '')).filter(Boolean))).sort());
|
|
||||||
const visible = $derived.by(() => {
|
|
||||||
const q = filter.toLowerCase().trim();
|
|
||||||
return punishments.filter((item) => {
|
|
||||||
const itemType = String(item.type ?? item.kind ?? '');
|
|
||||||
const active = Boolean(item.active ?? item.isActive ?? item.current);
|
|
||||||
const itemStatus = active ? 'active' : 'expired';
|
|
||||||
return (!q || lowerSearch(item).includes(q)) && (type === 'all' || itemType === type) && (status === 'all' || itemStatus === status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function displayValue(value: unknown) {
|
|
||||||
if (value == null || value === '') return '-';
|
|
||||||
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
||||||
if (Array.isArray(value)) return value.join(', ');
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function entries(item: Record<string, unknown>) {
|
|
||||||
return Object.entries(item).filter(([key]) => !['id', 'uuid'].includes(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
data = await api.punishments(id);
|
|
||||||
} catch (cause) {
|
|
||||||
error = cause instanceof Error ? cause.message : 'Unable to load punishments.';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
let {id}: Props = $props();
|
||||||
|
let data = $state<PunishmentsPayload | null>(null);
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let filter = $state('');
|
||||||
|
let type = $state('all');
|
||||||
|
let status = $state('all');
|
||||||
|
|
||||||
|
const punishments = $derived<Array<Record<string, unknown>>>(data?.punishments ?? []);
|
||||||
|
const types = $derived<string[]>(Array.from(new Set(punishments.map((item) => String(item.type ?? item.kind ?? '')).filter(Boolean))).sort());
|
||||||
|
const visible = $derived.by(() => {
|
||||||
|
const q = filter.toLowerCase().trim();
|
||||||
|
return punishments.filter((item) => {
|
||||||
|
const itemType = String(item.type ?? item.kind ?? '');
|
||||||
|
const active = Boolean(item.active ?? item.isActive ?? item.current);
|
||||||
|
const itemStatus = active ? 'active' : 'expired';
|
||||||
|
return (!q || lowerSearch(item).includes(q)) && (type === 'all' || itemType === type) && (status === 'all' || itemStatus === status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function displayValue(value: unknown) {
|
||||||
|
if (value == null || value === '') return '-';
|
||||||
|
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
||||||
|
if (Array.isArray(value)) return value.join(', ');
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entries(item: Record<string, unknown>) {
|
||||||
|
return Object.entries(item).filter(([key]) => !['id', 'uuid'].includes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
data = await api.punishments(id);
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Unable to load punishments.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="rise text-sm text-muted-foreground">Loading punishments...</p>
|
<p class="rise text-sm text-muted-foreground">Loading punishments...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<Card class="rise p-5"><p class="text-sm text-destructive">{error}</p></Card>
|
<Card class="rise p-5"><p class="text-sm text-destructive">{error}</p></Card>
|
||||||
{:else if data}
|
{:else if data}
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
<img class="size-12 rounded-xl bg-muted inventory-pixelated" src={`https://vzge.me/face/512/${encodeURIComponent(data.player.uuid)}.png`} alt="" loading="lazy" width="48" height="48" />
|
<img class="size-12 rounded-xl bg-muted inventory-pixelated"
|
||||||
<div class="min-w-0">
|
src={`https://vzge.me/face/512/${encodeURIComponent(data.player.uuid)}.png`} alt="" loading="lazy"
|
||||||
<h1 class="truncate text-3xl font-medium tracking-tight md:text-4xl">{data.player.name}</h1>
|
width="48" height="48"/>
|
||||||
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{data.player.uuid}</p>
|
<div class="min-w-0">
|
||||||
</div>
|
<h1 class="truncate text-3xl font-medium tracking-tight md:text-4xl">{data.player.name}</h1>
|
||||||
</div>
|
<p class="mt-1 break-all font-mono text-xs text-muted-foreground">{data.player.uuid}</p>
|
||||||
<span class="tabular text-sm text-muted-foreground"><span class="text-foreground">{punishments.length}</span> punishments</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rise mt-4 flex flex-wrap items-center gap-3">
|
|
||||||
<div class="relative w-full sm:max-w-md">
|
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<Input bind:value={filter} placeholder={data.canViewIps ? 'Filter by reason, punisher, type, IP...' : 'Filter by reason, punisher, type...'} autocomplete="off" class="pl-9" />
|
|
||||||
</div>
|
|
||||||
<Button href="/punishments/" variant="secondary"><HugeiconsIcon icon={Search01Icon} class="size-3.5" />New search</Button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rise mt-3 flex flex-wrap items-center gap-1.5">
|
|
||||||
<Button size="sm" variant={type === 'all' ? 'default' : 'outline'} onclick={() => (type = 'all')}>All</Button>
|
|
||||||
{#each types as item (item)}
|
|
||||||
<Button size="sm" variant={type === item ? 'default' : 'outline'} onclick={() => (type = item)}>{titleCase(item)}</Button>
|
|
||||||
{/each}
|
|
||||||
<span class="mx-1 h-4 w-px bg-border"></span>
|
|
||||||
{#each ['all', 'active', 'expired'] as item (item)}
|
|
||||||
<Button size="sm" variant={status === item ? 'default' : 'outline'} onclick={() => (status = item)}>{titleCase(item === 'all' ? 'any' : item)}</Button>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if visible.length === 0}
|
|
||||||
<p class="mt-4 text-sm text-muted-foreground">No punishments match those filters.</p>
|
|
||||||
{:else}
|
|
||||||
<section class="rise mt-4 grid gap-3 md:grid-cols-2">
|
|
||||||
{#each visible as punishment, index (String(punishment.id ?? index))}
|
|
||||||
{@const itemType = String(punishment.type ?? punishment.kind ?? 'punishment')}
|
|
||||||
{@const active = Boolean(punishment.active ?? punishment.isActive ?? punishment.current)}
|
|
||||||
<Card class="p-4">
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<h2 class="font-medium">{titleCase(itemType)}</h2>
|
|
||||||
<p class="mt-1 text-sm text-muted-foreground">{displayValue(punishment.reason)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={active ? 'destructive' : 'secondary'}>{active ? 'active' : 'expired'}</Badge>
|
</div>
|
||||||
</div>
|
<span class="tabular text-sm text-muted-foreground"><span class="text-foreground">{punishments.length}</span> punishments</span>
|
||||||
<dl class="mt-4 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-xs">
|
|
||||||
{#each entries(punishment) as [key, value] (key)}
|
|
||||||
<dt class="text-muted-foreground">{titleCase(key)}</dt>
|
|
||||||
<dd class="break-all font-mono text-foreground/80">{displayValue(value)}</dd>
|
|
||||||
{/each}
|
|
||||||
</dl>
|
|
||||||
</Card>
|
|
||||||
{/each}
|
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
|
||||||
|
<section class="rise mt-4 flex flex-wrap items-center gap-3">
|
||||||
|
<div class="relative w-full sm:max-w-md">
|
||||||
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
|
<Input bind:value={filter}
|
||||||
|
placeholder={data.canViewIps ? 'Filter by reason, punisher, type, IP...' : 'Filter by reason, punisher, type...'}
|
||||||
|
autocomplete="off" class="pl-9"/>
|
||||||
|
</div>
|
||||||
|
<Button href="/punishments/" variant="secondary">
|
||||||
|
<HugeiconsIcon icon={Search01Icon} class="size-3.5"/>
|
||||||
|
New search
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="rise mt-3 flex flex-wrap items-center gap-1.5">
|
||||||
|
<Button size="sm" variant={type === 'all' ? 'default' : 'outline'} onclick={() => (type = 'all')}>All</Button>
|
||||||
|
{#each types as item (item)}
|
||||||
|
<Button size="sm" variant={type === item ? 'default' : 'outline'}
|
||||||
|
onclick={() => (type = item)}>{titleCase(item)}</Button>
|
||||||
|
{/each}
|
||||||
|
<span class="mx-1 h-4 w-px bg-border"></span>
|
||||||
|
{#each ['all', 'active', 'expired'] as item (item)}
|
||||||
|
<Button size="sm" variant={status === item ? 'default' : 'outline'}
|
||||||
|
onclick={() => (status = item)}>{titleCase(item === 'all' ? 'any' : item)}</Button>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if visible.length === 0}
|
||||||
|
<p class="mt-4 text-sm text-muted-foreground">No punishments match those filters.</p>
|
||||||
|
{:else}
|
||||||
|
<section class="rise mt-4 grid gap-3 md:grid-cols-2">
|
||||||
|
{#each visible as punishment, index (String(punishment.id ?? index))}
|
||||||
|
{@const itemType = String(punishment.type ?? punishment.kind ?? 'punishment')}
|
||||||
|
{@const active = Boolean(punishment.active ?? punishment.isActive ?? punishment.current)}
|
||||||
|
<Card class="p-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-medium">{titleCase(itemType)}</h2>
|
||||||
|
<p class="mt-1 text-sm text-muted-foreground">{displayValue(punishment.reason)}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={active ? 'destructive' : 'secondary'}>{active ? 'active' : 'expired'}</Badge>
|
||||||
|
</div>
|
||||||
|
<dl class="mt-4 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-xs">
|
||||||
|
{#each entries(punishment) as [key, value] (key)}
|
||||||
|
<dt class="text-muted-foreground">{titleCase(key)}</dt>
|
||||||
|
<dd class="break-all font-mono text-foreground/80">{displayValue(value)}</dd>
|
||||||
|
{/each}
|
||||||
|
</dl>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { ArrowRight01Icon, Search01Icon } from '@hugeicons/core-free-icons';
|
import {ArrowRight01Icon, Search01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import { navigate } from '$lib/router';
|
import {navigate} from '$lib/router';
|
||||||
|
|
||||||
let query = $state('');
|
let query = $state('');
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const value = query.trim();
|
const value = query.trim();
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
navigate(`/punishments/${encodeURIComponent(value)}`);
|
navigate(`/punishments/${encodeURIComponent(value)}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise">
|
<section class="rise">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Punishments</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Punishments</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form class="rise mt-6 flex flex-col gap-3 sm:flex-row sm:items-center" onsubmit={(event) => { event.preventDefault(); submit(); }}>
|
<form class="rise mt-6 flex flex-col gap-3 sm:flex-row sm:items-center"
|
||||||
<div class="relative w-full sm:max-w-md">
|
onsubmit={(event) => { event.preventDefault(); submit(); }}>
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<div class="relative w-full sm:max-w-md">
|
||||||
<Input bind:value={query} autofocus placeholder="UUID or username" autocomplete="off" class="pl-9" />
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
</div>
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
<Button type="submit">
|
<Input bind:value={query} autofocus placeholder="UUID or username" autocomplete="off" class="pl-9"/>
|
||||||
Search
|
</div>
|
||||||
<HugeiconsIcon icon={ArrowRight01Icon} class="size-3.5" />
|
<Button type="submit">
|
||||||
</Button>
|
Search
|
||||||
|
<HugeiconsIcon icon={ArrowRight01Icon} class="size-3.5"/>
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,63 +1,65 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { Upload01Icon } from '@hugeicons/core-free-icons';
|
import {Upload01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { postForm } from '$lib/api';
|
import {postForm} from '$lib/api';
|
||||||
|
|
||||||
let file: File | null = $state(null);
|
let file: File | null = $state(null);
|
||||||
let message: string | null = $state(null);
|
let message: string | null = $state(null);
|
||||||
let error: string | null = $state(null);
|
let error: string | null = $state(null);
|
||||||
let submitting = $state(false);
|
let submitting = $state(false);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set('file', file);
|
form.set('file', file);
|
||||||
submitting = true;
|
submitting = true;
|
||||||
message = null;
|
message = null;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const result = await postForm<Record<string, unknown>>('/api/schematics/upload', form);
|
const result = await postForm<Record<string, unknown>>('/api/schematics/upload', form);
|
||||||
message = String(result.message ?? 'Upload complete.');
|
message = String(result.message ?? 'Upload complete.');
|
||||||
file = null;
|
file = null;
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
error = cause instanceof Error ? cause.message : 'Upload failed.';
|
error = cause instanceof Error ? cause.message : 'Upload failed.';
|
||||||
} finally {
|
} finally {
|
||||||
submitting = false;
|
submitting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise">
|
<section class="rise">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Upload schematic</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Upload schematic</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-6 max-w-2xl">
|
<section class="rise mt-6 max-w-2xl">
|
||||||
<Card class="p-5">
|
<Card class="p-5">
|
||||||
<form class="flex flex-col gap-4 sm:flex-row sm:items-center" onsubmit={(event) => { event.preventDefault(); submit(); }}>
|
<form class="flex flex-col gap-4 sm:flex-row sm:items-center"
|
||||||
<label for="formFile" class="flex flex-1 cursor-pointer items-center gap-3 rounded-xl border border-dashed border-border bg-muted/30 px-4 py-3 text-sm transition-colors hover:border-foreground/30 hover:bg-muted/50">
|
onsubmit={(event) => { event.preventDefault(); submit(); }}>
|
||||||
<HugeiconsIcon icon={Upload01Icon} class="size-5 text-muted-foreground" />
|
<label for="formFile"
|
||||||
<span class="min-w-0 text-muted-foreground">
|
class="flex flex-1 cursor-pointer items-center gap-3 rounded-xl border border-dashed border-border bg-muted/30 px-4 py-3 text-sm transition-colors hover:border-foreground/30 hover:bg-muted/50">
|
||||||
|
<HugeiconsIcon icon={Upload01Icon} class="size-5 text-muted-foreground"/>
|
||||||
|
<span class="min-w-0 text-muted-foreground">
|
||||||
<span class="text-foreground">{file ? file.name : 'Choose a file'}</span>
|
<span class="text-foreground">{file ? file.name : 'Choose a file'}</span>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
id="formFile"
|
id="formFile"
|
||||||
type="file"
|
type="file"
|
||||||
name="file"
|
name="file"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
onchange={(event) => {
|
onchange={(event) => {
|
||||||
file = (event.currentTarget as HTMLInputElement).files?.[0] ?? null;
|
file = (event.currentTarget as HTMLInputElement).files?.[0] ?? null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Button type="submit" disabled={!file || submitting}>{submitting ? 'Uploading...' : 'Upload'}</Button>
|
<Button type="submit" disabled={!file || submitting}>{submitting ? 'Uploading...' : 'Upload'}</Button>
|
||||||
</form>
|
</form>
|
||||||
{#if message}
|
{#if message}
|
||||||
<p class="mt-3 text-sm text-success">{message}</p>
|
<p class="mt-3 text-sm text-success">{message}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="mt-3 text-sm text-destructive">{error}</p>
|
<p class="mt-3 text-sm text-destructive">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,75 +1,88 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import { HugeiconsIcon } from '@hugeicons/svelte';
|
import {HugeiconsIcon} from '@hugeicons/svelte';
|
||||||
import { Download01Icon, Search01Icon, Upload01Icon } from '@hugeicons/core-free-icons';
|
import {Download01Icon, Search01Icon, Upload01Icon} from '@hugeicons/core-free-icons';
|
||||||
import { api } from '$lib/api';
|
import {api} from '$lib/api';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import {Button} from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import {Card} from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import {Input} from '$lib/components/ui/input';
|
||||||
import type { Schematic } from '$lib/types/api';
|
import type {Schematic} from '$lib/types/api';
|
||||||
|
|
||||||
let schematics: Schematic[] = $state([]);
|
interface Props {
|
||||||
let loading = $state(true);
|
staff: boolean;
|
||||||
let error = $state<string | null>(null);
|
|
||||||
let filter = $state('');
|
|
||||||
|
|
||||||
const visible = $derived(schematics.filter((schematic) => schematic.name.toLowerCase().includes(filter.toLowerCase().trim())));
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
schematics = (await api.schematics()).schematics ?? [];
|
|
||||||
} catch (cause) {
|
|
||||||
error = cause instanceof Error ? cause.message : 'Unable to load schematics.';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
let {staff}: Props = $props();
|
||||||
|
let schematics: Schematic[] = $state([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let filter = $state('');
|
||||||
|
|
||||||
|
const visible = $derived(schematics.filter((schematic) => schematic.name.toLowerCase().includes(filter.toLowerCase().trim())));
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
schematics = (await api.schematics()).schematics ?? [];
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Unable to load schematics.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
<section class="rise flex flex-wrap items-end justify-between gap-3">
|
||||||
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Schematics</h1>
|
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Schematics</h1>
|
||||||
<Button href="/schematics/upload/">
|
{#if staff}
|
||||||
<HugeiconsIcon icon={Upload01Icon} class="size-3.5" />
|
<Button href="/schematics/upload/">
|
||||||
Upload
|
<HugeiconsIcon icon={Upload01Icon} class="size-3.5"/>
|
||||||
</Button>
|
Upload
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rise mt-6">
|
<section class="rise mt-6">
|
||||||
<div class="relative w-full sm:max-w-md">
|
<div class="relative w-full sm:max-w-md">
|
||||||
<HugeiconsIcon icon={Search01Icon} class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
<HugeiconsIcon icon={Search01Icon}
|
||||||
<Input bind:value={filter} placeholder="Filter schematics..." autocomplete="off" class="pl-9" />
|
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
</div>
|
<Input bind:value={filter} placeholder="Filter schematics..." autocomplete="off" class="pl-9"/>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="mt-4 text-sm text-muted-foreground">Loading schematics...</p>
|
<p class="mt-4 text-sm text-muted-foreground">Loading schematics...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="mt-4 text-sm text-destructive">{error}</p>
|
<p class="mt-4 text-sm text-destructive">{error}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="rise mt-4 overflow-hidden">
|
<Card class="rise mt-4 overflow-hidden py-0">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="border-b border-border/60 bg-muted/40">
|
<thead class="border-b border-border/60 bg-muted/40">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Name</th>
|
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-muted-foreground">Name</th>
|
||||||
<th scope="col" class="px-4 py-2.5 text-right text-xs font-medium text-muted-foreground">Size</th>
|
<th scope="col" class="px-3 py-2 text-right text-xs font-medium text-muted-foreground">Size</th>
|
||||||
<th scope="col" class="w-12"></th>
|
<th scope="col" class="w-12"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-border/60">
|
<tbody class="divide-y divide-border/60">
|
||||||
{#each visible as schematic (schematic.name)}
|
{#each visible as schematic (schematic.name)}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="break-all px-4 py-3 font-mono text-xs">{schematic.name}</td>
|
<td class="break-all px-3 py-2.5 font-mono text-xs">{schematic.name}</td>
|
||||||
<td class="px-4 py-3 text-right tabular text-muted-foreground">{schematic.formattedSize || schematic.size}</td>
|
<td class="px-3 py-2.5 text-right tabular text-muted-foreground">{schematic.formattedSize || schematic.size}</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-3 py-2.5 text-right">
|
||||||
<a href={schematic.downloadUrl} download aria-label={`Download ${schematic.name}`} class="inline-flex size-8 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground">
|
<a href={schematic.downloadUrl} download aria-label={`Download ${schematic.name}`}
|
||||||
<HugeiconsIcon icon={Download01Icon} class="size-4" />
|
class="inline-flex size-8 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground">
|
||||||
</a>
|
<HugeiconsIcon icon={Download01Icon} class="size-4"/>
|
||||||
</td>
|
</a>
|
||||||
</tr>
|
</td>
|
||||||
{:else}
|
</tr>
|
||||||
<tr><td colspan="3" class="px-4 py-8 text-center text-muted-foreground">No schematics match that filter.</td></tr>
|
{:else}
|
||||||
{/each}
|
<tr>
|
||||||
</tbody>
|
<td colspan="3" class="px-3 py-6 text-center text-muted-foreground">No schematics match that
|
||||||
</table>
|
filter.
|
||||||
</Card>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
import { BufferGeometry } from 'three/src/core/BufferGeometry.js';
|
import {BufferGeometry} from 'three/src/core/BufferGeometry.js';
|
||||||
import { Float32BufferAttribute } from 'three/src/core/BufferAttribute.js';
|
import {Float32BufferAttribute} from 'three/src/core/BufferAttribute.js';
|
||||||
import { ClampToEdgeWrapping, DoubleSide, FrontSide, NearestFilter, SRGBColorSpace } from 'three/src/constants.js';
|
import {ClampToEdgeWrapping, DoubleSide, FrontSide, NearestFilter, SRGBColorSpace} from 'three/src/constants.js';
|
||||||
import { Color } from 'three/src/math/Color.js';
|
import {Color} from 'three/src/math/Color.js';
|
||||||
import * as MathUtils from 'three/src/math/MathUtils.js';
|
import * as MathUtils from 'three/src/math/MathUtils.js';
|
||||||
import { Group } from 'three/src/objects/Group.js';
|
import {Group} from 'three/src/objects/Group.js';
|
||||||
import { Mesh } from 'three/src/objects/Mesh.js';
|
import {Mesh} from 'three/src/objects/Mesh.js';
|
||||||
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial.js';
|
import {MeshBasicMaterial} from 'three/src/materials/MeshBasicMaterial.js';
|
||||||
import { OrthographicCamera } from 'three/src/cameras/OrthographicCamera.js';
|
import {OrthographicCamera} from 'three/src/cameras/OrthographicCamera.js';
|
||||||
import { Scene } from 'three/src/scenes/Scene.js';
|
import {Scene} from 'three/src/scenes/Scene.js';
|
||||||
import { TextureLoader } from 'three/src/loaders/TextureLoader.js';
|
import {TextureLoader} from 'three/src/loaders/TextureLoader.js';
|
||||||
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
|
import {WebGLRenderer} from 'three/src/renderers/WebGLRenderer.js';
|
||||||
import type { Material } from 'three/src/materials/Material.js';
|
import type {Material} from 'three/src/materials/Material.js';
|
||||||
import type { Texture } from 'three/src/textures/Texture.js';
|
import type {Texture} from 'three/src/textures/Texture.js';
|
||||||
|
|
||||||
const RENDER_SIZE = 96;
|
const RENDER_SIZE = 96;
|
||||||
const FACE_BRIGHTNESS: Record<string, number> = { up: 1.0, down: 0.5, north: 0.8, south: 0.8, east: 0.6, west: 0.6 };
|
const FACE_BRIGHTNESS: Record<string, number> = {up: 1.0, down: 0.5, north: 0.8, south: 0.8, east: 0.6, west: 0.6};
|
||||||
const DEFAULT_GUI_TRANSFORM = { rotation: [0, 0, 0], translation: [0, 0, 0], scale: [1, 1, 1] };
|
const DEFAULT_GUI_TRANSFORM = {rotation: [0, 0, 0], translation: [0, 0, 0], scale: [1, 1, 1]};
|
||||||
const DEFAULT_BLOCK_GUI = { rotation: [30, 225, 0], translation: [0, 0, 0], scale: [0.625, 0.625, 0.625] };
|
const DEFAULT_BLOCK_GUI = {rotation: [30, 225, 0], translation: [0, 0, 0], scale: [0.625, 0.625, 0.625]};
|
||||||
|
|
||||||
const GRASS = 0x91bd59;
|
const GRASS = 0x91bd59;
|
||||||
const FOLIAGE = 0x48b518;
|
const FOLIAGE = 0x48b518;
|
||||||
const WATER = 0x3f76e4;
|
const WATER = 0x3f76e4;
|
||||||
const TINT_RGB: Record<string, number> = {
|
const TINT_RGB: Record<string, number> = {
|
||||||
grass_block: GRASS,
|
grass_block: GRASS,
|
||||||
short_grass: GRASS,
|
short_grass: GRASS,
|
||||||
tall_grass: GRASS,
|
tall_grass: GRASS,
|
||||||
fern: GRASS,
|
fern: GRASS,
|
||||||
large_fern: GRASS,
|
large_fern: GRASS,
|
||||||
sugar_cane: GRASS,
|
sugar_cane: GRASS,
|
||||||
pink_petals: GRASS,
|
pink_petals: GRASS,
|
||||||
oak_leaves: FOLIAGE,
|
oak_leaves: FOLIAGE,
|
||||||
jungle_leaves: FOLIAGE,
|
jungle_leaves: FOLIAGE,
|
||||||
acacia_leaves: FOLIAGE,
|
acacia_leaves: FOLIAGE,
|
||||||
dark_oak_leaves: FOLIAGE,
|
dark_oak_leaves: FOLIAGE,
|
||||||
mangrove_leaves: FOLIAGE,
|
mangrove_leaves: FOLIAGE,
|
||||||
vine: FOLIAGE,
|
vine: FOLIAGE,
|
||||||
birch_leaves: 0x80a755,
|
birch_leaves: 0x80a755,
|
||||||
spruce_leaves: 0x619961,
|
spruce_leaves: 0x619961,
|
||||||
lily_pad: 0x208030,
|
lily_pad: 0x208030,
|
||||||
water: WATER,
|
water: WATER,
|
||||||
water_bucket: WATER,
|
water_bucket: WATER,
|
||||||
melon_stem: 0xe0c71c,
|
melon_stem: 0xe0c71c,
|
||||||
pumpkin_stem: 0xe0c71c,
|
pumpkin_stem: 0xe0c71c,
|
||||||
attached_melon_stem: 0xe0c71c,
|
attached_melon_stem: 0xe0c71c,
|
||||||
attached_pumpkin_stem: 0xe0c71c,
|
attached_pumpkin_stem: 0xe0c71c,
|
||||||
redstone_wire: 0xff0000
|
redstone_wire: 0xff0000
|
||||||
};
|
};
|
||||||
|
|
||||||
type Model = Record<string, any>;
|
type Model = Record<string, any>;
|
||||||
@@ -60,389 +60,389 @@ let scene: Scene | null = null;
|
|||||||
let camera: OrthographicCamera | null = null;
|
let camera: OrthographicCamera | null = null;
|
||||||
|
|
||||||
function initThree() {
|
function initThree() {
|
||||||
if (renderer) return;
|
if (renderer) return;
|
||||||
renderer = new WebGLRenderer({ alpha: true, antialias: false, preserveDrawingBuffer: false });
|
renderer = new WebGLRenderer({alpha: true, antialias: false, preserveDrawingBuffer: false});
|
||||||
renderer.setSize(RENDER_SIZE, RENDER_SIZE);
|
renderer.setSize(RENDER_SIZE, RENDER_SIZE);
|
||||||
renderer.setPixelRatio(1);
|
renderer.setPixelRatio(1);
|
||||||
renderer.setClearColor(0x000000, 0);
|
renderer.setClearColor(0x000000, 0);
|
||||||
scene = new Scene();
|
scene = new Scene();
|
||||||
const half = 8.2;
|
const half = 8.2;
|
||||||
camera = new OrthographicCamera(-half, half, half, -half, -200, 200);
|
camera = new OrthographicCamera(-half, half, half, -half, -200, 200);
|
||||||
camera.position.set(0, 0, 100);
|
camera.position.set(0, 0, 100);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripNs(ref: string | null | undefined) {
|
function stripNs(ref: string | null | undefined) {
|
||||||
if (!ref) return ref;
|
if (!ref) return ref;
|
||||||
const index = ref.indexOf(':');
|
const index = ref.indexOf(':');
|
||||||
return index >= 0 ? ref.substring(index + 1) : ref;
|
return index >= 0 ? ref.substring(index + 1) : ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadItemDef(name: string) {
|
async function loadItemDef(name: string) {
|
||||||
if (itemDefCache.has(name)) return itemDefCache.get(name) as Promise<ItemDef>;
|
if (itemDefCache.has(name)) return itemDefCache.get(name) as Promise<ItemDef>;
|
||||||
const promise = fetch(`/assets/items/${name}.json`).then((response) => {
|
const promise = fetch(`/assets/items/${name}.json`).then((response) => {
|
||||||
if (!response.ok) throw new Error(`item def ${name}: ${response.status}`);
|
if (!response.ok) throw new Error(`item def ${name}: ${response.status}`);
|
||||||
return response.json() as Promise<ItemDef>;
|
return response.json() as Promise<ItemDef>;
|
||||||
});
|
});
|
||||||
itemDefCache.set(name, promise);
|
itemDefCache.set(name, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUILTIN: Record<string, Model> = {
|
const BUILTIN: Record<string, Model> = {
|
||||||
'builtin/generated': { builtin: 'generated', textures: {}, display: {} },
|
'builtin/generated': {builtin: 'generated', textures: {}, display: {}},
|
||||||
'builtin/entity': { builtin: 'entity', textures: {}, display: {} }
|
'builtin/entity': {builtin: 'entity', textures: {}, display: {}}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadModel(path: string): Promise<Model> {
|
async function loadModel(path: string): Promise<Model> {
|
||||||
if (BUILTIN[path]) return BUILTIN[path];
|
if (BUILTIN[path]) return BUILTIN[path];
|
||||||
if (modelCache.has(path)) return modelCache.get(path) as Promise<Model>;
|
if (modelCache.has(path)) return modelCache.get(path) as Promise<Model>;
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
const response = await fetch(`/assets/models/${path}.json`);
|
const response = await fetch(`/assets/models/${path}.json`);
|
||||||
if (!response.ok) throw new Error(`model ${path}: ${response.status}`);
|
if (!response.ok) throw new Error(`model ${path}: ${response.status}`);
|
||||||
const data = (await response.json()) as Model;
|
const data = (await response.json()) as Model;
|
||||||
if (!data.parent) return data;
|
if (!data.parent) return data;
|
||||||
const parent = await loadModel(stripNs(data.parent) ?? '');
|
const parent = await loadModel(stripNs(data.parent) ?? '');
|
||||||
return mergeModel(parent, data);
|
return mergeModel(parent, data);
|
||||||
})();
|
})();
|
||||||
modelCache.set(path, promise);
|
modelCache.set(path, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeModel(parent: Model, child: Model): Model {
|
function mergeModel(parent: Model, child: Model): Model {
|
||||||
return {
|
return {
|
||||||
builtin: child.builtin ?? parent.builtin,
|
builtin: child.builtin ?? parent.builtin,
|
||||||
elements: child.elements ?? parent.elements,
|
elements: child.elements ?? parent.elements,
|
||||||
gui_light: child.gui_light ?? parent.gui_light,
|
gui_light: child.gui_light ?? parent.gui_light,
|
||||||
textures: { ...(parent.textures || {}), ...(child.textures || {}) },
|
textures: {...(parent.textures || {}), ...(child.textures || {})},
|
||||||
display: { ...(parent.display || {}), ...(child.display || {}) }
|
display: {...(parent.display || {}), ...(child.display || {})}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTextureRef(textures: Record<string, string>, ref: string) {
|
function resolveTextureRef(textures: Record<string, string>, ref: string) {
|
||||||
let current: string | undefined | null = ref;
|
let current: string | undefined | null = ref;
|
||||||
for (let i = 0; i < 16 && current; i += 1) {
|
for (let i = 0; i < 16 && current; i += 1) {
|
||||||
if (!current.startsWith('#')) return stripNs(current);
|
if (!current.startsWith('#')) return stripNs(current);
|
||||||
current = textures[current.substring(1)];
|
current = textures[current.substring(1)];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTexture(path: string) {
|
async function loadTexture(path: string) {
|
||||||
if (textureCache.has(path)) return textureCache.get(path) as Promise<Texture>;
|
if (textureCache.has(path)) return textureCache.get(path) as Promise<Texture>;
|
||||||
const promise = new Promise<Texture>((resolve, reject) => {
|
const promise = new Promise<Texture>((resolve, reject) => {
|
||||||
new TextureLoader().load(
|
new TextureLoader().load(
|
||||||
`/assets/textures/${path}.png`,
|
`/assets/textures/${path}.png`,
|
||||||
(texture) => {
|
(texture) => {
|
||||||
texture.magFilter = NearestFilter;
|
texture.magFilter = NearestFilter;
|
||||||
texture.minFilter = NearestFilter;
|
texture.minFilter = NearestFilter;
|
||||||
texture.colorSpace = SRGBColorSpace;
|
texture.colorSpace = SRGBColorSpace;
|
||||||
texture.wrapS = ClampToEdgeWrapping;
|
texture.wrapS = ClampToEdgeWrapping;
|
||||||
texture.wrapT = ClampToEdgeWrapping;
|
texture.wrapT = ClampToEdgeWrapping;
|
||||||
texture.generateMipmaps = false;
|
texture.generateMipmaps = false;
|
||||||
const image = texture.image as HTMLImageElement | undefined;
|
const image = texture.image as HTMLImageElement | undefined;
|
||||||
if (image && image.height > image.width) {
|
if (image && image.height > image.width) {
|
||||||
const frame = image.width / image.height;
|
const frame = image.width / image.height;
|
||||||
texture.repeat.y = frame;
|
texture.repeat.y = frame;
|
||||||
texture.offset.y = 1 - frame;
|
texture.offset.y = 1 - frame;
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
}
|
}
|
||||||
resolve(texture);
|
resolve(texture);
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
reject
|
reject
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
textureCache.set(path, promise);
|
textureCache.set(path, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractModelPath(node: any): string | null {
|
function extractModelPath(node: any): string | null {
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
if (typeof node === 'string') return node;
|
if (typeof node === 'string') return node;
|
||||||
if (node.type === 'minecraft:model' && typeof node.model === 'string') return node.model;
|
if (node.type === 'minecraft:model' && typeof node.model === 'string') return node.model;
|
||||||
if (node.type === 'minecraft:special' && typeof node.base === 'string') return node.base;
|
if (node.type === 'minecraft:special' && typeof node.base === 'string') return node.base;
|
||||||
for (const field of ['fallback', 'on_false', 'on_true', 'model']) {
|
for (const field of ['fallback', 'on_false', 'on_true', 'model']) {
|
||||||
if (node[field]) {
|
if (node[field]) {
|
||||||
const result = extractModelPath(node[field]);
|
const result = extractModelPath(node[field]);
|
||||||
if (result) return result;
|
if (result) return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
for (const arrayField of ['cases', 'entries']) {
|
||||||
for (const arrayField of ['cases', 'entries']) {
|
const items = node[arrayField];
|
||||||
const items = node[arrayField];
|
if (!Array.isArray(items)) continue;
|
||||||
if (!Array.isArray(items)) continue;
|
for (const entry of items) {
|
||||||
for (const entry of items) {
|
const result = extractModelPath(entry.model || entry);
|
||||||
const result = extractModelPath(entry.model || entry);
|
if (result) return result;
|
||||||
if (result) return result;
|
}
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tintToRgb(tint: any) {
|
function tintToRgb(tint: any) {
|
||||||
if (!tint || typeof tint !== 'object') return null;
|
if (!tint || typeof tint !== 'object') return null;
|
||||||
const type = stripNs(tint.type);
|
const type = stripNs(tint.type);
|
||||||
if (type === 'constant' && Number.isFinite(tint.value)) return tint.value & 0xffffff;
|
if (type === 'constant' && Number.isFinite(tint.value)) return tint.value & 0xffffff;
|
||||||
if (type === 'grass') return GRASS;
|
if (type === 'grass') return GRASS;
|
||||||
if (type === 'foliage') return FOLIAGE;
|
if (type === 'foliage') return FOLIAGE;
|
||||||
if (type === 'water') return WATER;
|
if (type === 'water') return WATER;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTintRgb(node: any, itemName: string): number | null {
|
function extractTintRgb(node: any, itemName: string): number | null {
|
||||||
if (!node || typeof node === 'string') return TINT_RGB[itemName] ?? null;
|
if (!node || typeof node === 'string') return TINT_RGB[itemName] ?? null;
|
||||||
if (Array.isArray(node.tints) && node.tints.length) {
|
if (Array.isArray(node.tints) && node.tints.length) {
|
||||||
const rgb = tintToRgb(node.tints[0]);
|
const rgb = tintToRgb(node.tints[0]);
|
||||||
if (rgb != null) return rgb;
|
if (rgb != null) return rgb;
|
||||||
}
|
|
||||||
for (const field of ['fallback', 'on_false', 'on_true', 'model']) {
|
|
||||||
if (node[field] && typeof node[field] !== 'string') {
|
|
||||||
const result = extractTintRgb(node[field], itemName);
|
|
||||||
if (result != null) return result;
|
|
||||||
}
|
}
|
||||||
}
|
for (const field of ['fallback', 'on_false', 'on_true', 'model']) {
|
||||||
for (const arrayField of ['cases', 'entries']) {
|
if (node[field] && typeof node[field] !== 'string') {
|
||||||
const items = node[arrayField];
|
const result = extractTintRgb(node[field], itemName);
|
||||||
if (!Array.isArray(items)) continue;
|
if (result != null) return result;
|
||||||
for (const entry of items) {
|
}
|
||||||
const result = extractTintRgb(entry.model || entry, itemName);
|
|
||||||
if (result != null) return result;
|
|
||||||
}
|
}
|
||||||
}
|
for (const arrayField of ['cases', 'entries']) {
|
||||||
return TINT_RGB[itemName] ?? null;
|
const items = node[arrayField];
|
||||||
|
if (!Array.isArray(items)) continue;
|
||||||
|
for (const entry of items) {
|
||||||
|
const result = extractTintRgb(entry.model || entry, itemName);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TINT_RGB[itemName] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function faceQuad(face: string, from: number[], to: number[], uv: number[]) {
|
function faceQuad(face: string, from: number[], to: number[], uv: number[]) {
|
||||||
const [x1, y1, z1] = from;
|
const [x1, y1, z1] = from;
|
||||||
const [x2, y2, z2] = to;
|
const [x2, y2, z2] = to;
|
||||||
const [u1, v1, u2, v2] = uv;
|
const [u1, v1, u2, v2] = uv;
|
||||||
let positions: number[];
|
let positions: number[];
|
||||||
switch (face) {
|
switch (face) {
|
||||||
case 'up':
|
case 'up':
|
||||||
positions = [x1, y2, z1, x1, y2, z2, x2, y2, z2, x2, y2, z1];
|
positions = [x1, y2, z1, x1, y2, z2, x2, y2, z2, x2, y2, z1];
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case 'down':
|
||||||
positions = [x1, y1, z2, x1, y1, z1, x2, y1, z1, x2, y1, z2];
|
positions = [x1, y1, z2, x1, y1, z1, x2, y1, z1, x2, y1, z2];
|
||||||
break;
|
break;
|
||||||
case 'north':
|
case 'north':
|
||||||
positions = [x2, y2, z1, x2, y1, z1, x1, y1, z1, x1, y2, z1];
|
positions = [x2, y2, z1, x2, y1, z1, x1, y1, z1, x1, y2, z1];
|
||||||
break;
|
break;
|
||||||
case 'south':
|
case 'south':
|
||||||
positions = [x1, y2, z2, x1, y1, z2, x2, y1, z2, x2, y2, z2];
|
positions = [x1, y2, z2, x1, y1, z2, x2, y1, z2, x2, y2, z2];
|
||||||
break;
|
break;
|
||||||
case 'east':
|
case 'east':
|
||||||
positions = [x2, y2, z2, x2, y1, z2, x2, y1, z1, x2, y2, z1];
|
positions = [x2, y2, z2, x2, y1, z2, x2, y1, z1, x2, y2, z1];
|
||||||
break;
|
break;
|
||||||
case 'west':
|
case 'west':
|
||||||
positions = [x1, y2, z1, x1, y1, z1, x1, y1, z2, x1, y2, z2];
|
positions = [x1, y2, z1, x1, y1, z1, x1, y1, z2, x1, y2, z2];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const uvs = [u1 / 16, 1 - v1 / 16, u1 / 16, 1 - v2 / 16, u2 / 16, 1 - v2 / 16, u2 / 16, 1 - v1 / 16];
|
const uvs = [u1 / 16, 1 - v1 / 16, u1 / 16, 1 - v2 / 16, u2 / 16, 1 - v2 / 16, u2 / 16, 1 - v1 / 16];
|
||||||
return { positions, uvs };
|
return {positions, uvs};
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultUV(face: string, from: number[], to: number[]) {
|
function defaultUV(face: string, from: number[], to: number[]) {
|
||||||
const [x1, y1, z1] = from;
|
const [x1, y1, z1] = from;
|
||||||
const [x2, y2, z2] = to;
|
const [x2, y2, z2] = to;
|
||||||
switch (face) {
|
switch (face) {
|
||||||
case 'up':
|
case 'up':
|
||||||
case 'down':
|
case 'down':
|
||||||
return [x1, z1, x2, z2];
|
return [x1, z1, x2, z2];
|
||||||
case 'north':
|
case 'north':
|
||||||
case 'south':
|
case 'south':
|
||||||
return [x1, 16 - y2, x2, 16 - y1];
|
return [x1, 16 - y2, x2, 16 - y1];
|
||||||
case 'east':
|
case 'east':
|
||||||
case 'west':
|
case 'west':
|
||||||
return [z1, 16 - y2, z2, 16 - y1];
|
return [z1, 16 - y2, z2, 16 - y1];
|
||||||
default:
|
default:
|
||||||
return [0, 0, 16, 16];
|
return [0, 0, 16, 16];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildElementGroup(elem: any, textures: Record<string, string>, guiLight: string | undefined, tintRgb: number | null) {
|
async function buildElementGroup(elem: any, textures: Record<string, string>, guiLight: string | undefined, tintRgb: number | null) {
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
const sideLit = guiLight !== 'front';
|
const sideLit = guiLight !== 'front';
|
||||||
for (const [face, data] of Object.entries<any>(elem.faces || {})) {
|
for (const [face, data] of Object.entries<any>(elem.faces || {})) {
|
||||||
const texturePath = resolveTextureRef(textures, data.texture);
|
const texturePath = resolveTextureRef(textures, data.texture);
|
||||||
if (!texturePath) continue;
|
if (!texturePath) continue;
|
||||||
let texture: Texture;
|
let texture: Texture;
|
||||||
try {
|
try {
|
||||||
texture = await loadTexture(texturePath);
|
texture = await loadTexture(texturePath);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
const quad = faceQuad(face, elem.from, elem.to, data.uv || defaultUV(face, elem.from, elem.to));
|
||||||
|
if (!quad) continue;
|
||||||
|
const geometry = new BufferGeometry();
|
||||||
|
geometry.setAttribute('position', new Float32BufferAttribute(quad.positions, 3));
|
||||||
|
geometry.setAttribute('uv', new Float32BufferAttribute(quad.uvs, 2));
|
||||||
|
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
||||||
|
const brightness = sideLit ? (FACE_BRIGHTNESS[face] ?? 1) : 1;
|
||||||
|
const tinted = data.tintindex !== undefined && tintRgb != null;
|
||||||
|
const red = tinted ? ((tintRgb >> 16) & 0xff) / 255 : 1;
|
||||||
|
const green = tinted ? ((tintRgb >> 8) & 0xff) / 255 : 1;
|
||||||
|
const blue = tinted ? (tintRgb & 0xff) / 255 : 1;
|
||||||
|
const material = new MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
color: new Color(brightness * red, brightness * green, brightness * blue),
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.01,
|
||||||
|
side: DoubleSide,
|
||||||
|
depthWrite: true,
|
||||||
|
polygonOffset: tinted,
|
||||||
|
polygonOffsetFactor: tinted ? -1 : 0,
|
||||||
|
polygonOffsetUnits: tinted ? -1 : 0
|
||||||
|
});
|
||||||
|
group.add(new Mesh(geometry, material));
|
||||||
}
|
}
|
||||||
const quad = faceQuad(face, elem.from, elem.to, data.uv || defaultUV(face, elem.from, elem.to));
|
if (!elem.rotation) return group;
|
||||||
if (!quad) continue;
|
const origin = elem.rotation.origin || [8, 8, 8];
|
||||||
const geometry = new BufferGeometry();
|
const angle = ((elem.rotation.angle || 0) * Math.PI) / 180;
|
||||||
geometry.setAttribute('position', new Float32BufferAttribute(quad.positions, 3));
|
const axis = elem.rotation.axis || 'y';
|
||||||
geometry.setAttribute('uv', new Float32BufferAttribute(quad.uvs, 2));
|
const wrapTo = new Group();
|
||||||
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
wrapTo.position.set(origin[0], origin[1], origin[2]);
|
||||||
const brightness = sideLit ? (FACE_BRIGHTNESS[face] ?? 1) : 1;
|
const rotation = new Group();
|
||||||
const tinted = data.tintindex !== undefined && tintRgb != null;
|
if (axis === 'x') rotation.rotation.x = angle;
|
||||||
const red = tinted ? ((tintRgb >> 16) & 0xff) / 255 : 1;
|
else if (axis === 'y') rotation.rotation.y = angle;
|
||||||
const green = tinted ? ((tintRgb >> 8) & 0xff) / 255 : 1;
|
else if (axis === 'z') rotation.rotation.z = angle;
|
||||||
const blue = tinted ? (tintRgb & 0xff) / 255 : 1;
|
wrapTo.add(rotation);
|
||||||
const material = new MeshBasicMaterial({
|
const wrapBack = new Group();
|
||||||
map: texture,
|
wrapBack.position.set(-origin[0], -origin[1], -origin[2]);
|
||||||
color: new Color(brightness * red, brightness * green, brightness * blue),
|
rotation.add(wrapBack);
|
||||||
transparent: true,
|
wrapBack.add(group);
|
||||||
alphaTest: 0.01,
|
return wrapTo;
|
||||||
side: DoubleSide,
|
|
||||||
depthWrite: true,
|
|
||||||
polygonOffset: tinted,
|
|
||||||
polygonOffsetFactor: tinted ? -1 : 0,
|
|
||||||
polygonOffsetUnits: tinted ? -1 : 0
|
|
||||||
});
|
|
||||||
group.add(new Mesh(geometry, material));
|
|
||||||
}
|
|
||||||
if (!elem.rotation) return group;
|
|
||||||
const origin = elem.rotation.origin || [8, 8, 8];
|
|
||||||
const angle = ((elem.rotation.angle || 0) * Math.PI) / 180;
|
|
||||||
const axis = elem.rotation.axis || 'y';
|
|
||||||
const wrapTo = new Group();
|
|
||||||
wrapTo.position.set(origin[0], origin[1], origin[2]);
|
|
||||||
const rotation = new Group();
|
|
||||||
if (axis === 'x') rotation.rotation.x = angle;
|
|
||||||
else if (axis === 'y') rotation.rotation.y = angle;
|
|
||||||
else if (axis === 'z') rotation.rotation.z = angle;
|
|
||||||
wrapTo.add(rotation);
|
|
||||||
const wrapBack = new Group();
|
|
||||||
wrapBack.position.set(-origin[0], -origin[1], -origin[2]);
|
|
||||||
rotation.add(wrapBack);
|
|
||||||
wrapBack.add(group);
|
|
||||||
return wrapTo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildElementsModel(model: Model, tintRgb: number | null) {
|
async function buildElementsModel(model: Model, tintRgb: number | null) {
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
for (const elem of model.elements || []) group.add(await buildElementGroup(elem, model.textures || {}, model.gui_light, tintRgb));
|
for (const elem of model.elements || []) group.add(await buildElementGroup(elem, model.textures || {}, model.gui_light, tintRgb));
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildLayeredSprite(model: Model, tintRgb: number | null) {
|
async function buildLayeredSprite(model: Model, tintRgb: number | null) {
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
const textures = model.textures || {};
|
const textures = model.textures || {};
|
||||||
for (let i = 0; ; i += 1) {
|
for (let i = 0; ; i += 1) {
|
||||||
const ref = textures[`layer${i}`];
|
const ref = textures[`layer${i}`];
|
||||||
if (!ref) break;
|
if (!ref) break;
|
||||||
let texture: Texture;
|
let texture: Texture;
|
||||||
try {
|
try {
|
||||||
texture = await loadTexture(stripNs(ref) ?? '');
|
texture = await loadTexture(stripNs(ref) ?? '');
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
const geometry = new BufferGeometry();
|
||||||
|
geometry.setAttribute('position', new Float32BufferAttribute([0, 16, 0, 0, 0, 0, 16, 0, 0, 16, 16, 0], 3));
|
||||||
|
geometry.setAttribute('uv', new Float32BufferAttribute([0, 1, 0, 0, 1, 0, 1, 1], 2));
|
||||||
|
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
||||||
|
const tinted = i === 0 && tintRgb != null;
|
||||||
|
const red = tinted ? ((tintRgb >> 16) & 0xff) / 255 : 1;
|
||||||
|
const green = tinted ? ((tintRgb >> 8) & 0xff) / 255 : 1;
|
||||||
|
const blue = tinted ? (tintRgb & 0xff) / 255 : 1;
|
||||||
|
const material = new MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
color: new Color(red, green, blue),
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.01,
|
||||||
|
side: DoubleSide
|
||||||
|
});
|
||||||
|
const mesh = new Mesh(geometry, material);
|
||||||
|
mesh.position.z = i * 0.05;
|
||||||
|
group.add(mesh);
|
||||||
}
|
}
|
||||||
const geometry = new BufferGeometry();
|
return group;
|
||||||
geometry.setAttribute('position', new Float32BufferAttribute([0, 16, 0, 0, 0, 0, 16, 0, 0, 16, 16, 0], 3));
|
|
||||||
geometry.setAttribute('uv', new Float32BufferAttribute([0, 1, 0, 0, 1, 0, 1, 1], 2));
|
|
||||||
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
|
||||||
const tinted = i === 0 && tintRgb != null;
|
|
||||||
const red = tinted ? ((tintRgb >> 16) & 0xff) / 255 : 1;
|
|
||||||
const green = tinted ? ((tintRgb >> 8) & 0xff) / 255 : 1;
|
|
||||||
const blue = tinted ? (tintRgb & 0xff) / 255 : 1;
|
|
||||||
const material = new MeshBasicMaterial({
|
|
||||||
map: texture,
|
|
||||||
color: new Color(red, green, blue),
|
|
||||||
transparent: true,
|
|
||||||
alphaTest: 0.01,
|
|
||||||
side: DoubleSide
|
|
||||||
});
|
|
||||||
const mesh = new Mesh(geometry, material);
|
|
||||||
mesh.position.z = i * 0.05;
|
|
||||||
group.add(mesh);
|
|
||||||
}
|
|
||||||
return group;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function atlasUv(x1: number, y1: number, x2: number, y2: number, tw = 64, th = 64) {
|
function atlasUv(x1: number, y1: number, x2: number, y2: number, tw = 64, th = 64) {
|
||||||
return [x1 / tw, 1 - y1 / th, x1 / tw, 1 - y2 / th, x2 / tw, 1 - y2 / th, x2 / tw, 1 - y1 / th];
|
return [x1 / tw, 1 - y1 / th, x1 / tw, 1 - y2 / th, x2 / tw, 1 - y2 / th, x2 / tw, 1 - y1 / th];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addBoxFace(group: Group, positions: number[], uv: number[], material: Material) {
|
function addBoxFace(group: Group, positions: number[], uv: number[], material: Material) {
|
||||||
const geometry = new BufferGeometry();
|
const geometry = new BufferGeometry();
|
||||||
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
|
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
|
||||||
geometry.setAttribute('uv', new Float32BufferAttribute(uv, 2));
|
geometry.setAttribute('uv', new Float32BufferAttribute(uv, 2));
|
||||||
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
geometry.setIndex([0, 1, 2, 0, 2, 3]);
|
||||||
group.add(new Mesh(geometry, material));
|
group.add(new Mesh(geometry, material));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUnwrappedBox(group: Group, from: number[], to: number[], texU: number, texV: number, sx: number, sy: number, sz: number, material: Material) {
|
function addUnwrappedBox(group: Group, from: number[], to: number[], texU: number, texV: number, sx: number, sy: number, sz: number, material: Material) {
|
||||||
const [x1, y1, z1] = from;
|
const [x1, y1, z1] = from;
|
||||||
const [x2, y2, z2] = to;
|
const [x2, y2, z2] = to;
|
||||||
const u1 = texU + sz;
|
const u1 = texU + sz;
|
||||||
const u2 = u1 + sx;
|
const u2 = u1 + sx;
|
||||||
const u3 = u2 + sz;
|
const u3 = u2 + sz;
|
||||||
const u4 = u3 + sx;
|
const u4 = u3 + sx;
|
||||||
const v1 = texV + sz;
|
const v1 = texV + sz;
|
||||||
const v2 = v1 + sy;
|
const v2 = v1 + sy;
|
||||||
addBoxFace(group, [x1, y2, z2, x1, y1, z2, x2, y1, z2, x2, y2, z2], atlasUv(u1, v1, u2, v2), material);
|
addBoxFace(group, [x1, y2, z2, x1, y1, z2, x2, y1, z2, x2, y2, z2], atlasUv(u1, v1, u2, v2), material);
|
||||||
addBoxFace(group, [x2, y2, z1, x2, y1, z1, x1, y1, z1, x1, y2, z1], atlasUv(u3, v1, u4, v2), material);
|
addBoxFace(group, [x2, y2, z1, x2, y1, z1, x1, y1, z1, x1, y2, z1], atlasUv(u3, v1, u4, v2), material);
|
||||||
addBoxFace(group, [x1, y2, z1, x1, y2, z2, x2, y2, z2, x2, y2, z1], atlasUv(u1, texV, u2, v1), material);
|
addBoxFace(group, [x1, y2, z1, x1, y2, z2, x2, y2, z2, x2, y2, z1], atlasUv(u1, texV, u2, v1), material);
|
||||||
addBoxFace(group, [x1, y1, z2, x1, y1, z1, x2, y1, z1, x2, y1, z2], atlasUv(u2, v1, u2 + sx, texV), material);
|
addBoxFace(group, [x1, y1, z2, x1, y1, z1, x2, y1, z1, x2, y1, z2], atlasUv(u2, v1, u2 + sx, texV), material);
|
||||||
addBoxFace(group, [x2, y2, z2, x2, y1, z2, x2, y1, z1, x2, y2, z1], atlasUv(u2, v1, u3, v2), material);
|
addBoxFace(group, [x2, y2, z2, x2, y1, z2, x2, y1, z1, x2, y2, z1], atlasUv(u2, v1, u3, v2), material);
|
||||||
addBoxFace(group, [x1, y2, z1, x1, y1, z1, x1, y1, z2, x1, y2, z2], atlasUv(texU, v1, u1, v2), material);
|
addBoxFace(group, [x1, y2, z1, x1, y1, z1, x1, y1, z2, x1, y2, z2], atlasUv(texU, v1, u1, v2), material);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildShieldSprite() {
|
async function buildShieldSprite() {
|
||||||
const texture = await loadTexture('entity/shield/shield_base_nopattern');
|
const texture = await loadTexture('entity/shield/shield_base_nopattern');
|
||||||
const material = new MeshBasicMaterial({ map: texture, transparent: true, alphaTest: 0.01, side: FrontSide });
|
const material = new MeshBasicMaterial({map: texture, transparent: true, alphaTest: 0.01, side: FrontSide});
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
addUnwrappedBox(group, [-6, -11, 1], [6, 11, 2], 0, 0, 12, 22, 1, material);
|
addUnwrappedBox(group, [-6, -11, 1], [6, 11, 2], 0, 0, 12, 22, 1, material);
|
||||||
addUnwrappedBox(group, [-1, -3, -5], [1, 3, 1], 26, 0, 2, 6, 6, material);
|
addUnwrappedBox(group, [-1, -3, -5], [1, 3, 1], 26, 0, 2, 6, 6, material);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGuiTransform(group: Group, display: any, isBlockShape: boolean) {
|
function applyGuiTransform(group: Group, display: any, isBlockShape: boolean) {
|
||||||
const gui = (display && display.gui) || (isBlockShape ? DEFAULT_BLOCK_GUI : DEFAULT_GUI_TRANSFORM);
|
const gui = (display && display.gui) || (isBlockShape ? DEFAULT_BLOCK_GUI : DEFAULT_GUI_TRANSFORM);
|
||||||
const rotation = gui.rotation || [0, 0, 0];
|
const rotation = gui.rotation || [0, 0, 0];
|
||||||
const translation = gui.translation || [0, 0, 0];
|
const translation = gui.translation || [0, 0, 0];
|
||||||
const scale = gui.scale || [1, 1, 1];
|
const scale = gui.scale || [1, 1, 1];
|
||||||
const inner = new Group();
|
const inner = new Group();
|
||||||
inner.position.set(-8, -8, -8);
|
inner.position.set(-8, -8, -8);
|
||||||
inner.add(group);
|
inner.add(group);
|
||||||
const outer = new Group();
|
const outer = new Group();
|
||||||
outer.rotation.set(MathUtils.degToRad(rotation[0]), MathUtils.degToRad(rotation[1]), MathUtils.degToRad(rotation[2]));
|
outer.rotation.set(MathUtils.degToRad(rotation[0]), MathUtils.degToRad(rotation[1]), MathUtils.degToRad(rotation[2]));
|
||||||
outer.scale.set(scale[0], scale[1], scale[2]);
|
outer.scale.set(scale[0], scale[1], scale[2]);
|
||||||
outer.position.set(translation[0], translation[1], translation[2]);
|
outer.position.set(translation[0], translation[1], translation[2]);
|
||||||
outer.add(inner);
|
outer.add(inner);
|
||||||
return outer;
|
return outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeGroup(group: Group) {
|
function disposeGroup(group: Group) {
|
||||||
group.traverse((object) => {
|
group.traverse((object) => {
|
||||||
const mesh = object as Mesh;
|
const mesh = object as Mesh;
|
||||||
mesh.geometry?.dispose();
|
mesh.geometry?.dispose();
|
||||||
const material = mesh.material;
|
const material = mesh.material;
|
||||||
if (Array.isArray(material)) material.forEach((item) => item.dispose());
|
if (Array.isArray(material)) material.forEach((item) => item.dispose());
|
||||||
else material?.dispose();
|
else material?.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderItem(name: string) {
|
export async function renderItem(name: string) {
|
||||||
if (itemCache.has(name)) return itemCache.get(name) as Promise<string | null>;
|
if (itemCache.has(name)) return itemCache.get(name) as Promise<string | null>;
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
try {
|
try {
|
||||||
initThree();
|
initThree();
|
||||||
if (!renderer || !scene || !camera) return null;
|
if (!renderer || !scene || !camera) return null;
|
||||||
const itemDef = await loadItemDef(name);
|
const itemDef = await loadItemDef(name);
|
||||||
const modelRef = extractModelPath(itemDef.model);
|
const modelRef = extractModelPath(itemDef.model);
|
||||||
if (!modelRef) return null;
|
if (!modelRef) return null;
|
||||||
const model = await loadModel(stripNs(modelRef) ?? '');
|
const model = await loadModel(stripNs(modelRef) ?? '');
|
||||||
const isBlockShape = Boolean(model.elements && model.elements.length);
|
const isBlockShape = Boolean(model.elements && model.elements.length);
|
||||||
const tintRgb = extractTintRgb(itemDef.model, name);
|
const tintRgb = extractTintRgb(itemDef.model, name);
|
||||||
const inner = name === 'shield' ? await buildShieldSprite() : isBlockShape ? await buildElementsModel(model, tintRgb) : await buildLayeredSprite(model, tintRgb);
|
const inner = name === 'shield' ? await buildShieldSprite() : isBlockShape ? await buildElementsModel(model, tintRgb) : await buildLayeredSprite(model, tintRgb);
|
||||||
const outer = applyGuiTransform(inner, model.display, isBlockShape);
|
const outer = applyGuiTransform(inner, model.display, isBlockShape);
|
||||||
scene.add(outer);
|
scene.add(outer);
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
const dataUrl = renderer.domElement.toDataURL('image/png');
|
const dataUrl = renderer.domElement.toDataURL('image/png');
|
||||||
scene.remove(outer);
|
scene.remove(outer);
|
||||||
disposeGroup(outer);
|
disposeGroup(outer);
|
||||||
return dataUrl;
|
return dataUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('itemRenderer: failed', name, error);
|
console.warn('itemRenderer: failed', name, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
itemCache.set(name, promise);
|
itemCache.set(name, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user