style(web): fix prettier config, format entire web/ tree
.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:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "vega",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "zinc"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "vega",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "zinc"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,11 @@ export async function fetchQuestions(sessionId: string): Promise<SessionQuestion
|
||||
return data.questions ?? []
|
||||
}
|
||||
|
||||
export async function answerQuestion(sessionId: string, questionId: string, answer: string): Promise<boolean> {
|
||||
export async function answerQuestion(
|
||||
sessionId: string,
|
||||
questionId: string,
|
||||
answer: string
|
||||
): Promise<boolean> {
|
||||
const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ answer })
|
||||
@@ -133,42 +137,45 @@ export function streamChat(
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
|
||||
signal: controller.signal
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
onError(`HTTP ${res.status}`)
|
||||
return
|
||||
}
|
||||
const reader = res.body?.getReader()
|
||||
if (!reader) {
|
||||
onError('no response body')
|
||||
return
|
||||
}
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
onError(`HTTP ${res.status}`)
|
||||
return
|
||||
}
|
||||
const reader = res.body?.getReader()
|
||||
if (!reader) {
|
||||
onError('no response body')
|
||||
return
|
||||
}
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() ?? ''
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() ?? ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const ev: ChatEvent = JSON.parse(line.slice(6))
|
||||
onEvent(ev)
|
||||
} catch {
|
||||
// skip malformed
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const ev: ChatEvent = JSON.parse(line.slice(6))
|
||||
onEvent(ev)
|
||||
} catch {
|
||||
// skip malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
onError(err.message)
|
||||
}).finally(() => {
|
||||
onDone()
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
onError(err.message)
|
||||
})
|
||||
.finally(() => {
|
||||
onDone()
|
||||
})
|
||||
|
||||
return controller
|
||||
}
|
||||
@@ -291,7 +298,9 @@ export interface EventFilters {
|
||||
severity?: string
|
||||
}
|
||||
|
||||
export async function fetchEvents(filters: EventFilters = {}): Promise<import('./stores/events').OikosEvent[]> {
|
||||
export async function fetchEvents(
|
||||
filters: EventFilters = {}
|
||||
): Promise<import('./stores/events').OikosEvent[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.type) params.set('type', filters.type)
|
||||
if (filters.severity) params.set('severity', filters.severity)
|
||||
@@ -483,7 +492,9 @@ export interface Signal {
|
||||
last_seen_at: string
|
||||
}
|
||||
|
||||
export async function fetchSignals(filters: { state?: string; severity?: string } = {}): Promise<Signal[]> {
|
||||
export async function fetchSignals(
|
||||
filters: { state?: string; severity?: string } = {}
|
||||
): Promise<Signal[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.state) params.set('state', filters.state)
|
||||
if (filters.severity) params.set('severity', filters.severity)
|
||||
@@ -509,7 +520,11 @@ export async function resolveSignal(id: string, note?: string): Promise<Signal |
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function muteSignal(id: string, muteUntil: string, note?: string): Promise<Signal | null> {
|
||||
export async function muteSignal(
|
||||
id: string,
|
||||
muteUntil: string,
|
||||
note?: string
|
||||
): Promise<Signal | null> {
|
||||
const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ mute_until: muteUntil, note })
|
||||
@@ -533,7 +548,9 @@ export interface Relationship {
|
||||
// reachable going forward from here), this hits a dedicated endpoint that
|
||||
// matches on source_id OR target_id directly.
|
||||
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
|
||||
const res = await fetchWithAuth(`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`)
|
||||
const res = await fetchWithAuth(
|
||||
`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`
|
||||
)
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
return data.items ?? []
|
||||
@@ -671,7 +688,9 @@ export async function fetchKnowledgeContent(id: string): Promise<KnowledgeConten
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchEntityEvents(entityId: string): Promise<import('./stores/events').OikosEvent[]> {
|
||||
export async function fetchEntityEvents(
|
||||
entityId: string
|
||||
): Promise<import('./stores/events').OikosEvent[]> {
|
||||
const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
|
||||
const res = await fetchWithAuth(`${API}/events?${params}`)
|
||||
if (!res.ok) return []
|
||||
@@ -718,13 +737,19 @@ export async function fetchEntityTasks(entity: Entity): Promise<EntityTask[]> {
|
||||
tasks.map(async (task): Promise<EntityTask | null> => {
|
||||
const g = await fetchGraph({ root: task.slug, depth: 1 })
|
||||
if (!g) return null
|
||||
const involvesThisEntity = g.edges.some((e) => e.type === 'involves' && e.target === entity.slug)
|
||||
const involvesThisEntity = g.edges.some(
|
||||
(e) => e.type === 'involves' && e.target === entity.slug
|
||||
)
|
||||
const nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
|
||||
const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
|
||||
const executionCount = g.edges.filter((e) => {
|
||||
if (e.type !== 'involves') return false
|
||||
const targetId = idBySlug.get(e.target)
|
||||
return targetId != null && nodeTypeById.get(targetId) === 'execution' && executionIds.has(targetId)
|
||||
return (
|
||||
targetId != null &&
|
||||
nodeTypeById.get(targetId) === 'execution' &&
|
||||
executionIds.has(targetId)
|
||||
)
|
||||
}).length
|
||||
if (!involvesThisEntity && executionCount === 0) return null
|
||||
return { task, executionCount }
|
||||
@@ -755,7 +780,11 @@ export async function fetchChecksForTarget(targetSlug: string): Promise<Check[]>
|
||||
return data.items ?? []
|
||||
}
|
||||
|
||||
export async function patchCheck(id: string, version: number, patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }): Promise<Check | null> {
|
||||
export async function patchCheck(
|
||||
id: string,
|
||||
version: number,
|
||||
patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }
|
||||
): Promise<Check | null> {
|
||||
const res = await fetchWithAuth(`${API}/checks/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'If-Match': `"${version}"` },
|
||||
@@ -781,12 +810,14 @@ export interface AgentActivity {
|
||||
correlation_id?: string | null
|
||||
}
|
||||
|
||||
export async function fetchAgentActivity(filters: {
|
||||
agent_id?: string
|
||||
activity_type?: string
|
||||
entity_id?: string
|
||||
limit?: number
|
||||
} = {}): Promise<AgentActivity[]> {
|
||||
export async function fetchAgentActivity(
|
||||
filters: {
|
||||
agent_id?: string
|
||||
activity_type?: string
|
||||
entity_id?: string
|
||||
limit?: number
|
||||
} = {}
|
||||
): Promise<AgentActivity[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.agent_id) params.set('agent_id', filters.agent_id)
|
||||
if (filters.activity_type) params.set('activity_type', filters.activity_type)
|
||||
@@ -821,14 +852,16 @@ export interface AuditEntry {
|
||||
correlation_id?: string | null
|
||||
}
|
||||
|
||||
export async function fetchAudit(filters: {
|
||||
actor_type?: string
|
||||
actor_id?: string
|
||||
entity_id?: string
|
||||
action?: string
|
||||
correlation_id?: string
|
||||
limit?: number
|
||||
} = {}): Promise<AuditEntry[]> {
|
||||
export async function fetchAudit(
|
||||
filters: {
|
||||
actor_type?: string
|
||||
actor_id?: string
|
||||
entity_id?: string
|
||||
action?: string
|
||||
correlation_id?: string
|
||||
limit?: number
|
||||
} = {}
|
||||
): Promise<AuditEntry[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.actor_type) params.set('actor_type', filters.actor_type)
|
||||
if (filters.actor_id) params.set('actor_id', filters.actor_id)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -60,10 +60,16 @@
|
||||
let wasStreaming = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
if (streaming) { indicatorDone = false; wasStreaming = true }
|
||||
if (streaming) {
|
||||
indicatorDone = false
|
||||
wasStreaming = true
|
||||
}
|
||||
if (!streaming && wasStreaming) {
|
||||
indicatorDone = true
|
||||
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000)
|
||||
const t = setTimeout(() => {
|
||||
indicatorDone = false
|
||||
wasStreaming = false
|
||||
}, 3000)
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
})
|
||||
@@ -91,7 +97,10 @@
|
||||
const lineHeight = parseFloat(taCs.lineHeight)
|
||||
if (!Number.isFinite(lineHeight)) return
|
||||
const taBoxY =
|
||||
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
|
||||
parseFloat(taCs.paddingTop) +
|
||||
parseFloat(taCs.paddingBottom) +
|
||||
parseFloat(taCs.borderTopWidth) +
|
||||
parseFloat(taCs.borderBottomWidth)
|
||||
// The wrapper's own padding/border (space around the textarea, not part
|
||||
// of it) also has to fit inside the minimum, or the textarea gets
|
||||
// squeezed below one line once the pane is dragged down to it.
|
||||
@@ -144,7 +153,9 @@
|
||||
}
|
||||
renderer.table = function (token) {
|
||||
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
|
||||
const body = token.rows.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`).join('')
|
||||
const body = token.rows
|
||||
.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`)
|
||||
.join('')
|
||||
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
|
||||
}
|
||||
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
|
||||
@@ -181,126 +192,172 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
|
||||
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1" on:resize={() => (userResizedInput = true)}>
|
||||
<Splitpanes
|
||||
horizontal
|
||||
theme="oikos-theme"
|
||||
dblClickSplitter={false}
|
||||
class="min-h-0 flex-1"
|
||||
on:resize={() => (userResizedInput = true)}
|
||||
>
|
||||
<Pane class="flex flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Nomos</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Your resident operator. Ask about the fleet, or tell it to act.</p>
|
||||
</div>
|
||||
{#if suggestions.length}
|
||||
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{#each suggestions as q}
|
||||
<Button variant="outline" size="sm" class="h-auto justify-start whitespace-normal py-2 text-left text-xs" onclick={() => ask(q)}>
|
||||
{q}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each messages as msg, idx (msg.id)}
|
||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||
{#if msg.role === 'user'}
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg">{msg.text}</div>
|
||||
{:else}
|
||||
{@const isLast = idx === messages.length - 1}
|
||||
{@const traceStatus = !isLast
|
||||
? 'idle'
|
||||
: error
|
||||
? 'error'
|
||||
: streaming
|
||||
? 'running'
|
||||
: indicatorDone
|
||||
? 'done'
|
||||
: 'idle'}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
||||
{/if}
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Nomos</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Your resident operator. Ask about the fleet, or tell it to act.
|
||||
</p>
|
||||
</div>
|
||||
<!-- The working trace sits above the answer: it's what happened
|
||||
first, and collapsed it keeps a long tool run from burying
|
||||
the text below it. -->
|
||||
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
||||
<AgentTrace
|
||||
tools={msg.tools}
|
||||
status={traceStatus}
|
||||
label={traceStatus === 'idle' ? null : indicatorLabel}
|
||||
/>
|
||||
{/if}
|
||||
{#if msg.text}
|
||||
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||
{@html render(msg.text)}
|
||||
{#if isLast && streaming}
|
||||
<span class="stream-cursor" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if suggestions.length}
|
||||
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{#each suggestions as q}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
|
||||
onclick={() => ask(q)}
|
||||
>
|
||||
{q}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each messages as msg, idx (msg.id)}
|
||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||
{#if msg.role === 'user'}
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50"
|
||||
>{formatTime(msg.created_at)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg"
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
{:else}
|
||||
{@const isLast = idx === messages.length - 1}
|
||||
{@const traceStatus = !isLast
|
||||
? 'idle'
|
||||
: error
|
||||
? 'error'
|
||||
: streaming
|
||||
? 'running'
|
||||
: indicatorDone
|
||||
? 'done'
|
||||
: 'idle'}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50"
|
||||
>{formatTime(msg.created_at)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- The working trace sits above the answer: it's what happened
|
||||
first, and collapsed it keeps a long tool run from burying
|
||||
the text below it. -->
|
||||
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
||||
<AgentTrace
|
||||
tools={msg.tools}
|
||||
status={traceStatus}
|
||||
label={traceStatus === 'idle' ? null : indicatorLabel}
|
||||
/>
|
||||
{/if}
|
||||
{#if msg.text}
|
||||
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||
{@html render(msg.text)}
|
||||
{#if isLast && streaming}
|
||||
<span class="stream-cursor" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if question}
|
||||
<OperatorQuestion {sessionId} {question} />
|
||||
{/if}
|
||||
<div bind:this={messagesEnd}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if connectionState === 'disconnected'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs"
|
||||
>
|
||||
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
||||
<span class="text-warning-foreground flex-1"
|
||||
>Agent connection lost. The task may still be running.</span
|
||||
>
|
||||
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}
|
||||
>Reconnect</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if connectionState === 'reconnecting'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon
|
||||
class="size-3 shrink-0 animate-spin text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each chatErrors as err (err.id)}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
<span class="flex-1">{err.message}</span>
|
||||
{#if err.action}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="h-6 text-[11px]"
|
||||
onclick={() => onDismissError(err.id)}>{err.action}</Button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
class="ml-1 text-muted-foreground hover:text-foreground"
|
||||
onclick={() => onDismissError(err.id)}
|
||||
aria-label="Dismiss">×</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question}
|
||||
<OperatorQuestion {sessionId} {question} />
|
||||
{/if}
|
||||
<div bind:this={messagesEnd}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if connectionState === 'disconnected'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
||||
<span class="text-warning-foreground flex-1">Agent connection lost. The task may still be running.</span>
|
||||
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}>Reconnect</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if connectionState === 'reconnecting'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon class="size-3 shrink-0 animate-spin text-muted-foreground" aria-hidden="true" />
|
||||
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each chatErrors as err (err.id)}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<span class="flex-1">{err.message}</span>
|
||||
{#if err.action}
|
||||
<Button size="xs" variant="ghost" class="h-6 text-[11px]" onclick={() => onDismissError(err.id)}>{err.action}</Button>
|
||||
{/if}
|
||||
<button class="ml-1 text-muted-foreground hover:text-foreground" onclick={() => onDismissError(err.id)} aria-label="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Pane>
|
||||
|
||||
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
|
||||
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
|
||||
<div
|
||||
class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative"
|
||||
bind:this={inputWrapperRef}
|
||||
>
|
||||
<form
|
||||
class="relative mx-auto flex h-full w-full max-w-3xl"
|
||||
onsubmit={(e) => {
|
||||
@@ -514,7 +571,13 @@
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 0.75rem 0;
|
||||
background: linear-gradient(to right, transparent, var(--border) 20%, var(--border) 80%, transparent);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--border) 20%,
|
||||
var(--border) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
|
||||
@@ -559,7 +622,9 @@
|
||||
border-radius: 0.375rem;
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, color 0.15s;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
color 0.15s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -586,7 +651,12 @@
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
0%, 100% { opacity: 0.75; }
|
||||
50% { opacity: 0; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.75;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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)`)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 & 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 & 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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
messages={[]}
|
||||
streaming={false}
|
||||
connectionState="connected"
|
||||
onSend={onSend}
|
||||
{onSend}
|
||||
onCancel={() => {}}
|
||||
onReconnect={() => {}}
|
||||
onDismissError={() => {}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { type VariantProps, tv } from 'tailwind-variants'
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
export const badgeVariants = tv({
|
||||
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
||||
destructive:
|
||||
'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
|
||||
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
||||
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
})
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant']
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = 'default',
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
this={href ? 'a' : 'span'}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,82 +1,89 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
|
||||
import { type VariantProps, tv } from 'tailwind-variants'
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
outline:
|
||||
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost:
|
||||
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
||||
destructive:
|
||||
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
|
||||
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
icon: 'size-9',
|
||||
'icon-xs':
|
||||
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm':
|
||||
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
|
||||
'icon-lg': 'size-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
})
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size']
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant
|
||||
size?: ButtonSize
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props()
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? 'link' : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants
|
||||
} from './button.svelte'
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn(
|
||||
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn(
|
||||
'cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-content"
|
||||
class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-content"
|
||||
class={cn('px-6 group-data-[size=sm]/card:px-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props()
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn(
|
||||
'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
'gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn('text-base leading-normal font-medium group-data-[size=sm]/card:text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = 'default',
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
'ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
import Root from './card.svelte'
|
||||
import Content from './card-content.svelte'
|
||||
import Description from './card-description.svelte'
|
||||
import Footer from './card-footer.svelte'
|
||||
import Header from './card-header.svelte'
|
||||
import Title from './card-title.svelte'
|
||||
import Action from './card-action.svelte'
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: CollapsiblePrimitive.RootProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: CollapsiblePrimitive.RootProps = $props()
|
||||
</script>
|
||||
|
||||
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Root from "./collapsible.svelte";
|
||||
import Trigger from "./collapsible-trigger.svelte";
|
||||
import Content from "./collapsible-content.svelte";
|
||||
import Root from './collapsible.svelte'
|
||||
import Trigger from './collapsible-trigger.svelte'
|
||||
import Content from './collapsible-content.svelte'
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger,
|
||||
};
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||
import type { Snippet } from 'svelte'
|
||||
import CheckIcon from '@lucide/svelte/icons/check'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
inset,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
||||
inset?: boolean;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
inset,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
||||
inset?: boolean
|
||||
children?: Snippet
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="context-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="context-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="absolute right-2 pointer-events-none">
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
{#snippet children({ checked })}
|
||||
<span class="absolute right-2 pointer-events-none">
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import ContextMenuPortal from "./context-menu-portal.svelte";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
import ContextMenuPortal from './context-menu-portal.svelte'
|
||||
import type { ComponentProps } from 'svelte'
|
||||
import type { WithoutChildrenOrChild } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPortal {...portalProps}>
|
||||
<ContextMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="context-menu-content"
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
<ContextMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="context-menu-content"
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</ContextMenuPortal>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.GroupHeadingProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.GroupHeadingProps & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="context-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn('text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ItemProps & {
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.RadioGroupProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.RadioGroupProps = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="context-menu-radio-group"
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="context-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||
import CheckIcon from '@lucide/svelte/icons/check'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="context-menu-radio-item"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-radio-item"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="absolute right-2 pointer-events-none">
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
{#snippet children({ checked })}
|
||||
<span class="absolute right-2 pointer-events-none">
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SeparatorProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SeparatorProps = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="context-menu-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-separator"
|
||||
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="context-menu-shortcut"
|
||||
class={cn("text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="context-menu-shortcut"
|
||||
class={cn(
|
||||
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</span>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SubContentProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SubContentProps = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-content"
|
||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-content"
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.TriggerProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.TriggerProps = $props()
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="context-menu-trigger"
|
||||
class={cn("cn-context-menu-trigger select-none", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="context-menu-trigger"
|
||||
class={cn('cn-context-menu-trigger select-none', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import Root from "./context-menu.svelte";
|
||||
import Sub from "./context-menu-sub.svelte";
|
||||
import Portal from "./context-menu-portal.svelte";
|
||||
import Trigger from "./context-menu-trigger.svelte";
|
||||
import Group from "./context-menu-group.svelte";
|
||||
import RadioGroup from "./context-menu-radio-group.svelte";
|
||||
import Item from "./context-menu-item.svelte";
|
||||
import GroupHeading from "./context-menu-group-heading.svelte";
|
||||
import Content from "./context-menu-content.svelte";
|
||||
import Shortcut from "./context-menu-shortcut.svelte";
|
||||
import RadioItem from "./context-menu-radio-item.svelte";
|
||||
import Separator from "./context-menu-separator.svelte";
|
||||
import SubContent from "./context-menu-sub-content.svelte";
|
||||
import SubTrigger from "./context-menu-sub-trigger.svelte";
|
||||
import CheckboxItem from "./context-menu-checkbox-item.svelte";
|
||||
import Label from "./context-menu-label.svelte";
|
||||
import Root from './context-menu.svelte'
|
||||
import Sub from './context-menu-sub.svelte'
|
||||
import Portal from './context-menu-portal.svelte'
|
||||
import Trigger from './context-menu-trigger.svelte'
|
||||
import Group from './context-menu-group.svelte'
|
||||
import RadioGroup from './context-menu-radio-group.svelte'
|
||||
import Item from './context-menu-item.svelte'
|
||||
import GroupHeading from './context-menu-group-heading.svelte'
|
||||
import Content from './context-menu-content.svelte'
|
||||
import Shortcut from './context-menu-shortcut.svelte'
|
||||
import RadioItem from './context-menu-radio-item.svelte'
|
||||
import Separator from './context-menu-separator.svelte'
|
||||
import SubContent from './context-menu-sub-content.svelte'
|
||||
import SubTrigger from './context-menu-sub-trigger.svelte'
|
||||
import CheckboxItem from './context-menu-checkbox-item.svelte'
|
||||
import Label from './context-menu-label.svelte'
|
||||
|
||||
export {
|
||||
Root,
|
||||
Sub,
|
||||
Portal,
|
||||
Item,
|
||||
GroupHeading,
|
||||
Label,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as ContextMenu,
|
||||
Sub as ContextMenuSub,
|
||||
Portal as ContextMenuPortal,
|
||||
Item as ContextMenuItem,
|
||||
GroupHeading as ContextMenuGroupHeading,
|
||||
Group as ContextMenuGroup,
|
||||
Content as ContextMenuContent,
|
||||
Trigger as ContextMenuTrigger,
|
||||
Shortcut as ContextMenuShortcut,
|
||||
RadioItem as ContextMenuRadioItem,
|
||||
Separator as ContextMenuSeparator,
|
||||
RadioGroup as ContextMenuRadioGroup,
|
||||
SubContent as ContextMenuSubContent,
|
||||
SubTrigger as ContextMenuSubTrigger,
|
||||
CheckboxItem as ContextMenuCheckboxItem,
|
||||
Label as ContextMenuLabel,
|
||||
};
|
||||
Root,
|
||||
Sub,
|
||||
Portal,
|
||||
Item,
|
||||
GroupHeading,
|
||||
Label,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as ContextMenu,
|
||||
Sub as ContextMenuSub,
|
||||
Portal as ContextMenuPortal,
|
||||
Item as ContextMenuItem,
|
||||
GroupHeading as ContextMenuGroupHeading,
|
||||
Group as ContextMenuGroup,
|
||||
Content as ContextMenuContent,
|
||||
Trigger as ContextMenuTrigger,
|
||||
Shortcut as ContextMenuShortcut,
|
||||
RadioItem as ContextMenuRadioItem,
|
||||
Separator as ContextMenuSeparator,
|
||||
RadioGroup as ContextMenuRadioGroup,
|
||||
SubContent as ContextMenuSubContent,
|
||||
SubTrigger as ContextMenuSubTrigger,
|
||||
CheckboxItem as ContextMenuCheckboxItem,
|
||||
Label as ContextMenuLabel
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = "button",
|
||||
...restProps
|
||||
}: DialogPrimitive.CloseProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = 'button',
|
||||
...restProps
|
||||
}: DialogPrimitive.CloseProps = $props()
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import DialogPortal from "./dialog-portal.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
import DialogPortal from './dialog-portal.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
import * as Dialog from './index.js'
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||
import type { ComponentProps } from 'svelte'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import XIcon from '@lucide/svelte/icons/x'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>
|
||||
children: Snippet
|
||||
showCloseButton?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close data-slot="dialog-close">
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close data-slot="dialog-close">
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props()
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn(
|
||||
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
showCloseButton = false,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
showCloseButton = false,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
showCloseButton?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" {...props}>Close</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" {...props}>Close</Button>
|
||||
{/snippet}
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("gap-2 flex flex-col", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn('gap-2 flex flex-col', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props()
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props()
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("leading-none font-medium", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn('leading-none font-medium', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = "button",
|
||||
...restProps
|
||||
}: DialogPrimitive.TriggerProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
type = 'button',
|
||||
...restProps
|
||||
}: DialogPrimitive.TriggerProps = $props()
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import Root from "./dialog.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
import Root from './dialog.svelte'
|
||||
import Portal from './dialog-portal.svelte'
|
||||
import Title from './dialog-title.svelte'
|
||||
import Footer from './dialog-footer.svelte'
|
||||
import Header from './dialog-header.svelte'
|
||||
import Overlay from './dialog-overlay.svelte'
|
||||
import Content from './dialog-content.svelte'
|
||||
import Description from './dialog-description.svelte'
|
||||
import Trigger from './dialog-trigger.svelte'
|
||||
import Close from './dialog-close.svelte'
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-checkbox-group"
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-checkbox-group"
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import MinusIcon from '@lucide/svelte/icons/minus'
|
||||
import CheckIcon from '@lucide/svelte/icons/check'
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
{#if indeterminate}
|
||||
<MinusIcon />
|
||||
{:else if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
{#if indeterminate}
|
||||
<MinusIcon />
|
||||
{:else if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||
import DropdownMenuPortal from './dropdown-menu-portal.svelte'
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import type { ComponentProps } from 'svelte'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
align = "start",
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
align = 'start',
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPortal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPortal>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
import type { ComponentProps } from 'svelte'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group-heading"
|
||||
data-inset={inset}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from '$lib/utils.js'
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import CheckIcon from '@lucide/svelte/icons/check'
|
||||
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
{#snippet children({ checked })}
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
{#if checked}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn(
|
||||
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</span>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto", className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||
import { cn } from '$lib/utils.js'
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user