style(web): fix prettier config, format entire web/ tree
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

.prettierrc.json was missing "semi": false, so prettier wanted to add
semicolons to a codebase written without them (763 semicolon-free
statements vs. 150 with, in hand-written .ts; zero hand-written .svelte
files use them at all). That's why prettier --check failed on 249 files
— not because the code was unformatted, but because the config didn't
match the actual house style. Added "semi": false; left printWidth/etc
as configured (printWidth barely moves the failure count: 218/213/212
files at 100/120/140).

Ran `prettier --write .` with the corrected config. Verified
semantics-preserving before and after:
- eslint: 142 problems both before and after, byte-identical
- build passes, 38/38 tests pass
- token-stream diff (whitespace/semicolons/quotes normalized) on all
  218 changed files: only 52 had any remaining token change, all either
  trailing-comma removal (matching trailingComma: "none") or import/
  ternary reflow — no semantic changes
- live smoke test: Knowledge, Tasks, Fleet map, and a chat window
  (AgentTrace, markdown, Scope graph, activity rail) all render
  correctly, no console errors

Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving
from the CLI's own style (double quotes, tabs, semicolons) to house
style; re-running `shadcn-svelte add` on a component will need a
follow-up format pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 12:56:07 +02:00
parent b345783eef
commit 873b00ac42
217 changed files with 4945 additions and 3538 deletions

View File

