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

@@ -1,6 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 2, "tabWidth": 2,
"semi": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,

View File

@@ -1,17 +1,17 @@
{ {
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "vega", "style": "vega",
"tailwind": { "tailwind": {
"css": "src/app.css", "css": "src/app.css",
"baseColor": "zinc" "baseColor": "zinc"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils", "utils": "$lib/utils",
"ui": "$lib/components/ui", "ui": "$lib/components/ui",
"hooks": "$lib/hooks", "hooks": "$lib/hooks",
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://shadcn-svelte.com/registry" "registry": "https://shadcn-svelte.com/registry"
} }

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -6,7 +6,10 @@
<title>Oikos</title> <title>Oikos</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Inknut+Antiqua:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Inknut+Antiqua:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="favicon.svg" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /> <link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="android-chrome-192.png" /> <link rel="icon" type="image/png" sizes="192x192" href="android-chrome-192.png" />
@@ -15,11 +18,20 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script> <script>
(function(){try{var t=localStorage.getItem('oikos-theme');if(!t){t=window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'} ;(function () {
if(t==='dark')document.documentElement.classList.add('dark')}catch(e){}})() try {
var t = localStorage.getItem('oikos-theme')
if (!t) {
t = window.matchMedia('(prefers-color-scheme:light)').matches ? 'light' : 'dark'
}
if (t === 'dark') document.documentElement.classList.add('dark')
} catch (e) {}
})()
</script> </script>
<script src="/wails/runtime.js"></script> <script src="/wails/runtime.js"></script>
<script>window.__OIKOS_CONFIG__ = {};</script> <script>
window.__OIKOS_CONFIG__ = {}
</script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View File

@@ -84,7 +84,7 @@
--chart-3: oklch(0.5 0.08 30); --chart-3: oklch(0.5 0.08 30);
--chart-4: oklch(0.6 0.06 90); --chart-4: oklch(0.6 0.06 90);
--chart-5: oklch(0.4 0.04 45); --chart-5: oklch(0.4 0.04 45);
--sidebar: oklch(0.90 0.025 55); --sidebar: oklch(0.9 0.025 55);
--sidebar-foreground: oklch(0.18 0.03 45); --sidebar-foreground: oklch(0.18 0.03 45);
--sidebar-primary: oklch(0.55 0.14 45); --sidebar-primary: oklch(0.55 0.14 45);
--sidebar-primary-foreground: oklch(0.95 0.02 55); --sidebar-primary-foreground: oklch(0.95 0.02 55);
@@ -159,7 +159,6 @@
--accent-orange: var(--warning); --accent-orange: var(--warning);
} }
/* Terminal-style block cursor — outside @layer so it overrides CodeMirror */ /* Terminal-style block cursor — outside @layer so it overrides CodeMirror */
.cm-cursor, .cm-cursor,
.cm-cursor-primary { .cm-cursor-primary {
@@ -181,7 +180,12 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading); font-family: var(--font-heading);
} }
@@ -274,7 +278,6 @@
outline: none; outline: none;
} }
[data-wm-window][data-wm-focused] { [data-wm-window][data-wm-focused] {
border-color: var(--ring); border-color: var(--ring);
box-shadow: 0 12px 32px oklch(0 0 0 / 0.28); box-shadow: 0 12px 32px oklch(0 0 0 / 0.28);
@@ -289,7 +292,6 @@
display: none; display: none;
} }
[data-wm-resize] { [data-wm-resize] {
position: absolute; position: absolute;
} }

View File

