feat(ui,api,admin): sveltekit admin spa, usage by day/agent/conversation/model, rate limits, memory tree, turn snapshot

This commit is contained in:
hh
2026-08-28 22:14:15 +02:00
parent 61562e947d
commit 98796a82c6
158 changed files with 8459 additions and 2531 deletions
+7 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+323
View File
@@ -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);
}
}
+140
View File
@@ -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";
}
}
}
+66
View File
@@ -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);
}
}
+278
View File
@@ -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;
}
+75
View File
@@ -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>
+79
View File
@@ -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>
+31
View File
@@ -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>
+18
View File
@@ -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>
+25
View File
@@ -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>
+69
View File
@@ -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>
+50
View File
@@ -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>
+44
View File
@@ -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>
+37
View File
@@ -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>
+39
View File
@@ -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>
+20
View File
@@ -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} />
+34
View File
@@ -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,
};
+7
View File
@@ -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}
+7
View File
@@ -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}
/>
+37
View File
@@ -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}
/>
+34
View File
@@ -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>
+1
View File
@@ -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>
+7
View File
@@ -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>
+28
View File
@@ -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>
+21
View File
@@ -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}
/>
+19
View File
@@ -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}
/>
+19
View File
@@ -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