feat(ui,api,admin): sveltekit admin spa, usage by day/agent/conversation/model, rate limits, memory tree, turn snapshot
This commit is contained in:
+7
-2
@@ -8,13 +8,14 @@
|
||||
"@biomejs/biome": "2.5.9",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@internationalized/date": "^3.12.3",
|
||||
"@lucide/svelte": "^1.34.0",
|
||||
"@lucide/svelte": "^1.35.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/node": "^26.4.0",
|
||||
"bits-ui": "^2.19.0",
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
@@ -83,7 +84,7 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@1.34.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-sHFL8KVSaPXv0dsmdUTq4q/tj8clT2XYejnoSg0mHObpXWn/9SfET4f14v/7VOba1IsbPIW9+sWaq+Qq1wTdNA=="],
|
||||
"@lucide/svelte": ["@lucide/svelte@1.35.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-1I5WeiEFc21HOLv/GR11vuN3MqN/k/mhCEMW7nEtwSAGOcZ0C5ZgI1Y2Rvs/egfKTUiOTw+R9Ys9BJ+GwAKSWw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
@@ -183,6 +184,8 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
@@ -449,6 +452,8 @@
|
||||
|
||||
"ultracite": ["ultracite@7.10.7", "", { "dependencies": { "@clack/prompts": "^1.5.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.0.1", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.4", "nypm": "^0.6.9", "resolve.exports": "^2.0.3", "string-width": "^8.2.2", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.79.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-rc+TG/zLCV+Uk1PL4XPmsq7CW8GtlaV4g7Q53Bt5o7UNGSKKNRoPY4syz1BDWwSoNBcvWKpSnfSVflsecok4EA=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
+2
-1
@@ -17,13 +17,14 @@
|
||||
"@biomejs/biome": "2.5.9",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@internationalized/date": "^3.12.3",
|
||||
"@lucide/svelte": "^1.34.0",
|
||||
"@lucide/svelte": "^1.35.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/node": "^26.4.0",
|
||||
"bits-ui": "^2.19.0",
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
|
||||
+19
-1
@@ -2,11 +2,29 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<meta content="width=device-width, initial-scale=1, viewport-fit=cover" name="viewport" />
|
||||
<meta content="light dark" name="color-scheme" />
|
||||
<meta content="scale" name="text-scale" />
|
||||
<title>Beaver</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<!--
|
||||
impeccable direction contract (seed 20df1617, mode operate, brief-pinned world)
|
||||
THESIS: activity first. The console opens on what the agents are doing right now, as a live
|
||||
ledger of turns and tool calls, and refuses the hero-metric dashboard grid of cards.
|
||||
OWN-WORLD: msos mauve/pink tokens on Inter; two neutral layers (cooler sidebar, content surface);
|
||||
one accent for selection and the live state; kind hues from the msos status palette; hairline
|
||||
borders, tabular numerals, rows and rails instead of cards; no cards inside cards.
|
||||
STORY: the operator lands, sees what runs and how much quota is left, opens a thread, watches
|
||||
tools and subagents stream, answers a question, checks spend, manages tokens and memory.
|
||||
FIRST VIEWPORT: sidebar left with the gateway connection dot; main opens with the "now" ledger
|
||||
(running turns as live rows), then quota bars and the 5 h / 7 d spend row, then live sessions.
|
||||
Primary action: open a running conversation.
|
||||
FORM: dense operator console, brief-pinned (roll assigned index 7, superseded by the pin).
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review,
|
||||
the verdict, and DESIGN.md
|
||||
-->
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import { readSse } from "./sse";
|
||||
import type {
|
||||
AgentsResponse,
|
||||
AuditPage,
|
||||
BusEvent,
|
||||
ConversationInfo,
|
||||
ConversationSummary,
|
||||
EntriesPage,
|
||||
HistoryMessage,
|
||||
LimitsResponse,
|
||||
MemoryFile,
|
||||
MemoryTree,
|
||||
Schedule,
|
||||
SessionsResponse,
|
||||
TokenRow,
|
||||
UsageGroup,
|
||||
UsageResponse,
|
||||
} from "./types";
|
||||
|
||||
const TRAILING_SLASHES = /\/+$/;
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
body: unknown;
|
||||
|
||||
constructor(status: number, message: string, body: unknown) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
function messageOf(status: number, body: unknown): string {
|
||||
if (body && typeof body === "object") {
|
||||
const record = body as Record<string, unknown>;
|
||||
for (const key of ["error", "detail"]) {
|
||||
if (typeof record[key] === "string") {
|
||||
return record[key] as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof body === "string" && body) {
|
||||
return body;
|
||||
}
|
||||
return `HTTP ${status}`;
|
||||
}
|
||||
|
||||
async function bodyOf(response: Response): Promise<unknown> {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export type Params = Record<string, string | number | boolean | undefined>;
|
||||
|
||||
function query(params?: Params): string {
|
||||
if (!params) {
|
||||
return "";
|
||||
}
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== "") {
|
||||
search.set(key, String(value));
|
||||
}
|
||||
}
|
||||
const text = search.toString();
|
||||
return text ? `?${text}` : "";
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
onUnauthorized?: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
// One client per signed-in session: the gateway origin, a bearer, and a
|
||||
// hook that refreshes the bearer once when the gateway rotated it.
|
||||
export class ApiClient {
|
||||
base: string;
|
||||
token: string;
|
||||
private readonly onUnauthorized?: () => Promise<string | null>;
|
||||
|
||||
constructor(base: string, token: string, options: ClientOptions = {}) {
|
||||
this.base = base.replace(TRAILING_SLASHES, "");
|
||||
this.token = token;
|
||||
this.onUnauthorized = options.onUnauthorized;
|
||||
}
|
||||
|
||||
url(path: string, params?: Params): string {
|
||||
return `${this.base}${path}${query(params)}`;
|
||||
}
|
||||
|
||||
private headers(extra: Record<string, string> = {}): Record<string, string> {
|
||||
return { Authorization: `Bearer ${this.token}`, ...extra };
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
params?: Params,
|
||||
retried = false
|
||||
): Promise<T> {
|
||||
const response = await fetch(this.url(path, params), {
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
headers: this.headers(
|
||||
body === undefined ? {} : { "Content-Type": "application/json" }
|
||||
),
|
||||
method,
|
||||
});
|
||||
if (response.status === 401 && !retried && this.onUnauthorized) {
|
||||
const token = await this.onUnauthorized();
|
||||
if (token) {
|
||||
this.token = token;
|
||||
return this.request<T>(method, path, body, params, true);
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payload = await bodyOf(response);
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
messageOf(response.status, payload),
|
||||
payload
|
||||
);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
get<T>(path: string, params?: Params): Promise<T> {
|
||||
return this.request<T>("GET", path, undefined, params);
|
||||
}
|
||||
|
||||
post<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>("POST", path, body ?? {});
|
||||
}
|
||||
|
||||
patch<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>("PATCH", path, body ?? {});
|
||||
}
|
||||
|
||||
async stream(
|
||||
path: string,
|
||||
onEvent: (event: BusEvent) => void,
|
||||
signal: AbortSignal
|
||||
): Promise<void> {
|
||||
const response = await fetch(this.url(path), {
|
||||
headers: this.headers({ Accept: "text/event-stream" }),
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await bodyOf(response);
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
messageOf(response.status, payload),
|
||||
payload
|
||||
);
|
||||
}
|
||||
await readSse(response, onEvent, signal);
|
||||
}
|
||||
|
||||
agents(): Promise<AgentsResponse> {
|
||||
return this.get("/api/agents");
|
||||
}
|
||||
|
||||
conversations(params?: {
|
||||
kind?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ conversations: ConversationSummary[] }> {
|
||||
return this.get("/api/conversations", params);
|
||||
}
|
||||
|
||||
conversation(id: string): Promise<ConversationInfo> {
|
||||
return this.get(`/api/conversations/${id}`);
|
||||
}
|
||||
|
||||
history(id: string): Promise<{ id: string; messages: HistoryMessage[] }> {
|
||||
return this.get(`/api/conversations/${id}/history`);
|
||||
}
|
||||
|
||||
entries(
|
||||
id: string,
|
||||
params?: { subpath?: string; offset?: number; limit?: number }
|
||||
): Promise<EntriesPage> {
|
||||
return this.get(`/api/conversations/${id}/entries`, params);
|
||||
}
|
||||
|
||||
postMessage(
|
||||
id: string,
|
||||
text: string,
|
||||
origin = "panel"
|
||||
): Promise<{ id: string; item: number; status: string }> {
|
||||
return this.post(`/api/conversations/${id}/messages`, { origin, text });
|
||||
}
|
||||
|
||||
inject(
|
||||
id: string,
|
||||
text: string,
|
||||
urgency: "normal" | "urgent",
|
||||
origin = "panel"
|
||||
): Promise<{ id: string; item: number; priority: string }> {
|
||||
return this.post(`/api/conversations/${id}/inject`, {
|
||||
origin,
|
||||
text,
|
||||
urgency,
|
||||
});
|
||||
}
|
||||
|
||||
answer(
|
||||
id: string,
|
||||
questionId: string,
|
||||
answer: string
|
||||
): Promise<{ id: string; question_id: string }> {
|
||||
return this.post(`/api/conversations/${id}/answer`, {
|
||||
answer,
|
||||
question_id: questionId,
|
||||
});
|
||||
}
|
||||
|
||||
branch(
|
||||
id: string,
|
||||
body: { seed?: string; text?: string; title?: string; window?: number }
|
||||
): Promise<ConversationInfo> {
|
||||
return this.post(`/api/conversations/${id}/branch`, body);
|
||||
}
|
||||
|
||||
merge(
|
||||
id: string
|
||||
): Promise<{ id: string; status: string; fork: string; text: string }> {
|
||||
return this.post(`/api/conversations/${id}/merge`);
|
||||
}
|
||||
|
||||
bind(
|
||||
id: string,
|
||||
frontend: string,
|
||||
externalId: string,
|
||||
visible: boolean
|
||||
): Promise<ConversationInfo> {
|
||||
return this.post(`/api/conversations/${id}/bind`, {
|
||||
external_id: externalId,
|
||||
frontend,
|
||||
visible,
|
||||
});
|
||||
}
|
||||
|
||||
setFlags(
|
||||
id: string,
|
||||
flags: Record<string, unknown>
|
||||
): Promise<ConversationSummary> {
|
||||
return this.patch(`/api/conversations/${id}/flags`, flags);
|
||||
}
|
||||
|
||||
update(
|
||||
id: string,
|
||||
body: { status?: string; title?: string }
|
||||
): Promise<ConversationSummary> {
|
||||
return this.patch(`/api/conversations/${id}`, body);
|
||||
}
|
||||
|
||||
spawn(body: {
|
||||
kind: string;
|
||||
agent?: string;
|
||||
seed?: string;
|
||||
text?: string;
|
||||
title?: string;
|
||||
}): Promise<ConversationInfo> {
|
||||
return this.post("/api/conversations", body);
|
||||
}
|
||||
|
||||
sessions(): Promise<SessionsResponse> {
|
||||
return this.get("/api/sessions");
|
||||
}
|
||||
|
||||
schedules(): Promise<{ schedules: Schedule[] }> {
|
||||
return this.get("/api/schedules");
|
||||
}
|
||||
|
||||
usage(params: {
|
||||
since?: string;
|
||||
until?: string;
|
||||
hours?: number;
|
||||
group_by: UsageGroup;
|
||||
}): Promise<UsageResponse> {
|
||||
return this.get("/api/usage", params);
|
||||
}
|
||||
|
||||
limits(): Promise<LimitsResponse> {
|
||||
return this.get("/api/limits");
|
||||
}
|
||||
|
||||
memory(): Promise<MemoryTree> {
|
||||
return this.get("/api/memory");
|
||||
}
|
||||
|
||||
memoryFile(path: string): Promise<MemoryFile> {
|
||||
return this.get("/api/memory/file", { path });
|
||||
}
|
||||
|
||||
tokens(includeRevoked = false): Promise<{ tokens: TokenRow[] }> {
|
||||
return this.get("/api/tokens", {
|
||||
include_revoked: includeRevoked ? 1 : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
createToken(
|
||||
name: string,
|
||||
scope: string
|
||||
): Promise<{ token: TokenRow; plaintext: string }> {
|
||||
return this.post("/api/tokens", { name, scope });
|
||||
}
|
||||
|
||||
revokeToken(id: number): Promise<{ id: number; revoked: boolean }> {
|
||||
return this.post(`/api/tokens/${id}/revoke`);
|
||||
}
|
||||
|
||||
audit(params?: { before?: number; limit?: number }): Promise<AuditPage> {
|
||||
return this.get("/api/audit", params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { type ApiClient, ApiError } from "./client";
|
||||
import type { BusEvent } from "./types";
|
||||
|
||||
export type LiveState = "off" | "connecting" | "open" | "retrying" | "failed";
|
||||
|
||||
const BACKOFF_MIN_MS = 1000;
|
||||
const BACKOFF_MAX_MS = 30_000;
|
||||
const JITTER_MS = 250;
|
||||
const FATAL = new Set([401, 403, 404]);
|
||||
|
||||
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (signal.aborted) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const done = () => {
|
||||
clearTimeout(timer);
|
||||
signal.removeEventListener("abort", done);
|
||||
resolve();
|
||||
};
|
||||
const timer = setTimeout(done, ms);
|
||||
signal.addEventListener("abort", done, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function describe(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export interface LiveOptions {
|
||||
onEvent: (event: BusEvent) => void;
|
||||
// Runs before every (re)connect; the snapshot that seeds state, so
|
||||
// events that follow land on the truth and not on a stale tree.
|
||||
prepare?: () => Promise<void>;
|
||||
}
|
||||
|
||||
// SSE subscription with reconnect and backoff. ``state`` is reactive so a
|
||||
// header can show "open" / "retrying …" without polling.
|
||||
export class LiveStream {
|
||||
state = $state<LiveState>("off");
|
||||
detail = $state<string | null>(null);
|
||||
private controller: AbortController | null = null;
|
||||
private readonly client: () => ApiClient | null;
|
||||
private readonly path: string;
|
||||
private readonly options: LiveOptions;
|
||||
|
||||
constructor(
|
||||
client: () => ApiClient | null,
|
||||
path: string,
|
||||
options: LiveOptions
|
||||
) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get running(): boolean {
|
||||
return this.controller !== null;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.controller) {
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
this.controller = controller;
|
||||
this.loop(controller.signal).catch((error: unknown) => {
|
||||
this.state = "failed";
|
||||
this.detail = describe(error);
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.controller?.abort();
|
||||
this.controller = null;
|
||||
this.state = "off";
|
||||
}
|
||||
|
||||
private async loop(signal: AbortSignal): Promise<void> {
|
||||
let delay = BACKOFF_MIN_MS;
|
||||
while (!signal.aborted) {
|
||||
this.state = delay === BACKOFF_MIN_MS ? "connecting" : "retrying";
|
||||
// biome-ignore lint/performance/noAwaitInLoops: one connection at a time, by design
|
||||
const outcome = await this.connectOnce(signal, () => {
|
||||
delay = BACKOFF_MIN_MS;
|
||||
});
|
||||
if (signal.aborted || outcome === "fatal") {
|
||||
return;
|
||||
}
|
||||
await sleep(delay + Math.random() * JITTER_MS, signal);
|
||||
delay = Math.min(delay * 2, BACKOFF_MAX_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private async connectOnce(
|
||||
signal: AbortSignal,
|
||||
onOpen: () => void
|
||||
): Promise<"retry" | "fatal"> {
|
||||
try {
|
||||
const client = this.client();
|
||||
if (!client) {
|
||||
throw new ApiError(401, "not signed in", null);
|
||||
}
|
||||
await this.options.prepare?.();
|
||||
if (signal.aborted) {
|
||||
return "fatal";
|
||||
}
|
||||
await client.stream(
|
||||
this.path,
|
||||
(event) => {
|
||||
if (event.type === "hello") {
|
||||
onOpen();
|
||||
this.state = "open";
|
||||
this.detail = null;
|
||||
return;
|
||||
}
|
||||
this.options.onEvent(event);
|
||||
},
|
||||
signal
|
||||
);
|
||||
this.state = "retrying";
|
||||
this.detail = "stream closed";
|
||||
return "retry";
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
return "fatal";
|
||||
}
|
||||
if (error instanceof ApiError && FATAL.has(error.status)) {
|
||||
this.state = "failed";
|
||||
this.detail = `${error.status}: ${error.message}`;
|
||||
this.controller = null;
|
||||
return "fatal";
|
||||
}
|
||||
this.state = "retrying";
|
||||
this.detail = describe(error);
|
||||
return "retry";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { BusEvent } from "./types";
|
||||
|
||||
const FRAME_BREAK = /\r?\n\r?\n/;
|
||||
const LINE_BREAK = /\r?\n/;
|
||||
|
||||
function parseFrame(frame: string): BusEvent | null {
|
||||
const data: string[] = [];
|
||||
for (const line of frame.split(LINE_BREAK)) {
|
||||
if (line.startsWith("data:")) {
|
||||
data.push(line.slice(5).trimStart());
|
||||
}
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(data.join("\n"));
|
||||
return parsed && typeof parsed === "object" ? (parsed as BusEvent) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function drain(buffer: string, onEvent: (event: BusEvent) => void): string {
|
||||
let rest = buffer;
|
||||
let index = rest.search(FRAME_BREAK);
|
||||
while (index >= 0) {
|
||||
const event = parseFrame(rest.slice(0, index));
|
||||
rest = rest.slice(index).replace(FRAME_BREAK, "");
|
||||
if (event) {
|
||||
onEvent(event);
|
||||
}
|
||||
index = rest.search(FRAME_BREAK);
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
|
||||
export async function readSse(
|
||||
response: Response,
|
||||
onEvent: (event: BusEvent) => void,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> {
|
||||
const { body } = response;
|
||||
if (!body) {
|
||||
throw new Error("SSE response has no body");
|
||||
}
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const abort = () => {
|
||||
reader.cancel().catch(() => undefined);
|
||||
};
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
try {
|
||||
for (;;) {
|
||||
// biome-ignore lint/performance/noAwaitInLoops: a stream is read chunk by chunk
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
buffer = drain(buffer + decoder.decode(value, { stream: true }), onEvent);
|
||||
}
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", abort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
export type Kind = "master" | "branch" | "deep" | "job" | "fork";
|
||||
export type ConversationStatus = "open" | "merged" | "closed" | "archived";
|
||||
|
||||
export interface Binding {
|
||||
external_id: string;
|
||||
frontend: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface ToolSnapshot {
|
||||
content: string | null;
|
||||
ended_at: string | null;
|
||||
input: unknown;
|
||||
is_error: boolean | null;
|
||||
name: string;
|
||||
parent_tool_use_id: string | null;
|
||||
started_at: string;
|
||||
tool_use_id: string;
|
||||
}
|
||||
|
||||
export interface TurnSnapshot {
|
||||
id: string;
|
||||
origin: string | null;
|
||||
started_at: string | null;
|
||||
text: string | null;
|
||||
tools: ToolSnapshot[];
|
||||
}
|
||||
|
||||
export interface QuestionOption {
|
||||
description?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
header?: string;
|
||||
multiSelect?: boolean;
|
||||
options?: QuestionOption[];
|
||||
question: string;
|
||||
}
|
||||
|
||||
export interface PendingQuestion {
|
||||
id: string;
|
||||
questions: Question[];
|
||||
}
|
||||
|
||||
export interface QueueItem {
|
||||
created_at: string;
|
||||
id: number;
|
||||
origin: string;
|
||||
priority: "urgent" | "user" | "normal";
|
||||
status: "queued" | "running" | "done" | "failed" | "interrupted";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ConversationSummary {
|
||||
agent: string;
|
||||
created_at: string | null;
|
||||
flags: Record<string, unknown>;
|
||||
id: string;
|
||||
kind: Kind;
|
||||
last_activity_at: string | null;
|
||||
last_user_activity_at: string | null;
|
||||
origin: string;
|
||||
parent_row: number | null;
|
||||
pending_question: boolean;
|
||||
running_turn: string | null;
|
||||
session_id: string | null;
|
||||
status: ConversationStatus;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface ConversationInfo extends ConversationSummary {
|
||||
bindings: Binding[];
|
||||
busy: boolean;
|
||||
live: boolean;
|
||||
parent: string | null;
|
||||
question: PendingQuestion | null;
|
||||
queue: QueueItem[];
|
||||
turn: TurnSnapshot | null;
|
||||
}
|
||||
|
||||
export interface BusEvent {
|
||||
conversation_id?: string;
|
||||
seq: number;
|
||||
ts: string;
|
||||
turn_id?: string;
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ContentBlock {
|
||||
content?: unknown;
|
||||
id?: string;
|
||||
input?: unknown;
|
||||
is_error?: boolean;
|
||||
name?: string;
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
tool_use_id?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface HistoryMessage {
|
||||
content: string | ContentBlock[];
|
||||
role: "user" | "assistant";
|
||||
}
|
||||
|
||||
export interface EntriesPage {
|
||||
entries: Record<string, unknown>[];
|
||||
id: string;
|
||||
offset: number;
|
||||
subpath: string;
|
||||
subpaths: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AgentInfo {
|
||||
effort: string | null;
|
||||
kinds: Kind[];
|
||||
model: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface FrontendInfo {
|
||||
default_agents: Record<string, string>;
|
||||
kinds: Kind[];
|
||||
name: string;
|
||||
port: number | null;
|
||||
public_base_url: string | null;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AgentsResponse {
|
||||
agents: AgentInfo[];
|
||||
frontends: FrontendInfo[];
|
||||
mcps: { name: string; kind: string }[];
|
||||
}
|
||||
|
||||
export interface SessionRow {
|
||||
age_seconds: number;
|
||||
agent: string;
|
||||
busy: boolean;
|
||||
dirty: boolean;
|
||||
idle_seconds: number;
|
||||
key: string;
|
||||
kind: string;
|
||||
pending_question: boolean;
|
||||
pid: number | null;
|
||||
pinned: boolean;
|
||||
rss: number | null;
|
||||
running_turn: string | null;
|
||||
session_id: string | null;
|
||||
turns: number;
|
||||
}
|
||||
|
||||
export interface SessionsResponse {
|
||||
rss: number | null;
|
||||
rss_limit: number | null;
|
||||
sessions: SessionRow[];
|
||||
}
|
||||
|
||||
export interface UsageTotals {
|
||||
cache_creation: number;
|
||||
cache_read: number;
|
||||
cost_usd: number;
|
||||
input: number;
|
||||
output: number;
|
||||
turns: number;
|
||||
web_searches: number;
|
||||
}
|
||||
|
||||
export type UsageGroup = "day" | "agent" | "conversation" | "model";
|
||||
|
||||
export interface UsageRow extends UsageTotals {
|
||||
agent?: string;
|
||||
conversation?: string;
|
||||
day?: string;
|
||||
key: string;
|
||||
kind?: Kind;
|
||||
model?: string;
|
||||
status?: ConversationStatus;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
export interface UsageResponse {
|
||||
group_by: UsageGroup;
|
||||
rows: UsageRow[];
|
||||
since: string;
|
||||
total: UsageTotals;
|
||||
until: string;
|
||||
}
|
||||
|
||||
export type LimitStatus = "allowed" | "allowed_warning" | "rejected";
|
||||
|
||||
export interface LimitRecord {
|
||||
agent: string | null;
|
||||
id: number;
|
||||
overage_resets_at: string | null;
|
||||
overage_status: LimitStatus | null;
|
||||
resets_at: string | null;
|
||||
session_id: string | null;
|
||||
status: LimitStatus;
|
||||
ts: string;
|
||||
utilization: number | null;
|
||||
window: string;
|
||||
}
|
||||
|
||||
export interface LimitWindow extends LimitRecord {
|
||||
gateway: UsageTotals & { since: string };
|
||||
}
|
||||
|
||||
export interface LimitsResponse {
|
||||
history: LimitRecord[];
|
||||
windows: LimitWindow[];
|
||||
}
|
||||
|
||||
export interface MemoryNode {
|
||||
children?: MemoryNode[];
|
||||
mtime: string;
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
type: "dir" | "file";
|
||||
}
|
||||
|
||||
export interface MemoryTree {
|
||||
root: string;
|
||||
tree: MemoryNode[];
|
||||
}
|
||||
|
||||
export interface MemoryFile {
|
||||
content: string;
|
||||
mtime: string;
|
||||
path: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface TokenRow {
|
||||
created_at: string;
|
||||
id: number;
|
||||
last_used_at: string | null;
|
||||
name: string;
|
||||
revoked_at: string | null;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface AuditRecord {
|
||||
actor: string;
|
||||
agent: string | null;
|
||||
detail: Record<string, unknown> | string;
|
||||
id: number;
|
||||
kind: string;
|
||||
ts: string;
|
||||
}
|
||||
|
||||
export interface AuditPage {
|
||||
next_before: number | null;
|
||||
records: AuditRecord[];
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
conversation_row: number;
|
||||
created_at: string;
|
||||
delivered_at: string | null;
|
||||
execute_at: string;
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TurnUsage {
|
||||
cache_creation?: number;
|
||||
cache_read?: number;
|
||||
cost_usd?: number | null;
|
||||
duration_ms?: number | null;
|
||||
input?: number;
|
||||
output?: number;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import LogOutIcon from "@lucide/svelte/icons/log-out";
|
||||
import { goto } from "$app/navigation";
|
||||
import { base } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import LiveDot from "$lib/components/live-dot.svelte";
|
||||
import ThemeToggle from "$lib/components/theme-toggle.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { gateway } from "$lib/gateway.svelte";
|
||||
import { isActive, NAV } from "$lib/nav";
|
||||
import { session } from "$lib/session.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
const running = $derived(gateway.running.length);
|
||||
|
||||
async function signOut() {
|
||||
await session.logout();
|
||||
await goto(`${base}/login`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="hidden w-52 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground sm:flex"
|
||||
>
|
||||
<a
|
||||
class="flex h-12 items-center gap-2 border-b px-4 font-semibold tracking-tight"
|
||||
href="{base}/"
|
||||
>
|
||||
<span class="size-2.5 rounded-sm bg-primary"></span>
|
||||
Beaver
|
||||
</a>
|
||||
<nav aria-label="Sections" class="flex flex-1 flex-col gap-0.5 p-2">
|
||||
{#each NAV as item (item.href)}
|
||||
{@const active = isActive(page.url.pathname, base, item.href)}
|
||||
<a
|
||||
aria-current={active ? "page" : undefined}
|
||||
class={cn(
|
||||
"flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm transition-colors",
|
||||
active
|
||||
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
|
||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
|
||||
)}
|
||||
href="{base}{item.href}"
|
||||
>
|
||||
<item.icon class="size-4 shrink-0 text-icon" />
|
||||
<span class="flex-1">{item.label}</span>
|
||||
{#if item.href === "/" && running > 0}
|
||||
<span
|
||||
class="tabular rounded-full bg-signal/12 px-1.5 font-medium text-signal text-xs"
|
||||
>
|
||||
{running}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="flex flex-col gap-2 border-t p-3">
|
||||
<LiveDot detail={gateway.live.detail} state={gateway.live.state} />
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="truncate text-muted-foreground text-xs">{session.user}</span>
|
||||
<div class="flex items-center">
|
||||
<ThemeToggle />
|
||||
<Button
|
||||
aria-label="Sign out"
|
||||
onclick={signOut}
|
||||
size="icon-sm"
|
||||
title="Sign out"
|
||||
variant="ghost"
|
||||
>
|
||||
<LogOutIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import MoreHorizontalIcon from "@lucide/svelte/icons/more-horizontal";
|
||||
import { goto } from "$app/navigation";
|
||||
import { base } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import LiveDot from "$lib/components/live-dot.svelte";
|
||||
import ThemeToggle from "$lib/components/theme-toggle.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Sheet from "$lib/components/ui/sheet";
|
||||
import { gateway } from "$lib/gateway.svelte";
|
||||
import { isActive, NAV } from "$lib/nav";
|
||||
import { session } from "$lib/session.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let moreOpen = $state(false);
|
||||
const primary = NAV.filter((item) => item.mobile);
|
||||
const secondary = NAV.filter((item) => !item.mobile);
|
||||
|
||||
async function signOut() {
|
||||
moreOpen = false;
|
||||
await session.logout();
|
||||
await goto(`${base}/login`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav
|
||||
aria-label="Sections"
|
||||
class="flex shrink-0 items-stretch border-t bg-sidebar pb-[env(safe-area-inset-bottom)] sm:hidden"
|
||||
>
|
||||
{#each primary as item (item.href)}
|
||||
{@const active = isActive(page.url.pathname, base, item.href)}
|
||||
<a
|
||||
aria-current={active ? "page" : undefined}
|
||||
class={cn(
|
||||
"flex h-14 flex-1 flex-col items-center justify-center gap-1 text-[11px]",
|
||||
active ? "text-link" : "text-muted-foreground"
|
||||
)}
|
||||
href="{base}{item.href}"
|
||||
>
|
||||
<item.icon class="size-5" />
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
<button
|
||||
class="flex h-14 flex-1 flex-col items-center justify-center gap-1 text-[11px] text-muted-foreground"
|
||||
onclick={() => {
|
||||
moreOpen = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<MoreHorizontalIcon class="size-5" />
|
||||
More
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<Sheet.Root bind:open={moreOpen}>
|
||||
<Sheet.Content class="flex flex-col gap-1 pt-10" side="bottom">
|
||||
<Sheet.Title class="px-2 pb-2">More</Sheet.Title>
|
||||
{#each secondary as item (item.href)}
|
||||
<a
|
||||
class="flex h-11 items-center gap-3 rounded-md px-3 text-sm hover:bg-muted"
|
||||
href="{base}{item.href}"
|
||||
onclick={() => {
|
||||
moreOpen = false;
|
||||
}}
|
||||
>
|
||||
<item.icon class="size-4 text-icon" />
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
<div class="mt-2 flex items-center justify-between border-t px-3 pt-3">
|
||||
<LiveDot detail={gateway.live.detail} state={gateway.live.state} />
|
||||
<div class="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
<Button onclick={signOut} size="sm" variant="ghost">Sign out</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let {
|
||||
title,
|
||||
hint = "",
|
||||
class: className = "",
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
hint?: string;
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col items-start gap-1 rounded-lg border border-dashed px-4 py-5 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<p class="font-medium">{title}</p>
|
||||
{#if hint}
|
||||
<p class="text-muted-foreground">{hint}</p>
|
||||
{/if}
|
||||
{#if children}
|
||||
<div class="pt-2">{@render children()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
|
||||
let {
|
||||
message,
|
||||
retry,
|
||||
}: { message: string; retry?: () => void | Promise<void> } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
<span class="text-destructive">{message}</span>
|
||||
{#if retry}
|
||||
<Button onclick={() => retry()} size="sm" variant="outline">Retry</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { Kind } from "$lib/api/types";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let { kind, class: className = "" }: { kind: Kind | string; class?: string } =
|
||||
$props();
|
||||
|
||||
const STYLE: Record<string, string> = {
|
||||
branch: "bg-kind-branch/12 text-kind-branch",
|
||||
deep: "bg-kind-deep/12 text-kind-deep",
|
||||
fork: "bg-kind-fork/12 text-kind-fork",
|
||||
job: "bg-kind-job/12 text-kind-job",
|
||||
master: "bg-kind-master/12 text-kind-master",
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"inline-flex h-5 items-center rounded-md px-1.5 font-medium text-xs leading-none",
|
||||
STYLE[kind] ?? "bg-muted text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{kind}
|
||||
</span>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { LimitWindow } from "$lib/api/types";
|
||||
import { fmtCountdown, fmtMoney, fmtTokens } from "$lib/format";
|
||||
import { limitLabel } from "$lib/limits";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let { window: w, now }: { window: LimitWindow; now: number } = $props();
|
||||
|
||||
const BAR: Record<string, string> = {
|
||||
allowed: "bg-primary",
|
||||
allowed_warning: "bg-warn",
|
||||
rejected: "bg-destructive",
|
||||
};
|
||||
const TEXT: Record<string, string> = {
|
||||
allowed: "text-foreground",
|
||||
allowed_warning: "text-warn",
|
||||
rejected: "text-destructive",
|
||||
};
|
||||
const known = $derived(w.utilization !== null && w.utilization !== undefined);
|
||||
const pct = $derived(Math.min(100, Math.round((w.utilization ?? 0) * 100)));
|
||||
const spent = $derived(
|
||||
w.gateway.input + w.gateway.output + w.gateway.cache_creation
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="ledger-grid grid-cols-[7rem_minmax(0,1fr)_3.5rem] gap-y-1 py-2">
|
||||
<span class="font-medium text-sm">{limitLabel(w.window)}</span>
|
||||
{#if known}
|
||||
<div
|
||||
aria-label="{limitLabel(w.window)} utilization"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow={pct}
|
||||
class="h-2 overflow-hidden rounded-full bg-muted"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
"h-full rounded-full transition-[width] duration-300",
|
||||
BAR[w.status]
|
||||
)}
|
||||
style="width: {pct}%"
|
||||
></div>
|
||||
</div>
|
||||
<span
|
||||
class={cn("tabular text-right font-semibold text-sm", TEXT[w.status])}
|
||||
>
|
||||
{pct}%
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground text-sm">
|
||||
utilization not reported yet
|
||||
</span>
|
||||
<span></span>
|
||||
{/if}
|
||||
<span class="tabular text-muted-foreground text-xs"
|
||||
>{fmtCountdown(w.resets_at, now)}</span
|
||||
>
|
||||
<span class="tabular col-span-2 text-muted-foreground text-xs">
|
||||
via gateway this window: {fmtTokens(spent)} tokens ·
|
||||
{fmtMoney(w.gateway.cost_usd)}
|
||||
· {w.gateway.turns} turns
|
||||
{#if w.status !== "allowed"}
|
||||
<span class={cn("ml-2 font-medium", TEXT[w.status])}
|
||||
>{w.status.replace("_", " ")}</span
|
||||
>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import type { LiveState } from "$lib/api/live.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let {
|
||||
state,
|
||||
detail = null,
|
||||
class: className = "",
|
||||
label = true,
|
||||
}: {
|
||||
state: LiveState;
|
||||
detail?: string | null;
|
||||
class?: string;
|
||||
label?: boolean;
|
||||
} = $props();
|
||||
|
||||
const COLORS: Record<LiveState, string> = {
|
||||
connecting: "bg-warn",
|
||||
failed: "bg-destructive",
|
||||
off: "bg-muted-foreground/40",
|
||||
open: "bg-ok",
|
||||
retrying: "bg-warn",
|
||||
};
|
||||
const TEXT: Record<LiveState, string> = {
|
||||
connecting: "connecting",
|
||||
failed: "failed",
|
||||
off: "offline",
|
||||
open: "live",
|
||||
retrying: "reconnecting",
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"inline-flex items-center gap-1.5 text-muted-foreground text-xs",
|
||||
className
|
||||
)}
|
||||
title={detail ?? TEXT[state]}
|
||||
>
|
||||
<span
|
||||
class={cn(
|
||||
"size-2 rounded-full",
|
||||
COLORS[state],
|
||||
state === "open" && "animate-pulse-dot"
|
||||
)}
|
||||
></span>
|
||||
{#if label}
|
||||
<span>{TEXT[state]}</span>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import LiveDot from "$lib/components/live-dot.svelte";
|
||||
import { gateway } from "$lib/gateway.svelte";
|
||||
|
||||
let {
|
||||
title,
|
||||
subtitle = "",
|
||||
children,
|
||||
actions,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children?: Snippet;
|
||||
actions?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="flex min-h-12 flex-wrap items-center gap-x-4 gap-y-2 border-b px-4 py-2 sm:px-6"
|
||||
>
|
||||
<div class="flex min-w-0 items-baseline gap-2">
|
||||
<h1 class="truncate font-semibold text-lg tracking-tight">{title}</h1>
|
||||
{#if subtitle}
|
||||
<span class="truncate text-muted-foreground text-sm">{subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if children}
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
{#if actions}
|
||||
{@render actions()}
|
||||
{/if}
|
||||
<LiveDot
|
||||
class="sm:hidden"
|
||||
detail={gateway.live.detail}
|
||||
label={false}
|
||||
state={gateway.live.state}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let {
|
||||
label,
|
||||
value,
|
||||
hint = "",
|
||||
tone = "default",
|
||||
class: className = "",
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
hint?: string;
|
||||
tone?: "default" | "ok" | "warn" | "danger" | "muted";
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const TONE: Record<string, string> = {
|
||||
danger: "text-destructive",
|
||||
default: "text-foreground",
|
||||
muted: "text-muted-foreground",
|
||||
ok: "text-ok",
|
||||
warn: "text-warn",
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={cn("flex min-w-0 flex-col gap-0.5", className)}>
|
||||
<span class="truncate text-muted-foreground text-xs">{label}</span>
|
||||
<span
|
||||
class={cn("tabular truncate font-semibold text-base leading-tight", TONE[tone])}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{#if hint}
|
||||
<span class="truncate text-muted-foreground text-xs">{hint}</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let { status, class: className = "" }: { status: string; class?: string } =
|
||||
$props();
|
||||
|
||||
const STYLE: Record<string, string> = {
|
||||
aborted: "text-muted-foreground",
|
||||
allowed: "text-ok",
|
||||
allowed_warning: "text-warn",
|
||||
archived: "text-muted-foreground",
|
||||
closed: "text-muted-foreground",
|
||||
done: "text-ok",
|
||||
error: "text-destructive",
|
||||
failed: "text-destructive",
|
||||
interrupted: "text-warn",
|
||||
merged: "text-kind-branch",
|
||||
open: "text-foreground",
|
||||
queued: "text-muted-foreground",
|
||||
rejected: "text-destructive",
|
||||
running: "text-signal",
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"inline-flex items-center gap-1.5 font-medium text-xs",
|
||||
STYLE[status] ?? "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
class={cn(
|
||||
"size-1.5 rounded-full bg-current",
|
||||
status === "running" && "animate-pulse-dot"
|
||||
)}
|
||||
></span>
|
||||
{status.replace("_", " ")}
|
||||
</span>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import MoonIcon from "@lucide/svelte/icons/moon";
|
||||
import SunIcon from "@lucide/svelte/icons/sun";
|
||||
import { mode, toggleMode } from "mode-watcher";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
onclick={toggleMode}
|
||||
size="icon-sm"
|
||||
title="Toggle theme"
|
||||
variant="ghost"
|
||||
>
|
||||
{#if mode.current === "dark"}
|
||||
<SunIcon class="size-4" />
|
||||
{:else}
|
||||
<MoonIcon class="size-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = "button",
|
||||
...restProps
|
||||
}: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close
|
||||
bind:ref
|
||||
data-slot="dialog-close"
|
||||
{type}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import * as Dialog from "./index.js";
|
||||
import DialogPortal from "./dialog-portal.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl bg-popover p-6 text-sm text-popover-foreground shadow-xl ring-1 ring-foreground/5 duration-100 sm:max-w-md dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close data-slot="dialog-close">
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="absolute top-4 right-4 bg-secondary"
|
||||
size="icon-sm"
|
||||
{...props}
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
showCloseButton = false,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" {...props}>Close</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("gap-1.5 flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn("bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 fixed inset-0 isolate z-50", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("font-heading text-base leading-none font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = "button",
|
||||
...restProps
|
||||
}: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="dialog-trigger"
|
||||
{type}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,34 @@
|
||||
import Close from "./dialog-close.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Root from "./dialog.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-checkbox-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
{#if indeterminate}
|
||||
<MinusIcon />
|
||||
{:else if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
align = "start",
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<
|
||||
ComponentProps<typeof DropdownMenuPortal>
|
||||
>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPortal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"min-w-48 rounded-3xl p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPortal>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"gap-2.5 rounded-2xl px-3 py-2 text-sm font-medium focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("px-3 py-2.5 text-xs text-muted-foreground data-inset:pl-9.5 data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn("-mx-1.5 my-1.5 h-px bg-border/50", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn("ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn("min-w-36 rounded-3xl p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 w-auto animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"gap-2 rounded-2xl px-3 py-2 text-sm font-medium focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-open:bg-accent data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,54 @@
|
||||
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
import Group from "./dropdown-menu-group.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import Portal from "./dropdown-menu-portal.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import Sub from "./dropdown-menu-sub.svelte";
|
||||
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||
import Root from "./dropdown-menu.svelte";
|
||||
|
||||
export {
|
||||
CheckboxGroup,
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Portal,
|
||||
Root as DropdownMenu,
|
||||
CheckboxGroup as DropdownMenuCheckboxGroup,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
Content as DropdownMenuContent,
|
||||
Portal as DropdownMenuPortal,
|
||||
Group as DropdownMenuGroup,
|
||||
Item as DropdownMenuItem,
|
||||
Label as DropdownMenuLabel,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
Sub as DropdownMenuSub,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
GroupHeading as DropdownMenuGroupHeading,
|
||||
Group,
|
||||
GroupHeading,
|
||||
Item,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Root,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type {
|
||||
HTMLInputAttributes,
|
||||
HTMLInputTypeAttribute,
|
||||
} from "svelte/elements";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
(
|
||||
| { type: "file"; files?: FileList }
|
||||
| { type?: InputType; files?: undefined }
|
||||
)
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "input",
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"h-9 rounded-3xl border border-transparent bg-input/50 px-3 py-1 text-base transition-[color,box-shadow,background-color] file:h-7 file:text-sm file:font-medium focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"h-9 rounded-3xl border border-transparent bg-input/50 px-3 py-1 text-base transition-[color,box-shadow,background-color] file:h-7 file:text-sm file:font-medium focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
>
|
||||
{/if}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
import Content from "./select-content.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
import Group from "./select-group.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Portal from "./select-portal.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Root from "./select.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Portal as SelectPortal,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import SelectPortal from "./select-portal.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
preventScroll = true,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPortal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{preventScroll}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"min-w-36 rounded-3xl text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 isolate z-50 overflow-x-hidden overflow-y-auto animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPortal>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="select-group"
|
||||
class={cn("scroll-my-1.5 p-1.5", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="cn-select-item-indicator-icon" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("px-3 py-2.5 text-xs text-muted-foreground", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("-mx-1.5 my-1.5 h-px bg-border pointer-events-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"gap-1.5 rounded-3xl border border-transparent bg-input/50 px-3 py-2 text-sm transition-[color,box-shadow,background-color] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 text-muted-foreground pointer-events-none" />
|
||||
</SelectPrimitive.Trigger>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: SelectPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "separator",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
// this is different in shadcn/ui but self-stretch breaks things for us
|
||||
"data-[orientation=vertical]:h-full",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,34 @@
|
||||
import Close from "./sheet-close.svelte";
|
||||
import Content from "./sheet-content.svelte";
|
||||
import Description from "./sheet-description.svelte";
|
||||
import Footer from "./sheet-footer.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
import Portal from "./sheet-portal.svelte";
|
||||
import Title from "./sheet-title.svelte";
|
||||
import Trigger from "./sheet-trigger.svelte";
|
||||
import Root from "./sheet.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" module>
|
||||
export type Side = "top" | "right" | "bottom" | "left";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import SheetOverlay from "./sheet-overlay.svelte";
|
||||
import SheetPortal from "./sheet-portal.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
||||
side?: Side;
|
||||
showCloseButton?: boolean;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPortal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
class={cn(
|
||||
"fixed z-50 flex flex-col bg-popover bg-clip-padding text-sm text-popover-foreground shadow-xl transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<SheetPrimitive.Close data-slot="sheet-close">
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="absolute top-4 right-4 bg-secondary"
|
||||
size="icon-sm"
|
||||
{...props}
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SheetPrimitive.Close>
|
||||
{/if}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="sheet-description"
|
||||
class={cn("text-sm text-muted-foreground", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sheet-footer"
|
||||
class={cn("gap-2 p-6 mt-auto flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="sheet-header"
|
||||
class={cn("gap-1.5 p-6 flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="sheet-overlay"
|
||||
class={cn("bg-black/30 supports-backdrop-filter:backdrop-blur-sm fixed inset-0 z-50", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: SheetPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="sheet-title"
|
||||
class={cn("font-heading text-base font-medium text-foreground", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: SheetPrimitive.RootProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./skeleton.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="skeleton"
|
||||
class={cn("rounded-2xl bg-muted animate-pulse", className)}
|
||||
{...restProps}
|
||||
></div>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { mode } from "mode-watcher";
|
||||
import {
|
||||
Toaster as Sonner,
|
||||
type ToasterProps as SonnerProps,
|
||||
} from "svelte-sonner";
|
||||
import Loader2Icon from "@lucide/svelte/icons/loader-2";
|
||||
import CircleCheckIcon from "@lucide/svelte/icons/circle-check";
|
||||
import OctagonXIcon from "@lucide/svelte/icons/octagon-x";
|
||||
import InfoIcon from "@lucide/svelte/icons/info";
|
||||
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
|
||||
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet loadingIcon()}
|
||||
<Loader2Icon class="size-4 animate-spin" />
|
||||
{/snippet}
|
||||
{#snippet successIcon()}
|
||||
<CircleCheckIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet errorIcon()}
|
||||
<OctagonXIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet infoIcon()}
|
||||
<InfoIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet warningIcon()}
|
||||
<TriangleAlertIcon class="size-4" />
|
||||
{/snippet}
|
||||
</Sonner>
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./switch.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
data-slot="switch"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"shrink-0 rounded-full border-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-5 data-[size=default]:w-11 data-[size=sm]:h-4 data-[size=sm]:w-7 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-unchecked:border-transparent data-unchecked:bg-input/90 peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
class="rounded-full bg-background shadow-sm not-dark:bg-clip-padding group-data-[size=default]/switch:h-4 group-data-[size=default]/switch:w-6 group-data-[size=sm]/switch:h-3 group-data-[size=sm]/switch:w-4 data-checked:translate-x-[calc(100%-8px)] dark:data-checked:bg-primary-foreground data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground pointer-events-none block ring-0 transition-transform rtl:data-[state=checked]:translate-x-[calc(-100%)]"
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
@@ -0,0 +1,28 @@
|
||||
import Body from "./table-body.svelte";
|
||||
import Caption from "./table-caption.svelte";
|
||||
import Cell from "./table-cell.svelte";
|
||||
import Footer from "./table-footer.svelte";
|
||||
import Head from "./table-head.svelte";
|
||||
import Header from "./table-header.svelte";
|
||||
import Row from "./table-row.svelte";
|
||||
import Root from "./table.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Body,
|
||||
Caption,
|
||||
Cell,
|
||||
Footer,
|
||||
Head,
|
||||
Header,
|
||||
Row,
|
||||
//
|
||||
Root as Table,
|
||||
Body as TableBody,
|
||||
Caption as TableCaption,
|
||||
Cell as TableCell,
|
||||
Footer as TableFooter,
|
||||
Head as TableHead,
|
||||
Header as TableHeader,
|
||||
Row as TableRow,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||
</script>
|
||||
|
||||
<tbody
|
||||
bind:this={ref}
|
||||
data-slot="table-body"
|
||||
class={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</tbody>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<caption
|
||||
bind:this={ref}
|
||||
data-slot="table-caption"
|
||||
class={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</caption>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLTdAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLTdAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<td
|
||||
bind:this={ref}
|
||||
data-slot="table-cell"
|
||||
class={cn("p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</td>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||
</script>
|
||||
|
||||
<tfoot
|
||||
bind:this={ref}
|
||||
data-slot="table-footer"
|
||||
class={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</tfoot>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLThAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLThAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<th
|
||||
bind:this={ref}
|
||||
data-slot="table-head"
|
||||
class={cn("h-12 px-3 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</th>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
|
||||
</script>
|
||||
|
||||
<thead
|
||||
bind:this={ref}
|
||||
data-slot="table-header"
|
||||
class={cn("[&_tr]:border-b", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</thead>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
|
||||
</script>
|
||||
|
||||
<tr
|
||||
bind:this={ref}
|
||||
data-slot="table-row"
|
||||
class={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</tr>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLTableAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLTableAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="table-container" class="relative w-full overflow-x-auto">
|
||||
<table
|
||||
bind:this={ref}
|
||||
data-slot="table"
|
||||
class={cn("w-full caption-bottom text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
import Content from "./tabs-content.svelte";
|
||||
import Trigger from "./tabs-trigger.svelte";
|
||||
import Root from "./tabs.svelte";
|
||||
import List, {
|
||||
tabsListVariants,
|
||||
type TabsListVariant,
|
||||
} from "./tabs-list.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
tabsListVariants,
|
||||
type TabsListVariant,
|
||||
//
|
||||
Root as Tabs,
|
||||
Content as TabsContent,
|
||||
List as TabsList,
|
||||
Trigger as TabsTrigger,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn("text-sm flex-1 outline-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
export const tabsListVariants = tv({
|
||||
base: "rounded-full p-1 group-data-horizontal/tabs:h-9 group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none group/tabs-list inline-flex w-fit items-center justify-center text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "cn-tabs-list-variant-default bg-muted",
|
||||
line: "cn-tabs-list-variant-line gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type TabsListVariant = VariantProps<
|
||||
typeof tabsListVariants
|
||||
>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
variant = "default",
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps & {
|
||||
variant?: TabsListVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
class={cn(tabsListVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="tabs-trigger"
|
||||
class={cn(
|
||||
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="tabs"
|
||||
class={cn("gap-2 group/tabs flex data-[orientation=horizontal]:flex-col", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
import Content from "./tooltip-content.svelte";
|
||||
import Portal from "./tooltip-portal.svelte";
|
||||
import Provider from "./tooltip-provider.svelte";
|
||||
import Trigger from "./tooltip-trigger.svelte";
|
||||
import Root from "./tooltip.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Trigger,
|
||||
Content,
|
||||
Provider,
|
||||
Portal,
|
||||
//
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider,
|
||||
Portal as TooltipPortal,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import TooltipPortal from "./tooltip-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 0,
|
||||
side = "top",
|
||||
children,
|
||||
arrowClasses,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: TooltipPrimitive.ContentProps & {
|
||||
arrowClasses?: string;
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPortal {...portalProps}>
|
||||
<TooltipPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tooltip-content"
|
||||
{sideOffset}
|
||||
{side}
|
||||
class={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-xl px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-lg data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin) bg-foreground text-background",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<TooltipPrimitive.Arrow>
|
||||
{#snippet child({ props })}
|
||||
<div
|
||||
class={cn(
|
||||
"size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px] z-50 bg-foreground fill-foreground",
|
||||
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%+2px)]",
|
||||
"data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%+1px)]",
|
||||
"data-[side=right]:translate-x-[calc(50%+2px)] data-[side=right]:translate-y-1/2",
|
||||
"data-[side=left]:-translate-y-[calc(50%-3px)]",
|
||||
arrowClasses
|
||||
)}
|
||||
{...props}
|
||||
></div>
|
||||
{/snippet}
|
||||
</TooltipPrimitive.Arrow>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPortal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
|
||||
let { delayDuration = 0, ...restProps }: TooltipPrimitive.ProviderProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Provider {delayDuration} {...restProps} />
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user