@@ -112,7 +112,11 @@ export async function fetchQuestions(sessionId: string): Promise<SessionQuestion
return data.questions ?? [] 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`, { const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ answer }) body: JSON.stringify({ answer })
@@ -133,42 +137,45 @@ export function streamChat(
method: 'POST', method: 'POST',
body: JSON.stringify({ message, session_id: sessionId ?? undefined }), body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
signal: controller.signal signal: controller.signal
}).then(async (res) => { })
if (!res.ok) { .then(async (res) => {
onError(`HTTP ${res.status}`) if (!res.ok) {
return onError(`HTTP ${res.status}`)
} return
const reader = res.body?.getReader() }
if (!reader) { const reader = res.body?.getReader()
onError('no response body') if (!reader) {
return onError('no response body')
} return
const decoder = new TextDecoder() }
let buffer = '' const decoder = new TextDecoder()
let buffer = ''
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
if (done) break if (done) break
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n') const lines = buffer.split('\n')
buffer = lines.pop() ?? '' buffer = lines.pop() ?? ''
for (const line of lines) { for (const line of lines) {
if (line.startsWith('data: ')) { if (line.startsWith('data: ')) {
try { try {
const ev: ChatEvent = JSON.parse(line.slice(6)) const ev: ChatEvent = JSON.parse(line.slice(6))
onEvent(ev) onEvent(ev)
} catch { } catch {
// skip malformed // skip malformed
}
} }
} }
} }
} })
}).catch((err) => { .catch((err) => {
onError(err.message) onError(err.message)
}).finally(() => { })
onDone() .finally(() => {
}) onDone()
})
return controller return controller
} }
@@ -291,7 +298,9 @@ export interface EventFilters {
severity?: string 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() const params = new URLSearchParams()
if (filters.type) params.set('type', filters.type) if (filters.type) params.set('type', filters.type)
if (filters.severity) params.set('severity', filters.severity) if (filters.severity) params.set('severity', filters.severity)
@@ -483,7 +492,9 @@ export interface Signal {
last_seen_at: string 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() const params = new URLSearchParams()
if (filters.state) params.set('state', filters.state) if (filters.state) params.set('state', filters.state)
if (filters.severity) params.set('severity', filters.severity) 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() 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`, { const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ mute_until: muteUntil, note }) 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 // reachable going forward from here), this hits a dedicated endpoint that
// matches on source_id OR target_id directly. // matches on source_id OR target_id directly.
export async function fetchEntityRelations(id: string): Promise<Relationship[]> { 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 [] if (!res.ok) return []
const data = await res.json() const data = await res.json()
return data.items ?? [] return data.items ?? []
@@ -671,7 +688,9 @@ export async function fetchKnowledgeContent(id: string): Promise<KnowledgeConten
return res.json() 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 params = new URLSearchParams({ entity_id: entityId, limit: '50' })
const res = await fetchWithAuth(`${API}/events?${params}`) const res = await fetchWithAuth(`${API}/events?${params}`)
if (!res.ok) return [] if (!res.ok) return []
@@ -718,13 +737,19 @@ export async function fetchEntityTasks(entity: Entity): Promise<EntityTask[]> {
tasks.map(async (task): Promise<EntityTask | null> => { tasks.map(async (task): Promise<EntityTask | null> => {
const g = await fetchGraph({ root: task.slug, depth: 1 }) const g = await fetchGraph({ root: task.slug, depth: 1 })
if (!g) return null 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 nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id])) const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
const executionCount = g.edges.filter((e) => { const executionCount = g.edges.filter((e) => {
if (e.type !== 'involves') return false if (e.type !== 'involves') return false
const targetId = idBySlug.get(e.target) 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 }).length
if (!involvesThisEntity && executionCount === 0) return null if (!involvesThisEntity && executionCount === 0) return null
return { task, executionCount } return { task, executionCount }
@@ -755,7 +780,11 @@ export async function fetchChecksForTarget(targetSlug: string): Promise<Check[]>
return data.items ?? [] 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}`, { const res = await fetchWithAuth(`${API}/checks/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'If-Match': `"${version}"` }, headers: { 'If-Match': `"${version}"` },
@@ -781,12 +810,14 @@ export interface AgentActivity {
correlation_id?: string | null correlation_id?: string | null
} }
export async function fetchAgentActivity(filters: { export async function fetchAgentActivity(
agent_id?: string filters: {
activity_type?: string agent_id?: string
entity_id?: string activity_type?: string
limit?: number entity_id?: string
} = {}): Promise<AgentActivity[]> { limit?: number
} = {}
): Promise<AgentActivity[]> {
const params = new URLSearchParams() const params = new URLSearchParams()
if (filters.agent_id) params.set('agent_id', filters.agent_id) if (filters.agent_id) params.set('agent_id', filters.agent_id)
if (filters.activity_type) params.set('activity_type', filters.activity_type) if (filters.activity_type) params.set('activity_type', filters.activity_type)
@@ -821,14 +852,16 @@ export interface AuditEntry {
correlation_id?: string | null correlation_id?: string | null
} }
export async function fetchAudit(filters: { export async function fetchAudit(
actor_type?: string filters: {
actor_id?: string actor_type?: string
entity_id?: string actor_id?: string
action?: string entity_id?: string
correlation_id?: string action?: string
limit?: number correlation_id?: string
} = {}): Promise<AuditEntry[]> { limit?: number
} = {}
): Promise<AuditEntry[]> {
const params = new URLSearchParams() const params = new URLSearchParams()
if (filters.actor_type) params.set('actor_type', filters.actor_type) if (filters.actor_type) params.set('actor_type', filters.actor_type)
if (filters.actor_id) params.set('actor_id', filters.actor_id) 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" 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> ></textarea>
<p class="shrink-0 text-xs text-muted-foreground"> <p class="shrink-0 text-xs text-muted-foreground">
A demo installable app — uninstall it from the App Store to remove its A demo installable app — uninstall it from the App Store to remove its icon and window. Its
icon and window. Its notes persist in localStorage under notes persist in localStorage under
<code class="font-mono">{storageKey}</code>. <code class="font-mono">{storageKey}</code>.
</p> </p>
</div> </div>

View File

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

View File

@@ -37,14 +37,23 @@
}) })
</script> </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 <button
class="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-muted/40" class="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-muted/40"
onclick={() => (expanded = !expanded)} onclick={() => (expanded = !expanded)}
aria-expanded={expanded} aria-expanded={expanded}
aria-label={expanded ? 'Hide agent trace' : 'Show agent trace'} 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'} {#if status === 'running'}
<Spinner class="size-3" /> <Spinner class="size-3" />
{:else if status === 'error'} {:else if status === 'error'}
@@ -71,7 +80,9 @@
{/if} {/if}
<ChevronRightIcon <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> </button>
@@ -82,7 +93,9 @@
<ToolCallCard {tool} /> <ToolCallCard {tool} />
{/each} {/each}
{:else} {: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} {/if}
</div> </div>
{/if} {/if}

View File

@@ -60,10 +60,16 @@
let wasStreaming = $state(false) let wasStreaming = $state(false)
$effect(() => { $effect(() => {
if (streaming) { indicatorDone = false; wasStreaming = true } if (streaming) {
indicatorDone = false
wasStreaming = true
}
if (!streaming && wasStreaming) { if (!streaming && wasStreaming) {
indicatorDone = true indicatorDone = true
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000) const t = setTimeout(() => {
indicatorDone = false
wasStreaming = false
}, 3000)
return () => clearTimeout(t) return () => clearTimeout(t)
} }
}) })
@@ -91,7 +97,10 @@
const lineHeight = parseFloat(taCs.lineHeight) const lineHeight = parseFloat(taCs.lineHeight)
if (!Number.isFinite(lineHeight)) return if (!Number.isFinite(lineHeight)) return
const taBoxY = 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 // 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 // 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. // squeezed below one line once the pane is dragged down to it.
@@ -144,7 +153,9 @@
} }
renderer.table = function (token) { renderer.table = function (token) {
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('') 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 `<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) return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
@@ -181,126 +192,172 @@
</script> </script>
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}> <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"> <Pane class="flex flex-col">
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}> <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"> <div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
{#if messages.length === 0} {#if messages.length === 0}
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center"> <div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
<div> <div>
<h2 class="text-xl font-semibold">Nomos</h2> <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> <p class="mt-1 text-sm text-muted-foreground">
</div> Your resident operator. Ask about the fleet, or tell it to act.
{#if suggestions.length} </p>
<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> </div>
<!-- The working trace sits above the answer: it's what happened {#if suggestions.length}
first, and collapsed it keeps a long tool run from burying <div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
the text below it. --> {#each suggestions as q}
{#if msg.tools.length > 0 || traceStatus !== 'idle'} <Button
<AgentTrace variant="outline"
tools={msg.tools} size="sm"
status={traceStatus} class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
label={traceStatus === 'idle' ? null : indicatorLabel} onclick={() => ask(q)}
/> >
{/if} {q}
{#if msg.text} </Button>
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg"> {/each}
<!-- 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> </div>
{/if} {/if}
</div> </div>
{/if} {/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> </div>
{/each} {/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>
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col"> <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 <form
class="relative mx-auto flex h-full w-full max-w-3xl" class="relative mx-auto flex h-full w-full max-w-3xl"
onsubmit={(e) => { onsubmit={(e) => {
@@ -514,7 +571,13 @@
border: none; border: none;
height: 1px; height: 1px;
margin: 0.75rem 0; 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 /* Bold is emphasis, not color — dark weight reads cleanly and lets the
@@ -559,7 +622,9 @@
border-radius: 0.375rem; border-radius: 0.375rem;
color: var(--muted-foreground); color: var(--muted-foreground);
opacity: 0; opacity: 0;
transition: opacity 0.15s, color 0.15s; transition:
opacity 0.15s,
color 0.15s;
cursor: pointer; cursor: pointer;
border: none; border: none;
background: transparent; background: transparent;
@@ -586,7 +651,12 @@
} }
@keyframes cursor-blink { @keyframes cursor-blink {
0%, 100% { opacity: 0.75; } 0%,
50% { opacity: 0; } 100% {
opacity: 0.75;
}
50% {
opacity: 0;
}
} }
</style> </style>

View File

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

View File

@@ -22,14 +22,20 @@
</script> </script>
<Collapsible.Root bind:open class="rounded-md border bg-card"> <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> <span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
<ChevronDownIcon <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" aria-hidden="true"
/> />
</Collapsible.Trigger> </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"> <div class="border-t px-2 py-1.5">
{@render children()} {@render children()}
</div> </div>

View File

@@ -11,7 +11,10 @@
</script> </script>
<tr> <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} {message}
</td> </td>
</tr> </tr>

View File

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

View File

@@ -51,7 +51,13 @@
return { sorted: true, direction: sortDir } 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 { function sortValue(entity: Entity, key: SortKey): string | number {
if (key === 'health') return entity.health ? (healthRank[entity.health] ?? -1) : -1 if (key === 'health') return entity.health ? (healthRank[entity.health] ?? -1) : -1
@@ -131,7 +137,9 @@
{:else} {:else}
{#snippet row(entity: Entity, level: number, ancestors: Set<string>)} {#snippet row(entity: Entity, level: number, ancestors: Set<string>)}
{@const ancestorsWithSelf = new Set(ancestors).add(entity.slug)} {@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 <Table.Row
class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}" class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}"
role="row" role="row"
@@ -139,7 +147,12 @@
aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined} aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined}
tabindex={0} tabindex={0}
onclick={() => onSelect(entity.slug)} 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"> <Table.Cell class="font-mono text-xs">
<span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem"> <span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem">
@@ -149,7 +162,9 @@
type="button" type="button"
class="rounded text-muted-foreground hover:text-foreground" class="rounded text-muted-foreground hover:text-foreground"
onclick={(e) => toggleNode(entity.slug, e)} 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)} {#if collapsedNodes.has(entity.slug)}
<ChevronRightIcon class="size-3.5" /> <ChevronRightIcon class="size-3.5" />
@@ -186,23 +201,48 @@
<Table.Row> <Table.Row>
{@const ssSlug = getSortState('slug')} {@const ssSlug = getSortState('slug')}
<Table.Head> <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> </Table.Head>
{@const ssType = getSortState('type')} {@const ssType = getSortState('type')}
<Table.Head> <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> </Table.Head>
{@const ssName = getSortState('name')} {@const ssName = getSortState('name')}
<Table.Head> <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> </Table.Head>
{@const ssState = getSortState('state')} {@const ssState = getSortState('state')}
<Table.Head> <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> </Table.Head>
{@const ssHealth = getSortState('health')} {@const ssHealth = getSortState('health')}
<Table.Head> <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.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>

View File

@@ -8,14 +8,22 @@
children children
}: { }: {
value?: string 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 class?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
children?: any children?: any
} = $props() } = $props()
</script> </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> <Tabs.List>
{#each tabs as tab} {#each tabs as tab}
<Tabs.Trigger value={tab.value}> <Tabs.Trigger value={tab.value}>

View File

@@ -60,7 +60,11 @@
$effect(() => { $effect(() => {
const ev = $liveEvents[0] const ev = $liveEvents[0]
if (!ev) return 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() load()
} }
}) })
@@ -276,9 +280,11 @@
const svcRaw = new Map<string, number>() const svcRaw = new Map<string, number>()
for (const s of services) { for (const s of services) {
const p = serviceProvider.get(s.slug) 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 let prevY = TOP0 - ROW_H
for (const s of services) { for (const s of services) {
const y = Math.max(svcRaw.get(s.slug)!, prevY + ROW_H) const y = Math.max(svcRaw.get(s.slug)!, prevY + ROW_H)
@@ -308,7 +314,9 @@
counts[h]++ counts[h]++
const ingress = routesTo.get(n.slug) const ingress = routesTo.get(n.slug)
const ingressEntity = ingress ? bySlugEntity.get(ingress) : undefined 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 fauth = ingressEntity ? boolAttr(ingressEntity, 'forward_auth') : undefined
const provider = kind === 'service' ? serviceProvider.get(n.slug) : containerHost.get(n.slug) const provider = kind === 'service' ? serviceProvider.get(n.slug) : containerHost.get(n.slug)
const mnt = (kind === 'container' ? mountsOf.get(n.slug) : mountsOf.get(provider ?? '')) ?? [] const mnt = (kind === 'container' ? mountsOf.get(n.slug) : mountsOf.get(provider ?? '')) ?? []
@@ -361,25 +369,41 @@
const n = bySlug.get(slug)! const n = bySlug.get(slug)!
return LANE_X[n.lane] + (side === 'r' ? LANE_W[n.lane] : 0) 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 edges: LaidEdge[] = []
const provChildren = new Map<string, string[]>() // provider -> [child] const provChildren = new Map<string, string[]>() // provider -> [child]
// provision edges: host->container, provider->service // provision edges: host->container, provider->service
for (const c of containers) { for (const c of containers) {
const host = containerHost.get(c.slug) const host = containerHost.get(c.slug)
if (host && bySlug.has(host)) { 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 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) pushMap(provChildren, host, c.slug)
} }
} }
for (const s of services) { for (const s of services) {
const p = serviceProvider.get(s.slug) const p = serviceProvider.get(s.slug)
if (p && bySlug.has(p)) { 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 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) pushMap(provChildren, p, s.slug)
} }
} }
@@ -388,9 +412,17 @@
if (!bySlug.has(srcSlug)) continue if (!bySlug.has(srcSlug)) continue
for (const t of targets) { for (const t of targets) {
if (!bySlug.has(t)) continue 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) 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() const q = search.trim().toLowerCase()
if (q) { if (q) {
const s = new Set<string>() 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 s
} }
return null return null
@@ -589,7 +622,9 @@
if (!m || !healthFilter) return [] as LaidNode[] if (!m || !healthFilter) return [] as LaidNode[]
return m.nodes.filter((n) => n.health === healthFilter) 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> </script>
{#if loading && !model} {#if loading && !model}
@@ -636,7 +671,8 @@
<div class="stage relative min-w-0 flex-1 overflow-auto"> <div class="stage relative min-w-0 flex-1 overflow-auto">
<div class="relative" style="width:{model.width}px;height:{model.height}px"> <div class="relative" style="width:{model.width}px;height:{model.height}px">
<!-- background click target: clears selection --> <!-- 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 --> <!-- lane headers -->
<div class="pointer-events-none absolute inset-x-0 top-0 z-10"> <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} {#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> </div>
<!-- edges --> <!-- 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} {#each model.edges as e}
<path d={e.d} class="edge {e.kind} {edgeState(e)}" /> <path d={e.d} class="edge {e.kind} {edgeState(e)}" />
{/each} {/each}
@@ -661,7 +701,9 @@
class:svc={n.kind === 'service'} class:svc={n.kind === 'service'}
class:is-open={selectedSlug === n.slug} class:is-open={selectedSlug === n.slug}
class:pulse={n.health === 'down'} 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)} onmouseenter={() => setFocus(n.slug)}
onmouseleave={scheduleClear} onmouseleave={scheduleClear}
onfocus={() => setFocus(n.slug)} onfocus={() => setFocus(n.slug)}
@@ -673,7 +715,11 @@
title={n.slug} title={n.slug}
> >
{#if n.publicHost} {#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} {#if n.fauth}
<LockIcon class="size-3" /> <LockIcon class="size-3" />
{:else if n.fauth === false} {:else if n.fauth === false}
@@ -748,7 +794,9 @@
<div class="blast"> <div class="blast">
<div class="n" class:warn={detailBlast.length > 0}>{detailBlast.length}</div> <div class="n" class:warn={detailBlast.length > 0}>{detailBlast.length}</div>
<div class="lbl"> <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 affected if this goes down
</div> </div>
</div> </div>
@@ -756,18 +804,40 @@
<div class="det-sec"> <div class="det-sec">
<h4>Attributes</h4> <h4>Attributes</h4>
{#if detailNode.kind === 'container'} {#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}
{#if detailNode.role}<div class="row"><span class="k">role</span><span class="v">{detailNode.role}</span></div>{/if} {#if detailNode.role}<div class="row">
{#if detailNode.ip}<div class="row"><span class="k">ip</span><span class="v">{detailNode.ip}</span></div>{/if} <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} {#if detailNode.publicHost}
<div class="row"><span class="k">url</span><span class="v">{detailNode.publicHost}</span></div> <div class="row">
<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> <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} {: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}
{#if detailNode.mounts.length}<div class="row"><span class="k">mounts</span><span class="v">{detailNode.mounts.join(', ')}</span></div>{/if} {#if detailNode.mounts.length}<div class="row">
{#if detailNode.repo}<div class="row"><span class="k">config</span><span class="v">{detailNode.repo}</span></div>{/if} <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> </div>
{#if detailRunsOn.length} {#if detailRunsOn.length}
@@ -824,18 +894,24 @@
<div class="p-4"> <div class="p-4">
<h3 class="legend-h">Health</h3> <h3 class="legend-h">Health</h3>
{#each HEALTH_ORDER as h} {#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} {/each}
<div class="sep"></div> <div class="sep"></div>
<h3 class="legend-h">Lanes</h3> <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>Hosts — physical machines</div>
<div class="legend-row"><span class="lane-swatch"></span>Containers — LXCs &amp; VMs</div> <div class="legend-row">
<div class="legend-row"><span class="lane-swatch"></span>Services — what you actually use</div> <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> <div class="sep"></div>
<p class="hint"> <p class="hint">
Every service flows right from the machine that runs it. Hover any node to trace its chain Every service flows right from the machine that runs it. Hover any node to trace its
and blast radius — what breaks if it goes down. Click to open it in a window. Dashed arcs are chain and blast radius — what breaks if it goes down. Click to open it in a window.
service-to-service dependencies. Dashed arcs are service-to-service dependencies.
</p> </p>
</div> </div>
{/if} {/if}
@@ -843,7 +919,9 @@
</div> </div>
</div> </div>
{:else} {: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} {/if}
<style> <style>
@@ -883,7 +961,9 @@
stroke: var(--border); stroke: var(--border);
stroke-width: 1.4; stroke-width: 1.4;
opacity: 0.6; opacity: 0.6;
transition: opacity 0.16s, stroke 0.16s; transition:
opacity 0.16s,
stroke 0.16s;
} }
.edge.dep { .edge.dep {
stroke-dasharray: 4 3; stroke-dasharray: 4 3;
@@ -919,7 +999,11 @@
border-radius: 9px; border-radius: 9px;
overflow: hidden; overflow: hidden;
cursor: pointer; 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 { .node::before {
content: ''; content: '';
@@ -1001,7 +1085,10 @@
background: transparent; background: transparent;
color: var(--muted-foreground); color: var(--muted-foreground);
cursor: pointer; 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 { .chip:hover {
border-color: var(--primary); border-color: var(--primary);

View File

@@ -5,7 +5,8 @@
import CircleHelpIcon from '@lucide/svelte/icons/circle-help' import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
// Prop-driven (not store-imported) — see SessionGraph.svelte for why. // 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 freeText = $state('')
let submitting = $state(false) let submitting = $state(false)
@@ -48,7 +49,13 @@
{#if q.context.options?.length} {#if q.context.options?.length}
<div class="ml-6 flex flex-wrap gap-1.5"> <div class="ml-6 flex flex-wrap gap-1.5">
{#each q.context.options as opt} {#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} {opt}
</Button> </Button>
{/each} {/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 Send
</Button> </Button>
</div> </div>

View File

@@ -5,7 +5,15 @@
// these can be open (and independently live) at once. // these can be open (and independently live) at once.
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes' 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 { activityLogFor } from '$lib/stores/activity'
import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace' import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace'
import ChatThread from '$lib/components/ChatThread.svelte' import ChatThread from '$lib/components/ChatThread.svelte'
@@ -77,7 +85,9 @@
<div class="flex h-full min-h-0"> <div class="flex h-full min-h-0">
{#if loading} {#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} {:else if $chatNotFound}
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center"> <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> <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 // 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 // page's global "current session" data or a floating task window's own
// per-session data — see TaskContextPanel.svelte, which supplies both. // 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 // 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 // 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)) const curSlugs = new Set(current.map((n) => n.slug))
let changed = desiredSlugs.size !== curSlugs.size 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 if (!changed) return
const bySlug = new Map(current.map((n) => [n.slug, n])) const bySlug = new Map(current.map((n) => [n.slug, n]))
@@ -178,10 +187,19 @@
return return
} }
sim = forceSimulation(nodes) 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('charge', forceManyBody().strength(-150).distanceMax(240))
.force('center', forceCenter(cw / 2, ch / 2)) .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('x', forceX(cw / 2).strength(0.06))
.force('y', forceY(ch / 2).strength(0.06)) .force('y', forceY(ch / 2).strength(0.06))
.velocityDecay(0.34) .velocityDecay(0.34)
@@ -232,7 +250,9 @@
unknown: 'var(--muted-foreground)' unknown: 'var(--muted-foreground)'
} }
function nodeColor(n: Node): string { 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 { function nodeRadius(n: Node): number {
return 6 + Math.min(Math.sqrt(n.degree) * 1.5, 6) return 6 + Math.min(Math.sqrt(n.degree) * 1.5, 6)
@@ -306,10 +326,17 @@
const selectedRelations = $derived( const selectedRelations = $derived(
selected selected
? links ? 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) => { .map((l) => {
const outgoing = endpointSlug(l.source) === selected!.slug 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"> <aside class="flex h-full min-h-0 flex-col bg-card/40">
{#if nowTouching} {#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> <span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
Now touching <code class="font-mono">{nowTouching.slug}</code> Now touching <code class="font-mono">{nowTouching.slug}</code>
</div> </div>
@@ -325,22 +354,85 @@
<div bind:this={container} class="relative min-h-0 flex-1 overflow-hidden"> <div bind:this={container} class="relative min-h-0 flex-1 overflow-hidden">
{#if nodes.length === 0} {#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"> <svg viewBox="0 0 120 120" class="size-24 text-muted-foreground/40" fill="none">
<circle cx="60" cy="60" r="6" fill="currentColor"> <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> </circle>
<g stroke="currentColor" stroke-width="1" opacity="0.5"> <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="26" y2="34"
<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> ><animate
<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> attributeName="opacity"
<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> 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>
<g fill="currentColor"> <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="26" cy="34" r="3.5"
<circle cx="96" cy="40" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.4s" repeatCount="indefinite" /></circle> ><animate
<circle cx="34" cy="92" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="2.8s" repeatCount="indefinite" /></circle> attributeName="opacity"
<circle cx="92" cy="90" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.1s" repeatCount="indefinite" /></circle> 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> </g>
</svg> </svg>
<p class="max-w-[16rem] text-xs leading-relaxed text-muted-foreground"> <p class="max-w-[16rem] text-xs leading-relaxed text-muted-foreground">
@@ -394,7 +486,8 @@
{#if node.x != null && node.y != null} {#if node.x != null && node.y != null}
{@const r = nodeRadius(node)} {@const r = nodeRadius(node)}
{@const isSel = selected?.slug === node.slug} {@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 isTouched = node.slug in touchedBySlug}
{@const diff = diffBySlug[node.slug]} {@const diff = diffBySlug[node.slug]}
<g <g
@@ -410,12 +503,33 @@
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" /> <circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
{/if} {/if}
{#if isTouched} {#if isTouched}
<circle r={r + 4} fill="none" stroke="var(--primary)" stroke-width="1.5" opacity="0.8"> <circle
<animate attributeName="r" values="{r + 3};{r + 8};{r + 3}" dur="1.6s" repeatCount="indefinite" /> r={r + 4}
<animate attributeName="opacity" values="0.8;0.1;0.8" dur="1.6s" repeatCount="indefinite" /> 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> </circle>
{/if} {/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 <text
y={r + 10} y={r + 10}
text-anchor="middle" text-anchor="middle"

View File

@@ -12,7 +12,11 @@
<svg viewBox="0 0 24 24" class={className} fill="none" aria-hidden="true"> <svg viewBox="0 0 24 24" class={className} fill="none" aria-hidden="true">
{#each Array.from({ length: TICKS }) as _, i (i)} {#each Array.from({ length: TICKS }) as _, i (i)}
<rect <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" fill="currentColor"
opacity="0.15" opacity="0.15"
transform="rotate({i * (360 / TICKS)} 12 12)" transform="rotate({i * (360 / TICKS)} 12 12)"

View File

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

View File

@@ -1,7 +1,15 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes' 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 { streaming, messages, chatFor } from '$lib/stores/chat'
import { activityLog, activityLogFor } from '$lib/stores/activity' import { activityLog, activityLogFor } from '$lib/stores/activity'
import SessionGraph from './SessionGraph.svelte' import SessionGraph from './SessionGraph.svelte'
@@ -70,7 +78,12 @@
<div class="flex h-full min-h-0 flex-col"> <div class="flex h-full min-h-0 flex-col">
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1"> <Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
<!-- Scope --> <!-- 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 <button
type="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" 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 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> <span>Scope</span>
{#if !scopeOpen} {#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} {/if}
</button> </button>
{#if scopeOpen} {#if scopeOpen}
<div class="min-h-0 flex-1"> <div class="min-h-0 flex-1">
<SessionGraph messages={$messagesStore} touched={$touchedStore} healthDiffs={$healthDiffsStore} /> <SessionGraph
messages={$messagesStore}
touched={$touchedStore}
healthDiffs={$healthDiffsStore}
/>
</div> </div>
{/if} {/if}
</Pane> </Pane>
<!-- Activity (merged plan + event log) --> <!-- 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 <button
type="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" 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 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> <span>Activity</span>
{#if $streamingStore && activityRunning > 0} {#if $streamingStore && activityRunning > 0}
<Spinner class="size-3 text-primary" /> <Spinner class="size-3 text-primary" />
{/if} {/if}
{#if planTotal > 0} {#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}
{#if !activityOpen && planTotal === 0} {#if !activityOpen && planTotal === 0}
{#if $taskStore?.goal} {#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} {: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}
{/if} {/if}
</button> </button>
{#if activityOpen} {#if activityOpen}
<div class="min-h-0 flex-1 overflow-hidden"> <div class="min-h-0 flex-1 overflow-hidden">
<UnifiedTimeline entries={$activityLogStore} planSteps={$planStepsStore} streaming={$streamingStore} /> <UnifiedTimeline
entries={$activityLogStore}
planSteps={$planStepsStore}
streaming={$streamingStore}
/>
</div> </div>
{/if} {/if}
</Pane> </Pane>

View File

@@ -51,13 +51,17 @@
<span class="min-w-0 flex-1"> <span class="min-w-0 flex-1">
<span class="block truncate text-xs text-foreground/90">{label}</span> <span class="block truncate text-xs text-foreground/90">{label}</span>
{#if argsSummary} {#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} {/if}
</span> </span>
<span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span> <span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span>
{#if hasDetail} {#if hasDetail}
<ChevronRight <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} {/if}
</button> </button>
@@ -66,20 +70,41 @@
<div class="space-y-2 px-2 pb-2 pl-7"> <div class="space-y-2 px-2 pb-2 pl-7">
{#if tool.args} {#if tool.args}
<div> <div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Args</div> <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> 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> </div>
{/if} {/if}
{#if tool.result !== undefined && tool.result !== null} {#if tool.result !== undefined && tool.result !== null}
<div> <div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Result</div> <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> 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> </div>
{/if} {/if}
{#if tool.error} {#if tool.error}
<div> <div>
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">Error</div> <div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">
<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> 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> </div>
{/if} {/if}
</div> </div>

View File

@@ -30,7 +30,11 @@
// current step visible while the agent works (follow mode disengages if // current step visible while the agent works (follow mode disengages if
// the operator scrolls down into history, re-engages when streaming // the operator scrolls down into history, re-engages when streaming
// starts again) // starts again)
let { entries, planSteps: steps, streaming = false }: { let {
entries,
planSteps: steps,
streaming = false
}: {
entries: ActivityEntry[] entries: ActivityEntry[]
planSteps: PlanStep[] planSteps: PlanStep[]
streaming?: boolean streaming?: boolean
@@ -75,7 +79,9 @@
continue continue
} }
const tools = entries.filter( 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) const stepEntry = entries.find((e) => e.id === s.id)
// Timed from the step's own entry, else its earliest tool — so a step // 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). // entries land while following (instant, to avoid scroll-queue jank).
$effect(() => { $effect(() => {
if (!currentId || !follow || !container) return 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' }) ?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}) })
let lastEntryCount = 0 let lastEntryCount = 0
@@ -157,7 +164,12 @@
// height so tools inside an expanded step stay on the line; first/last // 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 // items clip theirs to their node/tool centers so the line never dangles
// past the timeline's ends. // 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' let color = 'bg-border'
if (status === 'done') color = 'bg-primary/60' if (status === 'done') color = 'bg-primary/60'
else if (status === 'running') color = 'bg-primary/40' else if (status === 'running') color = 'bg-primary/40'
@@ -172,11 +184,16 @@
function entryIcon(entry: ActivityEntry) { function entryIcon(entry: ActivityEntry) {
switch (entry.type) { switch (entry.type) {
case 'goal': return MilestoneIcon case 'goal':
case 'knowledge': return SparklesIcon return MilestoneIcon
case 'complete': return FlagIcon case 'knowledge':
case 'question': return HelpCircleIcon return SparklesIcon
default: return WrenchIcon case 'complete':
return FlagIcon
case 'question':
return HelpCircleIcon
default:
return WrenchIcon
} }
} }
function hhmm(ts: number): string { function hhmm(ts: number): string {
@@ -185,10 +202,18 @@
} }
function hhmmss(ts: number): string { function hhmmss(ts: number): string {
if (!ts) return '' 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 { 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> </script>
@@ -197,19 +222,57 @@
{#if items.length === 0} {#if items.length === 0}
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center"> <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"> <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"> <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>
<circle cx="32" cy="55" r="4" fill="currentColor"> <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>
<circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5"> <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
<animate attributeName="opacity" values="0.6;0;0.6" dur="2.4s" begin="0.6s" repeatCount="indefinite" /> 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>
<circle cx="32" cy="88" r="4" fill="currentColor"> <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> </circle>
</svg> </svg>
<p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p> <p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p>
@@ -227,24 +290,42 @@
{@const expandedWithTools = open && item.tools.length > 0} {@const expandedWithTools = open && item.tools.length > 0}
<!-- Step node on the backbone --> <!-- Step node on the backbone -->
<div class="relative" data-tl-id={item.step.id}> <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 <button
type="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)} onclick={() => expandable && toggleStep(item.step)}
aria-expanded={open} aria-expanded={open}
disabled={!expandable} disabled={!expandable}
> >
<!-- Filled status node --> <!-- Filled status node -->
<span class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full <span
{st === 'done' ? 'bg-primary' class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
: st === 'running' ? 'bg-background' {st === 'done'
: st === 'failed' ? 'bg-destructive' ? 'bg-primary'
: st === 'blocked' ? 'bg-warning/25 border border-warning' : st === 'running'
: st === 'skipped' || st === 'replaced' ? 'bg-muted' ? 'bg-background'
: 'bg-background border border-muted-foreground/40'}"> : 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'} {#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" /> <Spinner class="relative size-3.5 text-primary" />
{:else if st === 'done'} {:else if st === 'done'}
<CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} /> <CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} />
@@ -256,15 +337,28 @@
<SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} /> <SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} />
{/if} {/if}
</span> </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} {item.step.title}
</span> </span>
{#if hhmm(item.ts)} {#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}
{#if item.tools.length > 0} {#if item.tools.length > 0}
<span class="shrink-0 text-muted-foreground/60"> <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> </span>
{/if} {/if}
</button> </button>
@@ -275,10 +369,19 @@
{@const tOpen = expandedTools.has(tool.id)} {@const tOpen = expandedTools.has(tool.id)}
<div class="relative" data-tl-id={tool.id}> <div class="relative" data-tl-id={tool.id}>
<!-- Branch stub: backbone → tool --> <!-- 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 <button
type="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)} onclick={() => (tool.args || tool.detail) && toggleTool(tool.id)}
> >
<span class="flex size-3 shrink-0 items-center justify-center"> <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} /> <CheckIcon class="size-2.5 text-primary/70" strokeWidth={3.5} />
{/if} {/if}
</span> </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} {tool.description}
</span> </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> </button>
{#if tOpen} {#if tOpen}
<div transition:slide={{ duration: 120 }} class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3"> <div
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70"> 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 class="capitalize">{tool.status}</span>
<span aria-hidden="true">·</span> <span aria-hidden="true">·</span>
<span>{hhmmss(tool.timestamp)}</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> </div>
{#if tool.args} {#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}
{#if tool.detail} {#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} {/if}
</div> </div>
{/if} {/if}
@@ -322,14 +448,31 @@
{@const Icon = entryIcon(e)} {@const Icon = entryIcon(e)}
{@const eOpen = expandedTools.has(e.id)} {@const eOpen = expandedTools.has(e.id)}
<div class="relative" data-tl-id={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 <button
type="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)} 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 <span
{e.status === 'failed' ? 'border-destructive text-destructive' : e.status === 'running' ? 'border-primary text-primary' : 'border-border text-primary'}"> 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'} {#if e.status === 'running'}
<Spinner class="size-2.5" /> <Spinner class="size-2.5" />
{:else if e.status === 'failed'} {:else if e.status === 'failed'}
@@ -338,24 +481,43 @@
<Icon class="size-2" strokeWidth={2.5} /> <Icon class="size-2" strokeWidth={2.5} />
{/if} {/if}
</span> </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} {e.description}
</span> </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> </button>
{#if eOpen} {#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"> <div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
<span class="capitalize">{e.status}</span> <span class="capitalize">{e.status}</span>
<span aria-hidden="true">·</span> <span aria-hidden="true">·</span>
<span>{hhmmss(e.timestamp)}</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> </div>
{#if e.args} {#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}
{#if e.detail} {#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} {/if}
</div> </div>
{/if} {/if}

View File

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

View File

@@ -2,7 +2,10 @@
import * as Select from '$lib/components/ui/select' import * as Select from '$lib/components/ui/select'
import type { TableHandler } from '@vincjo/datatables' 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] const options = [10, 20, 50, 100]
let value = $state('20') let value = $state('20')

View File

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

View File

@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Badge, type BadgeVariant } from '$lib/components/ui/badge' 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> </script>
<Badge {variant}>{String(value ?? '—')}</Badge> <Badge {variant}>{String(value ?? '—')}</Badge>

View File

@@ -23,7 +23,7 @@
</script> </script>
{#if health} {#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="size-2 shrink-0 rounded-full {dot[row.health ?? ''] ?? ''}"></span>
<span class="text-muted-foreground">{relativeTime(lastCheck)}</span> <span class="text-muted-foreground">{relativeTime(lastCheck)}</span>
</span> </span>

View File

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

View File

@@ -1,21 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Badge } from '$lib/components/ui/badge' 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 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: { risk: {
destructive: 'destructive', destructive: 'destructive',
config_mutation: 'secondary', config_mutation: 'secondary',
default: 'default', default: 'default'
}, },
severity: { severity: {
critical: 'destructive', critical: 'destructive',
warning: 'secondary', warning: 'secondary',
info: 'default', info: 'default',
default: 'default', default: 'default'
}, },
execution: { execution: {
failed: 'destructive', failed: 'destructive',
@@ -25,19 +32,19 @@
completed: 'default', completed: 'default',
running: 'secondary', running: 'secondary',
approved: 'secondary', approved: 'secondary',
default: 'outline', default: 'outline'
}, },
state: { state: {
active: 'default', active: 'default',
healthy: 'default', healthy: 'default',
default: 'outline', default: 'outline'
}, },
type: { type: {
runbook: 'secondary', runbook: 'secondary',
investigation: 'default', investigation: 'default',
default: 'outline', default: 'outline'
}, },
default: { default: 'default' }, default: { default: 'default' }
} }
const variant = $derived.by(() => { const variant = $derived.by(() => {

View File

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

View File

@@ -88,10 +88,14 @@
onkeydown={onKeydown} onkeydown={onKeydown}
title={app.title} 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" /> <app.icon class="size-5" />
{#if badge > 0} {#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} {badge > 99 ? '99+' : badge}
</span> </span>
{/if} {/if}

View File

@@ -29,7 +29,9 @@
{@const C = mod.default} {@const C = mod.default}
<C /> <C />
{:catch error} {: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} Failed to load app: {(error as Error).message}
</div> </div>
{/await} {/await}

View File

@@ -22,7 +22,7 @@
messages={[]} messages={[]}
streaming={false} streaming={false}
connectionState="connected" connectionState="connected"
onSend={onSend} {onSend}
onCancel={() => {}} onCancel={() => {}}
onReconnect={() => {}} onReconnect={() => {}}
onDismissError={() => {}} 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" 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"> <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"> <Button type="submit" size="icon" disabled={!input.trim()} aria-label="Start task">
<ArrowUpIcon /> <ArrowUpIcon />
</Button> </Button>

View File

@@ -51,7 +51,6 @@
wm.focus(id) wm.focus(id)
} }
} }
</script> </script>
<div class="flex h-11 shrink-0 items-center gap-1.5 border-t bg-muted/30 px-2"> <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 === 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' win.id && win.stage !== 'minimized'
? 'border-primary/50 bg-primary/10 text-foreground' ? '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)} onclick={() => toggle(win.id, win)}
title={win.title} title={win.title}
> >
{#if Icon}<Icon class="size-3.5 shrink-0" />{/if} {#if Icon}<Icon class="size-3.5 shrink-0" />{/if}
<span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span> <span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span>
{#if badge > 0} {#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} {badge > 99 ? '99+' : badge}
</span> </span>
{/if} {/if}

View File

@@ -9,7 +9,14 @@
// session:<id> -> SessionChatWindow (windows.ts openTaskWindow) // session:<id> -> SessionChatWindow (windows.ts openTaskWindow)
// new-task -> NewTaskChat (windows.ts openNewTaskWindow) // new-task -> NewTaskChat (windows.ts openNewTaskWindow)
// anything else -> entity slug -> EntityDetailContent // 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 { appById, appIdFromWindowId } from '$lib/apps'
import EntityDetailContent from '../EntityDetailContent.svelte' import EntityDetailContent from '../EntityDetailContent.svelte'
import SessionChatWindow from '../SessionChatWindow.svelte' import SessionChatWindow from '../SessionChatWindow.svelte'
@@ -57,8 +64,15 @@
{@const app = appId ? $appById.get(appId) : undefined} {@const app = appId ? $appById.get(appId) : undefined}
{#if win && (!appId || app)} {#if win && (!appId || app)}
<section use:dk.window={{ id }} class="min-w-0" aria-label={win.title}> <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"> <header
<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> 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"> <div class="flex shrink-0 items-center gap-0.5">
<button <button
type="button" type="button"

View File

@@ -1,49 +1,50 @@
<script lang="ts" module> <script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from 'tailwind-variants'
export const badgeVariants = tv({ 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", 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/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", destructive:
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", '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',
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
link: "text-primary underline-offset-4 hover:underline", ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
}, link: 'text-primary underline-offset-4 hover:underline'
}, }
defaultVariants: { },
variant: "default", defaultVariants: {
}, variant: 'default'
}); }
})
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"]; export type BadgeVariant = VariantProps<typeof badgeVariants>['variant']
</script> </script>
<script lang="ts"> <script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements"; import type { HTMLAnchorAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
href, href,
class: className, class: className,
variant = "default", variant = 'default',
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAnchorAttributes> & { }: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant; variant?: BadgeVariant
} = $props(); } = $props()
</script> </script>
<svelte:element <svelte:element
this={href ? "a" : "span"} this={href ? 'a' : 'span'}
bind:this={ref} bind:this={ref}
data-slot="badge" data-slot="badge"
{href} {href}
class={cn(badgeVariants({ variant }), className)} class={cn(badgeVariants({ variant }), className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</svelte:element> </svelte:element>

View File

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

View File

@@ -1,82 +1,89 @@
<script lang="ts" module> <script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils.js'
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements"; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from 'tailwind-variants'
export const buttonVariants = tv({ 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", 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80", 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", outline:
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", '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',
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", secondary:
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", 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
link: "text-primary underline-offset-4 hover:underline", ghost:
}, 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
size: { destructive:
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", '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',
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", link: 'text-primary underline-offset-4 hover:underline'
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", size: {
icon: "size-9", default:
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", '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',
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", 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",
"icon-lg": "size-10", 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',
defaultVariants: { 'icon-xs':
variant: "default", "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
size: "default", '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 ButtonVariant = VariantProps<typeof buttonVariants>['variant']
export type ButtonSize = VariantProps<typeof buttonVariants>["size"]; export type ButtonSize = VariantProps<typeof buttonVariants>['size']
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & { WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant; variant?: ButtonVariant
size?: ButtonSize; size?: ButtonSize
}; }
</script> </script>
<script lang="ts"> <script lang="ts">
let { let {
class: className, class: className,
variant = "default", variant = 'default',
size = "default", size = 'default',
ref = $bindable(null), ref = $bindable(null),
href = undefined, href = undefined,
type = "button", type = 'button',
disabled, disabled,
children, children,
...restProps ...restProps
}: ButtonProps = $props(); }: ButtonProps = $props()
</script> </script>
{#if href} {#if href}
<a <a
bind:this={ref} bind:this={ref}
data-slot="button" data-slot="button"
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href} href={disabled ? undefined : href}
aria-disabled={disabled} aria-disabled={disabled}
role={disabled ? "link" : undefined} role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined} tabindex={disabled ? -1 : undefined}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</a> </a>
{:else} {:else}
<button <button
bind:this={ref} bind:this={ref}
data-slot="button" data-slot="button"
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
{type} {type}
{disabled} {disabled}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</button> </button>
{/if} {/if}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,25 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements'
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
size = "default", size = 'default',
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props()
</script> </script>
<div <div
bind:this={ref} bind:this={ref}
data-slot="card" data-slot="card"
data-size={size} 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)} class={cn(
{...restProps} '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> </div>

View File

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

View File

@@ -25,7 +25,10 @@
{...restProps} {...restProps}
> >
{#snippet children({ checked, indeterminate })} {#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} {#if indeterminate}
<MinusIcon class="size-3.5" /> <MinusIcon class="size-3.5" />
{:else if checked} {:else if checked}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} /> <CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} /> <CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
import ContextMenuPortal from "./context-menu-portal.svelte"; import ContextMenuPortal from './context-menu-portal.svelte'
import type { ComponentProps } from "svelte"; import type { ComponentProps } from 'svelte'
import type { WithoutChildrenOrChild } from "$lib/utils.js"; import type { WithoutChildrenOrChild } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
portalProps, portalProps,
class: className, class: className,
...restProps ...restProps
}: ContextMenuPrimitive.ContentProps & { }: ContextMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>
} = $props(); } = $props()
</script> </script>
<ContextMenuPortal {...portalProps}> <ContextMenuPortal {...portalProps}>
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
bind:ref bind:ref
data-slot="context-menu-content" data-slot="context-menu-content"
class={cn( 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", '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 className
)} )}
{...restProps} {...restProps}
/> />
</ContextMenuPortal> </ContextMenuPortal>

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} /> <ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />

View File

@@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
variant = "default", variant = 'default',
...restProps ...restProps
}: ContextMenuPrimitive.ItemProps & { }: ContextMenuPrimitive.ItemProps & {
inset?: boolean; inset?: boolean
variant?: "default" | "destructive"; variant?: 'default' | 'destructive'
} = $props(); } = $props()
</script> </script>
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
bind:ref bind:ref
data-slot="context-menu-item" data-slot="context-menu-item"
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
/> />

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<ContextMenuPrimitive.Portal {...restProps} /> <ContextMenuPrimitive.Portal {...restProps} />

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: ContextMenuPrimitive.SubContentProps = $props(); }: ContextMenuPrimitive.SubContentProps = $props()
</script> </script>
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
bind:ref bind:ref
data-slot="context-menu-sub-content" 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)} class={cn(
{...restProps} '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"> <script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChild } from "$lib/utils.js"; import { cn, type WithoutChild } from '$lib/utils.js'
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'; import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
children, children,
...restProps ...restProps
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & { }: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
inset?: boolean; inset?: boolean
} = $props(); } = $props()
</script> </script>
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
bind:ref bind:ref
data-slot="context-menu-sub-trigger" data-slot="context-menu-sub-trigger"
data-inset={inset} data-inset={inset}
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronRightIcon class="ml-auto" /> <ChevronRightIcon class="ml-auto" />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<ContextMenuPrimitive.Sub bind:open {...restProps} /> <ContextMenuPrimitive.Sub bind:open {...restProps} />

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<ContextMenuPrimitive.Root bind:open {...restProps} /> <ContextMenuPrimitive.Root bind:open {...restProps} />

View File

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

View File

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

View File

@@ -1,48 +1,48 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui'
import DialogPortal from "./dialog-portal.svelte"; import DialogPortal from './dialog-portal.svelte'
import type { Snippet } from "svelte"; import type { Snippet } from 'svelte'
import * as Dialog from "./index.js"; import * as Dialog from './index.js'
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { ComponentProps } from "svelte"; import type { ComponentProps } from 'svelte'
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js'
import XIcon from '@lucide/svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
portalProps, portalProps,
children, children,
showCloseButton = true, showCloseButton = true,
...restProps ...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & { }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>
children: Snippet; children: Snippet
showCloseButton?: boolean; showCloseButton?: boolean
} = $props(); } = $props()
</script> </script>
<DialogPortal {...portalProps}> <DialogPortal {...portalProps}>
<Dialog.Overlay /> <Dialog.Overlay />
<DialogPrimitive.Content <DialogPrimitive.Content
bind:ref bind:ref
data-slot="dialog-content" data-slot="dialog-content"
class={cn( 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", '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 className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
{#if showCloseButton} {#if showCloseButton}
<DialogPrimitive.Close data-slot="dialog-close"> <DialogPrimitive.Close data-slot="dialog-close">
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}> <Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
<XIcon /> <XIcon />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</Button> </Button>
{/snippet} {/snippet}
</DialogPrimitive.Close> </DialogPrimitive.Close>
{/if} {/if}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>

View File

@@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: DialogPrimitive.DescriptionProps = $props(); }: DialogPrimitive.DescriptionProps = $props()
</script> </script>
<DialogPrimitive.Description <DialogPrimitive.Description
bind:ref bind:ref
data-slot="dialog-description" data-slot="dialog-description"
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)} class={cn(
{...restProps} '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"> <script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils.js'
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from 'svelte/elements'
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui'
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
showCloseButton = false, showCloseButton = false,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
showCloseButton?: boolean; showCloseButton?: boolean
} = $props(); } = $props()
</script> </script>
<div <div
bind:this={ref} bind:this={ref}
data-slot="dialog-footer" data-slot="dialog-footer"
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
{#if showCloseButton} {#if showCloseButton}
<DialogPrimitive.Close> <DialogPrimitive.Close>
{#snippet child({ props })} {#snippet child({ props })}
<Button variant="outline" {...props}>Close</Button> <Button variant="outline" {...props}>Close</Button>
{/snippet} {/snippet}
</DialogPrimitive.Close> </DialogPrimitive.Close>
{/if} {/if}
</div> </div>

View File

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

View File

@@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui"; import { Dialog as DialogPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: DialogPrimitive.OverlayProps = $props(); }: DialogPrimitive.OverlayProps = $props()
</script> </script>
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
bind:ref bind:ref
data-slot="dialog-overlay" 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)} class={cn(
{...restProps} '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"> <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> </script>
<DialogPrimitive.Portal {...restProps} /> <DialogPrimitive.Portal {...restProps} />

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DialogPrimitive.Root bind:open {...restProps} /> <DialogPrimitive.Root bind:open {...restProps} />

View File

@@ -1,34 +1,34 @@
import Root from "./dialog.svelte"; import Root from './dialog.svelte'
import Portal from "./dialog-portal.svelte"; import Portal from './dialog-portal.svelte'
import Title from "./dialog-title.svelte"; import Title from './dialog-title.svelte'
import Footer from "./dialog-footer.svelte"; import Footer from './dialog-footer.svelte'
import Header from "./dialog-header.svelte"; import Header from './dialog-header.svelte'
import Overlay from "./dialog-overlay.svelte"; import Overlay from './dialog-overlay.svelte'
import Content from "./dialog-content.svelte"; import Content from './dialog-content.svelte'
import Description from "./dialog-description.svelte"; import Description from './dialog-description.svelte'
import Trigger from "./dialog-trigger.svelte"; import Trigger from './dialog-trigger.svelte'
import Close from "./dialog-close.svelte"; import Close from './dialog-close.svelte'
export { export {
Root, Root,
Title, Title,
Portal, Portal,
Footer, Footer,
Header, Header,
Trigger, Trigger,
Overlay, Overlay,
Content, Content,
Description, Description,
Close, Close,
// //
Root as Dialog, Root as Dialog,
Title as DialogTitle, Title as DialogTitle,
Portal as DialogPortal, Portal as DialogPortal,
Footer as DialogFooter, Footer as DialogFooter,
Header as DialogHeader, Header as DialogHeader,
Trigger as DialogTrigger, Trigger as DialogTrigger,
Overlay as DialogOverlay, Overlay as DialogOverlay,
Content as DialogContent, Content as DialogContent,
Description as DialogDescription, Description as DialogDescription,
Close as DialogClose, Close as DialogClose
}; }

View File

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

View File

@@ -1,44 +1,44 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import MinusIcon from '@lucide/svelte/icons/minus'; import MinusIcon from '@lucide/svelte/icons/minus'
import CheckIcon from '@lucide/svelte/icons/check'; import CheckIcon from '@lucide/svelte/icons/check'
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { Snippet } from "svelte"; import type { Snippet } from 'svelte'
let { let {
ref = $bindable(null), ref = $bindable(null),
checked = $bindable(false), checked = $bindable(false),
indeterminate = $bindable(false), indeterminate = $bindable(false),
class: className, class: className,
children: childrenProp, children: childrenProp,
...restProps ...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & { }: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet; children?: Snippet
} = $props(); } = $props()
</script> </script>
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
bind:ref bind:ref
bind:checked bind:checked
bind:indeterminate bind:indeterminate
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked, indeterminate })} {#snippet children({ checked, indeterminate })}
<span <span
class="absolute right-2 flex items-center justify-center pointer-events-none" class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-checkbox-item-indicator" data-slot="dropdown-menu-checkbox-item-indicator"
> >
{#if indeterminate} {#if indeterminate}
<MinusIcon /> <MinusIcon />
{:else if checked} {:else if checked}
<CheckIcon /> <CheckIcon />
{/if} {/if}
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
{/snippet} {/snippet}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>

View File

@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import DropdownMenuPortal from "./dropdown-menu-portal.svelte"; import DropdownMenuPortal from './dropdown-menu-portal.svelte'
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import type { ComponentProps } from "svelte"; import type { ComponentProps } from 'svelte'
let { let {
ref = $bindable(null), ref = $bindable(null),
sideOffset = 4, sideOffset = 4,
align = "start", align = 'start',
portalProps, portalProps,
class: className, class: className,
...restProps ...restProps
}: DropdownMenuPrimitive.ContentProps & { }: DropdownMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>
} = $props(); } = $props()
</script> </script>
<DropdownMenuPortal {...portalProps}> <DropdownMenuPortal {...portalProps}>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
bind:ref bind:ref
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
{sideOffset} {sideOffset}
{align} {align}
class={cn( 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", '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 className
)} )}
{...restProps} {...restProps}
/> />
</DropdownMenuPortal> </DropdownMenuPortal>

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
import type { ComponentProps } from "svelte"; import type { ComponentProps } from 'svelte'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
...restProps ...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & { }: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean; inset?: boolean
} = $props(); } = $props()
</script> </script>
<DropdownMenuPrimitive.GroupHeading <DropdownMenuPrimitive.GroupHeading
bind:ref bind:ref
data-slot="dropdown-menu-group-heading" data-slot="dropdown-menu-group-heading"
data-inset={inset} data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)} class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
{...restProps} {...restProps}
/> />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} /> <DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View File

@@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
variant = "default", variant = 'default',
...restProps ...restProps
}: DropdownMenuPrimitive.ItemProps & { }: DropdownMenuPrimitive.ItemProps & {
inset?: boolean; inset?: boolean
variant?: "default" | "destructive"; variant?: 'default' | 'destructive'
} = $props(); } = $props()
</script> </script>
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
bind:ref bind:ref
data-slot="dropdown-menu-item" data-slot="dropdown-menu-item"
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
/> />

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.Portal {...restProps} /> <DropdownMenuPrimitive.Portal {...restProps} />

View File

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

View File

@@ -1,34 +1,34 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import CheckIcon from '@lucide/svelte/icons/check'; import CheckIcon from '@lucide/svelte/icons/check'
import { cn, type WithoutChild } from "$lib/utils.js"; import { cn, type WithoutChild } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children: childrenProp, children: childrenProp,
...restProps ...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props(); }: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props()
</script> </script>
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
bind:ref bind:ref
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ checked })} {#snippet children({ checked })}
<span <span
class="absolute right-2 flex items-center justify-center pointer-events-none" class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-radio-item-indicator" data-slot="dropdown-menu-radio-item-indicator"
> >
{#if checked} {#if checked}
<CheckIcon /> <CheckIcon />
{/if} {/if}
</span> </span>
{@render childrenProp?.({ checked })} {@render childrenProp?.({ checked })}
{/snippet} {/snippet}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>

View File

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

View File

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

View File

@@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: DropdownMenuPrimitive.SubContentProps = $props(); }: DropdownMenuPrimitive.SubContentProps = $props()
</script> </script>
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
bind:ref bind:ref
data-slot="dropdown-menu-sub-content" 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)} class={cn(
{...restProps} '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"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'; import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
import { cn } from "$lib/utils.js"; import { cn } from '$lib/utils.js'
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
inset, inset,
children, children,
...restProps ...restProps
}: DropdownMenuPrimitive.SubTriggerProps & { }: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean; inset?: boolean
} = $props(); } = $props()
</script> </script>
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
bind:ref bind:ref
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
class={cn( 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", "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 className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronRightIcon class="ml-auto" /> <ChevronRightIcon class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.Sub bind:open {...restProps} /> <DropdownMenuPrimitive.Sub bind:open {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} /> <DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <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> </script>
<DropdownMenuPrimitive.Root bind:open {...restProps} /> <DropdownMenuPrimitive.Root bind:open {...restProps} />

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