@@ -112,7 +112,11 @@ export async function fetchQuestions(sessionId: string): Promise<SessionQuestion
return data.questions ?? []
}
export async function answerQuestion(sessionId: string, questionId: string, answer: string): Promise<boolean> {
export async function answerQuestion(
sessionId: string,
questionId: string,
answer: string
): Promise<boolean> {
const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
method: 'POST',
body: JSON.stringify({ answer })
@@ -133,42 +137,45 @@ export function streamChat(
method: 'POST',
body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
signal: controller.signal
}).then(async (res) => {
if (!res.ok) {
onError(`HTTP ${res.status}`)
return
}
const reader = res.body?.getReader()
if (!reader) {
onError('no response body')
return
}
const decoder = new TextDecoder()
let buffer = ''
})
.then(async (res) => {
if (!res.ok) {
onError(`HTTP ${res.status}`)
return
}
const reader = res.body?.getReader()
if (!reader) {
onError('no response body')
return
}
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const ev: ChatEvent = JSON.parse(line.slice(6))
onEvent(ev)
} catch {
// skip malformed
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const ev: ChatEvent = JSON.parse(line.slice(6))
onEvent(ev)
} catch {
// skip malformed
}
}
}
}
}
}).catch((err) => {
onError(err.message)
}).finally(() => {
onDone()
})
})
.catch((err) => {
onError(err.message)
})
.finally(() => {
onDone()
})
return controller
}
@@ -291,7 +298,9 @@ export interface EventFilters {
severity?: string
}
export async function fetchEvents(filters: EventFilters = {}): Promise<import('./stores/events').OikosEvent[]> {
export async function fetchEvents(
filters: EventFilters = {}
): Promise<import('./stores/events').OikosEvent[]> {
const params = new URLSearchParams()
if (filters.type) params.set('type', filters.type)
if (filters.severity) params.set('severity', filters.severity)
@@ -483,7 +492,9 @@ export interface Signal {
last_seen_at: string
}
export async function fetchSignals(filters: { state?: string; severity?: string } = {}): Promise<Signal[]> {
export async function fetchSignals(
filters: { state?: string; severity?: string } = {}
): Promise<Signal[]> {
const params = new URLSearchParams()
if (filters.state) params.set('state', filters.state)
if (filters.severity) params.set('severity', filters.severity)
@@ -509,7 +520,11 @@ export async function resolveSignal(id: string, note?: string): Promise<Signal |
return res.json()
}
export async function muteSignal(id: string, muteUntil: string, note?: string): Promise<Signal | null> {
export async function muteSignal(
id: string,
muteUntil: string,
note?: string
): Promise<Signal | null> {
const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
method: 'POST',
body: JSON.stringify({ mute_until: muteUntil, note })
@@ -533,7 +548,9 @@ export interface Relationship {
// reachable going forward from here), this hits a dedicated endpoint that
// matches on source_id OR target_id directly.
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
const res = await fetchWithAuth(`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`)
const res = await fetchWithAuth(
`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`
)
if (!res.ok) return []
const data = await res.json()
return data.items ?? []
@@ -671,7 +688,9 @@ export async function fetchKnowledgeContent(id: string): Promise<KnowledgeConten
return res.json()
}
export async function fetchEntityEvents(entityId: string): Promise<import('./stores/events').OikosEvent[]> {
export async function fetchEntityEvents(
entityId: string
): Promise<import('./stores/events').OikosEvent[]> {
const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
const res = await fetchWithAuth(`${API}/events?${params}`)
if (!res.ok) return []
@@ -718,13 +737,19 @@ export async function fetchEntityTasks(entity: Entity): Promise<EntityTask[]> {
tasks.map(async (task): Promise<EntityTask | null> => {
const g = await fetchGraph({ root: task.slug, depth: 1 })
if (!g) return null
const involvesThisEntity = g.edges.some((e) => e.type === 'involves' && e.target === entity.slug)
const involvesThisEntity = g.edges.some(
(e) => e.type === 'involves' && e.target === entity.slug
)
const nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
const executionCount = g.edges.filter((e) => {
if (e.type !== 'involves') return false
const targetId = idBySlug.get(e.target)
return targetId != null && nodeTypeById.get(targetId) === 'execution' && executionIds.has(targetId)
return (
targetId != null &&
nodeTypeById.get(targetId) === 'execution' &&
executionIds.has(targetId)
)
}).length
if (!involvesThisEntity && executionCount === 0) return null
return { task, executionCount }
@@ -755,7 +780,11 @@ export async function fetchChecksForTarget(targetSlug: string): Promise<Check[]>
return data.items ?? []
}
export async function patchCheck(id: string, version: number, patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }): Promise<Check | null> {
export async function patchCheck(
id: string,
version: number,
patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }
): Promise<Check | null> {
const res = await fetchWithAuth(`${API}/checks/${id}`, {
method: 'PATCH',
headers: { 'If-Match': `"${version}"` },
@@ -781,12 +810,14 @@ export interface AgentActivity {
correlation_id?: string | null
}
export async function fetchAgentActivity(filters: {
agent_id?: string
activity_type?: string
entity_id?: string
limit?: number
} = {}): Promise<AgentActivity[]> {
export async function fetchAgentActivity(
filters: {
agent_id?: string
activity_type?: string
entity_id?: string
limit?: number
} = {}
): Promise<AgentActivity[]> {
const params = new URLSearchParams()
if (filters.agent_id) params.set('agent_id', filters.agent_id)
if (filters.activity_type) params.set('activity_type', filters.activity_type)
@@ -821,14 +852,16 @@ export interface AuditEntry {
correlation_id?: string | null
}
export async function fetchAudit(filters: {
actor_type?: string
actor_id?: string
entity_id?: string
action?: string
correlation_id?: string
limit?: number
} = {}): Promise<AuditEntry[]> {
export async function fetchAudit(
filters: {
actor_type?: string
actor_id?: string
entity_id?: string
action?: string
correlation_id?: string
limit?: number
} = {}
): Promise<AuditEntry[]> {
const params = new URLSearchParams()
if (filters.actor_type) params.set('actor_type', filters.actor_type)
if (filters.actor_id) params.set('actor_id', filters.actor_id)

View File

@@ -50,8 +50,8 @@
class="min-h-0 flex-1 resize-none rounded-md border bg-background p-3 font-mono text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-ring"
></textarea>
<p class="shrink-0 text-xs text-muted-foreground">
A demo installable app — uninstall it from the App Store to remove its
icon and window. Its notes persist in localStorage under
A demo installable app — uninstall it from the App Store to remove its icon and window. Its
notes persist in localStorage under
<code class="font-mono">{storageKey}</code>.
</p>
</div>

View File

@@ -23,7 +23,12 @@ import type { Component } from 'svelte'
import { writable, derived, get, type Readable } from 'svelte/store'
import type { DashboardSummary } from '$lib/api'
import { openSignalCount } from '$lib/stores/context'
import { catalogById, type AppManifest, type AppPermission, type CatalogEntry } from '$lib/app-store/catalog'
import {
catalogById,
type AppManifest,
type AppPermission,
type CatalogEntry
} from '$lib/app-store/catalog'
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
import BoxesIcon from '@lucide/svelte/icons/boxes'
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
@@ -237,8 +242,9 @@ export const apps: Readable<AppDef[]> = derived(installedIds, (ids) => {
return [...builtinApps, ...installed]
})
export const appById: Readable<Map<string, AppDef>> = derived(apps, (list) =>
new Map(list.map((a) => [a.id, a]))
export const appById: Readable<Map<string, AppDef>> = derived(
apps,
(list) => new Map(list.map((a) => [a.id, a]))
)
// Install/uninstall. Idempotent — installing an already-installed app or

View File

@@ -37,14 +37,23 @@
})
</script>
<div class="trace rounded-lg border border-border/60 bg-card/40 transition-colors" class:running={status === 'running'}>
<div
class="trace rounded-lg border border-border/60 bg-card/40 transition-colors"
class:running={status === 'running'}
>
<button
class="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-muted/40"
onclick={() => (expanded = !expanded)}
aria-expanded={expanded}
aria-label={expanded ? 'Hide agent trace' : 'Show agent trace'}
>
<span class="shrink-0 {status === 'error' ? 'text-destructive' : status === 'idle' ? 'text-muted-foreground' : 'text-primary'}">
<span
class="shrink-0 {status === 'error'
? 'text-destructive'
: status === 'idle'
? 'text-muted-foreground'
: 'text-primary'}"
>
{#if status === 'running'}
<Spinner class="size-3" />
{:else if status === 'error'}
@@ -71,7 +80,9 @@
{/if}
<ChevronRightIcon
class="size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded ? 'rotate-90' : ''}"
class="size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded
? 'rotate-90'
: ''}"
/>
</button>
@@ -82,7 +93,9 @@
<ToolCallCard {tool} />
{/each}
{:else}
<p class="px-2 py-1.5 text-[11px] text-muted-foreground">Nothing recorded for this turn yet.</p>
<p class="px-2 py-1.5 text-[11px] text-muted-foreground">
Nothing recorded for this turn yet.
</p>
{/if}
</div>
{/if}

View File

@@ -60,10 +60,16 @@
let wasStreaming = $state(false)
$effect(() => {
if (streaming) { indicatorDone = false; wasStreaming = true }
if (streaming) {
indicatorDone = false
wasStreaming = true
}
if (!streaming && wasStreaming) {
indicatorDone = true
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000)
const t = setTimeout(() => {
indicatorDone = false
wasStreaming = false
}, 3000)
return () => clearTimeout(t)
}
})
@@ -91,7 +97,10 @@
const lineHeight = parseFloat(taCs.lineHeight)
if (!Number.isFinite(lineHeight)) return
const taBoxY =
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
parseFloat(taCs.paddingTop) +
parseFloat(taCs.paddingBottom) +
parseFloat(taCs.borderTopWidth) +
parseFloat(taCs.borderBottomWidth)
// The wrapper's own padding/border (space around the textarea, not part
// of it) also has to fit inside the minimum, or the textarea gets
// squeezed below one line once the pane is dragged down to it.
@@ -144,7 +153,9 @@
}
renderer.table = function (token) {
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
const body = token.rows.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`).join('')
const body = token.rows
.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`)
.join('')
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
}
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
@@ -181,126 +192,172 @@
</script>
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1" on:resize={() => (userResizedInput = true)}>
<Splitpanes
horizontal
theme="oikos-theme"
dblClickSplitter={false}
class="min-h-0 flex-1"
on:resize={() => (userResizedInput = true)}
>
<Pane class="flex flex-col">
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
{#if messages.length === 0}
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
<div>
<h2 class="text-xl font-semibold">Nomos</h2>
<p class="mt-1 text-sm text-muted-foreground">Your resident operator. Ask about the fleet, or tell it to act.</p>
</div>
{#if suggestions.length}
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
{#each suggestions as q}
<Button variant="outline" size="sm" class="h-auto justify-start whitespace-normal py-2 text-left text-xs" onclick={() => ask(q)}>
{q}
</Button>
{/each}
</div>
{/if}
</div>
{/if}
{#each messages as msg, idx (msg.id)}
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
{#if msg.role === 'user'}
<div class="flex items-baseline gap-2 px-1">
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
{#if msg.created_at}
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
{/if}
</div>
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg">{msg.text}</div>
{:else}
{@const isLast = idx === messages.length - 1}
{@const traceStatus = !isLast
? 'idle'
: error
? 'error'
: streaming
? 'running'
: indicatorDone
? 'done'
: 'idle'}
<div class="flex w-full flex-col gap-2">
<div class="flex items-baseline gap-2 px-1">
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
{#if msg.created_at}
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
{/if}
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
{#if messages.length === 0}
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
<div>
<h2 class="text-xl font-semibold">Nomos</h2>
<p class="mt-1 text-sm text-muted-foreground">
Your resident operator. Ask about the fleet, or tell it to act.
</p>
</div>
<!-- The working trace sits above the answer: it's what happened
first, and collapsed it keeps a long tool run from burying
the text below it. -->
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
<AgentTrace
tools={msg.tools}
status={traceStatus}
label={traceStatus === 'idle' ? null : indicatorLabel}
/>
{/if}
{#if msg.text}
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
{@html render(msg.text)}
{#if isLast && streaming}
<span class="stream-cursor" aria-hidden="true"></span>
{/if}
{#if suggestions.length}
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
{#each suggestions as q}
<Button
variant="outline"
size="sm"
class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
onclick={() => ask(q)}
>
{q}
</Button>
{/each}
</div>
{/if}
</div>
{/if}
{#each messages as msg, idx (msg.id)}
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
{#if msg.role === 'user'}
<div class="flex items-baseline gap-2 px-1">
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
{#if msg.created_at}
<span class="text-[9px] text-muted-foreground/50"
>{formatTime(msg.created_at)}</span
>
{/if}
</div>
<div
class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg"
>
{msg.text}
</div>
{:else}
{@const isLast = idx === messages.length - 1}
{@const traceStatus = !isLast
? 'idle'
: error
? 'error'
: streaming
? 'running'
: indicatorDone
? 'done'
: 'idle'}
<div class="flex w-full flex-col gap-2">
<div class="flex items-baseline gap-2 px-1">
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
{#if msg.created_at}
<span class="text-[9px] text-muted-foreground/50"
>{formatTime(msg.created_at)}</span
>
{/if}
</div>
<!-- The working trace sits above the answer: it's what happened
first, and collapsed it keeps a long tool run from burying
the text below it. -->
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
<AgentTrace
tools={msg.tools}
status={traceStatus}
label={traceStatus === 'idle' ? null : indicatorLabel}
/>
{/if}
{#if msg.text}
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
{@html render(msg.text)}
{#if isLast && streaming}
<span class="stream-cursor" aria-hidden="true"></span>
{/if}
</div>
{/if}
</div>
{/if}
</div>
{/each}
{#if question}
<OperatorQuestion {sessionId} {question} />
{/if}
<div bind:this={messagesEnd}></div>
</div>
</div>
{#if connectionState === 'disconnected'}
<div class="mx-auto w-full max-w-3xl px-4">
<div
class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs"
>
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
<span class="text-warning-foreground flex-1"
>Agent connection lost. The task may still be running.</span
>
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}
>Reconnect</Button
>
</div>
</div>
{:else if connectionState === 'reconnecting'}
<div class="mx-auto w-full max-w-3xl px-4">
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
<RefreshCwIcon
class="size-3 shrink-0 animate-spin text-muted-foreground"
aria-hidden="true"
/>
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
</div>
</div>
{/if}
{#if error}
<div class="mx-auto w-full max-w-3xl px-4">
<div
class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
{error}
</div>
</div>
{/if}
{#each chatErrors as err (err.id)}
<div class="mx-auto w-full max-w-3xl px-4">
<div
class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
>
<span class="flex-1">{err.message}</span>
{#if err.action}
<Button
size="xs"
variant="ghost"
class="h-6 text-[11px]"
onclick={() => onDismissError(err.id)}>{err.action}</Button
>
{/if}
<button
class="ml-1 text-muted-foreground hover:text-foreground"
onclick={() => onDismissError(err.id)}
aria-label="Dismiss">×</button
>
</div>
</div>
{/each}
{#if question}
<OperatorQuestion {sessionId} {question} />
{/if}
<div bind:this={messagesEnd}></div>
</div>
</div>
{#if connectionState === 'disconnected'}
<div class="mx-auto w-full max-w-3xl px-4">
<div class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs">
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
<span class="text-warning-foreground flex-1">Agent connection lost. The task may still be running.</span>
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}>Reconnect</Button>
</div>
</div>
{:else if connectionState === 'reconnecting'}
<div class="mx-auto w-full max-w-3xl px-4">
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
<RefreshCwIcon class="size-3 shrink-0 animate-spin text-muted-foreground" aria-hidden="true" />
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
</div>
</div>
{/if}
{#if error}
<div class="mx-auto w-full max-w-3xl px-4">
<div class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{error}
</div>
</div>
{/if}
{#each chatErrors as err (err.id)}
<div class="mx-auto w-full max-w-3xl px-4">
<div class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<span class="flex-1">{err.message}</span>
{#if err.action}
<Button size="xs" variant="ghost" class="h-6 text-[11px]" onclick={() => onDismissError(err.id)}>{err.action}</Button>
{/if}
<button class="ml-1 text-muted-foreground hover:text-foreground" onclick={() => onDismissError(err.id)} aria-label="Dismiss">×</button>
</div>
</div>
{/each}
</Pane>
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
<div
class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative"
bind:this={inputWrapperRef}
>
<form
class="relative mx-auto flex h-full w-full max-w-3xl"
onsubmit={(e) => {
@@ -514,7 +571,13 @@
border: none;
height: 1px;
margin: 0.75rem 0;
background: linear-gradient(to right, transparent, var(--border) 20%, var(--border) 80%, transparent);
background: linear-gradient(
to right,
transparent,
var(--border) 20%,
var(--border) 80%,
transparent
);
}
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
@@ -559,7 +622,9 @@
border-radius: 0.375rem;
color: var(--muted-foreground);
opacity: 0;
transition: opacity 0.15s, color 0.15s;
transition:
opacity 0.15s,
color 0.15s;
cursor: pointer;
border: none;
background: transparent;
@@ -586,7 +651,12 @@
}
@keyframes cursor-blink {
0%, 100% { opacity: 0.75; }
50% { opacity: 0; }
0%,
100% {
opacity: 0.75;
}
50% {
opacity: 0;
}
}
</style>

View File

@@ -21,7 +21,9 @@
let canvas = $state<HTMLCanvasElement | null>(null)
let particles: Particle[] = []
let mouse = { x: -500, y: -500 }
let w = 0, h = 0, dpr = 1
let w = 0,
h = 0,
dpr = 1
let timer: ReturnType<typeof setTimeout> | 0 = 0
function spawn() {
@@ -73,15 +75,15 @@
// update + draw particles
for (const p of particles) {
// autonomous drift
p.vx += (Math.sin(t * 0.4 + p.phase) * 0.003) * 0.15
p.vy += (Math.cos(t * 0.35 + p.phase) * 0.003) * 0.15
p.vx += Math.sin(t * 0.4 + p.phase) * 0.003 * 0.15
p.vy += Math.cos(t * 0.35 + p.phase) * 0.003 * 0.15
// mouse interaction
const dx = p.x - mouse.x
const dy = p.y - mouse.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < MOUSE_RADIUS && dist > 0) {
const force = (MOUSE_RADIUS - dist) / MOUSE_RADIUS * MOUSE_FORCE
const force = ((MOUSE_RADIUS - dist) / MOUSE_RADIUS) * MOUSE_FORCE
p.vx += (dx / dist) * force * 0.6
p.vy += (dy / dist) * force * 0.6
}
@@ -104,9 +106,7 @@
// pulse brightness
const alpha = p.pulse * (0.35 + 0.15 * Math.sin(t * 1.2 + p.phase))
ctx.fillStyle = dark
? `rgba(140,175,230,${alpha})`
: `rgba(60,90,140,${alpha})`
ctx.fillStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})`
ctx.beginPath()
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
ctx.fill()
@@ -123,9 +123,7 @@
const dist = dx * dx + dy * dy
if (dist < CONNECT_DIST * CONNECT_DIST) {
const alpha = (1 - Math.sqrt(dist) / CONNECT_DIST) * 0.18
ctx.strokeStyle = dark
? `rgba(140,175,230,${alpha})`
: `rgba(60,90,140,${alpha})`
ctx.strokeStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})`
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
@@ -135,7 +133,8 @@
}
// radial scrim to keep center legible
const cx = w / 2, cy = h / 2
const cx = w / 2,
cy = h / 2
const scrim = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy))
const base = dark ? '13,17,23' : '255,255,255'
scrim.addColorStop(0, `rgba(${base},0.72)`)

View File

@@ -22,14 +22,20 @@
</script>
<Collapsible.Root bind:open class="rounded-md border bg-card">
<Collapsible.Trigger class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2 py-1 text-left hover:bg-muted/50">
<Collapsible.Trigger
class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2 py-1 text-left hover:bg-muted/50"
>
<span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
<ChevronDownIcon
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open ? 'rotate-180' : ''}"
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open
? 'rotate-180'
: ''}"
aria-hidden="true"
/>
</Collapsible.Trigger>
<Collapsible.Content class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in">
<Collapsible.Content
class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in"
>
<div class="border-t px-2 py-1.5">
{@render children()}
</div>

View File

@@ -11,7 +11,10 @@
</script>
<tr>
<td {colspan} class={['py-8 text-center text-muted-foreground', className].filter(Boolean).join(' ')}>
<td
{colspan}
class={['py-8 text-center text-muted-foreground', className].filter(Boolean).join(' ')}
>
{message}
</td>
</tr>

View File

@@ -62,7 +62,9 @@
// (source OR target = entity, both directions), so a plain split by which
// side matches is enough — no risk of an unrelated sibling-to-sibling edge
// sneaking into either group.
const outgoingRelations = $derived(entity ? relations.filter((r) => r.source === entity!.slug) : [])
const outgoingRelations = $derived(
entity ? relations.filter((r) => r.source === entity!.slug) : []
)
const incomingRelations = $derived(
entity ? relations.filter((r) => r.target === entity!.slug && r.source !== entity!.slug) : []
)
@@ -203,13 +205,23 @@
body?: string
}
const LONG_TEXT_KEYS = new Set(['description', 'content', 'summary', 'notes', 'note', 'body', 'details'])
const LONG_TEXT_KEYS = new Set([
'description',
'content',
'summary',
'notes',
'note',
'body',
'details'
])
function isChangelog(value: unknown): value is ChangelogEntry[] {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every((v) => v && typeof v === 'object' && !Array.isArray(v) && ('title' in v || 'body' in v))
value.every(
(v) => v && typeof v === 'object' && !Array.isArray(v) && ('title' in v || 'body' in v)
)
)
}
@@ -263,14 +275,22 @@
</div>
<div class="flex items-center justify-between gap-3 border-b pb-1">
<dt class="shrink-0 text-muted-foreground">State</dt>
<dd>{#if entity.state}<Badge>{entity.state}</Badge>{:else}<span class="text-muted-foreground"></span>{/if}</dd>
<dd>
{#if entity.state}<Badge>{entity.state}</Badge>{:else}<span
class="text-muted-foreground"></span
>{/if}
</dd>
</div>
<div class="flex items-center justify-between gap-3 border-b pb-1">
<dt class="shrink-0 text-muted-foreground">Health</dt>
<dd>
{#if entity.health}
<span class="flex items-center gap-1.5" title="checked {relativeTime(entity.last_check_at)}">
<span class="size-2 rounded-full {healthDot[entity.health] ?? healthDot.unknown}"></span>
<span
class="flex items-center gap-1.5"
title="checked {relativeTime(entity.last_check_at)}"
>
<span class="size-2 rounded-full {healthDot[entity.health] ?? healthDot.unknown}"
></span>
{entity.health} · checked {relativeTime(entity.last_check_at)}
</span>
{:else}
@@ -286,7 +306,11 @@
<dt class="shrink-0 text-muted-foreground">Created</dt>
<dd title={entity.created_at}>{relativeTime(entity.created_at)}</dd>
</div>
<div class="flex items-center justify-between gap-3 {entity.maintenance_until ? 'border-b pb-1' : ''}">
<div
class="flex items-center justify-between gap-3 {entity.maintenance_until
? 'border-b pb-1'
: ''}"
>
<dt class="shrink-0 text-muted-foreground">Updated</dt>
<dd title={entity.updated_at}>{relativeTime(entity.updated_at)}</dd>
</div>
@@ -313,7 +337,9 @@
onclick={() => toggleCheck(check)}
title={check.enabled ? 'Click to disable' : 'Click to enable'}
>
<Badge variant={check.enabled ? 'default' : 'secondary'}>{check.enabled ? 'enabled' : 'disabled'}</Badge>
<Badge variant={check.enabled ? 'default' : 'secondary'}
>{check.enabled ? 'enabled' : 'disabled'}</Badge
>
</button>
</div>
{:else}
@@ -339,7 +365,9 @@
{#if row.kind === 'long-text'}
<div class="flex flex-col gap-0.5">
<p class="font-mono text-muted-foreground">{row.key}</p>
<p class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-1.5">{row.value}</p>
<p class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-1.5">
{row.value}
</p>
</div>
{:else if row.kind === 'changelog'}
<div class="flex flex-col gap-0.5">
@@ -348,10 +376,16 @@
{#each row.value as entry}
<div class="rounded-sm border-l-2 border-muted-foreground/30 pl-1.5">
<div class="flex items-baseline gap-1.5">
{#if entry.date}<span class="shrink-0 font-mono text-muted-foreground">{entry.date}</span>{/if}
{#if entry.date}<span class="shrink-0 font-mono text-muted-foreground"
>{entry.date}</span
>{/if}
{#if entry.title}<span class="font-medium">{entry.title}</span>{/if}
</div>
{#if entry.body}<p class="whitespace-pre-wrap break-words text-muted-foreground">{entry.body}</p>{/if}
{#if entry.body}<p
class="whitespace-pre-wrap break-words text-muted-foreground"
>
{entry.body}
</p>{/if}
</div>
{/each}
</div>
@@ -373,7 +407,12 @@
<dt class="shrink-0 font-mono text-muted-foreground">{row.key}</dt>
<dd class="min-w-0 flex-1 break-words text-right">
{#if row.value !== null && typeof row.value === 'object'}
<pre class="overflow-x-auto whitespace-pre-wrap break-words text-left">{JSON.stringify(row.value, null, 2)}</pre>
<pre
class="overflow-x-auto whitespace-pre-wrap break-words text-left">{JSON.stringify(
row.value,
null,
2
)}</pre>
{:else}
{String(row.value)}
{/if}
@@ -390,13 +429,27 @@
{#snippet relationRow(rel: Relationship)}
<div class="flex min-w-0 items-center gap-1 font-mono text-xs">
{#if onSelectEntity}
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={rel.source} onclick={() => onSelectEntity(rel.source)}>{truncateMiddle(rel.source)}</button>
<button
type="button"
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
title={rel.source}
onclick={() => onSelectEntity(rel.source)}>{truncateMiddle(rel.source)}</button
>
<span class="shrink-0 text-muted-foreground">{rel.type}</span>
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={rel.target} onclick={() => onSelectEntity(rel.target)}>{truncateMiddle(rel.target)}</button>
<button
type="button"
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
title={rel.target}
onclick={() => onSelectEntity(rel.target)}>{truncateMiddle(rel.target)}</button
>
{:else}
<span class="min-w-0 flex-1 truncate" title={rel.source}>{truncateMiddle(rel.source)}</span>
<span class="min-w-0 flex-1 truncate" title={rel.source}
>{truncateMiddle(rel.source)}</span
>
<span class="shrink-0 text-muted-foreground">{rel.type}</span>
<span class="min-w-0 flex-1 truncate" title={rel.target}>{truncateMiddle(rel.target)}</span>
<span class="min-w-0 flex-1 truncate" title={rel.target}
>{truncateMiddle(rel.target)}</span
>
{/if}
</div>
{/snippet}
@@ -408,7 +461,11 @@
<div class="flex flex-col gap-3">
{#if outgoingRelations.length}
<div>
<div class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">Outgoing ({outgoingRelations.length})</div>
<div
class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase"
>
Outgoing ({outgoingRelations.length})
</div>
<div class="flex flex-col gap-1">
{#each outgoingRelations as rel}
{@render relationRow(rel)}
@@ -418,7 +475,11 @@
{/if}
{#if incomingRelations.length}
<div>
<div class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">Incoming ({incomingRelations.length})</div>
<div
class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase"
>
Incoming ({incomingRelations.length})
</div>
<div class="flex flex-col gap-1">
{#each incomingRelations as rel}
{@render relationRow(rel)}
@@ -492,21 +553,32 @@
{#snippet tasksContent()}
<div class="flex flex-col gap-1">
{#each tasks as { task, executionCount } (task.id)}
{@const title = typeof task.attributes?.title === 'string' ? task.attributes.title : task.name}
{@const outcome = typeof task.attributes?.outcome === 'string' ? task.attributes.outcome : undefined}
<div class="flex items-center justify-between gap-2 border-b pb-1 text-xs last:border-0 last:pb-0">
{@const title =
typeof task.attributes?.title === 'string' ? task.attributes.title : task.name}
{@const outcome =
typeof task.attributes?.outcome === 'string' ? task.attributes.outcome : undefined}
<div
class="flex items-center justify-between gap-2 border-b pb-1 text-xs last:border-0 last:pb-0"
>
{#if onSelectEntity}
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={title} onclick={() => onSelectEntity(task.slug)}>
<button
type="button"
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
{title}
onclick={() => onSelectEntity(task.slug)}
>
{title}
</button>
{:else}
<span class="min-w-0 flex-1 truncate" title={title}>{title}</span>
<span class="min-w-0 flex-1 truncate" {title}>{title}</span>
{/if}
<div class="flex shrink-0 items-center gap-1">
{#if outcome}
<Badge variant={outcome === 'success' ? 'default' : 'destructive'}>{outcome}</Badge>
{/if}
<Badge variant="outline">{executionCount} action{executionCount === 1 ? '' : 's'}</Badge>
<Badge variant="outline"
>{executionCount} action{executionCount === 1 ? '' : 's'}</Badge
>
</div>
</div>
{:else}
@@ -545,8 +617,12 @@
{#each agentActivity as activity (activity.id)}
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
<div class="flex items-center justify-between gap-2">
<span class="font-mono text-muted-foreground">{new Date(activity.ts).toLocaleString()}</span>
<Badge variant={activity.success === false ? 'destructive' : 'outline'}>{activity.activity_type}</Badge>
<span class="font-mono text-muted-foreground"
>{new Date(activity.ts).toLocaleString()}</span
>
<Badge variant={activity.success === false ? 'destructive' : 'outline'}
>{activity.activity_type}</Badge
>
</div>
<span class="truncate text-muted-foreground"
>{activity.agent_id}{activity.tool_name ? ` · ${activity.tool_name}` : ''}</span
@@ -563,10 +639,14 @@
{#each auditEntries as entry (entry.id)}
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
<div class="flex items-center justify-between gap-2">
<span class="font-mono text-muted-foreground">{new Date(entry.ts).toLocaleString()}</span>
<span class="font-mono text-muted-foreground"
>{new Date(entry.ts).toLocaleString()}</span
>
<Badge variant="outline">{entry.actor_type}</Badge>
</div>
<span class="truncate text-muted-foreground">{entry.actor_id ?? '—'} · {entry.action}</span>
<span class="truncate text-muted-foreground"
>{entry.actor_id ?? '—'} · {entry.action}</span
>
</div>
{:else}
<p class="text-xs text-muted-foreground">No audit entries.</p>
@@ -575,17 +655,34 @@
{/snippet}
{@const sections = [
...(ownContent ? [{ key: 'content', title: 'Content', count: 1, content: contentContent }] : []),
...(ownContent
? [{ key: 'content', title: 'Content', count: 1, content: contentContent }]
: []),
{ key: 'details', title: 'Details', count: 1, content: detailsContent },
{ key: 'monitoring', title: 'Monitoring', count: checks.length, content: monitoringContent },
{ key: 'attributes', title: 'Attributes', count: Object.keys(entity.attributes ?? {}).length, content: attributesContent },
{ key: 'relations', title: 'Relations', count: outgoingRelations.length + incomingRelations.length, content: relationsContent },
{
key: 'attributes',
title: 'Attributes',
count: Object.keys(entity.attributes ?? {}).length,
content: attributesContent
},
{
key: 'relations',
title: 'Relations',
count: outgoingRelations.length + incomingRelations.length,
content: relationsContent
},
{ key: 'metrics', title: 'Metrics', count: metrics.length, content: metricsContent },
{ key: 'signals', title: 'Signals', count: signals.length, content: signalsContent },
{ key: 'tasks', title: 'Tasks', count: tasks.length, content: tasksContent },
{ key: 'knowledge', title: 'Knowledge', count: knowledge.length, content: knowledgeContent },
{ key: 'events', title: 'Recent events', count: events.length, content: eventsContent },
{ key: 'agentActivity', title: 'Agent activity', count: agentActivity.length, content: agentActivityContent },
{
key: 'agentActivity',
title: 'Agent activity',
count: agentActivity.length,
content: agentActivityContent
},
{ key: 'audit', title: 'Audit trail', count: auditEntries.length, content: auditContent }
].sort((a, b) => (b.count > 0 ? 1 : 0) - (a.count > 0 ? 1 : 0))}

View File

@@ -51,7 +51,13 @@
return { sorted: true, direction: sortDir }
}
const healthRank: Record<string, number> = { down: 0, degraded: 1, stale: 2, unknown: 3, healthy: 4 }
const healthRank: Record<string, number> = {
down: 0,
degraded: 1,
stale: 2,
unknown: 3,
healthy: 4
}
function sortValue(entity: Entity, key: SortKey): string | number {
if (key === 'health') return entity.health ? (healthRank[entity.health] ?? -1) : -1
@@ -131,7 +137,9 @@
{:else}
{#snippet row(entity: Entity, level: number, ancestors: Set<string>)}
{@const ancestorsWithSelf = new Set(ancestors).add(entity.slug)}
{@const children = (childrenByParent.get(entity.slug) ?? []).filter((c) => !ancestorsWithSelf.has(c.slug))}
{@const children = (childrenByParent.get(entity.slug) ?? []).filter(
(c) => !ancestorsWithSelf.has(c.slug)
)}
<Table.Row
class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}"
role="row"
@@ -139,7 +147,12 @@
aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined}
tabindex={0}
onclick={() => onSelect(entity.slug)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(entity.slug) } }}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onSelect(entity.slug)
}
}}
>
<Table.Cell class="font-mono text-xs">
<span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem">
@@ -149,7 +162,9 @@
type="button"
class="rounded text-muted-foreground hover:text-foreground"
onclick={(e) => toggleNode(entity.slug, e)}
aria-label={collapsedNodes.has(entity.slug) ? `Expand ${entity.slug}` : `Collapse ${entity.slug}`}
aria-label={collapsedNodes.has(entity.slug)
? `Expand ${entity.slug}`
: `Collapse ${entity.slug}`}
>
{#if collapsedNodes.has(entity.slug)}
<ChevronRightIcon class="size-3.5" />
@@ -186,23 +201,48 @@
<Table.Row>
{@const ssSlug = getSortState('slug')}
<Table.Head>
<SortHeader label="Slug" sorted={ssSlug.sorted} direction={ssSlug.direction} onclick={() => sortBy('slug')} />
<SortHeader
label="Slug"
sorted={ssSlug.sorted}
direction={ssSlug.direction}
onclick={() => sortBy('slug')}
/>
</Table.Head>
{@const ssType = getSortState('type')}
<Table.Head>
<SortHeader label="Type" sorted={ssType.sorted} direction={ssType.direction} onclick={() => sortBy('type')} />
<SortHeader
label="Type"
sorted={ssType.sorted}
direction={ssType.direction}
onclick={() => sortBy('type')}
/>
</Table.Head>
{@const ssName = getSortState('name')}
<Table.Head>
<SortHeader label="Name" sorted={ssName.sorted} direction={ssName.direction} onclick={() => sortBy('name')} />
<SortHeader
label="Name"
sorted={ssName.sorted}
direction={ssName.direction}
onclick={() => sortBy('name')}
/>
</Table.Head>
{@const ssState = getSortState('state')}
<Table.Head>
<SortHeader label="State" sorted={ssState.sorted} direction={ssState.direction} onclick={() => sortBy('state')} />
<SortHeader
label="State"
sorted={ssState.sorted}
direction={ssState.direction}
onclick={() => sortBy('state')}
/>
</Table.Head>
{@const ssHealth = getSortState('health')}
<Table.Head>
<SortHeader label="Health" sorted={ssHealth.sorted} direction={ssHealth.direction} onclick={() => sortBy('health')} />
<SortHeader
label="Health"
sorted={ssHealth.sorted}
direction={ssHealth.direction}
onclick={() => sortBy('health')}
/>
</Table.Head>
</Table.Row>
</Table.Header>

View File

@@ -8,14 +8,22 @@
children
}: {
value?: string
tabs: { value: string; label: string; count?: number; variant?: 'destructive' | 'default' | 'secondary' | 'outline' }[]
tabs: {
value: string
label: string
count?: number
variant?: 'destructive' | 'default' | 'secondary' | 'outline'
}[]
class?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children?: any
} = $props()
</script>
<Tabs.Root bind:value class={['flex flex-1 flex-col overflow-hidden', className].filter(Boolean).join(' ')}>
<Tabs.Root
bind:value
class={['flex flex-1 flex-col overflow-hidden', className].filter(Boolean).join(' ')}
>
<Tabs.List>
{#each tabs as tab}
<Tabs.Trigger value={tab.value}>

View File

@@ -60,7 +60,11 @@
$effect(() => {
const ev = $liveEvents[0]
if (!ev) return
if (ev.type.startsWith('entity.') || ev.type.startsWith('relationship.') || ev.type === 'health.changed') {
if (
ev.type.startsWith('entity.') ||
ev.type.startsWith('relationship.') ||
ev.type === 'health.changed'
) {
load()
}
})
@@ -276,9 +280,11 @@
const svcRaw = new Map<string, number>()
for (const s of services) {
const p = serviceProvider.get(s.slug)
svcRaw.set(s.slug, (p && yById.has(p) ? yById.get(p)! : TOP0))
svcRaw.set(s.slug, p && yById.has(p) ? yById.get(p)! : TOP0)
}
services.sort((a, b) => (svcRaw.get(a.slug)! - svcRaw.get(b.slug)!) || a.name.localeCompare(b.name))
services.sort(
(a, b) => svcRaw.get(a.slug)! - svcRaw.get(b.slug)! || a.name.localeCompare(b.name)
)
let prevY = TOP0 - ROW_H
for (const s of services) {
const y = Math.max(svcRaw.get(s.slug)!, prevY + ROW_H)
@@ -308,7 +314,9 @@
counts[h]++
const ingress = routesTo.get(n.slug)
const ingressEntity = ingress ? bySlugEntity.get(ingress) : undefined
const publicHost = ingress ? (ingressEntity?.name ?? ingress.replace(/^ingress:/, '')) : undefined
const publicHost = ingress
? (ingressEntity?.name ?? ingress.replace(/^ingress:/, ''))
: undefined
const fauth = ingressEntity ? boolAttr(ingressEntity, 'forward_auth') : undefined
const provider = kind === 'service' ? serviceProvider.get(n.slug) : containerHost.get(n.slug)
const mnt = (kind === 'container' ? mountsOf.get(n.slug) : mountsOf.get(provider ?? '')) ?? []
@@ -361,25 +369,41 @@
const n = bySlug.get(slug)!
return LANE_X[n.lane] + (side === 'r' ? LANE_W[n.lane] : 0)
}
const cy = (slug: string) => (bySlug.get(slug)!.y) + NODE_H / 2
const cy = (slug: string) => bySlug.get(slug)!.y + NODE_H / 2
const edges: LaidEdge[] = []
const provChildren = new Map<string, string[]>() // provider -> [child]
// provision edges: host->container, provider->service
for (const c of containers) {
const host = containerHost.get(c.slug)
if (host && bySlug.has(host)) {
const x1 = cx(host, 'r'), y1 = cy(host), x2 = cx(c.slug, 'l'), y2 = cy(c.slug)
const x1 = cx(host, 'r'),
y1 = cy(host),
x2 = cx(c.slug, 'l'),
y2 = cy(c.slug)
const mx = (x1 + x2) / 2
edges.push({ d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, kind: 'prov', s: host, t: c.slug })
edges.push({
d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
kind: 'prov',
s: host,
t: c.slug
})
pushMap(provChildren, host, c.slug)
}
}
for (const s of services) {
const p = serviceProvider.get(s.slug)
if (p && bySlug.has(p)) {
const x1 = cx(p, 'r'), y1 = cy(p), x2 = cx(s.slug, 'l'), y2 = cy(s.slug)
const x1 = cx(p, 'r'),
y1 = cy(p),
x2 = cx(s.slug, 'l'),
y2 = cy(s.slug)
const mx = (x1 + x2) / 2
edges.push({ d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, kind: 'prov', s: p, t: s.slug })
edges.push({
d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
kind: 'prov',
s: p,
t: s.slug
})
pushMap(provChildren, p, s.slug)
}
}
@@ -388,9 +412,17 @@
if (!bySlug.has(srcSlug)) continue
for (const t of targets) {
if (!bySlug.has(t)) continue
const x1 = cx(srcSlug, 'r'), y1 = cy(srcSlug), x2 = cx(t, 'r'), y2 = cy(t)
const x1 = cx(srcSlug, 'r'),
y1 = cy(srcSlug),
x2 = cx(t, 'r'),
y2 = cy(t)
const bulge = Math.max(x1, x2) + 34 + Math.min(70, Math.abs(y1 - y2) * 0.32)
edges.push({ d: `M${x1},${y1} C${bulge},${y1} ${bulge},${y2} ${x2},${y2}`, kind: 'dep', s: srcSlug, t })
edges.push({
d: `M${x1},${y1} C${bulge},${y1} ${bulge},${y2} ${x2},${y2}`,
kind: 'dep',
s: srcSlug,
t
})
}
}
@@ -508,7 +540,8 @@
const q = search.trim().toLowerCase()
if (q) {
const s = new Set<string>()
for (const n of m.nodes) if (n.slug.toLowerCase().includes(q) || n.name.toLowerCase().includes(q)) s.add(n.slug)
for (const n of m.nodes)
if (n.slug.toLowerCase().includes(q) || n.name.toLowerCase().includes(q)) s.add(n.slug)
return s
}
return null
@@ -589,7 +622,9 @@
if (!m || !healthFilter) return [] as LaidNode[]
return m.nodes.filter((n) => n.health === healthFilter)
})
const problemCount = $derived(model ? model.nodes.filter((n) => n.health !== 'healthy').length : 0)
const problemCount = $derived(
model ? model.nodes.filter((n) => n.health !== 'healthy').length : 0
)
</script>
{#if loading && !model}
@@ -636,7 +671,8 @@
<div class="stage relative min-w-0 flex-1 overflow-auto">
<div class="relative" style="width:{model.width}px;height:{model.height}px">
<!-- background click target: clears selection -->
<button type="button" class="reset-layer" aria-label="Clear selection" onclick={reset}></button>
<button type="button" class="reset-layer" aria-label="Clear selection" onclick={reset}
></button>
<!-- lane headers -->
<div class="pointer-events-none absolute inset-x-0 top-0 z-10">
{#each [['Hosts', model.nodes.filter((n) => n.kind === 'host').length], ['Containers', model.nodes.filter((n) => n.kind === 'container').length], ['Services', model.nodes.filter((n) => n.kind === 'service').length]] as [label, count], i}
@@ -647,7 +683,11 @@
</div>
<!-- edges -->
<svg class="pointer-events-none absolute inset-0" width={model.width} height={model.height}>
<svg
class="pointer-events-none absolute inset-0"
width={model.width}
height={model.height}
>
{#each model.edges as e}
<path d={e.d} class="edge {e.kind} {edgeState(e)}" />
{/each}
@@ -661,7 +701,9 @@
class:svc={n.kind === 'service'}
class:is-open={selectedSlug === n.slug}
class:pulse={n.health === 'down'}
style="left:{n.x}px;top:{n.y}px;width:{n.w}px;height:{n.h}px;--nc:{HEALTH_COLOR[n.health]}"
style="left:{n.x}px;top:{n.y}px;width:{n.w}px;height:{n.h}px;--nc:{HEALTH_COLOR[
n.health
]}"
onmouseenter={() => setFocus(n.slug)}
onmouseleave={scheduleClear}
onfocus={() => setFocus(n.slug)}
@@ -673,7 +715,11 @@
title={n.slug}
>
{#if n.publicHost}
<span class="badge" title={n.publicHost + (n.fauth ? ' · forward-auth' : n.fauth === false ? ' · no auth gate' : '')}>
<span
class="badge"
title={n.publicHost +
(n.fauth ? ' · forward-auth' : n.fauth === false ? ' · no auth gate' : '')}
>
{#if n.fauth}
<LockIcon class="size-3" />
{:else if n.fauth === false}
@@ -748,7 +794,9 @@
<div class="blast">
<div class="n" class:warn={detailBlast.length > 0}>{detailBlast.length}</div>
<div class="lbl">
{detailNode.kind === 'service' ? 'downstream service' : 'service'}{detailBlast.length === 1 ? '' : 's'}
{detailNode.kind === 'service'
? 'downstream service'
: 'service'}{detailBlast.length === 1 ? '' : 's'}
affected if this goes down
</div>
</div>
@@ -756,18 +804,40 @@
<div class="det-sec">
<h4>Attributes</h4>
{#if detailNode.kind === 'container'}
<div class="row"><span class="k">type</span><span class="v">{detailNode.type}</span></div>
<div class="row">
<span class="k">type</span><span class="v">{detailNode.type}</span>
</div>
{/if}
{#if detailNode.role}<div class="row"><span class="k">role</span><span class="v">{detailNode.role}</span></div>{/if}
{#if detailNode.ip}<div class="row"><span class="k">ip</span><span class="v">{detailNode.ip}</span></div>{/if}
{#if detailNode.role}<div class="row">
<span class="k">role</span><span class="v">{detailNode.role}</span>
</div>{/if}
{#if detailNode.ip}<div class="row">
<span class="k">ip</span><span class="v">{detailNode.ip}</span>
</div>{/if}
{#if detailNode.publicHost}
<div class="row"><span class="k">url</span><span class="v">{detailNode.publicHost}</span></div>
<div class="row"><span class="k">exposure</span><span class="v">{detailNode.fauth ? 'forward-auth gated' : detailNode.fauth === false ? 'no auth gate' : 'public'}</span></div>
<div class="row">
<span class="k">url</span><span class="v">{detailNode.publicHost}</span>
</div>
<div class="row">
<span class="k">exposure</span><span class="v"
>{detailNode.fauth
? 'forward-auth gated'
: detailNode.fauth === false
? 'no auth gate'
: 'public'}</span
>
</div>
{:else if detailNode.url}
<div class="row"><span class="k">url</span><span class="v">{detailNode.url}</span></div>
<div class="row">
<span class="k">url</span><span class="v">{detailNode.url}</span>
</div>
{/if}
{#if detailNode.mounts.length}<div class="row"><span class="k">mounts</span><span class="v">{detailNode.mounts.join(', ')}</span></div>{/if}
{#if detailNode.repo}<div class="row"><span class="k">config</span><span class="v">{detailNode.repo}</span></div>{/if}
{#if detailNode.mounts.length}<div class="row">
<span class="k">mounts</span><span class="v">{detailNode.mounts.join(', ')}</span>
</div>{/if}
{#if detailNode.repo}<div class="row">
<span class="k">config</span><span class="v">{detailNode.repo}</span>
</div>{/if}
</div>
{#if detailRunsOn.length}
@@ -824,18 +894,24 @@
<div class="p-4">
<h3 class="legend-h">Health</h3>
{#each HEALTH_ORDER as h}
<div class="legend-row"><span class="sw" style="background:{HEALTH_COLOR[h]}"></span>{HEALTH_LABEL[h]}</div>
<div class="legend-row">
<span class="sw" style="background:{HEALTH_COLOR[h]}"></span>{HEALTH_LABEL[h]}
</div>
{/each}
<div class="sep"></div>
<h3 class="legend-h">Lanes</h3>
<div class="legend-row"><span class="lane-swatch"></span>Hosts — physical machines</div>
<div class="legend-row"><span class="lane-swatch"></span>Containers — LXCs &amp; VMs</div>
<div class="legend-row"><span class="lane-swatch"></span>Services — what you actually use</div>
<div class="legend-row">
<span class="lane-swatch"></span>Containers — LXCs &amp; VMs
</div>
<div class="legend-row">
<span class="lane-swatch"></span>Services — what you actually use
</div>
<div class="sep"></div>
<p class="hint">
Every service flows right from the machine that runs it. Hover any node to trace its chain
and blast radius — what breaks if it goes down. Click to open it in a window. Dashed arcs are
service-to-service dependencies.
Every service flows right from the machine that runs it. Hover any node to trace its
chain and blast radius — what breaks if it goes down. Click to open it in a window.
Dashed arcs are service-to-service dependencies.
</p>
</div>
{/if}
@@ -843,7 +919,9 @@
</div>
</div>
{:else}
<div class="flex h-full items-center justify-center text-xs text-muted-foreground">Failed to load graph</div>
<div class="flex h-full items-center justify-center text-xs text-muted-foreground">
Failed to load graph
</div>
{/if}
<style>
@@ -883,7 +961,9 @@
stroke: var(--border);
stroke-width: 1.4;
opacity: 0.6;
transition: opacity 0.16s, stroke 0.16s;
transition:
opacity 0.16s,
stroke 0.16s;
}
.edge.dep {
stroke-dasharray: 4 3;
@@ -919,7 +999,11 @@
border-radius: 9px;
overflow: hidden;
cursor: pointer;
transition: opacity 0.16s, border-color 0.14s, transform 0.12s, box-shadow 0.14s;
transition:
opacity 0.16s,
border-color 0.14s,
transform 0.12s,
box-shadow 0.14s;
}
.node::before {
content: '';
@@ -1001,7 +1085,10 @@
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
transition: color 0.12s, border-color 0.12s, background 0.12s;
transition:
color 0.12s,
border-color 0.12s,
background 0.12s;
}
.chip:hover {
border-color: var(--primary);

View File

@@ -5,7 +5,8 @@
import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
// Prop-driven (not store-imported) — see SessionGraph.svelte for why.
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } = $props()
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } =
$props()
let freeText = $state('')
let submitting = $state(false)
@@ -48,7 +49,13 @@
{#if q.context.options?.length}
<div class="ml-6 flex flex-wrap gap-1.5">
{#each q.context.options as opt}
<Button size="sm" variant="outline" class="h-7 px-2.5 text-xs" disabled={submitting} onclick={() => submit(opt)}>
<Button
size="sm"
variant="outline"
class="h-7 px-2.5 text-xs"
disabled={submitting}
onclick={() => submit(opt)}
>
{opt}
</Button>
{/each}
@@ -69,7 +76,12 @@
}
}}
/>
<Button size="sm" class="h-7 px-2.5 text-xs" disabled={!freeText.trim() || submitting} onclick={() => submit(freeText)}>
<Button
size="sm"
class="h-7 px-2.5 text-xs"
disabled={!freeText.trim() || submitting}
onclick={() => submit(freeText)}
>
Send
</Button>
</div>

View File

@@ -5,7 +5,15 @@
// these can be open (and independently live) at once.
import { onDestroy, onMount } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
import {
chatFor,
loadSessionChat,
sendSessionMessage,
cancelSessionStream,
stopSessionPolling,
dismissError,
chatErrors
} from '$lib/stores/chat'
import { activityLogFor } from '$lib/stores/activity'
import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace'
import ChatThread from '$lib/components/ChatThread.svelte'
@@ -77,7 +85,9 @@
<div class="flex h-full min-h-0">
{#if loading}
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">Loading…</div>
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
Loading…
</div>
{:else if $chatNotFound}
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center">
<p class="text-sm text-muted-foreground">Task not found.</p>

View File

@@ -18,7 +18,11 @@
// Prop-driven (not store-imported) so this can render either the main
// page's global "current session" data or a floating task window's own
// per-session data — see TaskContextPanel.svelte, which supplies both.
let { messages, touched, healthDiffs }: { messages: ChatMessage[]; touched: TouchedEntity[]; healthDiffs: HealthDiff[] } = $props()
let {
messages,
touched,
healthDiffs
}: { messages: ChatMessage[]; touched: TouchedEntity[]; healthDiffs: HealthDiff[] } = $props()
// SVG ids are document-global, not scoped to this <svg> — several task
// windows can each have their own Scope graph open at once, and without a
@@ -138,7 +142,12 @@
const curSlugs = new Set(current.map((n) => n.slug))
let changed = desiredSlugs.size !== curSlugs.size
if (!changed) for (const s of desiredSlugs) if (!curSlugs.has(s)) { changed = true; break }
if (!changed)
for (const s of desiredSlugs)
if (!curSlugs.has(s)) {
changed = true
break
}
if (!changed) return
const bySlug = new Map(current.map((n) => [n.slug, n]))
@@ -178,10 +187,19 @@
return
}
sim = forceSimulation(nodes)
.force('link', forceLink<Node, Edge>(links).id((n) => n.slug).distance(48).strength(0.5))
.force(
'link',
forceLink<Node, Edge>(links)
.id((n) => n.slug)
.distance(48)
.strength(0.5)
)
.force('charge', forceManyBody().strength(-150).distanceMax(240))
.force('center', forceCenter(cw / 2, ch / 2))
.force('collide', forceCollide<Node>((n) => nodeRadius(n) + 6))
.force(
'collide',
forceCollide<Node>((n) => nodeRadius(n) + 6)
)
.force('x', forceX(cw / 2).strength(0.06))
.force('y', forceY(ch / 2).strength(0.06))
.velocityDecay(0.34)
@@ -232,7 +250,9 @@
unknown: 'var(--muted-foreground)'
}
function nodeColor(n: Node): string {
return n.health ? healthColor[n.health] ?? 'var(--muted-foreground)' : 'var(--muted-foreground)'
return n.health
? (healthColor[n.health] ?? 'var(--muted-foreground)')
: 'var(--muted-foreground)'
}
function nodeRadius(n: Node): number {
return 6 + Math.min(Math.sqrt(n.degree) * 1.5, 6)
@@ -306,10 +326,17 @@
const selectedRelations = $derived(
selected
? links
.filter((l) => endpointSlug(l.source) === selected!.slug || endpointSlug(l.target) === selected!.slug)
.filter(
(l) =>
endpointSlug(l.source) === selected!.slug || endpointSlug(l.target) === selected!.slug
)
.map((l) => {
const outgoing = endpointSlug(l.source) === selected!.slug
return { dir: outgoing ? '→' : '←', type: l.type, other: outgoing ? endpointSlug(l.target) : endpointSlug(l.source) }
return {
dir: outgoing ? '→' : '←',
type: l.type,
other: outgoing ? endpointSlug(l.target) : endpointSlug(l.source)
}
})
: []
)
@@ -317,7 +344,9 @@
<aside class="flex h-full min-h-0 flex-col bg-card/40">
{#if nowTouching}
<div class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary">
<div
class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary"
>
<span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
Now touching <code class="font-mono">{nowTouching.slug}</code>
</div>
@@ -325,22 +354,85 @@
<div bind:this={container} class="relative min-h-0 flex-1 overflow-hidden">
{#if nodes.length === 0}
<div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-4 px-6 text-center">
<div
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-4 px-6 text-center"
>
<svg viewBox="0 0 120 120" class="size-24 text-muted-foreground/40" fill="none">
<circle cx="60" cy="60" r="6" fill="currentColor">
<animate attributeName="opacity" values="0.4;1;0.4" dur="2.4s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0.4;1;0.4"
dur="2.4s"
repeatCount="indefinite"
/>
</circle>
<g stroke="currentColor" stroke-width="1" opacity="0.5">
<line x1="60" y1="60" x2="26" y2="34"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3s" repeatCount="indefinite" /></line>
<line x1="60" y1="60" x2="96" y2="40"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3.4s" repeatCount="indefinite" /></line>
<line x1="60" y1="60" x2="34" y2="92"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="2.8s" repeatCount="indefinite" /></line>
<line x1="60" y1="60" x2="92" y2="90"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3.1s" repeatCount="indefinite" /></line>
<line x1="60" y1="60" x2="26" y2="34"
><animate
attributeName="opacity"
values="0.1;0.5;0.1"
dur="3s"
repeatCount="indefinite"
/></line
>
<line x1="60" y1="60" x2="96" y2="40"
><animate
attributeName="opacity"
values="0.1;0.5;0.1"
dur="3.4s"
repeatCount="indefinite"
/></line
>
<line x1="60" y1="60" x2="34" y2="92"
><animate
attributeName="opacity"
values="0.1;0.5;0.1"
dur="2.8s"
repeatCount="indefinite"
/></line
>
<line x1="60" y1="60" x2="92" y2="90"
><animate
attributeName="opacity"
values="0.1;0.5;0.1"
dur="3.1s"
repeatCount="indefinite"
/></line
>
</g>
<g fill="currentColor">
<circle cx="26" cy="34" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3s" repeatCount="indefinite" /></circle>
<circle cx="96" cy="40" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.4s" repeatCount="indefinite" /></circle>
<circle cx="34" cy="92" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="2.8s" repeatCount="indefinite" /></circle>
<circle cx="92" cy="90" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.1s" repeatCount="indefinite" /></circle>
<circle cx="26" cy="34" r="3.5"
><animate
attributeName="opacity"
values="0.2;0.7;0.2"
dur="3s"
repeatCount="indefinite"
/></circle
>
<circle cx="96" cy="40" r="3.5"
><animate
attributeName="opacity"
values="0.2;0.7;0.2"
dur="3.4s"
repeatCount="indefinite"
/></circle
>
<circle cx="34" cy="92" r="3.5"
><animate
attributeName="opacity"
values="0.2;0.7;0.2"
dur="2.8s"
repeatCount="indefinite"
/></circle
>
<circle cx="92" cy="90" r="3.5"
><animate
attributeName="opacity"
values="0.2;0.7;0.2"
dur="3.1s"
repeatCount="indefinite"
/></circle
>
</g>
</svg>
<p class="max-w-[16rem] text-xs leading-relaxed text-muted-foreground">
@@ -394,7 +486,8 @@
{#if node.x != null && node.y != null}
{@const r = nodeRadius(node)}
{@const isSel = selected?.slug === node.slug}
{@const dim = selected && !isSel && !selectedRelations.some((rel) => rel.other === node.slug)}
{@const dim =
selected && !isSel && !selectedRelations.some((rel) => rel.other === node.slug)}
{@const isTouched = node.slug in touchedBySlug}
{@const diff = diffBySlug[node.slug]}
<g
@@ -410,12 +503,33 @@
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
{/if}
{#if isTouched}
<circle r={r + 4} fill="none" stroke="var(--primary)" stroke-width="1.5" opacity="0.8">
<animate attributeName="r" values="{r + 3};{r + 8};{r + 3}" dur="1.6s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.8;0.1;0.8" dur="1.6s" repeatCount="indefinite" />
<circle
r={r + 4}
fill="none"
stroke="var(--primary)"
stroke-width="1.5"
opacity="0.8"
>
<animate
attributeName="r"
values="{r + 3};{r + 8};{r + 3}"
dur="1.6s"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0.8;0.1;0.8"
dur="1.6s"
repeatCount="indefinite"
/>
</circle>
{/if}
<circle r={r} fill={nodeColor(node)} stroke={isSel ? 'var(--foreground)' : 'var(--background)'} stroke-width={isSel ? 2 : 1.5} />
<circle
{r}
fill={nodeColor(node)}
stroke={isSel ? 'var(--foreground)' : 'var(--background)'}
stroke-width={isSel ? 2 : 1.5}
/>
<text
y={r + 10}
text-anchor="middle"

View File

@@ -12,7 +12,11 @@
<svg viewBox="0 0 24 24" class={className} fill="none" aria-hidden="true">
{#each Array.from({ length: TICKS }) as _, i (i)}
<rect
x="11" y="1.5" width="2" height="6" rx="1"
x="11"
y="1.5"
width="2"
height="6"
rx="1"
fill="currentColor"
opacity="0.15"
transform="rotate({i * (360 / TICKS)} 12 12)"

View File

@@ -13,15 +13,18 @@
class?: string
} = $props()
const variantMap: Record<StatusKind, Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>> = {
const variantMap: Record<
StatusKind,
Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>
> = {
risk: {
destructive: 'destructive',
config_mutation: 'secondary',
config_mutation: 'secondary'
},
severity: {
critical: 'destructive',
warning: 'secondary',
info: 'default',
info: 'default'
},
execution: {
failed: 'destructive',
@@ -30,13 +33,13 @@
cancelled: 'destructive',
completed: 'default',
running: 'secondary',
approved: 'secondary',
approved: 'secondary'
},
type: {
runbook: 'secondary',
investigation: 'default',
investigation: 'default'
},
default: {},
default: {}
}
function variant(): 'default' | 'secondary' | 'destructive' | 'outline' {

View File

@@ -1,7 +1,15 @@
<script lang="ts">
import { onMount } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { startWorkspace, planSteps, currentTask, touched, healthDiffs, workspaceFor, taskFor } from '$lib/stores/workspace'
import {
startWorkspace,
planSteps,
currentTask,
touched,
healthDiffs,
workspaceFor,
taskFor
} from '$lib/stores/workspace'
import { streaming, messages, chatFor } from '$lib/stores/chat'
import { activityLog, activityLogFor } from '$lib/stores/activity'
import SessionGraph from './SessionGraph.svelte'
@@ -70,7 +78,12 @@
<div class="flex h-full min-h-0 flex-col">
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
<!-- Scope -->
<Pane bind:size={sizes[0]} minSize={scopeOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={scopeOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
<Pane
bind:size={sizes[0]}
minSize={scopeOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE}
maxSize={scopeOpen ? 100 : COLLAPSED_SIZE}
class="flex flex-col"
>
<button
type="button"
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
@@ -79,21 +92,36 @@
scopeOpen = !scopeOpen
}}
>
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
class="size-3"
/>{/if}
<span>Scope</span>
{#if !scopeOpen}
<span class="ml-auto font-normal normal-case">{$touchedStore.length ? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}` : 'Graph'}</span>
<span class="ml-auto font-normal normal-case"
>{$touchedStore.length
? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}`
: 'Graph'}</span
>
{/if}
</button>
{#if scopeOpen}
<div class="min-h-0 flex-1">
<SessionGraph messages={$messagesStore} touched={$touchedStore} healthDiffs={$healthDiffsStore} />
<SessionGraph
messages={$messagesStore}
touched={$touchedStore}
healthDiffs={$healthDiffsStore}
/>
</div>
{/if}
</Pane>
<!-- Activity (merged plan + event log) -->
<Pane bind:size={sizes[1]} minSize={activityOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={activityOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
<Pane
bind:size={sizes[1]}
minSize={activityOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE}
maxSize={activityOpen ? 100 : COLLAPSED_SIZE}
class="flex flex-col"
>
<button
type="button"
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
@@ -102,25 +130,39 @@
activityOpen = !activityOpen
}}
>
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
class="size-3"
/>{/if}
<span>Activity</span>
{#if $streamingStore && activityRunning > 0}
<Spinner class="size-3 text-primary" />
{/if}
{#if planTotal > 0}
<span class="font-normal normal-case tabular-nums {planDone === planTotal ? 'text-muted-foreground' : 'text-primary'}">{planDone}/{planTotal}</span>
<span
class="font-normal normal-case tabular-nums {planDone === planTotal
? 'text-muted-foreground'
: 'text-primary'}">{planDone}/{planTotal}</span
>
{/if}
{#if !activityOpen && planTotal === 0}
{#if $taskStore?.goal}
<span class="ml-auto max-w-[120px] truncate font-normal normal-case">{$taskStore.goal}</span>
<span class="ml-auto max-w-[120px] truncate font-normal normal-case"
>{$taskStore.goal}</span
>
{:else}
<span class="ml-auto font-normal normal-case text-muted-foreground">No activity yet</span>
<span class="ml-auto font-normal normal-case text-muted-foreground"
>No activity yet</span
>
{/if}
{/if}
</button>
{#if activityOpen}
<div class="min-h-0 flex-1 overflow-hidden">
<UnifiedTimeline entries={$activityLogStore} planSteps={$planStepsStore} streaming={$streamingStore} />
<UnifiedTimeline
entries={$activityLogStore}
planSteps={$planStepsStore}
streaming={$streamingStore}
/>
</div>
{/if}
</Pane>

View File

@@ -51,13 +51,17 @@
<span class="min-w-0 flex-1">
<span class="block truncate text-xs text-foreground/90">{label}</span>
{#if argsSummary}
<span class="block truncate font-mono text-[10px] text-muted-foreground/60">{argsSummary}</span>
<span class="block truncate font-mono text-[10px] text-muted-foreground/60"
>{argsSummary}</span
>
{/if}
</span>
<span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span>
{#if hasDetail}
<ChevronRight
class="mt-px size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded ? 'rotate-90' : ''}"
class="mt-px size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded
? 'rotate-90'
: ''}"
/>
{/if}
</button>
@@ -66,20 +70,41 @@
<div class="space-y-2 px-2 pb-2 pl-7">
{#if tool.args}
<div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Args</div>
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.args, null, 2)}</pre>
<div
class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"
>
Args
</div>
<pre
class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(
tool.args,
null,
2
)}</pre>
</div>
{/if}
{#if tool.result !== undefined && tool.result !== null}
<div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Result</div>
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.result, null, 2)}</pre>
<div
class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"
>
Result
</div>
<pre
class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(
tool.result,
null,
2
)}</pre>
</div>
{/if}
{#if tool.error}
<div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">Error</div>
<pre class="max-h-48 overflow-x-auto rounded-md border border-destructive/20 bg-destructive/5 p-2 text-[11px] text-destructive">{tool.error}</pre>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">
Error
</div>
<pre
class="max-h-48 overflow-x-auto rounded-md border border-destructive/20 bg-destructive/5 p-2 text-[11px] text-destructive">{tool.error}</pre>
</div>
{/if}
</div>

View File

@@ -30,7 +30,11 @@
// current step visible while the agent works (follow mode disengages if
// the operator scrolls down into history, re-engages when streaming
// starts again)
let { entries, planSteps: steps, streaming = false }: {
let {
entries,
planSteps: steps,
streaming = false
}: {
entries: ActivityEntry[]
planSteps: PlanStep[]
streaming?: boolean
@@ -75,7 +79,9 @@
continue
}
const tools = entries.filter(
(e) => e.stepSeq === s.seq && (e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
(e) =>
e.stepSeq === s.seq &&
(e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
)
const stepEntry = entries.find((e) => e.id === s.id)
// Timed from the step's own entry, else its earliest tool — so a step
@@ -134,7 +140,8 @@
// entries land while following (instant, to avoid scroll-queue jank).
$effect(() => {
if (!currentId || !follow || !container) return
container.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
container
.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
})
let lastEntryCount = 0
@@ -157,7 +164,12 @@
// height so tools inside an expanded step stay on the line; first/last
// items clip theirs to their node/tool centers so the line never dangles
// past the timeline's ends.
function segClass(status: string, isFirst: boolean, isLast: boolean, expandedWithTools: boolean): string {
function segClass(
status: string,
isFirst: boolean,
isLast: boolean,
expandedWithTools: boolean
): string {
let color = 'bg-border'
if (status === 'done') color = 'bg-primary/60'
else if (status === 'running') color = 'bg-primary/40'
@@ -172,11 +184,16 @@
function entryIcon(entry: ActivityEntry) {
switch (entry.type) {
case 'goal': return MilestoneIcon
case 'knowledge': return SparklesIcon
case 'complete': return FlagIcon
case 'question': return HelpCircleIcon
default: return WrenchIcon
case 'goal':
return MilestoneIcon
case 'knowledge':
return SparklesIcon
case 'complete':
return FlagIcon
case 'question':
return HelpCircleIcon
default:
return WrenchIcon
}
}
function hhmm(ts: number): string {
@@ -185,10 +202,18 @@
}
function hhmmss(ts: number): string {
if (!ts) return ''
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
return new Date(ts).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
function prettyPrint(raw: string): string {
try { return JSON.stringify(JSON.parse(raw), null, 2) } catch { return raw }
try {
return JSON.stringify(JSON.parse(raw), null, 2)
} catch {
return raw
}
}
</script>
@@ -197,19 +222,57 @@
{#if items.length === 0}
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
<svg viewBox="0 0 64 110" class="h-14 w-auto text-muted-foreground/40" fill="none">
<line x1="32" y1="8" x2="32" y2="102" stroke="currentColor" stroke-width="1" stroke-dasharray="2.5 4" opacity="0.35" />
<line
x1="32"
y1="8"
x2="32"
y2="102"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="2.5 4"
opacity="0.35"
/>
<circle cx="32" cy="22" r="4" fill="currentColor">
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0.25;0.9;0.25"
dur="2.4s"
repeatCount="indefinite"
/>
</circle>
<circle cx="32" cy="55" r="4" fill="currentColor">
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0.25;0.9;0.25"
dur="2.4s"
begin="0.6s"
repeatCount="indefinite"
/>
</circle>
<circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5">
<animate attributeName="r" values="4;11;4" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0;0.6" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
<animate
attributeName="r"
values="4;11;4"
dur="2.4s"
begin="0.6s"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0.6;0;0.6"
dur="2.4s"
begin="0.6s"
repeatCount="indefinite"
/>
</circle>
<circle cx="32" cy="88" r="4" fill="currentColor">
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0.25;0.9;0.25"
dur="2.4s"
begin="1.2s"
repeatCount="indefinite"
/>
</circle>
</svg>
<p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p>
@@ -227,24 +290,42 @@
{@const expandedWithTools = open && item.tools.length > 0}
<!-- Step node on the backbone -->
<div class="relative" data-tl-id={item.step.id}>
<span class="pointer-events-none absolute left-[17px] w-px {segClass(st, isFirst, isLast, expandedWithTools)}" aria-hidden="true"></span>
<span
class="pointer-events-none absolute left-[17px] w-px {segClass(
st,
isFirst,
isLast,
expandedWithTools
)}"
aria-hidden="true"
></span>
<button
type="button"
class="relative flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs {expandable ? 'cursor-pointer hover:bg-muted/30' : 'cursor-default'} {st === 'running' ? 'bg-primary/5' : ''}"
class="relative flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs {expandable
? 'cursor-pointer hover:bg-muted/30'
: 'cursor-default'} {st === 'running' ? 'bg-primary/5' : ''}"
onclick={() => expandable && toggleStep(item.step)}
aria-expanded={open}
disabled={!expandable}
>
<!-- Filled status node -->
<span class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
{st === 'done' ? 'bg-primary'
: st === 'running' ? 'bg-background'
: st === 'failed' ? 'bg-destructive'
: st === 'blocked' ? 'bg-warning/25 border border-warning'
: st === 'skipped' || st === 'replaced' ? 'bg-muted'
: 'bg-background border border-muted-foreground/40'}">
<span
class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
{st === 'done'
? 'bg-primary'
: st === 'running'
? 'bg-background'
: st === 'failed'
? 'bg-destructive'
: st === 'blocked'
? 'bg-warning/25 border border-warning'
: st === 'skipped' || st === 'replaced'
? 'bg-muted'
: 'bg-background border border-muted-foreground/40'}"
>
{#if st === 'running'}
<span class="absolute -inset-0.5 animate-ping rounded-full bg-primary/30"></span>
<span class="absolute -inset-0.5 animate-ping rounded-full bg-primary/30"
></span>
<Spinner class="relative size-3.5 text-primary" />
{:else if st === 'done'}
<CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} />
@@ -256,15 +337,28 @@
<SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} />
{/if}
</span>
<span title={item.step.title} class="min-w-0 flex-1 leading-snug {open ? 'whitespace-normal' : 'truncate'} {st === 'done' ? 'text-muted-foreground' : st === 'running' ? 'font-medium text-foreground' : 'text-muted-foreground'}">
<span
title={item.step.title}
class="min-w-0 flex-1 leading-snug {open
? 'whitespace-normal'
: 'truncate'} {st === 'done'
? 'text-muted-foreground'
: st === 'running'
? 'font-medium text-foreground'
: 'text-muted-foreground'}"
>
{item.step.title}
</span>
{#if hhmm(item.ts)}
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60">{hhmm(item.ts)}</span>
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
>{hhmm(item.ts)}</span
>
{/if}
{#if item.tools.length > 0}
<span class="shrink-0 text-muted-foreground/60">
{#if open}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
{#if open}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
class="size-3"
/>{/if}
</span>
{/if}
</button>
@@ -275,10 +369,19 @@
{@const tOpen = expandedTools.has(tool.id)}
<div class="relative" data-tl-id={tool.id}>
<!-- Branch stub: backbone → tool -->
<span class="pointer-events-none absolute left-[17px] top-[9.5px] h-px w-[17px] {tool.status === 'failed' ? 'bg-destructive/40' : 'bg-border'}" aria-hidden="true"></span>
<span
class="pointer-events-none absolute left-[17px] top-[9.5px] h-px w-[17px] {tool.status ===
'failed'
? 'bg-destructive/40'
: 'bg-border'}"
aria-hidden="true"
></span>
<button
type="button"
class="flex w-full items-center gap-1.5 py-1 pl-9 pr-3 text-left text-[11px] {(tool.args || tool.detail) ? 'cursor-pointer hover:bg-muted/20' : 'cursor-default'}"
class="flex w-full items-center gap-1.5 py-1 pl-9 pr-3 text-left text-[11px] {tool.args ||
tool.detail
? 'cursor-pointer hover:bg-muted/20'
: 'cursor-default'}"
onclick={() => (tool.args || tool.detail) && toggleTool(tool.id)}
>
<span class="flex size-3 shrink-0 items-center justify-center">
@@ -290,24 +393,47 @@
<CheckIcon class="size-2.5 text-primary/70" strokeWidth={3.5} />
{/if}
</span>
<span title={tool.description} class="min-w-0 flex-1 truncate leading-snug {tool.status === 'done' ? 'text-muted-foreground' : tool.status === 'failed' ? 'text-destructive' : 'text-foreground/80'}">
<span
title={tool.description}
class="min-w-0 flex-1 truncate leading-snug {tool.status === 'done'
? 'text-muted-foreground'
: tool.status === 'failed'
? 'text-destructive'
: 'text-foreground/80'}"
>
{tool.description}
</span>
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/50">{hhmm(tool.timestamp)}</span>
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/50"
>{hhmm(tool.timestamp)}</span
>
</button>
{#if tOpen}
<div transition:slide={{ duration: 120 }} class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3">
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
<div
transition:slide={{ duration: 120 }}
class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3"
>
<div
class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70"
>
<span class="capitalize">{tool.status}</span>
<span aria-hidden="true">·</span>
<span>{hhmmss(tool.timestamp)}</span>
{#if tool.toolName}<span aria-hidden="true">·</span><code class="font-mono">{tool.toolName}</code>{/if}
{#if tool.toolName}<span aria-hidden="true">·</span><code
class="font-mono">{tool.toolName}</code
>{/if}
</div>
{#if tool.args}
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(tool.args)}</pre>
<pre
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
tool.args
)}</pre>
{/if}
{#if tool.detail}
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {tool.status === 'failed' ? 'text-destructive' : 'text-muted-foreground'}">{prettyPrint(tool.detail)}</pre>
<pre
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {tool.status ===
'failed'
? 'text-destructive'
: 'text-muted-foreground'}">{prettyPrint(tool.detail)}</pre>
{/if}
</div>
{/if}
@@ -322,14 +448,31 @@
{@const Icon = entryIcon(e)}
{@const eOpen = expandedTools.has(e.id)}
<div class="relative" data-tl-id={e.id}>
<span class="pointer-events-none absolute left-[17px] w-px {segClass(e.status, isFirst, isLast, false)}" aria-hidden="true"></span>
<span
class="pointer-events-none absolute left-[17px] w-px {segClass(
e.status,
isFirst,
isLast,
false
)}"
aria-hidden="true"
></span>
<button
type="button"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] {(e.args || e.detail) ? 'cursor-pointer hover:bg-muted/30' : 'cursor-default'}"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] {e.args ||
e.detail
? 'cursor-pointer hover:bg-muted/30'
: 'cursor-default'}"
onclick={() => (e.args || e.detail) && toggleTool(e.id)}
>
<span class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background
{e.status === 'failed' ? 'border-destructive text-destructive' : e.status === 'running' ? 'border-primary text-primary' : 'border-border text-primary'}">
<span
class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background
{e.status === 'failed'
? 'border-destructive text-destructive'
: e.status === 'running'
? 'border-primary text-primary'
: 'border-border text-primary'}"
>
{#if e.status === 'running'}
<Spinner class="size-2.5" />
{:else if e.status === 'failed'}
@@ -338,24 +481,43 @@
<Icon class="size-2" strokeWidth={2.5} />
{/if}
</span>
<span title={e.description} class="min-w-0 flex-1 truncate leading-snug {e.status === 'done' ? 'text-muted-foreground' : 'text-foreground/80'}">
<span
title={e.description}
class="min-w-0 flex-1 truncate leading-snug {e.status === 'done'
? 'text-muted-foreground'
: 'text-foreground/80'}"
>
{e.description}
</span>
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60">{hhmm(e.timestamp)}</span>
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
>{hhmm(e.timestamp)}</span
>
</button>
{#if eOpen}
<div transition:slide={{ duration: 120 }} class="flex flex-col gap-1 pb-1.5 pl-9 pr-3">
<div
transition:slide={{ duration: 120 }}
class="flex flex-col gap-1 pb-1.5 pl-9 pr-3"
>
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
<span class="capitalize">{e.status}</span>
<span aria-hidden="true">·</span>
<span>{hhmmss(e.timestamp)}</span>
{#if e.toolName}<span aria-hidden="true">·</span><code class="font-mono">{e.toolName}</code>{/if}
{#if e.toolName}<span aria-hidden="true">·</span><code class="font-mono"
>{e.toolName}</code
>{/if}
</div>
{#if e.args}
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(e.args)}</pre>
<pre
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
e.args
)}</pre>
{/if}
{#if e.detail}
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {e.status === 'failed' ? 'text-destructive' : 'text-muted-foreground'}">{prettyPrint(e.detail)}</pre>
<pre
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {e.status ===
'failed'
? 'text-destructive'
: 'text-muted-foreground'}">{prettyPrint(e.detail)}</pre>
{/if}
</div>
{/if}

View File

@@ -17,11 +17,11 @@
type Row = Record<string, any>
const renderers: Record<string, unknown> = {
'badge': BadgeRenderer,
badge: BadgeRenderer,
'health-dot': HealthDotRenderer,
'relative-time': RelativeTimeRenderer,
'date': DateRenderer,
'status-badge': StatusBadgeRenderer,
date: DateRenderer,
'status-badge': StatusBadgeRenderer
}
let {
@@ -119,7 +119,11 @@
<div class={['flex flex-col h-full min-h-0', className].filter(Boolean).join(' ')}>
<Toolbar {table} {searchable} {paginated} onSearchChange={handleSearch} {children} />
<div class={['flex flex-col min-h-0 flex-1', bordered ? 'rounded-xl border' : ''].filter(Boolean).join(' ')}>
<div
class={['flex flex-col min-h-0 flex-1', bordered ? 'rounded-xl border' : '']
.filter(Boolean)
.join(' ')}
>
<table class="w-full caption-bottom text-sm table-fixed">
<thead class="[&_tr]:border-b">
<tr>
@@ -130,7 +134,9 @@
'bg-card/95',
col.headerClass,
colAlignClass(col)
].filter(Boolean).join(' ')}
]
.filter(Boolean)
.join(' ')}
style={colStyle(col)}
>
{#if col.sortable !== false}
@@ -156,8 +162,17 @@
{#each skeletonWidths as w, i}
<tr class="border-b transition-colors hover:bg-transparent">
{#each visibleCols as col (col.key)}
<td class={[col.class, colAlignClass(col), colTruncateClass(col)].filter(Boolean).join(' ')} style={colStyle(col)}>
<Skeleton class="h-4 {skeletonWidths[(i + visibleCols.indexOf(col)) % skeletonWidths.length]}" />
<td
class={[col.class, colAlignClass(col), colTruncateClass(col)]
.filter(Boolean)
.join(' ')}
style={colStyle(col)}
>
<Skeleton
class="h-4 {skeletonWidths[
(i + visibleCols.indexOf(col)) % skeletonWidths.length
]}"
/>
</td>
{/each}
</tr>
@@ -171,11 +186,18 @@
'border-b transition-colors hover:bg-muted/50',
onRowClick ? 'cursor-pointer' : '',
selected === (row.id ?? row.slug) ? 'bg-muted' : ''
].filter(Boolean).join(' ')}
]
.filter(Boolean)
.join(' ')}
tabindex={onRowClick ? 0 : undefined}
onclick={onRowClick ? () => onRowClick(row) : undefined}
onkeydown={onRowClick
? (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRowClick(row) } }
? (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onRowClick(row)
}
}
: undefined}
>
{#each visibleCols as col (col.key)}
@@ -186,19 +208,21 @@
col.class,
colAlignClass(col),
colTruncateClass(col)
].filter(Boolean).join(' ')}
]
.filter(Boolean)
.join(' ')}
style={colStyle(col)}
>
{#if typeof col.render === 'string'}
{@const R = renderers[col.render]}
{#if R}
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
<R value={val} {row} {...(col.renderProps ?? {})} />
<R value={val} {row} {...col.renderProps ?? {}} />
{:else}
{String(val ?? '—')}
{/if}
{:else if typeof col.render === 'function'}
<col.render {row} value={val} {...(col.renderProps ?? {})} />
<col.render {row} value={val} {...col.renderProps ?? {}} />
{:else}
{String(val ?? '—')}
{/if}

View File

@@ -2,7 +2,10 @@
import * as Select from '$lib/components/ui/select'
import type { TableHandler } from '@vincjo/datatables'
let { table, class: className }: { table: TableHandler<Record<string, unknown>>; class?: string } = $props()
let {
table,
class: className
}: { table: TableHandler<Record<string, unknown>>; class?: string } = $props()
const options = [10, 20, 50, 100]
let value = $state('20')

View File

@@ -16,6 +16,13 @@
</script>
<div class="flex justify-end gap-2">
<Button size="sm" disabled={deciding === row.id} onclick={() => onApprove?.(row.id)}>Approve</Button>
<Button size="sm" variant="destructive" disabled={deciding === row.id} onclick={() => onDeny?.(row.id)}>Deny</Button>
<Button size="sm" disabled={deciding === row.id} onclick={() => onApprove?.(row.id)}
>Approve</Button
>
<Button
size="sm"
variant="destructive"
disabled={deciding === row.id}
onclick={() => onDeny?.(row.id)}>Deny</Button
>
</div>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { Badge, type BadgeVariant } from '$lib/components/ui/badge'
let { value, variant = 'outline' as BadgeVariant }: { value: unknown; variant?: BadgeVariant } = $props()
let { value, variant = 'outline' as BadgeVariant }: { value: unknown; variant?: BadgeVariant } =
$props()
</script>
<Badge {variant}>{String(value ?? '—')}</Badge>

View File

@@ -23,7 +23,7 @@
</script>
{#if health}
<span class="flex items-center gap-1.5 text-xs" title={title}>
<span class="flex items-center gap-1.5 text-xs" {title}>
<span class="size-2 shrink-0 rounded-full {dot[row.health ?? ''] ?? ''}"></span>
<span class="text-muted-foreground">{relativeTime(lastCheck)}</span>
</span>

View File

@@ -19,8 +19,13 @@
<div class="flex justify-end gap-2">
{#if row.state === 'raised'}
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onAck?.(row.id)}>Ack</Button>
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onAck?.(row.id)}
>Ack</Button
>
{/if}
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onMute?.(row.id)}>Mute 1h</Button>
<Button size="sm" disabled={acting === row.id} onclick={() => onResolve?.(row.id)}>Resolve</Button>
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onMute?.(row.id)}
>Mute 1h</Button
>
<Button size="sm" disabled={acting === row.id} onclick={() => onResolve?.(row.id)}>Resolve</Button
>
</div>

View File

@@ -1,21 +1,28 @@
<script lang="ts">
import { Badge } from '$lib/components/ui/badge'
let { value, kind = 'default' }: { value: unknown; kind?: 'risk' | 'severity' | 'execution' | 'state' | 'type' | 'default' } = $props()
let {
value,
kind = 'default'
}: { value: unknown; kind?: 'risk' | 'severity' | 'execution' | 'state' | 'type' | 'default' } =
$props()
const v = $derived(String(value ?? ''))
const variantMap: Record<string, Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>> = {
const variantMap: Record<
string,
Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>
> = {
risk: {
destructive: 'destructive',
config_mutation: 'secondary',
default: 'default',
default: 'default'
},
severity: {
critical: 'destructive',
warning: 'secondary',
info: 'default',
default: 'default',
default: 'default'
},
execution: {
failed: 'destructive',
@@ -25,19 +32,19 @@
completed: 'default',
running: 'secondary',
approved: 'secondary',
default: 'outline',
default: 'outline'
},
state: {
active: 'default',
healthy: 'default',
default: 'outline',
default: 'outline'
},
type: {
runbook: 'secondary',
investigation: 'default',
default: 'outline',
default: 'outline'
},
default: { default: 'default' },
default: { default: 'default' }
}
const variant = $derived.by(() => {

View File

@@ -45,7 +45,9 @@
// field.
function onWindowKeydown(e: KeyboardEvent) {
const target = e.target as HTMLElement | null
const editable = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
const editable =
!!target &&
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
if (editable) return
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z') return
e.preventDefault()
@@ -84,7 +86,11 @@
<div class="fixed inset-0 flex flex-col">
<div class="relative min-h-0 flex-1 overflow-hidden" role="presentation">
{#if bgActive}
<div class="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden="true" style={bgOuterStyle}>
<div
class="pointer-events-none absolute inset-0 z-0 overflow-hidden"
aria-hidden="true"
style={bgOuterStyle}
>
<div class="absolute" style={bgInnerStyle}></div>
</div>
{/if}
@@ -97,9 +103,7 @@
old `currentTarget === target` event check. Left-click on bare
desktop blurs the focused window (the familiar "click empty
desktop to deselect" affordance). -->
<ContextMenu.Trigger
class="absolute inset-0 z-0"
onclick={() => wm.blur()}
<ContextMenu.Trigger class="absolute inset-0 z-0" onclick={() => wm.blur()}
></ContextMenu.Trigger>
<ContextMenu.Content class="min-w-48">
<ContextMenu.Item onSelect={() => wm.arrange('cascade')}>

View File

@@ -88,10 +88,14 @@
onkeydown={onKeydown}
title={app.title}
>
<span class="relative flex size-10 items-center justify-center rounded-xl border bg-card/80 text-foreground shadow-sm backdrop-blur">
<span
class="relative flex size-10 items-center justify-center rounded-xl border bg-card/80 text-foreground shadow-sm backdrop-blur"
>
<app.icon class="size-5" />
{#if badge > 0}
<span class="absolute -right-1.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold text-destructive-foreground">
<span
class="absolute -right-1.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold text-destructive-foreground"
>
{badge > 99 ? '99+' : badge}
</span>
{/if}

View File

@@ -29,7 +29,9 @@
{@const C = mod.default}
<C />
{:catch error}
<div class="flex h-full min-h-0 items-center justify-center p-4 text-center text-sm text-destructive">
<div
class="flex h-full min-h-0 items-center justify-center p-4 text-center text-sm text-destructive"
>
Failed to load app: {(error as Error).message}
</div>
{/await}

View File

@@ -22,7 +22,7 @@
messages={[]}
streaming={false}
connectionState="connected"
onSend={onSend}
{onSend}
onCancel={() => {}}
onReconnect={() => {}}
onDismissError={() => {}}

View File

@@ -53,7 +53,8 @@
class="max-h-52 min-h-24 resize-none field-sizing-fixed border-0 bg-transparent px-4 py-3.5 text-base shadow-none focus-visible:ring-0"
/>
<div class="flex items-center justify-between px-3 pb-3">
<span class="text-[11px] text-muted-foreground">Enter to start · Shift+Enter for newline</span>
<span class="text-[11px] text-muted-foreground">Enter to start · Shift+Enter for newline</span
>
<Button type="submit" size="icon" disabled={!input.trim()} aria-label="Start task">
<ArrowUpIcon />
</Button>

View File

@@ -51,7 +51,6 @@
wm.focus(id)
}
}
</script>
<div class="flex h-11 shrink-0 items-center gap-1.5 border-t bg-muted/30 px-2">
@@ -77,14 +76,19 @@
class="flex h-8 max-w-56 items-center gap-1.5 rounded-md border px-2 font-mono text-xs transition-colors {$wmState.focusedId ===
win.id && win.stage !== 'minimized'
? 'border-primary/50 bg-primary/10 text-foreground'
: 'border-transparent bg-card/60 text-muted-foreground hover:bg-muted'} {win.stage === 'minimized' ? 'opacity-60' : ''}"
: 'border-transparent bg-card/60 text-muted-foreground hover:bg-muted'} {win.stage ===
'minimized'
? 'opacity-60'
: ''}"
onclick={() => toggle(win.id, win)}
title={win.title}
>
{#if Icon}<Icon class="size-3.5 shrink-0" />{/if}
<span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span>
{#if badge > 0}
<span class="flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold text-destructive-foreground">
<span
class="flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold text-destructive-foreground"
>
{badge > 99 ? '99+' : badge}
</span>
{/if}

View File

@@ -9,7 +9,14 @@
// session:<id> -> SessionChatWindow (windows.ts openTaskWindow)
// new-task -> NewTaskChat (windows.ts openNewTaskWindow)
// anything else -> entity slug -> EntityDetailContent
import { wm, dk, wmState, openEntityWindow, NEW_TASK_WINDOW_ID, SESSION_PREFIX } from '$lib/stores/windows'
import {
wm,
dk,
wmState,
openEntityWindow,
NEW_TASK_WINDOW_ID,
SESSION_PREFIX
} from '$lib/stores/windows'
import { appById, appIdFromWindowId } from '$lib/apps'
import EntityDetailContent from '../EntityDetailContent.svelte'
import SessionChatWindow from '../SessionChatWindow.svelte'
@@ -57,8 +64,15 @@
{@const app = appId ? $appById.get(appId) : undefined}
{#if win && (!appId || app)}
<section use:dk.window={{ id }} class="min-w-0" aria-label={win.title}>
<header data-wm-drag class="flex shrink-0 cursor-move items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5">
<span data-wm-title class="flex min-w-0 flex-1 items-center self-stretch truncate font-mono text-xs font-medium">{win.title}</span>
<header
data-wm-drag
class="flex shrink-0 cursor-move items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5"
>
<span
data-wm-title
class="flex min-w-0 flex-1 items-center self-stretch truncate font-mono text-xs font-medium"
>{win.title}</span
>
<div class="flex shrink-0 items-center gap-0.5">
<button
type="button"

View File

@@ -1,49 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
import { type VariantProps, tv } from 'tailwind-variants'
export const badgeVariants = tv({
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
});
export const badgeVariants = tv({
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
destructive:
'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline-offset-4 hover:underline'
}
},
defaultVariants: {
variant: 'default'
}
})
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant']
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
let {
ref = $bindable(null),
href,
class: className,
variant = 'default',
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant
} = $props()
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
this={href ? 'a' : 'span'}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</svelte:element>

View File

@@ -1,2 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
export { default as Badge } from './badge.svelte'
export { badgeVariants, type BadgeVariant } from './badge.svelte'

View File

@@ -1,82 +1,89 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
import { cn, type WithElementRef } from '$lib/utils.js'
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
import { type VariantProps, tv } from 'tailwind-variants'
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-9",
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
destructive:
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default:
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-9',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
'icon-lg': 'size-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
})
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']
export type ButtonSize = VariantProps<typeof buttonVariants>['size']
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant
size?: ButtonSize
}
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
disabled,
children,
...restProps
}: ButtonProps = $props()
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -1,17 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants
} from './button.svelte'
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant
}

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn(
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...restProps}
bind:this={ref}
data-slot="card-action"
class={cn(
'cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="card-content"
class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
{...restProps}
bind:this={ref}
data-slot="card-content"
class={cn('px-6 group-data-[size=sm]/card:px-4', className)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props()
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
bind:this={ref}
data-slot="card-description"
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</p>

View File

@@ -1,20 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
{...restProps}
bind:this={ref}
data-slot="card-footer"
class={cn(
'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
className
)}
{...restProps}
bind:this={ref}
data-slot="card-header"
class={cn(
'gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)}
{...restProps}
bind:this={ref}
data-slot="card-title"
class={cn('text-base leading-normal font-medium group-data-[size=sm]/card:text-sm', className)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,22 +1,25 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
size = "default",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props()
</script>
<div
bind:this={ref}
data-slot="card"
data-size={size}
class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
{...restProps}
bind:this={ref}
data-slot="card"
data-size={size}
class={cn(
'ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,25 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
import Root from './card.svelte'
import Content from './card-content.svelte'
import Description from './card-description.svelte'
import Footer from './card-footer.svelte'
import Header from './card-header.svelte'
import Title from './card-title.svelte'
import Action from './card-action.svelte'
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction
}

View File

@@ -25,7 +25,10 @@
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="flex items-center justify-center text-current transition-none">
<div
data-slot="checkbox-indicator"
class="flex items-center justify-center text-current transition-none"
>
{#if indeterminate}
<MinusIcon class="size-3.5" />
{:else if checked}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props()
</script>
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props()
</script>
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
let {
ref = $bindable(null),
open = $bindable(false),
...restProps
}: CollapsiblePrimitive.RootProps = $props();
let {
ref = $bindable(null),
open = $bindable(false),
...restProps
}: CollapsiblePrimitive.RootProps = $props()
</script>
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />

View File

@@ -1,13 +1,13 @@
import Root from "./collapsible.svelte";
import Trigger from "./collapsible-trigger.svelte";
import Content from "./collapsible-content.svelte";
import Root from './collapsible.svelte'
import Trigger from './collapsible-trigger.svelte'
import Content from './collapsible-content.svelte'
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger
}

View File

@@ -1,41 +1,41 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
import CheckIcon from '@lucide/svelte/icons/check';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { Snippet } from 'svelte'
import CheckIcon from '@lucide/svelte/icons/check'
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
inset,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet;
} = $props();
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
inset,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean
children?: Snippet
} = $props()
</script>
<ContextMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="context-menu-checkbox-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
bind:ref
bind:checked
bind:indeterminate
data-slot="context-menu-checkbox-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
{#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</ContextMenuPrimitive.CheckboxItem>

View File

@@ -1,28 +1,28 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import ContextMenuPortal from "./context-menu-portal.svelte";
import type { ComponentProps } from "svelte";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
import ContextMenuPortal from './context-menu-portal.svelte'
import type { ComponentProps } from 'svelte'
import type { WithoutChildrenOrChild } from '$lib/utils.js'
let {
ref = $bindable(null),
portalProps,
class: className,
...restProps
}: ContextMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
} = $props();
let {
ref = $bindable(null),
portalProps,
class: className,
...restProps
}: ContextMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>
} = $props()
</script>
<ContextMenuPortal {...portalProps}>
<ContextMenuPrimitive.Content
bind:ref
data-slot="context-menu-content"
class={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none",
className
)}
{...restProps}
/>
<ContextMenuPrimitive.Content
bind:ref
data-slot="context-menu-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none',
className
)}
{...restProps}
/>
</ContextMenuPortal>

View File

@@ -1,21 +1,21 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ContextMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ContextMenuPrimitive.GroupHeadingProps & {
inset?: boolean
} = $props()
</script>
<ContextMenuPrimitive.GroupHeading
bind:ref
data-slot="context-menu-group-heading"
data-inset={inset}
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8", className)}
{...restProps}
bind:ref
data-slot="context-menu-group-heading"
data-inset={inset}
class={cn('text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8', className)}
{...restProps}
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props();
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props()
</script>
<ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />

View File

@@ -1,27 +1,27 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: ContextMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
let {
ref = $bindable(null),
class: className,
inset,
variant = 'default',
...restProps
}: ContextMenuPrimitive.ItemProps & {
inset?: boolean
variant?: 'default' | 'destructive'
} = $props()
</script>
<ContextMenuPrimitive.Item
bind:ref
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item 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}
bind:ref
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item 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}
/>

View File

@@ -1,24 +1,27 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean
} = $props()
</script>
<div
bind:this={ref}
data-slot="context-menu-label"
data-inset={inset}
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8", className)}
{...restProps}
bind:this={ref}
data-slot="context-menu-label"
data-inset={inset}
class={cn(
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props()
</script>
<ContextMenuPrimitive.Portal {...restProps} />

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
value = $bindable(""),
...restProps
}: ContextMenuPrimitive.RadioGroupProps = $props();
let {
ref = $bindable(null),
value = $bindable(''),
...restProps
}: ContextMenuPrimitive.RadioGroupProps = $props()
</script>
<ContextMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="context-menu-radio-group"
{...restProps}
bind:ref
bind:value
data-slot="context-menu-radio-group"
{...restProps}
/>

View File

@@ -1,35 +1,35 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
import CheckIcon from '@lucide/svelte/icons/check';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChild } from '$lib/utils.js'
import CheckIcon from '@lucide/svelte/icons/check'
let {
ref = $bindable(null),
class: className,
inset,
children: childrenProp,
...restProps
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
inset?: boolean;
} = $props();
let {
ref = $bindable(null),
class: className,
inset,
children: childrenProp,
...restProps
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
inset?: boolean
} = $props()
</script>
<ContextMenuPrimitive.RadioItem
bind:ref
data-slot="context-menu-radio-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
bind:ref
data-slot="context-menu-radio-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
{#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</ContextMenuPrimitive.RadioItem>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.SeparatorProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.SeparatorProps = $props()
</script>
<ContextMenuPrimitive.Separator
bind:ref
data-slot="context-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
bind:ref
data-slot="context-menu-separator"
class={cn('bg-border -mx-1 my-1 h-px', className)}
{...restProps}
/>

View File

@@ -1,20 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
</script>
<span
bind:this={ref}
data-slot="context-menu-shortcut"
class={cn("text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
bind:this={ref}
data-slot="context-menu-shortcut"
class={cn(
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</span>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.SubContentProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.SubContentProps = $props()
</script>
<ContextMenuPrimitive.SubContent
bind:ref
data-slot="context-menu-sub-content"
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100", className)}
{...restProps}
bind:ref
data-slot="context-menu-sub-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100',
className
)}
{...restProps}
/>

View File

@@ -1,29 +1,29 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChild } from '$lib/utils.js'
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
inset?: boolean;
} = $props();
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
inset?: boolean
} = $props()
</script>
<ContextMenuPrimitive.SubTrigger
bind:ref
data-slot="context-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
bind:ref
data-slot="context-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ml-auto" />
{@render children?.()}
<ChevronRightIcon class="ml-auto" />
</ContextMenuPrimitive.SubTrigger>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props();
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props()
</script>
<ContextMenuPrimitive.Sub bind:open {...restProps} />

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.TriggerProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.TriggerProps = $props()
</script>
<ContextMenuPrimitive.Trigger
bind:ref
data-slot="context-menu-trigger"
class={cn("cn-context-menu-trigger select-none", className)}
{...restProps}
bind:ref
data-slot="context-menu-trigger"
class={cn('cn-context-menu-trigger select-none', className)}
{...restProps}
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props();
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props()
</script>
<ContextMenuPrimitive.Root bind:open {...restProps} />

View File

@@ -1,52 +1,52 @@
import Root from "./context-menu.svelte";
import Sub from "./context-menu-sub.svelte";
import Portal from "./context-menu-portal.svelte";
import Trigger from "./context-menu-trigger.svelte";
import Group from "./context-menu-group.svelte";
import RadioGroup from "./context-menu-radio-group.svelte";
import Item from "./context-menu-item.svelte";
import GroupHeading from "./context-menu-group-heading.svelte";
import Content from "./context-menu-content.svelte";
import Shortcut from "./context-menu-shortcut.svelte";
import RadioItem from "./context-menu-radio-item.svelte";
import Separator from "./context-menu-separator.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
import Label from "./context-menu-label.svelte";
import Root from './context-menu.svelte'
import Sub from './context-menu-sub.svelte'
import Portal from './context-menu-portal.svelte'
import Trigger from './context-menu-trigger.svelte'
import Group from './context-menu-group.svelte'
import RadioGroup from './context-menu-radio-group.svelte'
import Item from './context-menu-item.svelte'
import GroupHeading from './context-menu-group-heading.svelte'
import Content from './context-menu-content.svelte'
import Shortcut from './context-menu-shortcut.svelte'
import RadioItem from './context-menu-radio-item.svelte'
import Separator from './context-menu-separator.svelte'
import SubContent from './context-menu-sub-content.svelte'
import SubTrigger from './context-menu-sub-trigger.svelte'
import CheckboxItem from './context-menu-checkbox-item.svelte'
import Label from './context-menu-label.svelte'
export {
Root,
Sub,
Portal,
Item,
GroupHeading,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as ContextMenu,
Sub as ContextMenuSub,
Portal as ContextMenuPortal,
Item as ContextMenuItem,
GroupHeading as ContextMenuGroupHeading,
Group as ContextMenuGroup,
Content as ContextMenuContent,
Trigger as ContextMenuTrigger,
Shortcut as ContextMenuShortcut,
RadioItem as ContextMenuRadioItem,
Separator as ContextMenuSeparator,
RadioGroup as ContextMenuRadioGroup,
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
Label as ContextMenuLabel,
};
Root,
Sub,
Portal,
Item,
GroupHeading,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as ContextMenu,
Sub as ContextMenuSub,
Portal as ContextMenuPortal,
Item as ContextMenuItem,
GroupHeading as ContextMenuGroupHeading,
Group as ContextMenuGroup,
Content as ContextMenuContent,
Trigger as ContextMenuTrigger,
Shortcut as ContextMenuShortcut,
RadioItem as ContextMenuRadioItem,
Separator as ContextMenuSeparator,
RadioGroup as ContextMenuRadioGroup,
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
Label as ContextMenuLabel
}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
type = "button",
...restProps
}: DialogPrimitive.CloseProps = $props();
let {
ref = $bindable(null),
type = 'button',
...restProps
}: DialogPrimitive.CloseProps = $props()
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />

View File

@@ -1,48 +1,48 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import DialogPortal from "./dialog-portal.svelte";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
import { Button } from "$lib/components/ui/button/index.js";
import XIcon from '@lucide/svelte/icons/x';
import { Dialog as DialogPrimitive } from 'bits-ui'
import DialogPortal from './dialog-portal.svelte'
import type { Snippet } from 'svelte'
import * as Dialog from './index.js'
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { ComponentProps } from 'svelte'
import { Button } from '$lib/components/ui/button/index.js'
import XIcon from '@lucide/svelte/icons/x'
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
children: Snippet;
showCloseButton?: boolean;
} = $props();
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(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close data-slot="dialog-close">
{#snippet child({ props })}
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
<XIcon />
<span class="sr-only">Close</span>
</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close data-slot="dialog-close">
{#snippet child({ props })}
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
<XIcon />
<span class="sr-only">Close</span>
</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</DialogPortal>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props()
</script>
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
{...restProps}
bind:ref
data-slot="dialog-description"
class={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
className
)}
{...restProps}
/>

View File

@@ -1,32 +1,32 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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'
import { Dialog as DialogPrimitive } from 'bits-ui'
import { Button } from '$lib/components/ui/button/index.js'
let {
ref = $bindable(null),
class: className,
children,
showCloseButton = false,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
showCloseButton?: boolean;
} = $props();
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}
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}
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close>
{#snippet child({ props })}
<Button variant="outline" {...props}>Close</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</div>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
</script>
<div
bind:this={ref}
data-slot="dialog-header"
class={cn("gap-2 flex flex-col", className)}
{...restProps}
bind:this={ref}
data-slot="dialog-header"
class={cn('gap-2 flex flex-col', className)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props()
</script>
<DialogPrimitive.Overlay
bind:ref
data-slot="dialog-overlay"
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
{...restProps}
bind:ref
data-slot="dialog-overlay"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
className
)}
{...restProps}
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui'
let { ...restProps }: DialogPrimitive.PortalProps = $props();
let { ...restProps }: DialogPrimitive.PortalProps = $props()
</script>
<DialogPrimitive.Portal {...restProps} />

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Dialog as DialogPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props()
</script>
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn("leading-none font-medium", className)}
{...restProps}
bind:ref
data-slot="dialog-title"
class={cn('leading-none font-medium', className)}
{...restProps}
/>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
type = "button",
...restProps
}: DialogPrimitive.TriggerProps = $props();
let {
ref = $bindable(null),
type = 'button',
...restProps
}: DialogPrimitive.TriggerProps = $props()
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { Dialog as DialogPrimitive } from 'bits-ui'
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props()
</script>
<DialogPrimitive.Root bind:open {...restProps} />

View File

@@ -1,34 +1,34 @@
import Root from "./dialog.svelte";
import Portal from "./dialog-portal.svelte";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
import Root from './dialog.svelte'
import Portal from './dialog-portal.svelte'
import Title from './dialog-title.svelte'
import Footer from './dialog-footer.svelte'
import Header from './dialog-header.svelte'
import Overlay from './dialog-overlay.svelte'
import Content from './dialog-content.svelte'
import Description from './dialog-description.svelte'
import Trigger from './dialog-trigger.svelte'
import Close from './dialog-close.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,
};
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
}

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
value = $bindable([]),
...restProps
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
let {
ref = $bindable(null),
value = $bindable([]),
...restProps
}: DropdownMenuPrimitive.CheckboxGroupProps = $props()
</script>
<DropdownMenuPrimitive.CheckboxGroup
bind:ref
bind:value
data-slot="dropdown-menu-checkbox-group"
{...restProps}
bind:ref
bind:value
data-slot="dropdown-menu-checkbox-group"
{...restProps}
/>

View File

@@ -1,44 +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";
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();
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(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
{#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>

View File

@@ -1,31 +1,31 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import DropdownMenuPortal from './dropdown-menu-portal.svelte'
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
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();
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(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
className
)}
{...restProps}
/>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
{align}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
className
)}
{...restProps}
/>
</DropdownMenuPortal>

View File

@@ -1,22 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
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();
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}
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}
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props()
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View File

@@ -1,27 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
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(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View File

@@ -1,24 +1,27 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
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();
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("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8", className)}
{...restProps}
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn(
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props()
</script>
<DropdownMenuPrimitive.Portal {...restProps} />

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props()
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View File

@@ -1,34 +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";
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();
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(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_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}
{#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>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props()
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
bind:ref
data-slot="dropdown-menu-separator"
class={cn('bg-border -mx-1 my-1 h-px', className)}
{...restProps}
/>

View File

@@ -1,20 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps}
>
{@render children?.()}
{@render children?.()}
</span>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props()
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto", className)}
{...restProps}
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto',
className
)}
{...restProps}
/>

View File

@@ -1,29 +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";
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();
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(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_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}
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_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" />
{@render children?.()}
<ChevronRightIcon class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props()
</script>
<DropdownMenuPrimitive.Sub bind:open {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props()
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props()
</script>
<DropdownMenuPrimitive.Root bind:open {...restProps} />

View File

@@ -1,54 +1,54 @@
import Root from "./dropdown-menu.svelte";
import Sub from "./dropdown-menu-sub.svelte";
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.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 Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Portal from "./dropdown-menu-portal.svelte";
import Root from './dropdown-menu.svelte'
import Sub from './dropdown-menu-sub.svelte'
import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'
import CheckboxItem from './dropdown-menu-checkbox-item.svelte'
import Content from './dropdown-menu-content.svelte'
import Group from './dropdown-menu-group.svelte'
import Item from './dropdown-menu-item.svelte'
import Label from './dropdown-menu-label.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 Trigger from './dropdown-menu-trigger.svelte'
import SubContent from './dropdown-menu-sub-content.svelte'
import SubTrigger from './dropdown-menu-sub-trigger.svelte'
import GroupHeading from './dropdown-menu-group-heading.svelte'
import Portal from './dropdown-menu-portal.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,
};
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
}

View File

@@ -1,7 +1,7 @@
import Root from "./input.svelte";
import Root from './input.svelte'
export {
Root,
//
Root as Input,
};
Root,
//
Root as Input
}

View File

@@ -1,48 +1,48 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements'
import { cn, type WithElementRef } from '$lib/utils.js'
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
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();
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(
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{#if type === 'file'}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{type}
bind:value
{...restProps}
/>
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

@@ -1,7 +1,7 @@
import Root from "./label.svelte";
import Root from './label.svelte'
export {
Root,
//
Root as Label,
};
Root,
//
Root as Label
}

Some files were not shown because too many files have changed in this diff Show More