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,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -6,7 +6,10 @@
<title>Oikos</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="android-chrome-192.png" />
@@ -15,11 +18,20 @@
<body>
<div id="app"></div>
<script>
(function(){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){}})()
;(function () {
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 src="/wails/runtime.js"></script>
<script>window.__OIKOS_CONFIG__ = {};</script>
<script>
window.__OIKOS_CONFIG__ = {}
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,10 +60,16 @@
let wasStreaming = $state(false)
$effect(() => {
if (streaming) { indicatorDone = false; wasStreaming = true }
if (streaming) {
indicatorDone = false
wasStreaming = true
}
if (!streaming && wasStreaming) {
indicatorDone = true
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000)
const t = setTimeout(() => {
indicatorDone = false
wasStreaming = false
}, 3000)
return () => clearTimeout(t)
}
})
@@ -91,7 +97,10 @@
const lineHeight = parseFloat(taCs.lineHeight)
if (!Number.isFinite(lineHeight)) return
const taBoxY =
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
parseFloat(taCs.paddingTop) +
parseFloat(taCs.paddingBottom) +
parseFloat(taCs.borderTopWidth) +
parseFloat(taCs.borderBottomWidth)
// The wrapper's own padding/border (space around the textarea, not part
// of it) also has to fit inside the minimum, or the textarea gets
// squeezed below one line once the pane is dragged down to it.
@@ -144,7 +153,9 @@
}
renderer.table = function (token) {
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
const body = token.rows.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`).join('')
const body = token.rows
.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`)
.join('')
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
}
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
@@ -181,7 +192,13 @@
</script>
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1" on:resize={() => (userResizedInput = true)}>
<Splitpanes
horizontal
theme="oikos-theme"
dblClickSplitter={false}
class="min-h-0 flex-1"
on:resize={() => (userResizedInput = true)}
>
<Pane class="flex flex-col">
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
@@ -189,12 +206,19 @@
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
<div>
<h2 class="text-xl font-semibold">Nomos</h2>
<p class="mt-1 text-sm text-muted-foreground">Your resident operator. Ask about the fleet, or tell it to act.</p>
<p class="mt-1 text-sm text-muted-foreground">
Your resident operator. Ask about the fleet, or tell it to act.
</p>
</div>
{#if suggestions.length}
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
{#each suggestions as q}
<Button variant="outline" size="sm" class="h-auto justify-start whitespace-normal py-2 text-left text-xs" onclick={() => ask(q)}>
<Button
variant="outline"
size="sm"
class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
onclick={() => ask(q)}
>
{q}
</Button>
{/each}
@@ -209,10 +233,16 @@
<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>
<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>
<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
@@ -228,7 +258,9 @@
<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>
<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
@@ -263,16 +295,25 @@
{#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">
<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>
<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" />
<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>
@@ -280,7 +321,9 @@
{#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">
<div
class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
{error}
</div>
</div>
@@ -288,19 +331,33 @@
{#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">
<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>
<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>
<button
class="ml-1 text-muted-foreground hover:text-foreground"
onclick={() => onDismissError(err.id)}
aria-label="Dismiss">×</button
>
</div>
</div>
{/each}
</Pane>
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
<div
class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative"
bind:this={inputWrapperRef}
>
<form
class="relative mx-auto flex h-full w-full max-w-3xl"
onsubmit={(e) => {
@@ -514,7 +571,13 @@
border: none;
height: 1px;
margin: 0.75rem 0;
background: linear-gradient(to right, transparent, var(--border) 20%, var(--border) 80%, transparent);
background: linear-gradient(
to right,
transparent,
var(--border) 20%,
var(--border) 80%,
transparent
);
}
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
@@ -559,7 +622,9 @@
border-radius: 0.375rem;
color: var(--muted-foreground);
opacity: 0;
transition: opacity 0.15s, color 0.15s;
transition:
opacity 0.15s,
color 0.15s;
cursor: pointer;
border: none;
background: transparent;
@@ -586,7 +651,12 @@
}
@keyframes cursor-blink {
0%, 100% { opacity: 0.75; }
50% { opacity: 0; }
0%,
100% {
opacity: 0.75;
}
50% {
opacity: 0;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
buttonVariants
} from './button.svelte'
export {
Root,
@@ -13,5 +13,5 @@ export {
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
type ButtonVariant
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
import CheckIcon from '@lucide/svelte/icons/check';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { Snippet } from 'svelte'
import CheckIcon from '@lucide/svelte/icons/check'
let {
ref = $bindable(null),
@@ -13,9 +13,9 @@
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet;
} = $props();
inset?: boolean
children?: Snippet
} = $props()
</script>
<ContextMenuPrimitive.CheckboxItem

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
import CheckIcon from '@lucide/svelte/icons/check';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChild } from '$lib/utils.js'
import CheckIcon from '@lucide/svelte/icons/check'
let {
ref = $bindable(null),
@@ -10,8 +10,8 @@
children: childrenProp,
...restProps
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
inset?: boolean;
} = $props();
inset?: boolean
} = $props()
</script>
<ContextMenuPrimitive.RadioItem

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.SeparatorProps = $props();
}: ContextMenuPrimitive.SeparatorProps = $props()
</script>
<ContextMenuPrimitive.Separator
bind:ref
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}
/>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
import { cn, type WithoutChild } from '$lib/utils.js'
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
let {
ref = $bindable(null),
@@ -10,8 +10,8 @@
children,
...restProps
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
inset?: boolean;
} = $props();
inset?: boolean
} = $props()
</script>
<ContextMenuPrimitive.SubTrigger

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
import Root from "./context-menu.svelte";
import Sub from "./context-menu-sub.svelte";
import Portal from "./context-menu-portal.svelte";
import Trigger from "./context-menu-trigger.svelte";
import Group from "./context-menu-group.svelte";
import RadioGroup from "./context-menu-radio-group.svelte";
import Item from "./context-menu-item.svelte";
import GroupHeading from "./context-menu-group-heading.svelte";
import Content from "./context-menu-content.svelte";
import Shortcut from "./context-menu-shortcut.svelte";
import RadioItem from "./context-menu-radio-item.svelte";
import Separator from "./context-menu-separator.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
import Label from "./context-menu-label.svelte";
import Root from './context-menu.svelte'
import Sub from './context-menu-sub.svelte'
import Portal from './context-menu-portal.svelte'
import Trigger from './context-menu-trigger.svelte'
import Group from './context-menu-group.svelte'
import RadioGroup from './context-menu-radio-group.svelte'
import Item from './context-menu-item.svelte'
import GroupHeading from './context-menu-group-heading.svelte'
import Content from './context-menu-content.svelte'
import Shortcut from './context-menu-shortcut.svelte'
import RadioItem from './context-menu-radio-item.svelte'
import Separator from './context-menu-separator.svelte'
import SubContent from './context-menu-sub-content.svelte'
import SubTrigger from './context-menu-sub-trigger.svelte'
import CheckboxItem from './context-menu-checkbox-item.svelte'
import Label from './context-menu-label.svelte'
export {
Root,
@@ -48,5 +48,5 @@ export {
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
Label as ContextMenuLabel,
};
Label as ContextMenuLabel
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { Dialog as DialogPrimitive } from "bits-ui";
import { Button } from "$lib/components/ui/button/index.js";
import { cn, type WithElementRef } from '$lib/utils.js'
import type { HTMLAttributes } from 'svelte/elements'
import { Dialog as DialogPrimitive } from 'bits-ui'
import { Button } from '$lib/components/ui/button/index.js'
let {
ref = $bindable(null),
@@ -11,14 +11,14 @@
showCloseButton = false,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
showCloseButton?: boolean;
} = $props();
showCloseButton?: boolean
} = $props()
</script>
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...restProps}
>
{@render children?.()}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import MinusIcon from '@lucide/svelte/icons/minus';
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import MinusIcon from '@lucide/svelte/icons/minus'
import CheckIcon from '@lucide/svelte/icons/check'
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
import type { Snippet } from 'svelte'
let {
ref = $bindable(null),
@@ -13,8 +13,8 @@
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
children?: Snippet
} = $props()
</script>
<DropdownMenuPrimitive.CheckboxItem

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChild } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import CheckIcon from '@lucide/svelte/icons/check'
import { cn, type WithoutChild } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props()
</script>
<DropdownMenuPrimitive.RadioItem

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
}: DropdownMenuPrimitive.SeparatorProps = $props()
</script>
<DropdownMenuPrimitive.Separator
bind:ref
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}
/>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
import { cn } from '$lib/utils.js'
let {
ref = $bindable(null),
@@ -10,8 +10,8 @@
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
inset?: boolean
} = $props()
</script>
<DropdownMenuPrimitive.SubTrigger

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
import Root from "./dropdown-menu.svelte";
import Sub from "./dropdown-menu-sub.svelte";
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Portal from "./dropdown-menu-portal.svelte";
import Root from './dropdown-menu.svelte'
import Sub from './dropdown-menu-sub.svelte'
import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'
import CheckboxItem from './dropdown-menu-checkbox-item.svelte'
import Content from './dropdown-menu-content.svelte'
import Group from './dropdown-menu-group.svelte'
import Item from './dropdown-menu-item.svelte'
import Label from './dropdown-menu-label.svelte'
import RadioGroup from './dropdown-menu-radio-group.svelte'
import RadioItem from './dropdown-menu-radio-item.svelte'
import Separator from './dropdown-menu-separator.svelte'
import Shortcut from './dropdown-menu-shortcut.svelte'
import Trigger from './dropdown-menu-trigger.svelte'
import SubContent from './dropdown-menu-sub-content.svelte'
import SubTrigger from './dropdown-menu-sub-trigger.svelte'
import GroupHeading from './dropdown-menu-group-heading.svelte'
import Portal from './dropdown-menu-portal.svelte'
export {
CheckboxGroup,
@@ -50,5 +50,5 @@ export {
Sub,
SubContent,
SubTrigger,
Trigger,
};
Trigger
}

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