feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and apply functionalities. Update go.mod to include necessary dependencies and complete todos in architecture plan for improved observability and deployment practices.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { buttonVariants } from '$lib/components/ui/button/button.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import Activity from '@lucide/svelte/icons/activity';
|
||||
import Boxes from '@lucide/svelte/icons/boxes';
|
||||
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
|
||||
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
|
||||
import Network from '@lucide/svelte/icons/network';
|
||||
import Settings from '@lucide/svelte/icons/settings';
|
||||
import Gauge from '@lucide/svelte/icons/gauge';
|
||||
|
||||
const nav = [
|
||||
{ href: '/', label: 'Обзор', icon: LayoutDashboard },
|
||||
{ href: '/modules', label: 'Модули', icon: Boxes },
|
||||
{ href: '/revisions', label: 'Ревизии', icon: Activity },
|
||||
{ href: '/peers', label: 'Пиры и спикеры', icon: Network },
|
||||
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
|
||||
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge },
|
||||
{ href: '/settings', label: 'Настройки', icon: Settings }
|
||||
] as const;
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="bg-background flex min-h-screen">
|
||||
<aside
|
||||
class="border-border bg-card/30 flex w-56 shrink-0 flex-col border-r"
|
||||
aria-label="Основная навигация"
|
||||
>
|
||||
<div class="p-4">
|
||||
<a href={resolve('/')} class="text-foreground font-semibold tracking-tight">EvoBGP</a>
|
||||
<p class="text-muted-foreground mt-0.5 text-xs">Control plane</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<nav class="flex flex-1 flex-col gap-0.5 p-2">
|
||||
{#each nav as item (item.href)}
|
||||
{@const Icon = item.icon}
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: page.url.pathname === item.href ? 'secondary' : 'ghost',
|
||||
size: 'sm'
|
||||
}),
|
||||
'w-full justify-start gap-2 no-underline'
|
||||
)}
|
||||
>
|
||||
<Icon class="size-4" />
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="min-w-0 flex-1 p-6">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const TOKEN_STORAGE_KEY = 'evobgp_api_token';
|
||||
|
||||
export type Problem = {
|
||||
title?: string;
|
||||
status?: number;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
function mergeHeaders(init?: RequestInit): Headers {
|
||||
const h = new Headers(init?.headers);
|
||||
if (!h.has('Accept')) {
|
||||
h.set('Accept', 'application/json');
|
||||
}
|
||||
if (browser) {
|
||||
const t = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (t && !h.has('Authorization')) {
|
||||
h.set('Authorization', `Bearer ${t}`);
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export async function apiFetch(path: string, init?: RequestInit): Promise<Response> {
|
||||
if (!browser) {
|
||||
throw new Error('API is only available in the browser');
|
||||
}
|
||||
return fetch(path, { ...init, headers: mergeHeaders(init) });
|
||||
}
|
||||
|
||||
export async function apiJSON<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await apiFetch(path, init);
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let detail = text;
|
||||
try {
|
||||
const j = JSON.parse(text) as Problem;
|
||||
if (j?.detail) detail = j.detail;
|
||||
} catch {
|
||||
/* plain text */
|
||||
}
|
||||
throw new Error(detail || `HTTP ${res.status}`);
|
||||
}
|
||||
if (!text) return undefined as T;
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
export type ModuleRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
refresh_interval_sec: number;
|
||||
cron_expr: string;
|
||||
default_community_id: string | null;
|
||||
doh_profile_id: string | null;
|
||||
};
|
||||
|
||||
export type ModulesResponse = {
|
||||
items: ModuleRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type RevisionRow = {
|
||||
id: string;
|
||||
content_hash: string;
|
||||
created_at: string;
|
||||
parent_revision_id: string | null;
|
||||
materialized_prefix_count: number;
|
||||
module_id: string | null;
|
||||
};
|
||||
|
||||
export type RevisionsResponse = {
|
||||
items: RevisionRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type PeerRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
neighbor: string;
|
||||
session_state: string;
|
||||
bgp_speaker_id: string | null;
|
||||
};
|
||||
|
||||
export type PeersResponse = {
|
||||
items: PeerRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type SpeakerRow = {
|
||||
id: string;
|
||||
role: string;
|
||||
endpoint: string;
|
||||
last_applied_revision_id: string | null;
|
||||
};
|
||||
|
||||
export type SpeakersResponse = {
|
||||
items: SpeakerRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type JobRow = {
|
||||
job_id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
idempotency_key?: string | null;
|
||||
created_at?: string;
|
||||
started_at?: string | null;
|
||||
finished_at?: string | null;
|
||||
error?: string | null;
|
||||
meta?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type JobsResponse = {
|
||||
items: JobRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: 'focus:ring-ring inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground border-transparent shadow hover:bg-primary/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground border-transparent hover:bg-secondary/80',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground border-transparent shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLSpanElement> & {
|
||||
variant?: BadgeVariant;
|
||||
children?: import('svelte').Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<span class={cn(badgeVariants({ variant }), className)} {...rest}>{@render children?.()}</span>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Root, { badgeVariants, type BadgeVariant } from './badge.svelte';
|
||||
export { Root, Root as Badge, badgeVariants, type BadgeVariant };
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: 'focus-visible:border-ring focus-visible:ring-ring/50 inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:hover:bg-input/50',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
type = 'button',
|
||||
children,
|
||||
...rest
|
||||
}: HTMLButtonAttributes & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
children?: import('svelte').Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<button class={cn(buttonVariants({ variant, size }), className)} {type} {...rest}
|
||||
>{@render children?.()}</button
|
||||
>
|
||||
@@ -0,0 +1,8 @@
|
||||
import Root, { buttonVariants, type ButtonSize, type ButtonVariant } from './button.svelte';
|
||||
export {
|
||||
Root,
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonSize,
|
||||
type ButtonVariant
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn('p-6 pt-0', className)} {...rest}>{@render children?.()}</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLParagraphElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<p class={cn('text-muted-foreground text-sm', className)} {...rest}>{@render children?.()}</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col gap-1.5 p-6', className)} {...rest}>{@render children?.()}</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLHeadingElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class={cn('leading-none font-semibold tracking-tight', className)} {...rest}>
|
||||
{@render children?.()}
|
||||
</h3>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('bg-card text-card-foreground rounded-xl border shadow-sm', className)}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
import Root from './card.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as Card,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Description as CardDescription,
|
||||
Content as CardContent
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
import Root from './input.svelte';
|
||||
export { Root, Root as Input };
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
value = $bindable(),
|
||||
...rest
|
||||
}: HTMLInputAttributes = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...rest}
|
||||
/>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Root from './label.svelte';
|
||||
export { Root, Root as Label };
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLLabelAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLLabelAttributes & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={cn('text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)}
|
||||
{...rest}>{@render children?.()}</label
|
||||
>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Root from './scroll-area.svelte';
|
||||
export { Root, Root as ScrollArea };
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLDivElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn('relative overflow-auto', className)} {...rest}>{@render children?.()}</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Root from './separator.svelte';
|
||||
export { Root, Root as Separator };
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
orientation = 'horizontal',
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLDivElement> & { orientation?: 'horizontal' | 'vertical' } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="separator"
|
||||
class={cn(
|
||||
'bg-border shrink-0',
|
||||
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
></div>
|
||||
@@ -0,0 +1,16 @@
|
||||
import Root from './table.svelte';
|
||||
import Header from './table-header.svelte';
|
||||
import Body from './table-body.svelte';
|
||||
import Row from './table-row.svelte';
|
||||
import Head from './table-head.svelte';
|
||||
import Cell from './table-cell.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as Table,
|
||||
Header as TableHeader,
|
||||
Body as TableBody,
|
||||
Row as TableRow,
|
||||
Head as TableHead,
|
||||
Cell as TableCell
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableSectionElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<tbody class={cn('[&_tr:last-child]:border-0', className)} {...rest}>{@render children?.()}</tbody>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
colspan,
|
||||
rowspan,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableCellElement> & {
|
||||
colspan?: number;
|
||||
rowspan?: number;
|
||||
children?: import('svelte').Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<td
|
||||
class={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
|
||||
{colspan}
|
||||
{rowspan}
|
||||
{...rest}>{@render children?.()}</td
|
||||
>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableCellElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<th
|
||||
class={cn(
|
||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...rest}>{@render children?.()}</th
|
||||
>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableSectionElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<thead class={cn('[&_tr]:border-b', className)} {...rest}>{@render children?.()}</thead>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableRowElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<tr
|
||||
class={cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', className)}
|
||||
{...rest}>{@render children?.()}</tr
|
||||
>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: HTMLAttributes<HTMLTableElement> & { children?: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative w-full overflow-auto">
|
||||
<table class={cn('w-full caption-bottom text-sm', className)} {...rest}>{@render children?.()}</table>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
Reference in New Issue
Block a user