Compare commits

..

4 Commits

Author SHA1 Message Date
igm
a02e1f0862 Merge branch 'tea-cli' 2026-01-10 19:47:11 +08:00
igm
2fb42c0fa5 add gitea cli 2026-01-10 19:47:06 +08:00
igm
3d42324750 Merge branch 'svelte-lint'
# Conflicts:
#	apps/publisher-dashboard/src/lib/components/account/account-nav.svelte
2026-01-10 19:42:12 +08:00
igm
ac4b8dc99a Add eslint-plugin-svelte and fix all Svelte linting errors
- Configure eslint-plugin-svelte with TypeScript parser support
- Add keys to all {#each} blocks for proper reactivity
- Wrap navigation paths with resolve() from $app/paths
- Remove unnecessary children snippets and useless mustaches
- Add @typescript-eslint/parser and svelte-eslint-parser dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 19:34:25 +08:00
54 changed files with 217 additions and 128 deletions

View File

@@ -1,10 +1,24 @@
import { configs } from "@macalinao/eslint-config"; import { configs } from "@macalinao/eslint-config";
import tsParser from "@typescript-eslint/parser";
import svelte from "eslint-plugin-svelte";
import svelteParser from "svelte-eslint-parser";
export default [ export default [
{ {
ignores: [".svelte-kit/**", "build/**"], ignores: [".svelte-kit/**", "build/**"],
}, },
...configs.fast, ...configs.fast,
...svelte.configs["flat/recommended"],
{
files: ["**/*.svelte", "**/*.svelte.ts"],
languageOptions: {
parser: svelteParser,
parserOptions: {
parser: tsParser,
tsconfigRootDir: import.meta.dirname,
},
},
},
{ {
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {

View File

@@ -39,9 +39,12 @@
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/zxcvbn": "^4.4.5", "@types/zxcvbn": "^4.4.5",
"@typescript-eslint/parser": "^8.52.0",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-plugin-svelte": "^3.14.0",
"svelte": "^5.28.2", "svelte": "^5.28.2",
"svelte-check": "^4.2.1", "svelte-check": "^4.2.1",
"svelte-eslint-parser": "^1.4.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "catalog:", "typescript": "catalog:",

View File

@@ -5,6 +5,7 @@ import MonitorIcon from "@lucide/svelte/icons/monitor";
import ShieldCheckIcon from "@lucide/svelte/icons/shield-check"; import ShieldCheckIcon from "@lucide/svelte/icons/shield-check";
import UserIcon from "@lucide/svelte/icons/user"; import UserIcon from "@lucide/svelte/icons/user";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
@@ -56,10 +57,10 @@ function isActive(href: string, pathname: string): boolean {
className className
)} )}
> >
{#each navItems as item} {#each navItems as item (item.href)}
{@const active = isActive(item.href, $page.url.pathname)} {@const active = isActive(item.href, $page.url.pathname)}
<a <a
href={item.href} href={resolve(item.href)}
class={cn( class={cn(
"inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow]", "inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow]",
active active

View File

@@ -3,6 +3,7 @@ import { AlertTriangle } from "@lucide/svelte";
import { useQueryClient } from "@tanstack/svelte-query"; import { useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -51,7 +52,7 @@ async function handleDelete(e: Event) {
open = false; open = false;
// Redirect to login // Redirect to login
goto("/auth/login"); goto(resolve("/auth/login"));
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : "Failed to delete account"; error = e instanceof Error ? e.message : "Failed to delete account";
isDeleting = false; isDeleting = false;

View File

@@ -2,6 +2,7 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
@@ -25,7 +26,9 @@ const userQuery = createQuery(() => ({
// Redirect to login if not authenticated on non-auth pages // Redirect to login if not authenticated on non-auth pages
$effect(() => { $effect(() => {
if (!isAuthPage && userQuery.error) { if (!isAuthPage && userQuery.error) {
goto(`/auth/login?redirect=${encodeURIComponent(page.url.pathname)}`); goto(
resolve(`/auth/login?redirect=${encodeURIComponent(page.url.pathname)}`),
);
} }
}); });
</script> </script>

View File

@@ -32,7 +32,7 @@ const config = $derived(strengthConfig[score]);
<div class="space-y-2"> <div class="space-y-2">
<!-- Strength bars --> <!-- Strength bars -->
<div class="flex gap-1"> <div class="flex gap-1">
{#each Array(4) as _, i} {#each Array(4) as _, i (i)}
<div <div
class="h-1 flex-1 rounded-full transition-colors {i < score class="h-1 flex-1 rounded-full transition-colors {i < score
? config.color ? config.color
@@ -52,7 +52,7 @@ const config = $derived(strengthConfig[score]);
{#if result.feedback.warning} {#if result.feedback.warning}
<p class="text-destructive">{result.feedback.warning}</p> <p class="text-destructive">{result.feedback.warning}</p>
{/if} {/if}
{#each result.feedback.suggestions as suggestion} {#each result.feedback.suggestions as suggestion, i (i)}
<p>{suggestion}</p> <p>{suggestion}</p>
{/each} {/each}
</div> </div>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from "$app/paths";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
interface Props { interface Props {
@@ -25,9 +26,9 @@ const filters = [
</div> </div>
<div class="divide-y divide-border/50"> <div class="divide-y divide-border/50">
{#each filters as filter} {#each filters as filter (filter.label)}
<a <a
href={filter.href} href={resolve(filter.href)}
class="group flex items-center gap-3 px-5 py-3 transition-colors hover:bg-muted/30" class="group flex items-center gap-3 px-5 py-3 transition-colors hover:bg-muted/30"
> >
<div class="flex h-7 w-7 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-foreground/10 group-hover:text-foreground"> <div class="flex h-7 w-7 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-foreground/10 group-hover:text-foreground">

View File

@@ -46,7 +46,7 @@ function hourToPercent(hour: number): number {
<div class="flex"> <div class="flex">
<!-- Y-axis labels --> <!-- Y-axis labels -->
<div class="flex w-10 flex-col justify-between pr-2" style="height: 210px"> <div class="flex w-10 flex-col justify-between pr-2" style="height: 210px">
{#each hours as hour} {#each hours as hour (hour)}
<span class="text-[11px] tabular-nums text-muted-foreground">{hour}</span> <span class="text-[11px] tabular-nums text-muted-foreground">{hour}</span>
{/each} {/each}
</div> </div>
@@ -55,14 +55,14 @@ function hourToPercent(hour: number): number {
<div class="relative flex-1"> <div class="relative flex-1">
<!-- Grid lines --> <!-- Grid lines -->
<div class="absolute inset-0 flex flex-col justify-between" style="height: 210px"> <div class="absolute inset-0 flex flex-col justify-between" style="height: 210px">
{#each hours as _} {#each hours as hour (hour)}
<div class="h-px w-full bg-border"></div> <div class="h-px w-full bg-border"></div>
{/each} {/each}
</div> </div>
<!-- Bars container --> <!-- Bars container -->
<div class="relative grid grid-cols-7 gap-4 px-2" style="height: 210px"> <div class="relative grid grid-cols-7 gap-4 px-2" style="height: 210px">
{#each days as _, dayIndex} {#each days as day, dayIndex (day)}
{@const thisMonth = thisMonthData[dayIndex]} {@const thisMonth = thisMonthData[dayIndex]}
{@const lastMonth = lastMonthData[dayIndex]} {@const lastMonth = lastMonthData[dayIndex]}
<div class="relative flex justify-center"> <div class="relative flex justify-center">
@@ -104,7 +104,7 @@ function hourToPercent(hour: number): number {
<!-- X-axis labels --> <!-- X-axis labels -->
<div class="mt-2 grid grid-cols-7 gap-4 px-2"> <div class="mt-2 grid grid-cols-7 gap-4 px-2">
{#each days as day} {#each days as day (day)}
<div class="text-center text-[11px] text-muted-foreground">{day}</div> <div class="text-center text-[11px] text-muted-foreground">{day}</div>
{/each} {/each}
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import { import {
@@ -39,7 +40,7 @@ function handleTabChange(tabId: string) {
} else { } else {
url.searchParams.set("tab", tabId); url.searchParams.set("tab", tabId);
} }
goto(url.toString(), { replaceState: true, noScroll: true }); goto(resolve(url.toString()), { replaceState: true, noScroll: true });
} }
</script> </script>
@@ -60,7 +61,7 @@ function handleTabChange(tabId: string) {
<!-- Tab navigation --> <!-- Tab navigation -->
<div class="flex items-center gap-0.5" role="tablist"> <div class="flex items-center gap-0.5" role="tablist">
{#each tabs as tab} {#each tabs as tab (tab.id)}
{@const isActive = activeTab === tab.id} {@const isActive = activeTab === tab.id}
<button <button
role="tab" role="tab"

View File

@@ -78,7 +78,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each tableData as row, i} {#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30"> <Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5"> <Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground"> <div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">

View File

@@ -77,7 +77,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each tableData as row, i} {#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30"> <Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5"> <Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground"> <div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">

View File

@@ -47,7 +47,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each tableData as row, i} {#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30"> <Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5"> <Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground"> <div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">

View File

@@ -69,7 +69,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each tableData as row, i} {#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30"> <Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5"> <Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground"> <div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">

View File

@@ -63,7 +63,7 @@ const maxRevPercent = Math.max(...tableData.map((d) => d.revPercent));
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#each tableData as row, i} {#each tableData as row, i (row.id)}
<Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30"> <Table.Row class="group border-b border-border/50 transition-colors last:border-0 hover:bg-muted/30">
<Table.Cell class="w-10 py-3 pl-5"> <Table.Cell class="w-10 py-3 pl-5">
<div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground"> <div class="flex h-4 w-4 items-center justify-center rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground">

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from "$app/paths";
import { Badge } from "$lib/components/ui/badge"; import { Badge } from "$lib/components/ui/badge";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import AdminMobileNav from "./admin-mobile-nav.svelte"; import AdminMobileNav from "./admin-mobile-nav.svelte";
@@ -27,7 +28,7 @@ let { title, class: className }: Props = $props();
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a <a
href="/dashboard" href={resolve("/dashboard")}
class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"> <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -52,7 +53,7 @@ async function handleSignOut() {
await api.auth.logout(); await api.auth.logout();
queryClient.clear(); queryClient.clear();
open = false; open = false;
goto("/login"); goto(resolve("/login"));
} catch (error) { } catch (error) {
console.error("Failed to sign out:", error); console.error("Failed to sign out:", error);
} }
@@ -92,13 +93,13 @@ const navItems = [
<nav class="flex flex-1 flex-col p-4"> <nav class="flex flex-1 flex-col p-4">
<div class="space-y-1"> <div class="space-y-1">
{#each navItems as item} {#each navItems as item (item.href)}
{@const isActive = {@const isActive =
item.href === "/admin" item.href === "/admin"
? $page.url.pathname === "/admin" ? $page.url.pathname === "/admin"
: $page.url.pathname.startsWith(item.href)} : $page.url.pathname.startsWith(item.href)}
<a <a
href={item.href} href={resolve(item.href)}
onclick={handleNavClick} onclick={handleNavClick}
class={cn( class={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
@@ -135,7 +136,7 @@ const navItems = [
<div class="mt-6"> <div class="mt-6">
<Separator class="bg-zinc-800" /> <Separator class="bg-zinc-800" />
<a <a
href="/dashboard" href={resolve("/dashboard")}
onclick={handleNavClick} onclick={handleNavClick}
class="mt-4 flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200" class="mt-4 flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200"
> >
@@ -165,7 +166,7 @@ const navItems = [
<div class="mt-2 space-y-1"> <div class="mt-2 space-y-1">
<a <a
href="/account" href={resolve("/account")}
onclick={handleNavClick} onclick={handleNavClick}
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-zinc-400 transition-colors hover:bg-zinc-800/50 hover:text-zinc-200"
> >

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
@@ -43,7 +44,7 @@ async function handleSignOut() {
try { try {
await api.auth.logout(); await api.auth.logout();
queryClient.clear(); queryClient.clear();
goto("/login"); goto(resolve("/login"));
} catch (error) { } catch (error) {
console.error("Failed to sign out:", error); console.error("Failed to sign out:", error);
} }
@@ -66,7 +67,7 @@ const navItems = [
<!-- Admin Logo --> <!-- Admin Logo -->
<div class="flex h-[94px] items-center justify-center"> <div class="flex h-[94px] items-center justify-center">
<a <a
href="/admin" href={resolve("/admin")}
class="group flex h-8 w-8 items-center justify-center rounded-lg bg-red-600 shadow-sm transition-transform duration-200 hover:scale-105" class="group flex h-8 w-8 items-center justify-center rounded-lg bg-red-600 shadow-sm transition-transform duration-200 hover:scale-105"
aria-label="Admin Home" aria-label="Admin Home"
> >
@@ -84,13 +85,13 @@ const navItems = [
<!-- Main Navigation --> <!-- Main Navigation -->
<nav class="flex flex-1 flex-col items-center gap-3"> <nav class="flex flex-1 flex-col items-center gap-3">
{#each navItems as item} {#each navItems as item (item.href)}
{@const isActive = {@const isActive =
item.href === "/admin" item.href === "/admin"
? $page.url.pathname === "/admin" ? $page.url.pathname === "/admin"
: $page.url.pathname.startsWith(item.href)} : $page.url.pathname.startsWith(item.href)}
<a <a
href={item.href} href={resolve(item.href)}
class={cn( class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150", "group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isActive isActive
@@ -157,7 +158,7 @@ const navItems = [
<div class="flex flex-col items-center gap-3 pb-6"> <div class="flex flex-col items-center gap-3 pb-6">
<!-- Back to Dashboard link --> <!-- Back to Dashboard link -->
<a <a
href="/dashboard" href={resolve("/dashboard")}
class="group relative flex h-8 w-8 items-center justify-center rounded-lg text-zinc-400 transition-all duration-150 hover:bg-zinc-800 hover:text-zinc-200" class="group relative flex h-8 w-8 items-center justify-center rounded-lg text-zinc-400 transition-all duration-150 hover:bg-zinc-800 hover:text-zinc-200"
aria-label="Back to Dashboard" aria-label="Back to Dashboard"
> >
@@ -210,7 +211,7 @@ const navItems = [
</div> </div>
</div> </div>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => goto("/account")}> <DropdownMenu.Item onSelect={() => goto(resolve("/account"))}>
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"> <svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="7" r="4" /> <circle cx="12" cy="7" r="4" />

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Settings } from "@lucide/svelte"; import { Settings } from "@lucide/svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import OrgSwitcher from "./org-switcher.svelte"; import OrgSwitcher from "./org-switcher.svelte";
@@ -67,14 +68,14 @@ const navItems = $derived.by(() => {
<!-- Main Navigation --> <!-- Main Navigation -->
<nav class="flex flex-1 flex-col items-center gap-3"> <nav class="flex flex-1 flex-col items-center gap-3">
{#each navItems as item} {#each navItems as item (item.href)}
{@const isActive = {@const isActive =
item.icon === "home" item.icon === "home"
? $page.url.pathname === item.href ? $page.url.pathname === item.href
: $page.url.pathname === item.href || : $page.url.pathname === item.href ||
$page.url.pathname.startsWith(item.href + "/")} $page.url.pathname.startsWith(item.href + "/")}
<a <a
href={item.href} href={resolve(item.href)}
class={cn( class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150", "group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isActive isActive
@@ -162,7 +163,7 @@ const navItems = $derived.by(() => {
{#if currentSlug} {#if currentSlug}
{@const isSettingsActive = $page.url.pathname.startsWith(`/dashboard/${currentSlug}/settings`)} {@const isSettingsActive = $page.url.pathname.startsWith(`/dashboard/${currentSlug}/settings`)}
<a <a
href="/dashboard/{currentSlug}/settings" href={resolve(`/dashboard/${currentSlug}/settings`)}
class={cn( class={cn(
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150", "group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
isSettingsActive isSettingsActive

View File

@@ -2,6 +2,7 @@
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -85,7 +86,7 @@ async function handleSignOut() {
await api.auth.logout(); await api.auth.logout();
queryClient.clear(); queryClient.clear();
open = false; open = false;
goto("/login"); goto(resolve("/login"));
} catch (error) { } catch (error) {
console.error("Failed to sign out:", error); console.error("Failed to sign out:", error);
} }
@@ -118,12 +119,12 @@ async function handleSignOut() {
<nav class="flex flex-1 flex-col p-4"> <nav class="flex flex-1 flex-col p-4">
<div class="space-y-1"> <div class="space-y-1">
{#each navItems as item} {#each navItems as item (item.href)}
{@const isActive = {@const isActive =
$page.url.pathname === item.href || $page.url.pathname === item.href ||
(item.href !== "/" && $page.url.pathname.startsWith(item.href))} (item.href !== "/" && $page.url.pathname.startsWith(item.href))}
<a <a
href={item.href} href={resolve(item.href)}
onclick={handleNavClick} onclick={handleNavClick}
class={cn( class={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
@@ -184,7 +185,7 @@ async function handleSignOut() {
<div class="mt-2 space-y-1"> <div class="mt-2 space-y-1">
<a <a
href="/account" href={resolve("/account")}
onclick={handleNavClick} onclick={handleNavClick}
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
> >

View File

@@ -2,6 +2,7 @@
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
@@ -19,7 +20,7 @@ const orgsQuery = createQuery(() => ({
const orgs = $derived(orgsQuery.data ?? []); const orgs = $derived(orgsQuery.data ?? []);
function handleOrgSelect(slug: string) { function handleOrgSelect(slug: string) {
goto(`/dashboard/${slug}`); goto(resolve(`/dashboard/${slug}`));
} }
</script> </script>
@@ -51,7 +52,7 @@ function handleOrgSelect(slug: string) {
{:else if orgs.length === 0} {:else if orgs.length === 0}
<DropdownMenu.Item disabled>No organizations</DropdownMenu.Item> <DropdownMenu.Item disabled>No organizations</DropdownMenu.Item>
{:else} {:else}
{#each orgs as org} {#each orgs as org (org.slug)}
{@const isActive = currentSlug === org.slug} {@const isActive = currentSlug === org.slug}
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => handleOrgSelect(org.slug)} onSelect={() => handleOrgSelect(org.slug)}
@@ -76,7 +77,7 @@ function handleOrgSelect(slug: string) {
{/each} {/each}
{/if} {/if}
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => goto("/dashboard/new")}> <DropdownMenu.Item onSelect={() => goto(resolve("/dashboard/new"))}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19" stroke-linecap="round" /> <line x1="12" y1="5" x2="12" y2="19" stroke-linecap="round" />

View File

@@ -2,6 +2,7 @@
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
@@ -43,7 +44,7 @@ async function handleSignOut() {
await api.auth.logout(); await api.auth.logout();
// Clear all cached queries // Clear all cached queries
queryClient.clear(); queryClient.clear();
goto("/login"); goto(resolve("/login"));
} catch (error) { } catch (error) {
console.error("Failed to sign out:", error); console.error("Failed to sign out:", error);
} }
@@ -92,7 +93,7 @@ async function handleSignOut() {
</div> </div>
</div> </div>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => goto("/account")}> <DropdownMenu.Item onSelect={() => goto(resolve("/account"))}>
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"> <svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="7" r="4" /> <circle cx="12" cy="7" r="4" />

View File

@@ -2,6 +2,7 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { Building2, Globe, Settings, Users } from "@lucide/svelte"; import { Building2, Globe, Settings, Users } from "@lucide/svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { DashboardLayout } from "$lib/components/layout"; import { DashboardLayout } from "$lib/components/layout";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
@@ -58,10 +59,10 @@ function isActive(href: string): boolean {
<nav class="w-full shrink-0 lg:w-64"> <nav class="w-full shrink-0 lg:w-64">
<!-- Mobile: horizontal scroll --> <!-- Mobile: horizontal scroll -->
<div class="flex gap-2 overflow-x-auto pb-2 lg:hidden"> <div class="flex gap-2 overflow-x-auto pb-2 lg:hidden">
{#each navItems as item} {#each navItems as item (item.href)}
{@const active = isActive(item.href)} {@const active = isActive(item.href)}
<a <a
href={item.href} href={resolve(item.href)}
class={cn( class={cn(
"flex shrink-0 items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors", "flex shrink-0 items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors",
active active
@@ -77,10 +78,10 @@ function isActive(href: string): boolean {
<!-- Desktop: vertical list --> <!-- Desktop: vertical list -->
<div class="hidden space-y-1 lg:block"> <div class="hidden space-y-1 lg:block">
{#each navItems as item} {#each navItems as item (item.href)}
{@const active = isActive(item.href)} {@const active = isActive(item.href)}
<a <a
href={item.href} href={resolve(item.href)}
class={cn( class={cn(
"group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors", "group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors",
active active

View File

@@ -48,6 +48,7 @@ export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
</script> </script>
<script lang="ts"> <script lang="ts">
/* eslint-disable svelte/no-navigation-without-resolve -- Button receives href as prop, callers must use resolve() */
let { let {
class: className, class: className,
variant = "default", variant = "default",

View File

@@ -24,9 +24,7 @@ const queryClient = new QueryClient({
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthGuard> <AuthGuard>
{#snippet children()} {@render children()}
{@render children()}
{/snippet}
</AuthGuard> </AuthGuard>
<SvelteQueryDevtools /> <SvelteQueryDevtools />
</QueryClientProvider> </QueryClientProvider>

View File

@@ -2,6 +2,7 @@
import { Loader2 } from "@lucide/svelte"; import { Loader2 } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
/** /**
@@ -16,14 +17,16 @@ const orgsQuery = createQuery(() => ({
$effect(() => { $effect(() => {
if (orgsQuery.error) { if (orgsQuery.error) {
// Not authenticated, redirect to login // Not authenticated, redirect to login
goto(`/auth/login?redirect=${encodeURIComponent("/")}`); goto(resolve(`/auth/login?redirect=${encodeURIComponent("/")}`));
} else if (orgsQuery.data) { } else if (orgsQuery.data) {
if (orgsQuery.data.length > 0) { if (orgsQuery.data.length > 0) {
// Redirect to first org's dashboard // Redirect to first org's dashboard
goto(`/dashboard/${orgsQuery.data[0].slug}`, { replaceState: true }); goto(resolve(`/dashboard/${orgsQuery.data[0].slug}`), {
replaceState: true,
});
} else { } else {
// No orgs, show org list (empty state) // No orgs, show org list (empty state)
goto("/dashboard", { replaceState: true }); goto(resolve("/dashboard"), { replaceState: true });
} }
} }
}); });

View File

@@ -17,6 +17,7 @@ import {
} from "@tanstack/svelte-query"; } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
@@ -52,9 +53,9 @@ const acceptMutation = createMutation(() => ({
queryClient.invalidateQueries({ queryKey: ["orgs"] }); queryClient.invalidateQueries({ queryKey: ["orgs"] });
// Redirect to the org dashboard // Redirect to the org dashboard
if (inviteQuery.data) { if (inviteQuery.data) {
goto(`/dashboard/${inviteQuery.data.org.slug}`); goto(resolve(`/dashboard/${inviteQuery.data.org.slug}`));
} else { } else {
goto("/dashboard"); goto(resolve("/dashboard"));
} }
}, },
onError: (error) => { onError: (error) => {
@@ -71,7 +72,7 @@ const declineMutation = createMutation(() => ({
toast.success("Invitation declined"); toast.success("Invitation declined");
// Invalidate queries // Invalidate queries
queryClient.invalidateQueries({ queryKey: ["me", "invites"] }); queryClient.invalidateQueries({ queryKey: ["me", "invites"] });
goto("/dashboard"); goto(resolve("/dashboard"));
}, },
onError: (error) => { onError: (error) => {
toast.error( toast.error(
@@ -102,6 +103,7 @@ function formatDate(date: Date): string {
* Check if invite is expiring soon (within 3 days) * Check if invite is expiring soon (within 3 days)
*/ */
function isExpiringSoon(expiresAt: Date): boolean { function isExpiringSoon(expiresAt: Date): boolean {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- pure function, no reactivity needed
const threeDaysFromNow = new Date(); const threeDaysFromNow = new Date();
threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3); threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
return expiresAt < threeDaysFromNow; return expiresAt < threeDaysFromNow;
@@ -114,7 +116,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
<div class="space-y-6"> <div class="space-y-6">
<!-- Back link --> <!-- Back link -->
<Button variant="ghost" size="sm" href="/dashboard" class="-ml-2"> <Button variant="ghost" size="sm" href={resolve("/dashboard")} class="-ml-2">
<ArrowLeft class="mr-2 h-4 w-4" /> <ArrowLeft class="mr-2 h-4 w-4" />
Back to Dashboard Back to Dashboard
</Button> </Button>
@@ -131,7 +133,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
{inviteQuery.error instanceof Error ? inviteQuery.error.message : "Failed to load invitation"} {inviteQuery.error instanceof Error ? inviteQuery.error.message : "Failed to load invitation"}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button variant="outline" href="/dashboard"> <Button variant="outline" href={resolve("/dashboard")}>
Go to Dashboard Go to Dashboard
</Button> </Button>
{:else if inviteQuery.data} {:else if inviteQuery.data}

View File

@@ -4,6 +4,7 @@ import { createQuery } from "@tanstack/svelte-query";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
interface Props { interface Props {
@@ -22,11 +23,13 @@ const userQuery = createQuery(() => ({
$effect(() => { $effect(() => {
if (userQuery.data && !userQuery.data.isSuperuser) { if (userQuery.data && !userQuery.data.isSuperuser) {
toast.error("Access denied. Superuser privileges required."); toast.error("Access denied. Superuser privileges required.");
goto("/dashboard"); goto(resolve("/dashboard"));
} }
if (userQuery.error) { if (userQuery.error) {
goto( goto(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`, resolve(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
),
); );
} }
}); });

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AlertCircle, Building, Loader2, Plus, Users } from "@lucide/svelte"; import { AlertCircle, Building, Loader2, Plus, Users } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
import { AdminLayout } from "$lib/components/layout"; import { AdminLayout } from "$lib/components/layout";
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from "$lib/components/ui/button/index.js";
@@ -55,7 +56,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
<!-- Summary cards --> <!-- Summary cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<!-- Organizations card --> <!-- Organizations card -->
<a href="/admin/orgs" class="group block transition-transform hover:scale-[1.02]"> <a href={resolve("/admin/orgs")} class="group block transition-transform hover:scale-[1.02]">
<Card class="h-full transition-colors group-hover:border-primary/50"> <Card class="h-full transition-colors group-hover:border-primary/50">
<CardHeader class="pb-2"> <CardHeader class="pb-2">
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
@@ -71,7 +72,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
</a> </a>
<!-- Users card --> <!-- Users card -->
<a href="/admin/users" class="group block transition-transform hover:scale-[1.02]"> <a href={resolve("/admin/users")} class="group block transition-transform hover:scale-[1.02]">
<Card class="h-full transition-colors group-hover:border-primary/50"> <Card class="h-full transition-colors group-hover:border-primary/50">
<CardHeader class="pb-2"> <CardHeader class="pb-2">
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center gap-2 text-base">
@@ -94,7 +95,7 @@ const hasError = $derived(orgsQuery.error || usersQuery.error);
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<Button href="/admin/orgs/new"> <Button href={resolve("/admin/orgs/new")}>
<Plus class="mr-2 h-4 w-4" /> <Plus class="mr-2 h-4 w-4" />
New Organization New Organization
</Button> </Button>

View File

@@ -2,6 +2,7 @@
import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte"; import { AlertCircle, Building, Eye, Plus, Trash2 } from "@lucide/svelte";
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
import { AdminLayout } from "$lib/components/layout"; import { AdminLayout } from "$lib/components/layout";
import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte"; import ConfirmDialog from "$lib/components/org/confirm-dialog.svelte";
@@ -106,7 +107,7 @@ async function executeConfirmAction() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{#each Array(5) as _} {#each Array(5) as _, i (i)}
<TableRow> <TableRow>
<TableCell><Skeleton class="h-4 w-24" /></TableCell> <TableCell><Skeleton class="h-4 w-24" /></TableCell>
<TableCell><Skeleton class="h-4 w-32" /></TableCell> <TableCell><Skeleton class="h-4 w-32" /></TableCell>
@@ -137,7 +138,7 @@ async function executeConfirmAction() {
<h2 class="text-lg font-semibold"> <h2 class="text-lg font-semibold">
Organizations ({orgsQuery.data.length}) Organizations ({orgsQuery.data.length})
</h2> </h2>
<Button href="/admin/orgs/new"> <Button href={resolve("/admin/orgs/new")}>
<Plus class="mr-2 h-4 w-4" /> <Plus class="mr-2 h-4 w-4" />
New Organization New Organization
</Button> </Button>
@@ -154,7 +155,7 @@ async function executeConfirmAction() {
<p class="mt-2 text-center text-sm text-muted-foreground"> <p class="mt-2 text-center text-sm text-muted-foreground">
Create your first organization to get started. Create your first organization to get started.
</p> </p>
<Button href="/admin/orgs/new" class="mt-4"> <Button href={resolve("/admin/orgs/new")} class="mt-4">
<Plus class="mr-2 h-4 w-4" /> <Plus class="mr-2 h-4 w-4" />
New Organization New Organization
</Button> </Button>
@@ -192,7 +193,7 @@ async function executeConfirmAction() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
href="/dashboard/{org.slug}" href={resolve(`/dashboard/${org.slug}`)}
title="View organization" title="View organization"
> >
<Eye class="h-4 w-4" /> <Eye class="h-4 w-4" />
@@ -221,7 +222,7 @@ async function executeConfirmAction() {
<!-- Back link --> <!-- Back link -->
<div class="pt-4"> <div class="pt-4">
<a <a
href="/admin" href={resolve("/admin")}
class="text-sm text-muted-foreground hover:text-foreground" class="text-sm text-muted-foreground hover:text-foreground"
> >
&larr; Back to admin dashboard &larr; Back to admin dashboard

View File

@@ -12,6 +12,7 @@ import {
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { AdminLayout } from "$lib/components/layout"; import { AdminLayout } from "$lib/components/layout";
@@ -186,7 +187,7 @@ function handleDelete() {
await api.admin.orgs.delete({ slug: slug ?? "" }); await api.admin.orgs.delete({ slug: slug ?? "" });
toast.success("Organization deleted"); toast.success("Organization deleted");
await queryClient.invalidateQueries({ queryKey: ["admin", "orgs"] }); await queryClient.invalidateQueries({ queryKey: ["admin", "orgs"] });
goto("/admin/orgs"); goto(resolve("/admin/orgs"));
} catch (e) { } catch (e) {
toast.error( toast.error(
e instanceof Error ? e.message : "Failed to delete organization", e instanceof Error ? e.message : "Failed to delete organization",
@@ -235,7 +236,7 @@ async function executeConfirmAction() {
: "Failed to load organization"} : "Failed to load organization"}
</p> </p>
<a <a
href="/admin/orgs" href={resolve("/admin/orgs")}
class="mt-4 text-sm text-muted-foreground hover:text-foreground" class="mt-4 text-sm text-muted-foreground hover:text-foreground"
> >
<ArrowLeft class="mr-1 inline h-4 w-4" /> <ArrowLeft class="mr-1 inline h-4 w-4" />
@@ -247,7 +248,7 @@ async function executeConfirmAction() {
<div class="mx-auto max-w-2xl space-y-6"> <div class="mx-auto max-w-2xl space-y-6">
<!-- Back link --> <!-- Back link -->
<a <a
href="/admin/orgs" href={resolve("/admin/orgs")}
class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground" class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
> >
<ArrowLeft class="mr-1 h-4 w-4" /> <ArrowLeft class="mr-1 h-4 w-4" />

View File

@@ -2,6 +2,7 @@
import { ArrowLeft, Loader2 } from "@lucide/svelte"; import { ArrowLeft, Loader2 } from "@lucide/svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
import { AdminLayout } from "$lib/components/layout"; import { AdminLayout } from "$lib/components/layout";
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from "$lib/components/ui/button/index.js";
@@ -49,7 +50,7 @@ async function handleSubmit() {
ownerEmail: ownerEmail.trim(), ownerEmail: ownerEmail.trim(),
}); });
toast.success("Organization created successfully"); toast.success("Organization created successfully");
goto("/admin/orgs"); goto(resolve("/admin/orgs"));
} catch (e) { } catch (e) {
toast.error( toast.error(
e instanceof Error ? e.message : "Failed to create organization", e instanceof Error ? e.message : "Failed to create organization",
@@ -78,7 +79,7 @@ function handleSlugInput(event: Event) {
<div class="mx-auto max-w-2xl space-y-6"> <div class="mx-auto max-w-2xl space-y-6">
<!-- Back link --> <!-- Back link -->
<a <a
href="/admin/orgs" href={resolve("/admin/orgs")}
class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground" class="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
> >
<ArrowLeft class="mr-1 h-4 w-4" /> <ArrowLeft class="mr-1 h-4 w-4" />

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AlertCircle, Check, Eye, Users, X } from "@lucide/svelte"; import { AlertCircle, Check, Eye, Users, X } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
import { SuperuserBadge } from "$lib/components/admin/index.js"; import { SuperuserBadge } from "$lib/components/admin/index.js";
import { AdminLayout } from "$lib/components/layout"; import { AdminLayout } from "$lib/components/layout";
@@ -59,7 +60,7 @@ const usersQuery = createQuery(() => ({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{#each Array(5) as _} {#each Array(5) as _, i (i)}
<TableRow> <TableRow>
<TableCell><Skeleton class="h-4 w-40" /></TableCell> <TableCell><Skeleton class="h-4 w-40" /></TableCell>
<TableCell><Skeleton class="h-4 w-24" /></TableCell> <TableCell><Skeleton class="h-4 w-24" /></TableCell>
@@ -124,7 +125,7 @@ const usersQuery = createQuery(() => ({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
href="/admin/users/{encodeURIComponent(user.email)}" href={resolve(`/admin/users/${encodeURIComponent(user.email)}`)}
> >
<Eye class="mr-1 h-4 w-4" /> <Eye class="mr-1 h-4 w-4" />
View View

View File

@@ -11,6 +11,7 @@ import {
} from "@lucide/svelte"; } from "@lucide/svelte";
import { createQuery, useQueryClient } from "@tanstack/svelte-query"; import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client.js"; import { api } from "$lib/api/client.js";
import { SuperuserBadge } from "$lib/components/admin/index.js"; import { SuperuserBadge } from "$lib/components/admin/index.js";
@@ -150,7 +151,7 @@ async function handleConfirmEmail() {
<AdminLayout title="User Details"> <AdminLayout title="User Details">
<!-- Back navigation --> <!-- Back navigation -->
<div class="mb-6"> <div class="mb-6">
<Button variant="ghost" size="sm" href="/admin/users" class="gap-1"> <Button variant="ghost" size="sm" href={resolve("/admin/users")} class="gap-1">
<ArrowLeft class="h-4 w-4" /> <ArrowLeft class="h-4 w-4" />
Back to users Back to users
</Button> </Button>
@@ -179,7 +180,7 @@ async function handleConfirmEmail() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
{#each Array(5) as _} {#each Array(5) as _, i (i)}
<div class="space-y-1"> <div class="space-y-1">
<Skeleton class="h-4 w-20" /> <Skeleton class="h-4 w-20" />
<Skeleton class="h-5 w-32" /> <Skeleton class="h-5 w-32" />

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { resolve } from "$app/paths";
interface Props { interface Props {
children: Snippet; children: Snippet;
@@ -80,9 +81,9 @@ let { children }: Props = $props();
<!-- Footer --> <!-- Footer -->
<p class="text-center text-xs text-muted-foreground"> <p class="text-center text-xs text-muted-foreground">
By continuing, you agree to our By continuing, you agree to our
<a href="/terms" class="underline underline-offset-4 hover:text-foreground">Terms of Service</a> <a href={resolve("/terms")} class="underline underline-offset-4 hover:text-foreground">Terms of Service</a>
and and
<a href="/privacy" class="underline underline-offset-4 hover:text-foreground">Privacy Policy</a> <a href={resolve("/privacy")} class="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,7 @@ import {
} from "@lucide/svelte"; } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
@@ -40,7 +41,7 @@ async function copyToClipboard() {
// Guard: redirect to /auth/login if no active login flow // Guard: redirect to /auth/login if no active login flow
$effect(() => { $effect(() => {
if (!loginFlowState.email) { if (!loginFlowState.email) {
goto("/auth/login"); goto(resolve("/auth/login"));
} }
}); });
@@ -58,7 +59,7 @@ const statusQuery = createQuery(() => ({
$effect(() => { $effect(() => {
if (statusQuery.data?.status === "completed") { if (statusQuery.data?.status === "completed") {
clearLoginFlowState(); clearLoginFlowState();
goto(statusQuery.data.redirectTo || "/"); goto(resolve(statusQuery.data.redirectTo || "/"));
} }
}); });
@@ -88,7 +89,7 @@ async function handleResendEmail() {
function handleDifferentEmail() { function handleDifferentEmail() {
clearLoginFlowState(); clearLoginFlowState();
goto("/auth/login"); goto(resolve("/auth/login"));
} }
</script> </script>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { CheckCircle2 } from "@lucide/svelte"; import { CheckCircle2 } from "@lucide/svelte";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
@@ -119,8 +120,8 @@ async function handleSubmit(e: Event) {
<!-- Back to login link --> <!-- Back to login link -->
<div class="text-center text-sm text-muted-foreground"> <div class="text-center text-sm text-muted-foreground">
Remember your password?{" "} Remember your password?
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary"> <a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
Sign in Sign in
</a> </a>
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Input } from "$lib/components/ui/input"; import { Input } from "$lib/components/ui/input";
@@ -21,12 +22,12 @@ async function handleSubmit(e: SubmitEvent) {
setLoginFlowState(response); setLoginFlowState(response);
if (response.hasPasskey) { if (response.hasPasskey) {
goto("/auth/login/passkey"); goto(resolve("/auth/login/passkey"));
} else if (response.hasPassword) { } else if (response.hasPassword) {
goto("/auth/login/password"); goto(resolve("/auth/login/password"));
} else { } else {
// Anti-enumeration: always redirect to confirm even if user doesn't exist // Anti-enumeration: always redirect to confirm even if user doesn't exist
goto("/auth/confirm"); goto(resolve("/auth/confirm"));
} }
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : "An unexpected error occurred"; error = err instanceof Error ? err.message : "An unexpected error occurred";
@@ -75,7 +76,7 @@ async function handleSubmit(e: SubmitEvent) {
<div class="text-center"> <div class="text-center">
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Don't have an account? Don't have an account?
<a href="/auth/signup" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"> <a href={resolve("/auth/signup")} class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground">
Sign up Sign up
</a> </a>
</p> </p>

View File

@@ -2,6 +2,7 @@
import { Fingerprint, KeyRound, Loader2 } from "@lucide/svelte"; import { Fingerprint, KeyRound, Loader2 } from "@lucide/svelte";
import { startAuthentication } from "@simplewebauthn/browser"; import { startAuthentication } from "@simplewebauthn/browser";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -43,7 +44,7 @@ async function authenticate(): Promise<void> {
}); });
// Success - redirect to confirm for session creation // Success - redirect to confirm for session creation
goto("/auth/confirm"); goto(resolve("/auth/confirm"));
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : "Authentication failed"; error = e instanceof Error ? e.message : "Authentication failed";
hasAttempted = true; hasAttempted = true;
@@ -55,7 +56,7 @@ async function authenticate(): Promise<void> {
// Guard: redirect to /auth/login if no active login flow // Guard: redirect to /auth/login if no active login flow
$effect(() => { $effect(() => {
if (!loginFlowState.email) { if (!loginFlowState.email) {
goto("/auth/login"); goto(resolve("/auth/login"));
} }
}); });
@@ -134,7 +135,7 @@ $effect(() => {
<!-- Fallback links --> <!-- Fallback links -->
{#if loginFlowState.hasPassword} {#if loginFlowState.hasPassword}
<Button variant="outline" class="h-10 w-full" href="/auth/login/password"> <Button variant="outline" class="h-10 w-full" href={resolve("/auth/login/password")}>
Use password instead Use password instead
</Button> </Button>
{/if} {/if}
@@ -142,7 +143,7 @@ $effect(() => {
<div class="text-center"> <div class="text-center">
<button <button
type="button" type="button"
onclick={() => goto("/auth/login")} onclick={() => goto(resolve("/auth/login"))}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
> >
Use a different email Use a different email

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert, PasswordInput } from "$lib/components/auth"; import { ErrorAlert, PasswordInput } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -14,7 +15,7 @@ let error = $state<string | null>(null);
// Guard: redirect to /auth/login if no active login flow // Guard: redirect to /auth/login if no active login flow
$effect(() => { $effect(() => {
if (!loginFlowState.email) { if (!loginFlowState.email) {
goto("/auth/login"); goto(resolve("/auth/login"));
} }
}); });
@@ -26,7 +27,7 @@ async function handleSubmit(e: SubmitEvent) {
try { try {
await api.auth.loginPassword({ password }); await api.auth.loginPassword({ password });
// On success, redirect to confirm page for email verification // On success, redirect to confirm page for email verification
goto("/auth/confirm"); goto(resolve("/auth/confirm"));
} catch (err) { } catch (err) {
error = error =
err instanceof Error err instanceof Error
@@ -38,7 +39,7 @@ async function handleSubmit(e: SubmitEvent) {
function handleDifferentEmail() { function handleDifferentEmail() {
clearLoginFlowState(); clearLoginFlowState();
goto("/auth/login"); goto(resolve("/auth/login"));
} }
</script> </script>
@@ -82,7 +83,7 @@ function handleDifferentEmail() {
<!-- Secondary Links --> <!-- Secondary Links -->
<div class="space-y-3 text-center"> <div class="space-y-3 text-center">
<a <a
href="/auth/forgot-password" href={resolve("/auth/forgot-password")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
> >
Forgot password? Forgot password?
@@ -91,7 +92,7 @@ function handleDifferentEmail() {
{#if loginFlowState.hasPasskey} {#if loginFlowState.hasPasskey}
<div> <div>
<a <a
href="/auth/login/passkey" href={resolve("/auth/login/passkey")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
> >
Use passkey instead Use passkey instead

View File

@@ -3,6 +3,7 @@ import { AlertCircle } from "@lucide/svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { import {
@@ -56,7 +57,7 @@ async function handleSubmit(e: Event) {
toast.success("Password reset successfully", { toast.success("Password reset successfully", {
description: "You can now sign in with your new password.", description: "You can now sign in with your new password.",
}); });
await goto("/auth/login"); await goto(resolve("/auth/login"));
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
// Handle specific error cases // Handle specific error cases
@@ -97,7 +98,7 @@ async function handleSubmit(e: Event) {
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button variant="outline" class="h-10 w-full" onclick={() => goto("/auth/forgot-password")}> <Button variant="outline" class="h-10 w-full" onclick={() => goto(resolve("/auth/forgot-password"))}>
Request new reset link Request new reset link
</Button> </Button>
{:else} {:else}
@@ -147,8 +148,8 @@ async function handleSubmit(e: Event) {
<!-- Back to login link --> <!-- Back to login link -->
<div class="text-center text-sm text-muted-foreground"> <div class="text-center text-sm text-muted-foreground">
Remember your password?{" "} Remember your password?
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary"> <a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
Sign in Sign in
</a> </a>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { AlertCircle, Loader2 } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
@@ -22,7 +23,7 @@ const userQuery = createQuery(() => ({
// Redirect if user doesn't need setup // Redirect if user doesn't need setup
$effect(() => { $effect(() => {
if (userQuery.data && !userQuery.data.needsSetup) { if (userQuery.data && !userQuery.data.needsSetup) {
goto("/"); goto(resolve("/"));
} }
}); });
@@ -68,7 +69,7 @@ async function handleSubmit(e: Event) {
}); });
toast.success("Profile setup complete!"); toast.success("Profile setup complete!");
goto("/"); goto(resolve("/"));
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : "Failed to save profile"; error = e instanceof Error ? e.message : "Failed to save profile";
} finally { } finally {

View File

@@ -5,6 +5,7 @@ import {
} from "@simplewebauthn/browser"; } from "@simplewebauthn/browser";
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { import {
ErrorAlert, ErrorAlert,
@@ -75,7 +76,7 @@ async function handlePasskeySignup() {
}); });
// Redirect to user setup // Redirect to user setup
await goto("/auth/setup/user"); await goto(resolve("/auth/setup/user"));
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
// Handle WebAuthn cancellation // Handle WebAuthn cancellation
@@ -103,7 +104,7 @@ async function handlePasswordSignup() {
}); });
// Redirect to user setup // Redirect to user setup
await goto("/auth/setup/user"); await goto(resolve("/auth/setup/user"));
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
error = err.message; error = err.message;
@@ -249,8 +250,8 @@ function switchToPasskey() {
<!-- Sign in link --> <!-- Sign in link -->
<div class="text-center text-sm text-muted-foreground"> <div class="text-center text-sm text-muted-foreground">
Already have an account?{" "} Already have an account?
<a href="/auth/login" class="text-foreground underline underline-offset-4 hover:text-primary"> <a href={resolve("/auth/login")} class="text-foreground underline underline-offset-4 hover:text-primary">
Sign in Sign in
</a> </a>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { createQuery } from "@tanstack/svelte-query";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -52,7 +53,7 @@ async function handleTrust() {
try { try {
await api.me.devices.trust({ name: deviceName.trim() }); await api.me.devices.trust({ name: deviceName.trim() });
toast.success("Device trusted successfully!"); toast.success("Device trusted successfully!");
goto("/"); goto(resolve("/"));
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : "Failed to trust device"; error = e instanceof Error ? e.message : "Failed to trust device";
} finally { } finally {
@@ -61,7 +62,7 @@ async function handleTrust() {
} }
async function handleSkip() { async function handleSkip() {
goto("/performance"); goto(resolve("/performance"));
} }
// Get device icon based on type // Get device icon based on type

View File

@@ -2,6 +2,7 @@
import { CheckCircle2, Loader2, Mail, XCircle } from "@lucide/svelte"; import { CheckCircle2, Loader2, Mail, XCircle } from "@lucide/svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
@@ -31,7 +32,7 @@ async function verifyEmail(): Promise<void> {
try { try {
await api.auth.verifyEmail({ token }); await api.auth.verifyEmail({ token });
toast.success("Email verified successfully!"); toast.success("Email verified successfully!");
goto("/"); goto(resolve("/"));
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : "Verification failed"; error = e instanceof Error ? e.message : "Verification failed";
} finally { } finally {
@@ -132,7 +133,7 @@ async function resendVerification(): Promise<void> {
<div class="text-center"> <div class="text-center">
<a <a
href="/auth/login" href={resolve("/auth/login")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
> >
Back to login Back to login

View File

@@ -8,6 +8,7 @@ import {
} from "@lucide/svelte"; } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { DashboardLayout } from "$lib/components/layout"; import { DashboardLayout } from "$lib/components/layout";
import { Badge } from "$lib/components/ui/badge"; import { Badge } from "$lib/components/ui/badge";
@@ -40,7 +41,9 @@ const invitesQuery = createQuery(() => ({
$effect(() => { $effect(() => {
if (orgsQuery.error) { if (orgsQuery.error) {
goto( goto(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`, resolve(
`/auth/login?redirect=${encodeURIComponent(window.location.pathname)}`,
),
); );
} }
}); });
@@ -98,7 +101,7 @@ function formatRole(role: string): string {
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#each invitesQuery.data as invite (invite.id)} {#each invitesQuery.data as invite (invite.id)}
<a <a
href="/account/org-invites/{invite.id}" href={resolve(`/account/org-invites/${invite.id}`)}
class="group block" class="group block"
> >
<Card class="h-full border-primary/30 bg-primary/5 transition-colors group-hover:border-primary/50"> <Card class="h-full border-primary/30 bg-primary/5 transition-colors group-hover:border-primary/50">
@@ -183,7 +186,7 @@ function formatRole(role: string): string {
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each orgsQuery.data as org (org.id)} {#each orgsQuery.data as org (org.id)}
<a <a
href="/dashboard/{org.slug}" href={resolve(`/dashboard/${org.slug}`)}
class="group block transition-transform hover:scale-[1.02]" class="group block transition-transform hover:scale-[1.02]"
> >
<Card class="h-full transition-colors group-hover:border-primary/50"> <Card class="h-full transition-colors group-hover:border-primary/50">

View File

@@ -10,6 +10,7 @@ import {
} from "@lucide/svelte"; } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { DashboardLayout } from "$lib/components/layout"; import { DashboardLayout } from "$lib/components/layout";
import { RoleBadge } from "$lib/components/org"; import { RoleBadge } from "$lib/components/org";
@@ -86,7 +87,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
: "Failed to load organization"} : "Failed to load organization"}
</p> </p>
<a <a
href="/dashboard" href={resolve("/dashboard")}
class="mt-4 text-sm text-primary underline underline-offset-4 hover:text-primary/80" class="mt-4 text-sm text-primary underline underline-offset-4 hover:text-primary/80"
> >
Back to organizations Back to organizations
@@ -117,7 +118,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
</div> </div>
</div> </div>
{#if canManageOrg} {#if canManageOrg}
<Button variant="outline" href="/dashboard/{slug}/settings"> <Button variant="outline" href={resolve(`/dashboard/${slug}/settings`)}>
<Settings class="mr-2 h-4 w-4" /> <Settings class="mr-2 h-4 w-4" />
Settings Settings
</Button> </Button>
@@ -126,7 +127,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
<!-- Stats cards --> <!-- Stats cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<a href="/dashboard/{slug}/members" class="group"> <a href={resolve(`/dashboard/${slug}/members`)} class="group">
<Card class="transition-colors group-hover:border-primary/50"> <Card class="transition-colors group-hover:border-primary/50">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Members</CardTitle> <CardTitle class="text-sm font-medium">Members</CardTitle>
@@ -163,7 +164,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<CardTitle class="text-base">Team Members</CardTitle> <CardTitle class="text-base">Team Members</CardTitle>
<a <a
href="/dashboard/{slug}/members" href={resolve(`/dashboard/${slug}/members`)}
class="flex items-center text-sm text-primary hover:underline" class="flex items-center text-sm text-primary hover:underline"
> >
View all View all

View File

@@ -300,7 +300,7 @@ const availableInviteRoles = $derived.by(() => {
{inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)} {inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{#each availableInviteRoles as role} {#each availableInviteRoles as role (role)}
<SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} /> <SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} />
{/each} {/each}
</SelectContent> </SelectContent>

View File

@@ -47,7 +47,7 @@ const metrics = [
<!-- Metric Cards --> <!-- Metric Cards -->
<section> <section>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
{#each metrics as metric} {#each metrics as metric (metric.label)}
<MetricCard <MetricCard
label={metric.label} label={metric.label}
value={metric.value} value={metric.value}

View File

@@ -11,6 +11,7 @@ import { createQuery, useQueryClient } from "@tanstack/svelte-query";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { SettingsLayout } from "$lib/components/layout"; import { SettingsLayout } from "$lib/components/layout";
import { ConfirmDialog } from "$lib/components/org"; import { ConfirmDialog } from "$lib/components/org";
@@ -124,7 +125,7 @@ function handleLeave() {
await api.orgs.leave({ slug }); await api.orgs.leave({ slug });
toast.success("You have left the organization"); toast.success("You have left the organization");
await queryClient.invalidateQueries({ queryKey: ["orgs"] }); await queryClient.invalidateQueries({ queryKey: ["orgs"] });
goto("/dashboard"); goto(resolve("/dashboard"));
} catch (e) { } catch (e) {
toast.error( toast.error(
e instanceof Error ? e.message : "Failed to leave organization", e instanceof Error ? e.message : "Failed to leave organization",
@@ -147,7 +148,7 @@ function handleDelete() {
await api.orgs.delete({ slug }); await api.orgs.delete({ slug });
toast.success("Organization deleted"); toast.success("Organization deleted");
await queryClient.invalidateQueries({ queryKey: ["orgs"] }); await queryClient.invalidateQueries({ queryKey: ["orgs"] });
goto("/dashboard"); goto(resolve("/dashboard"));
} catch (e) { } catch (e) {
toast.error( toast.error(
e instanceof Error ? e.message : "Failed to delete organization", e instanceof Error ? e.message : "Failed to delete organization",

View File

@@ -300,7 +300,7 @@ const availableInviteRoles = $derived.by(() => {
{inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)} {inviteRole.charAt(0).toUpperCase() + inviteRole.slice(1)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{#each availableInviteRoles as role} {#each availableInviteRoles as role (role)}
<SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} /> <SelectItem value={role} label={role.charAt(0).toUpperCase() + role.slice(1)} />
{/each} {/each}
</SelectContent> </SelectContent>

View File

@@ -2,6 +2,7 @@
import { CheckCircle2, Loader2, UserPlus, XCircle } from "@lucide/svelte"; import { CheckCircle2, Loader2, UserPlus, XCircle } from "@lucide/svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
@@ -45,7 +46,7 @@ async function acceptInvite(): Promise<void> {
if (!isAuthenticated) { if (!isAuthenticated) {
// Redirect to login with return URL // Redirect to login with return URL
const returnUrl = `/invite/accept?token=${encodeURIComponent(token)}`; const returnUrl = `/invite/accept?token=${encodeURIComponent(token)}`;
goto(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`); goto(resolve(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`));
return; return;
} }
@@ -55,7 +56,7 @@ async function acceptInvite(): Promise<void> {
toast.success("You've joined the organization!"); toast.success("You've joined the organization!");
// Redirect to dashboard after a short delay // Redirect to dashboard after a short delay
setTimeout(() => { setTimeout(() => {
goto("/dashboard"); goto(resolve("/dashboard"));
}, 1500); }, 1500);
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
@@ -168,13 +169,13 @@ $effect(() => {
</Button> </Button>
{/if} {/if}
<Button variant="outline" class="h-10 w-full" href="/dashboard"> <Button variant="outline" class="h-10 w-full" href={resolve("/dashboard")}>
Go to Dashboard Go to Dashboard
</Button> </Button>
<div class="text-center"> <div class="text-center">
<a <a
href="/auth/login" href={resolve("/auth/login")}
class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground" class="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
> >
Sign in with a different account Sign in with a different account

View File

@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
// Redirect old /login route to new /auth/login // Redirect old /login route to new /auth/login
$effect(() => { $effect(() => {
goto("/auth/login", { replaceState: true }); goto(resolve("/auth/login"), { replaceState: true });
}); });
</script> </script>

View File

@@ -101,9 +101,12 @@
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/zxcvbn": "^4.4.5", "@types/zxcvbn": "^4.4.5",
"@typescript-eslint/parser": "^8.52.0",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-plugin-svelte": "^3.14.0",
"svelte": "^5.28.2", "svelte": "^5.28.2",
"svelte-check": "^4.2.1", "svelte-check": "^4.2.1",
"svelte-eslint-parser": "^1.4.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "catalog:", "typescript": "catalog:",
@@ -619,6 +622,8 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -671,6 +676,8 @@
"eslint-config-turbo": ["eslint-config-turbo@2.7.3", "", { "dependencies": { "eslint-plugin-turbo": "2.7.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-1ik3XQLJoE9d9ljhw60wTQf7rlwnz8tc6vnhSL7/Ciep2+qPMJpNg+mapcmGhirfDSceVNI8r9pv+HyvrBXhpQ=="], "eslint-config-turbo": ["eslint-config-turbo@2.7.3", "", { "dependencies": { "eslint-plugin-turbo": "2.7.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-1ik3XQLJoE9d9ljhw60wTQf7rlwnz8tc6vnhSL7/Ciep2+qPMJpNg+mapcmGhirfDSceVNI8r9pv+HyvrBXhpQ=="],
"eslint-plugin-svelte": ["eslint-plugin-svelte@3.14.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g=="],
"eslint-plugin-turbo": ["eslint-plugin-turbo@2.7.3", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-q7kYzJCyvceSLVwHgmn3ZBhqpUihQHxC7LEddq5a1eLe5P+/Ob4TnJrdocP38qO1n9MCuO+cJSUTGUtZb1X3bQ=="], "eslint-plugin-turbo": ["eslint-plugin-turbo@2.7.3", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-q7kYzJCyvceSLVwHgmn3ZBhqpUihQHxC7LEddq5a1eLe5P+/Ob4TnJrdocP38qO1n9MCuO+cJSUTGUtZb1X3bQ=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
@@ -799,6 +806,8 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
"kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="], "kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="],
"kysely-codegen": ["kysely-codegen@0.19.0", "", { "dependencies": { "chalk": "4.1.2", "cosmiconfig": "^9.0.0", "dotenv": "^17.2.1", "dotenv-expand": "^12.0.2", "git-diff": "^2.0.6", "micromatch": "^4.0.8", "minimist": "^1.2.8", "pluralize": "^8.0.0", "zod": "^4.1.5" }, "peerDependencies": { "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", "@tediousjs/connection-string": ">=0.5.0 <0.6.0", "better-sqlite3": ">=7.6.2 <13.0.0", "kysely": ">=0.27.0 <1.0.0", "kysely-bun-sqlite": ">=0.3.2 <1.0.0", "kysely-bun-worker": ">=1.2.0 <2.0.0", "mysql2": ">=2.3.3 <4.0.0", "pg": ">=8.8.0 <9.0.0", "tarn": ">=3.0.0 <4.0.0", "tedious": ">=18.0.0 <20.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-sqlite", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-ZpdQQnpfY0kh45CA6yPA9vdFsBE+b06Fx7QVcbL5rX//yjbA0yYGZGhnH7GTd4P4BY/HIv5uAfuOD83JVZf95w=="], "kysely-codegen": ["kysely-codegen@0.19.0", "", { "dependencies": { "chalk": "4.1.2", "cosmiconfig": "^9.0.0", "dotenv": "^17.2.1", "dotenv-expand": "^12.0.2", "git-diff": "^2.0.6", "micromatch": "^4.0.8", "minimist": "^1.2.8", "pluralize": "^8.0.0", "zod": "^4.1.5" }, "peerDependencies": { "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", "@tediousjs/connection-string": ">=0.5.0 <0.6.0", "better-sqlite3": ">=7.6.2 <13.0.0", "kysely": ">=0.27.0 <1.0.0", "kysely-bun-sqlite": ">=0.3.2 <1.0.0", "kysely-bun-worker": ">=1.2.0 <2.0.0", "mysql2": ">=2.3.3 <4.0.0", "pg": ">=8.8.0 <9.0.0", "tarn": ">=3.0.0 <4.0.0", "tedious": ">=18.0.0 <20.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-sqlite", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-ZpdQQnpfY0kh45CA6yPA9vdFsBE+b06Fx7QVcbL5rX//yjbA0yYGZGhnH7GTd4P4BY/HIv5uAfuOD83JVZf95w=="],
@@ -831,6 +840,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
@@ -929,6 +940,14 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
"postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
@@ -1011,6 +1030,8 @@
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
"svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="],
"svelte-sonner": ["svelte-sonner@1.0.7", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A=="], "svelte-sonner": ["svelte-sonner@1.0.7", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A=="],
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
@@ -1075,6 +1096,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
@@ -1087,6 +1110,8 @@
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],

View File

@@ -5,6 +5,7 @@
nixfmt-rfc-style nixfmt-rfc-style
biome biome
git git
tea
dbmate dbmate
ast-grep ast-grep
dbip-city-lite dbip-city-lite