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,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "vega",
|
"style": "vega",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "zinc"
|
"baseColor": "zinc"
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "$lib/components",
|
"components": "$lib/components",
|
||||||
"utils": "$lib/utils",
|
"utils": "$lib/utils",
|
||||||
"ui": "$lib/components/ui",
|
"ui": "$lib/components/ui",
|
||||||
"hooks": "$lib/hooks",
|
"hooks": "$lib/hooks",
|
||||||
"lib": "$lib"
|
"lib": "$lib"
|
||||||
},
|
},
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"registry": "https://shadcn-svelte.com/registry"
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
<title>Oikos</title>
|
<title>Oikos</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Inknut+Antiqua:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Inknut+Antiqua:wght@300;400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="android-chrome-192.png" />
|
<link rel="icon" type="image/png" sizes="192x192" href="android-chrome-192.png" />
|
||||||
@@ -15,11 +18,20 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
<script>
|
||||||
(function(){try{var t=localStorage.getItem('oikos-theme');if(!t){t=window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'}
|
;(function () {
|
||||||
if(t==='dark')document.documentElement.classList.add('dark')}catch(e){}})()
|
try {
|
||||||
|
var t = localStorage.getItem('oikos-theme')
|
||||||
|
if (!t) {
|
||||||
|
t = window.matchMedia('(prefers-color-scheme:light)').matches ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
if (t === 'dark') document.documentElement.classList.add('dark')
|
||||||
|
} catch (e) {}
|
||||||
|
})()
|
||||||
</script>
|
</script>
|
||||||
<script src="/wails/runtime.js"></script>
|
<script src="/wails/runtime.js"></script>
|
||||||
<script>window.__OIKOS_CONFIG__ = {};</script>
|
<script>
|
||||||
|
window.__OIKOS_CONFIG__ = {}
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
--chart-3: oklch(0.5 0.08 30);
|
--chart-3: oklch(0.5 0.08 30);
|
||||||
--chart-4: oklch(0.6 0.06 90);
|
--chart-4: oklch(0.6 0.06 90);
|
||||||
--chart-5: oklch(0.4 0.04 45);
|
--chart-5: oklch(0.4 0.04 45);
|
||||||
--sidebar: oklch(0.90 0.025 55);
|
--sidebar: oklch(0.9 0.025 55);
|
||||||
--sidebar-foreground: oklch(0.18 0.03 45);
|
--sidebar-foreground: oklch(0.18 0.03 45);
|
||||||
--sidebar-primary: oklch(0.55 0.14 45);
|
--sidebar-primary: oklch(0.55 0.14 45);
|
||||||
--sidebar-primary-foreground: oklch(0.95 0.02 55);
|
--sidebar-primary-foreground: oklch(0.95 0.02 55);
|
||||||
@@ -159,7 +159,6 @@
|
|||||||
--accent-orange: var(--warning);
|
--accent-orange: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Terminal-style block cursor — outside @layer so it overrides CodeMirror */
|
/* Terminal-style block cursor — outside @layer so it overrides CodeMirror */
|
||||||
.cm-cursor,
|
.cm-cursor,
|
||||||
.cm-cursor-primary {
|
.cm-cursor-primary {
|
||||||
@@ -181,7 +180,12 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +278,6 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[data-wm-window][data-wm-focused] {
|
[data-wm-window][data-wm-focused] {
|
||||||
border-color: var(--ring);
|
border-color: var(--ring);
|
||||||
box-shadow: 0 12px 32px oklch(0 0 0 / 0.28);
|
box-shadow: 0 12px 32px oklch(0 0 0 / 0.28);
|
||||||
@@ -289,7 +292,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[data-wm-resize] {
|
[data-wm-resize] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ export async function fetchQuestions(sessionId: string): Promise<SessionQuestion
|
|||||||
return data.questions ?? []
|
return data.questions ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function answerQuestion(sessionId: string, questionId: string, answer: string): Promise<boolean> {
|
export async function answerQuestion(
|
||||||
|
sessionId: string,
|
||||||
|
questionId: string,
|
||||||
|
answer: string
|
||||||
|
): Promise<boolean> {
|
||||||
const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
|
const res = await fetchWithAuth(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ answer })
|
body: JSON.stringify({ answer })
|
||||||
@@ -133,42 +137,45 @@ export function streamChat(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
|
body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
}).then(async (res) => {
|
})
|
||||||
if (!res.ok) {
|
.then(async (res) => {
|
||||||
onError(`HTTP ${res.status}`)
|
if (!res.ok) {
|
||||||
return
|
onError(`HTTP ${res.status}`)
|
||||||
}
|
return
|
||||||
const reader = res.body?.getReader()
|
}
|
||||||
if (!reader) {
|
const reader = res.body?.getReader()
|
||||||
onError('no response body')
|
if (!reader) {
|
||||||
return
|
onError('no response body')
|
||||||
}
|
return
|
||||||
const decoder = new TextDecoder()
|
}
|
||||||
let buffer = ''
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
const lines = buffer.split('\n')
|
const lines = buffer.split('\n')
|
||||||
buffer = lines.pop() ?? ''
|
buffer = lines.pop() ?? ''
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
try {
|
try {
|
||||||
const ev: ChatEvent = JSON.parse(line.slice(6))
|
const ev: ChatEvent = JSON.parse(line.slice(6))
|
||||||
onEvent(ev)
|
onEvent(ev)
|
||||||
} catch {
|
} catch {
|
||||||
// skip malformed
|
// skip malformed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}).catch((err) => {
|
.catch((err) => {
|
||||||
onError(err.message)
|
onError(err.message)
|
||||||
}).finally(() => {
|
})
|
||||||
onDone()
|
.finally(() => {
|
||||||
})
|
onDone()
|
||||||
|
})
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
@@ -291,7 +298,9 @@ export interface EventFilters {
|
|||||||
severity?: string
|
severity?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEvents(filters: EventFilters = {}): Promise<import('./stores/events').OikosEvent[]> {
|
export async function fetchEvents(
|
||||||
|
filters: EventFilters = {}
|
||||||
|
): Promise<import('./stores/events').OikosEvent[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (filters.type) params.set('type', filters.type)
|
if (filters.type) params.set('type', filters.type)
|
||||||
if (filters.severity) params.set('severity', filters.severity)
|
if (filters.severity) params.set('severity', filters.severity)
|
||||||
@@ -483,7 +492,9 @@ export interface Signal {
|
|||||||
last_seen_at: string
|
last_seen_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSignals(filters: { state?: string; severity?: string } = {}): Promise<Signal[]> {
|
export async function fetchSignals(
|
||||||
|
filters: { state?: string; severity?: string } = {}
|
||||||
|
): Promise<Signal[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (filters.state) params.set('state', filters.state)
|
if (filters.state) params.set('state', filters.state)
|
||||||
if (filters.severity) params.set('severity', filters.severity)
|
if (filters.severity) params.set('severity', filters.severity)
|
||||||
@@ -509,7 +520,11 @@ export async function resolveSignal(id: string, note?: string): Promise<Signal |
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function muteSignal(id: string, muteUntil: string, note?: string): Promise<Signal | null> {
|
export async function muteSignal(
|
||||||
|
id: string,
|
||||||
|
muteUntil: string,
|
||||||
|
note?: string
|
||||||
|
): Promise<Signal | null> {
|
||||||
const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
|
const res = await fetchWithAuth(`${API}/signals/${id}/mute`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ mute_until: muteUntil, note })
|
body: JSON.stringify({ mute_until: muteUntil, note })
|
||||||
@@ -533,7 +548,9 @@ export interface Relationship {
|
|||||||
// reachable going forward from here), this hits a dedicated endpoint that
|
// reachable going forward from here), this hits a dedicated endpoint that
|
||||||
// matches on source_id OR target_id directly.
|
// matches on source_id OR target_id directly.
|
||||||
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
|
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
|
||||||
const res = await fetchWithAuth(`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`)
|
const res = await fetchWithAuth(
|
||||||
|
`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`
|
||||||
|
)
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.items ?? []
|
return data.items ?? []
|
||||||
@@ -671,7 +688,9 @@ export async function fetchKnowledgeContent(id: string): Promise<KnowledgeConten
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEntityEvents(entityId: string): Promise<import('./stores/events').OikosEvent[]> {
|
export async function fetchEntityEvents(
|
||||||
|
entityId: string
|
||||||
|
): Promise<import('./stores/events').OikosEvent[]> {
|
||||||
const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
|
const params = new URLSearchParams({ entity_id: entityId, limit: '50' })
|
||||||
const res = await fetchWithAuth(`${API}/events?${params}`)
|
const res = await fetchWithAuth(`${API}/events?${params}`)
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
@@ -718,13 +737,19 @@ export async function fetchEntityTasks(entity: Entity): Promise<EntityTask[]> {
|
|||||||
tasks.map(async (task): Promise<EntityTask | null> => {
|
tasks.map(async (task): Promise<EntityTask | null> => {
|
||||||
const g = await fetchGraph({ root: task.slug, depth: 1 })
|
const g = await fetchGraph({ root: task.slug, depth: 1 })
|
||||||
if (!g) return null
|
if (!g) return null
|
||||||
const involvesThisEntity = g.edges.some((e) => e.type === 'involves' && e.target === entity.slug)
|
const involvesThisEntity = g.edges.some(
|
||||||
|
(e) => e.type === 'involves' && e.target === entity.slug
|
||||||
|
)
|
||||||
const nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
|
const nodeTypeById = new Map(g.nodes.map((n) => [n.id, n.type]))
|
||||||
const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
|
const idBySlug = new Map(g.nodes.map((n) => [n.slug, n.id]))
|
||||||
const executionCount = g.edges.filter((e) => {
|
const executionCount = g.edges.filter((e) => {
|
||||||
if (e.type !== 'involves') return false
|
if (e.type !== 'involves') return false
|
||||||
const targetId = idBySlug.get(e.target)
|
const targetId = idBySlug.get(e.target)
|
||||||
return targetId != null && nodeTypeById.get(targetId) === 'execution' && executionIds.has(targetId)
|
return (
|
||||||
|
targetId != null &&
|
||||||
|
nodeTypeById.get(targetId) === 'execution' &&
|
||||||
|
executionIds.has(targetId)
|
||||||
|
)
|
||||||
}).length
|
}).length
|
||||||
if (!involvesThisEntity && executionCount === 0) return null
|
if (!involvesThisEntity && executionCount === 0) return null
|
||||||
return { task, executionCount }
|
return { task, executionCount }
|
||||||
@@ -755,7 +780,11 @@ export async function fetchChecksForTarget(targetSlug: string): Promise<Check[]>
|
|||||||
return data.items ?? []
|
return data.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function patchCheck(id: string, version: number, patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }): Promise<Check | null> {
|
export async function patchCheck(
|
||||||
|
id: string,
|
||||||
|
version: number,
|
||||||
|
patch: { enabled?: boolean; interval_s?: number; timeout_s?: number }
|
||||||
|
): Promise<Check | null> {
|
||||||
const res = await fetchWithAuth(`${API}/checks/${id}`, {
|
const res = await fetchWithAuth(`${API}/checks/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'If-Match': `"${version}"` },
|
headers: { 'If-Match': `"${version}"` },
|
||||||
@@ -781,12 +810,14 @@ export interface AgentActivity {
|
|||||||
correlation_id?: string | null
|
correlation_id?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAgentActivity(filters: {
|
export async function fetchAgentActivity(
|
||||||
agent_id?: string
|
filters: {
|
||||||
activity_type?: string
|
agent_id?: string
|
||||||
entity_id?: string
|
activity_type?: string
|
||||||
limit?: number
|
entity_id?: string
|
||||||
} = {}): Promise<AgentActivity[]> {
|
limit?: number
|
||||||
|
} = {}
|
||||||
|
): Promise<AgentActivity[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (filters.agent_id) params.set('agent_id', filters.agent_id)
|
if (filters.agent_id) params.set('agent_id', filters.agent_id)
|
||||||
if (filters.activity_type) params.set('activity_type', filters.activity_type)
|
if (filters.activity_type) params.set('activity_type', filters.activity_type)
|
||||||
@@ -821,14 +852,16 @@ export interface AuditEntry {
|
|||||||
correlation_id?: string | null
|
correlation_id?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAudit(filters: {
|
export async function fetchAudit(
|
||||||
actor_type?: string
|
filters: {
|
||||||
actor_id?: string
|
actor_type?: string
|
||||||
entity_id?: string
|
actor_id?: string
|
||||||
action?: string
|
entity_id?: string
|
||||||
correlation_id?: string
|
action?: string
|
||||||
limit?: number
|
correlation_id?: string
|
||||||
} = {}): Promise<AuditEntry[]> {
|
limit?: number
|
||||||
|
} = {}
|
||||||
|
): Promise<AuditEntry[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (filters.actor_type) params.set('actor_type', filters.actor_type)
|
if (filters.actor_type) params.set('actor_type', filters.actor_type)
|
||||||
if (filters.actor_id) params.set('actor_id', filters.actor_id)
|
if (filters.actor_id) params.set('actor_id', filters.actor_id)
|
||||||
|
|||||||
@@ -50,8 +50,8 @@
|
|||||||
class="min-h-0 flex-1 resize-none rounded-md border bg-background p-3 font-mono text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-ring"
|
class="min-h-0 flex-1 resize-none rounded-md border bg-background p-3 font-mono text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-ring"
|
||||||
></textarea>
|
></textarea>
|
||||||
<p class="shrink-0 text-xs text-muted-foreground">
|
<p class="shrink-0 text-xs text-muted-foreground">
|
||||||
A demo installable app — uninstall it from the App Store to remove its
|
A demo installable app — uninstall it from the App Store to remove its icon and window. Its
|
||||||
icon and window. Its notes persist in localStorage under
|
notes persist in localStorage under
|
||||||
<code class="font-mono">{storageKey}</code>.
|
<code class="font-mono">{storageKey}</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ import type { Component } from 'svelte'
|
|||||||
import { writable, derived, get, type Readable } from 'svelte/store'
|
import { writable, derived, get, type Readable } from 'svelte/store'
|
||||||
import type { DashboardSummary } from '$lib/api'
|
import type { DashboardSummary } from '$lib/api'
|
||||||
import { openSignalCount } from '$lib/stores/context'
|
import { openSignalCount } from '$lib/stores/context'
|
||||||
import { catalogById, type AppManifest, type AppPermission, type CatalogEntry } from '$lib/app-store/catalog'
|
import {
|
||||||
|
catalogById,
|
||||||
|
type AppManifest,
|
||||||
|
type AppPermission,
|
||||||
|
type CatalogEntry
|
||||||
|
} from '$lib/app-store/catalog'
|
||||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
|
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
|
||||||
import BoxesIcon from '@lucide/svelte/icons/boxes'
|
import BoxesIcon from '@lucide/svelte/icons/boxes'
|
||||||
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
|
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
|
||||||
@@ -237,8 +242,9 @@ export const apps: Readable<AppDef[]> = derived(installedIds, (ids) => {
|
|||||||
return [...builtinApps, ...installed]
|
return [...builtinApps, ...installed]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const appById: Readable<Map<string, AppDef>> = derived(apps, (list) =>
|
export const appById: Readable<Map<string, AppDef>> = derived(
|
||||||
new Map(list.map((a) => [a.id, a]))
|
apps,
|
||||||
|
(list) => new Map(list.map((a) => [a.id, a]))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Install/uninstall. Idempotent — installing an already-installed app or
|
// Install/uninstall. Idempotent — installing an already-installed app or
|
||||||
|
|||||||
@@ -37,14 +37,23 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="trace rounded-lg border border-border/60 bg-card/40 transition-colors" class:running={status === 'running'}>
|
<div
|
||||||
|
class="trace rounded-lg border border-border/60 bg-card/40 transition-colors"
|
||||||
|
class:running={status === 'running'}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-muted/40"
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-muted/40"
|
||||||
onclick={() => (expanded = !expanded)}
|
onclick={() => (expanded = !expanded)}
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-label={expanded ? 'Hide agent trace' : 'Show agent trace'}
|
aria-label={expanded ? 'Hide agent trace' : 'Show agent trace'}
|
||||||
>
|
>
|
||||||
<span class="shrink-0 {status === 'error' ? 'text-destructive' : status === 'idle' ? 'text-muted-foreground' : 'text-primary'}">
|
<span
|
||||||
|
class="shrink-0 {status === 'error'
|
||||||
|
? 'text-destructive'
|
||||||
|
: status === 'idle'
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: 'text-primary'}"
|
||||||
|
>
|
||||||
{#if status === 'running'}
|
{#if status === 'running'}
|
||||||
<Spinner class="size-3" />
|
<Spinner class="size-3" />
|
||||||
{:else if status === 'error'}
|
{:else if status === 'error'}
|
||||||
@@ -71,7 +80,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
class="size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded ? 'rotate-90' : ''}"
|
class="size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded
|
||||||
|
? 'rotate-90'
|
||||||
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -82,7 +93,9 @@
|
|||||||
<ToolCallCard {tool} />
|
<ToolCallCard {tool} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<p class="px-2 py-1.5 text-[11px] text-muted-foreground">Nothing recorded for this turn yet.</p>
|
<p class="px-2 py-1.5 text-[11px] text-muted-foreground">
|
||||||
|
Nothing recorded for this turn yet.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -60,10 +60,16 @@
|
|||||||
let wasStreaming = $state(false)
|
let wasStreaming = $state(false)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (streaming) { indicatorDone = false; wasStreaming = true }
|
if (streaming) {
|
||||||
|
indicatorDone = false
|
||||||
|
wasStreaming = true
|
||||||
|
}
|
||||||
if (!streaming && wasStreaming) {
|
if (!streaming && wasStreaming) {
|
||||||
indicatorDone = true
|
indicatorDone = true
|
||||||
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000)
|
const t = setTimeout(() => {
|
||||||
|
indicatorDone = false
|
||||||
|
wasStreaming = false
|
||||||
|
}, 3000)
|
||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -91,7 +97,10 @@
|
|||||||
const lineHeight = parseFloat(taCs.lineHeight)
|
const lineHeight = parseFloat(taCs.lineHeight)
|
||||||
if (!Number.isFinite(lineHeight)) return
|
if (!Number.isFinite(lineHeight)) return
|
||||||
const taBoxY =
|
const taBoxY =
|
||||||
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
|
parseFloat(taCs.paddingTop) +
|
||||||
|
parseFloat(taCs.paddingBottom) +
|
||||||
|
parseFloat(taCs.borderTopWidth) +
|
||||||
|
parseFloat(taCs.borderBottomWidth)
|
||||||
// The wrapper's own padding/border (space around the textarea, not part
|
// The wrapper's own padding/border (space around the textarea, not part
|
||||||
// of it) also has to fit inside the minimum, or the textarea gets
|
// of it) also has to fit inside the minimum, or the textarea gets
|
||||||
// squeezed below one line once the pane is dragged down to it.
|
// squeezed below one line once the pane is dragged down to it.
|
||||||
@@ -144,7 +153,9 @@
|
|||||||
}
|
}
|
||||||
renderer.table = function (token) {
|
renderer.table = function (token) {
|
||||||
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
|
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
|
||||||
const body = token.rows.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`).join('')
|
const body = token.rows
|
||||||
|
.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`)
|
||||||
|
.join('')
|
||||||
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
|
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
|
||||||
}
|
}
|
||||||
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
|
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
|
||||||
@@ -181,126 +192,172 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
|
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
|
||||||
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1" on:resize={() => (userResizedInput = true)}>
|
<Splitpanes
|
||||||
|
horizontal
|
||||||
|
theme="oikos-theme"
|
||||||
|
dblClickSplitter={false}
|
||||||
|
class="min-h-0 flex-1"
|
||||||
|
on:resize={() => (userResizedInput = true)}
|
||||||
|
>
|
||||||
<Pane class="flex flex-col">
|
<Pane class="flex flex-col">
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||||
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
||||||
{#if messages.length === 0}
|
{#if messages.length === 0}
|
||||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">Nomos</h2>
|
<h2 class="text-xl font-semibold">Nomos</h2>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">Your resident operator. Ask about the fleet, or tell it to act.</p>
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
</div>
|
Your resident operator. Ask about the fleet, or tell it to act.
|
||||||
{#if suggestions.length}
|
</p>
|
||||||
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
|
||||||
{#each suggestions as q}
|
|
||||||
<Button variant="outline" size="sm" class="h-auto justify-start whitespace-normal py-2 text-left text-xs" onclick={() => ask(q)}>
|
|
||||||
{q}
|
|
||||||
</Button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each messages as msg, idx (msg.id)}
|
|
||||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
|
||||||
{#if msg.role === 'user'}
|
|
||||||
<div class="flex items-baseline gap-2 px-1">
|
|
||||||
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
|
||||||
{#if msg.created_at}
|
|
||||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg">{msg.text}</div>
|
|
||||||
{:else}
|
|
||||||
{@const isLast = idx === messages.length - 1}
|
|
||||||
{@const traceStatus = !isLast
|
|
||||||
? 'idle'
|
|
||||||
: error
|
|
||||||
? 'error'
|
|
||||||
: streaming
|
|
||||||
? 'running'
|
|
||||||
: indicatorDone
|
|
||||||
? 'done'
|
|
||||||
: 'idle'}
|
|
||||||
<div class="flex w-full flex-col gap-2">
|
|
||||||
<div class="flex items-baseline gap-2 px-1">
|
|
||||||
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
|
||||||
{#if msg.created_at}
|
|
||||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<!-- The working trace sits above the answer: it's what happened
|
{#if suggestions.length}
|
||||||
first, and collapsed it keeps a long tool run from burying
|
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
the text below it. -->
|
{#each suggestions as q}
|
||||||
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
<Button
|
||||||
<AgentTrace
|
variant="outline"
|
||||||
tools={msg.tools}
|
size="sm"
|
||||||
status={traceStatus}
|
class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
|
||||||
label={traceStatus === 'idle' ? null : indicatorLabel}
|
onclick={() => ask(q)}
|
||||||
/>
|
>
|
||||||
{/if}
|
{q}
|
||||||
{#if msg.text}
|
</Button>
|
||||||
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
{/each}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
|
||||||
{@html render(msg.text)}
|
|
||||||
{#if isLast && streaming}
|
|
||||||
<span class="stream-cursor" aria-hidden="true"></span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#each messages as msg, idx (msg.id)}
|
||||||
|
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||||
|
{#if msg.role === 'user'}
|
||||||
|
<div class="flex items-baseline gap-2 px-1">
|
||||||
|
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
||||||
|
{#if msg.created_at}
|
||||||
|
<span class="text-[9px] text-muted-foreground/50"
|
||||||
|
>{formatTime(msg.created_at)}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg"
|
||||||
|
>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{@const isLast = idx === messages.length - 1}
|
||||||
|
{@const traceStatus = !isLast
|
||||||
|
? 'idle'
|
||||||
|
: error
|
||||||
|
? 'error'
|
||||||
|
: streaming
|
||||||
|
? 'running'
|
||||||
|
: indicatorDone
|
||||||
|
? 'done'
|
||||||
|
: 'idle'}
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<div class="flex items-baseline gap-2 px-1">
|
||||||
|
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
||||||
|
{#if msg.created_at}
|
||||||
|
<span class="text-[9px] text-muted-foreground/50"
|
||||||
|
>{formatTime(msg.created_at)}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- The working trace sits above the answer: it's what happened
|
||||||
|
first, and collapsed it keeps a long tool run from burying
|
||||||
|
the text below it. -->
|
||||||
|
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
||||||
|
<AgentTrace
|
||||||
|
tools={msg.tools}
|
||||||
|
status={traceStatus}
|
||||||
|
label={traceStatus === 'idle' ? null : indicatorLabel}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if msg.text}
|
||||||
|
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||||
|
{@html render(msg.text)}
|
||||||
|
{#if isLast && streaming}
|
||||||
|
<span class="stream-cursor" aria-hidden="true"></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if question}
|
||||||
|
<OperatorQuestion {sessionId} {question} />
|
||||||
|
{/if}
|
||||||
|
<div bind:this={messagesEnd}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if connectionState === 'disconnected'}
|
||||||
|
<div class="mx-auto w-full max-w-3xl px-4">
|
||||||
|
<div
|
||||||
|
class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
||||||
|
<span class="text-warning-foreground flex-1"
|
||||||
|
>Agent connection lost. The task may still be running.</span
|
||||||
|
>
|
||||||
|
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}
|
||||||
|
>Reconnect</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if connectionState === 'reconnecting'}
|
||||||
|
<div class="mx-auto w-full max-w-3xl px-4">
|
||||||
|
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
||||||
|
<RefreshCwIcon
|
||||||
|
class="size-3 shrink-0 animate-spin text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="mx-auto w-full max-w-3xl px-4">
|
||||||
|
<div
|
||||||
|
class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each chatErrors as err (err.id)}
|
||||||
|
<div class="mx-auto w-full max-w-3xl px-4">
|
||||||
|
<div
|
||||||
|
class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
|
<span class="flex-1">{err.message}</span>
|
||||||
|
{#if err.action}
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-6 text-[11px]"
|
||||||
|
onclick={() => onDismissError(err.id)}>{err.action}</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="ml-1 text-muted-foreground hover:text-foreground"
|
||||||
|
onclick={() => onDismissError(err.id)}
|
||||||
|
aria-label="Dismiss">×</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if question}
|
|
||||||
<OperatorQuestion {sessionId} {question} />
|
|
||||||
{/if}
|
|
||||||
<div bind:this={messagesEnd}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if connectionState === 'disconnected'}
|
|
||||||
<div class="mx-auto w-full max-w-3xl px-4">
|
|
||||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs">
|
|
||||||
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
|
||||||
<span class="text-warning-foreground flex-1">Agent connection lost. The task may still be running.</span>
|
|
||||||
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}>Reconnect</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if connectionState === 'reconnecting'}
|
|
||||||
<div class="mx-auto w-full max-w-3xl px-4">
|
|
||||||
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
|
||||||
<RefreshCwIcon class="size-3 shrink-0 animate-spin text-muted-foreground" aria-hidden="true" />
|
|
||||||
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div class="mx-auto w-full max-w-3xl px-4">
|
|
||||||
<div class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each chatErrors as err (err.id)}
|
|
||||||
<div class="mx-auto w-full max-w-3xl px-4">
|
|
||||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
|
||||||
<span class="flex-1">{err.message}</span>
|
|
||||||
{#if err.action}
|
|
||||||
<Button size="xs" variant="ghost" class="h-6 text-[11px]" onclick={() => onDismissError(err.id)}>{err.action}</Button>
|
|
||||||
{/if}
|
|
||||||
<button class="ml-1 text-muted-foreground hover:text-foreground" onclick={() => onDismissError(err.id)} aria-label="Dismiss">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
||||||
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
|
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
|
||||||
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
|
<div
|
||||||
|
class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative"
|
||||||
|
bind:this={inputWrapperRef}
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
class="relative mx-auto flex h-full w-full max-w-3xl"
|
class="relative mx-auto flex h-full w-full max-w-3xl"
|
||||||
onsubmit={(e) => {
|
onsubmit={(e) => {
|
||||||
@@ -514,7 +571,13 @@
|
|||||||
border: none;
|
border: none;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
background: linear-gradient(to right, transparent, var(--border) 20%, var(--border) 80%, transparent);
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent,
|
||||||
|
var(--border) 20%,
|
||||||
|
var(--border) 80%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
|
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
|
||||||
@@ -559,7 +622,9 @@
|
|||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s, color 0.15s;
|
transition:
|
||||||
|
opacity 0.15s,
|
||||||
|
color 0.15s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -586,7 +651,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes cursor-blink {
|
@keyframes cursor-blink {
|
||||||
0%, 100% { opacity: 0.75; }
|
0%,
|
||||||
50% { opacity: 0; }
|
100% {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
let canvas = $state<HTMLCanvasElement | null>(null)
|
let canvas = $state<HTMLCanvasElement | null>(null)
|
||||||
let particles: Particle[] = []
|
let particles: Particle[] = []
|
||||||
let mouse = { x: -500, y: -500 }
|
let mouse = { x: -500, y: -500 }
|
||||||
let w = 0, h = 0, dpr = 1
|
let w = 0,
|
||||||
|
h = 0,
|
||||||
|
dpr = 1
|
||||||
let timer: ReturnType<typeof setTimeout> | 0 = 0
|
let timer: ReturnType<typeof setTimeout> | 0 = 0
|
||||||
|
|
||||||
function spawn() {
|
function spawn() {
|
||||||
@@ -73,15 +75,15 @@
|
|||||||
// update + draw particles
|
// update + draw particles
|
||||||
for (const p of particles) {
|
for (const p of particles) {
|
||||||
// autonomous drift
|
// autonomous drift
|
||||||
p.vx += (Math.sin(t * 0.4 + p.phase) * 0.003) * 0.15
|
p.vx += Math.sin(t * 0.4 + p.phase) * 0.003 * 0.15
|
||||||
p.vy += (Math.cos(t * 0.35 + p.phase) * 0.003) * 0.15
|
p.vy += Math.cos(t * 0.35 + p.phase) * 0.003 * 0.15
|
||||||
|
|
||||||
// mouse interaction
|
// mouse interaction
|
||||||
const dx = p.x - mouse.x
|
const dx = p.x - mouse.x
|
||||||
const dy = p.y - mouse.y
|
const dy = p.y - mouse.y
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
if (dist < MOUSE_RADIUS && dist > 0) {
|
if (dist < MOUSE_RADIUS && dist > 0) {
|
||||||
const force = (MOUSE_RADIUS - dist) / MOUSE_RADIUS * MOUSE_FORCE
|
const force = ((MOUSE_RADIUS - dist) / MOUSE_RADIUS) * MOUSE_FORCE
|
||||||
p.vx += (dx / dist) * force * 0.6
|
p.vx += (dx / dist) * force * 0.6
|
||||||
p.vy += (dy / dist) * force * 0.6
|
p.vy += (dy / dist) * force * 0.6
|
||||||
}
|
}
|
||||||
@@ -104,9 +106,7 @@
|
|||||||
|
|
||||||
// pulse brightness
|
// pulse brightness
|
||||||
const alpha = p.pulse * (0.35 + 0.15 * Math.sin(t * 1.2 + p.phase))
|
const alpha = p.pulse * (0.35 + 0.15 * Math.sin(t * 1.2 + p.phase))
|
||||||
ctx.fillStyle = dark
|
ctx.fillStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})`
|
||||||
? `rgba(140,175,230,${alpha})`
|
|
||||||
: `rgba(60,90,140,${alpha})`
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
|
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
@@ -123,9 +123,7 @@
|
|||||||
const dist = dx * dx + dy * dy
|
const dist = dx * dx + dy * dy
|
||||||
if (dist < CONNECT_DIST * CONNECT_DIST) {
|
if (dist < CONNECT_DIST * CONNECT_DIST) {
|
||||||
const alpha = (1 - Math.sqrt(dist) / CONNECT_DIST) * 0.18
|
const alpha = (1 - Math.sqrt(dist) / CONNECT_DIST) * 0.18
|
||||||
ctx.strokeStyle = dark
|
ctx.strokeStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})`
|
||||||
? `rgba(140,175,230,${alpha})`
|
|
||||||
: `rgba(60,90,140,${alpha})`
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(a.x, a.y)
|
ctx.moveTo(a.x, a.y)
|
||||||
ctx.lineTo(b.x, b.y)
|
ctx.lineTo(b.x, b.y)
|
||||||
@@ -135,7 +133,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// radial scrim to keep center legible
|
// radial scrim to keep center legible
|
||||||
const cx = w / 2, cy = h / 2
|
const cx = w / 2,
|
||||||
|
cy = h / 2
|
||||||
const scrim = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy))
|
const scrim = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy))
|
||||||
const base = dark ? '13,17,23' : '255,255,255'
|
const base = dark ? '13,17,23' : '255,255,255'
|
||||||
scrim.addColorStop(0, `rgba(${base},0.72)`)
|
scrim.addColorStop(0, `rgba(${base},0.72)`)
|
||||||
|
|||||||
@@ -22,14 +22,20 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapsible.Root bind:open class="rounded-md border bg-card">
|
<Collapsible.Root bind:open class="rounded-md border bg-card">
|
||||||
<Collapsible.Trigger class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2 py-1 text-left hover:bg-muted/50">
|
<Collapsible.Trigger
|
||||||
|
class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2 py-1 text-left hover:bg-muted/50"
|
||||||
|
>
|
||||||
<span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
|
<span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open ? 'rotate-180' : ''}"
|
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open
|
||||||
|
? 'rotate-180'
|
||||||
|
: ''}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in">
|
<Collapsible.Content
|
||||||
|
class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in"
|
||||||
|
>
|
||||||
<div class="border-t px-2 py-1.5">
|
<div class="border-t px-2 py-1.5">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td {colspan} class={['py-8 text-center text-muted-foreground', className].filter(Boolean).join(' ')}>
|
<td
|
||||||
|
{colspan}
|
||||||
|
class={['py-8 text-center text-muted-foreground', className].filter(Boolean).join(' ')}
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -62,7 +62,9 @@
|
|||||||
// (source OR target = entity, both directions), so a plain split by which
|
// (source OR target = entity, both directions), so a plain split by which
|
||||||
// side matches is enough — no risk of an unrelated sibling-to-sibling edge
|
// side matches is enough — no risk of an unrelated sibling-to-sibling edge
|
||||||
// sneaking into either group.
|
// sneaking into either group.
|
||||||
const outgoingRelations = $derived(entity ? relations.filter((r) => r.source === entity!.slug) : [])
|
const outgoingRelations = $derived(
|
||||||
|
entity ? relations.filter((r) => r.source === entity!.slug) : []
|
||||||
|
)
|
||||||
const incomingRelations = $derived(
|
const incomingRelations = $derived(
|
||||||
entity ? relations.filter((r) => r.target === entity!.slug && r.source !== entity!.slug) : []
|
entity ? relations.filter((r) => r.target === entity!.slug && r.source !== entity!.slug) : []
|
||||||
)
|
)
|
||||||
@@ -203,13 +205,23 @@
|
|||||||
body?: string
|
body?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LONG_TEXT_KEYS = new Set(['description', 'content', 'summary', 'notes', 'note', 'body', 'details'])
|
const LONG_TEXT_KEYS = new Set([
|
||||||
|
'description',
|
||||||
|
'content',
|
||||||
|
'summary',
|
||||||
|
'notes',
|
||||||
|
'note',
|
||||||
|
'body',
|
||||||
|
'details'
|
||||||
|
])
|
||||||
|
|
||||||
function isChangelog(value: unknown): value is ChangelogEntry[] {
|
function isChangelog(value: unknown): value is ChangelogEntry[] {
|
||||||
return (
|
return (
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.length > 0 &&
|
value.length > 0 &&
|
||||||
value.every((v) => v && typeof v === 'object' && !Array.isArray(v) && ('title' in v || 'body' in v))
|
value.every(
|
||||||
|
(v) => v && typeof v === 'object' && !Array.isArray(v) && ('title' in v || 'body' in v)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,14 +275,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-3 border-b pb-1">
|
<div class="flex items-center justify-between gap-3 border-b pb-1">
|
||||||
<dt class="shrink-0 text-muted-foreground">State</dt>
|
<dt class="shrink-0 text-muted-foreground">State</dt>
|
||||||
<dd>{#if entity.state}<Badge>{entity.state}</Badge>{:else}<span class="text-muted-foreground">—</span>{/if}</dd>
|
<dd>
|
||||||
|
{#if entity.state}<Badge>{entity.state}</Badge>{:else}<span
|
||||||
|
class="text-muted-foreground">—</span
|
||||||
|
>{/if}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-3 border-b pb-1">
|
<div class="flex items-center justify-between gap-3 border-b pb-1">
|
||||||
<dt class="shrink-0 text-muted-foreground">Health</dt>
|
<dt class="shrink-0 text-muted-foreground">Health</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{#if entity.health}
|
{#if entity.health}
|
||||||
<span class="flex items-center gap-1.5" title="checked {relativeTime(entity.last_check_at)}">
|
<span
|
||||||
<span class="size-2 rounded-full {healthDot[entity.health] ?? healthDot.unknown}"></span>
|
class="flex items-center gap-1.5"
|
||||||
|
title="checked {relativeTime(entity.last_check_at)}"
|
||||||
|
>
|
||||||
|
<span class="size-2 rounded-full {healthDot[entity.health] ?? healthDot.unknown}"
|
||||||
|
></span>
|
||||||
{entity.health} · checked {relativeTime(entity.last_check_at)}
|
{entity.health} · checked {relativeTime(entity.last_check_at)}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -286,7 +306,11 @@
|
|||||||
<dt class="shrink-0 text-muted-foreground">Created</dt>
|
<dt class="shrink-0 text-muted-foreground">Created</dt>
|
||||||
<dd title={entity.created_at}>{relativeTime(entity.created_at)}</dd>
|
<dd title={entity.created_at}>{relativeTime(entity.created_at)}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-3 {entity.maintenance_until ? 'border-b pb-1' : ''}">
|
<div
|
||||||
|
class="flex items-center justify-between gap-3 {entity.maintenance_until
|
||||||
|
? 'border-b pb-1'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
<dt class="shrink-0 text-muted-foreground">Updated</dt>
|
<dt class="shrink-0 text-muted-foreground">Updated</dt>
|
||||||
<dd title={entity.updated_at}>{relativeTime(entity.updated_at)}</dd>
|
<dd title={entity.updated_at}>{relativeTime(entity.updated_at)}</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,7 +337,9 @@
|
|||||||
onclick={() => toggleCheck(check)}
|
onclick={() => toggleCheck(check)}
|
||||||
title={check.enabled ? 'Click to disable' : 'Click to enable'}
|
title={check.enabled ? 'Click to disable' : 'Click to enable'}
|
||||||
>
|
>
|
||||||
<Badge variant={check.enabled ? 'default' : 'secondary'}>{check.enabled ? 'enabled' : 'disabled'}</Badge>
|
<Badge variant={check.enabled ? 'default' : 'secondary'}
|
||||||
|
>{check.enabled ? 'enabled' : 'disabled'}</Badge
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -339,7 +365,9 @@
|
|||||||
{#if row.kind === 'long-text'}
|
{#if row.kind === 'long-text'}
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<p class="font-mono text-muted-foreground">{row.key}</p>
|
<p class="font-mono text-muted-foreground">{row.key}</p>
|
||||||
<p class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-1.5">{row.value}</p>
|
<p class="whitespace-pre-wrap break-words rounded-md bg-muted/40 p-1.5">
|
||||||
|
{row.value}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if row.kind === 'changelog'}
|
{:else if row.kind === 'changelog'}
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
@@ -348,10 +376,16 @@
|
|||||||
{#each row.value as entry}
|
{#each row.value as entry}
|
||||||
<div class="rounded-sm border-l-2 border-muted-foreground/30 pl-1.5">
|
<div class="rounded-sm border-l-2 border-muted-foreground/30 pl-1.5">
|
||||||
<div class="flex items-baseline gap-1.5">
|
<div class="flex items-baseline gap-1.5">
|
||||||
{#if entry.date}<span class="shrink-0 font-mono text-muted-foreground">{entry.date}</span>{/if}
|
{#if entry.date}<span class="shrink-0 font-mono text-muted-foreground"
|
||||||
|
>{entry.date}</span
|
||||||
|
>{/if}
|
||||||
{#if entry.title}<span class="font-medium">{entry.title}</span>{/if}
|
{#if entry.title}<span class="font-medium">{entry.title}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if entry.body}<p class="whitespace-pre-wrap break-words text-muted-foreground">{entry.body}</p>{/if}
|
{#if entry.body}<p
|
||||||
|
class="whitespace-pre-wrap break-words text-muted-foreground"
|
||||||
|
>
|
||||||
|
{entry.body}
|
||||||
|
</p>{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +407,12 @@
|
|||||||
<dt class="shrink-0 font-mono text-muted-foreground">{row.key}</dt>
|
<dt class="shrink-0 font-mono text-muted-foreground">{row.key}</dt>
|
||||||
<dd class="min-w-0 flex-1 break-words text-right">
|
<dd class="min-w-0 flex-1 break-words text-right">
|
||||||
{#if row.value !== null && typeof row.value === 'object'}
|
{#if row.value !== null && typeof row.value === 'object'}
|
||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-words text-left">{JSON.stringify(row.value, null, 2)}</pre>
|
<pre
|
||||||
|
class="overflow-x-auto whitespace-pre-wrap break-words text-left">{JSON.stringify(
|
||||||
|
row.value,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
{:else}
|
{:else}
|
||||||
{String(row.value)}
|
{String(row.value)}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -390,13 +429,27 @@
|
|||||||
{#snippet relationRow(rel: Relationship)}
|
{#snippet relationRow(rel: Relationship)}
|
||||||
<div class="flex min-w-0 items-center gap-1 font-mono text-xs">
|
<div class="flex min-w-0 items-center gap-1 font-mono text-xs">
|
||||||
{#if onSelectEntity}
|
{#if onSelectEntity}
|
||||||
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={rel.source} onclick={() => onSelectEntity(rel.source)}>{truncateMiddle(rel.source)}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
|
||||||
|
title={rel.source}
|
||||||
|
onclick={() => onSelectEntity(rel.source)}>{truncateMiddle(rel.source)}</button
|
||||||
|
>
|
||||||
<span class="shrink-0 text-muted-foreground">—{rel.type}→</span>
|
<span class="shrink-0 text-muted-foreground">—{rel.type}→</span>
|
||||||
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={rel.target} onclick={() => onSelectEntity(rel.target)}>{truncateMiddle(rel.target)}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
|
||||||
|
title={rel.target}
|
||||||
|
onclick={() => onSelectEntity(rel.target)}>{truncateMiddle(rel.target)}</button
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="min-w-0 flex-1 truncate" title={rel.source}>{truncateMiddle(rel.source)}</span>
|
<span class="min-w-0 flex-1 truncate" title={rel.source}
|
||||||
|
>{truncateMiddle(rel.source)}</span
|
||||||
|
>
|
||||||
<span class="shrink-0 text-muted-foreground">—{rel.type}→</span>
|
<span class="shrink-0 text-muted-foreground">—{rel.type}→</span>
|
||||||
<span class="min-w-0 flex-1 truncate" title={rel.target}>{truncateMiddle(rel.target)}</span>
|
<span class="min-w-0 flex-1 truncate" title={rel.target}
|
||||||
|
>{truncateMiddle(rel.target)}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -408,7 +461,11 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#if outgoingRelations.length}
|
{#if outgoingRelations.length}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">Outgoing ({outgoingRelations.length})</div>
|
<div
|
||||||
|
class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase"
|
||||||
|
>
|
||||||
|
Outgoing ({outgoingRelations.length})
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#each outgoingRelations as rel}
|
{#each outgoingRelations as rel}
|
||||||
{@render relationRow(rel)}
|
{@render relationRow(rel)}
|
||||||
@@ -418,7 +475,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if incomingRelations.length}
|
{#if incomingRelations.length}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">Incoming ({incomingRelations.length})</div>
|
<div
|
||||||
|
class="mb-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase"
|
||||||
|
>
|
||||||
|
Incoming ({incomingRelations.length})
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#each incomingRelations as rel}
|
{#each incomingRelations as rel}
|
||||||
{@render relationRow(rel)}
|
{@render relationRow(rel)}
|
||||||
@@ -492,21 +553,32 @@
|
|||||||
{#snippet tasksContent()}
|
{#snippet tasksContent()}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{#each tasks as { task, executionCount } (task.id)}
|
{#each tasks as { task, executionCount } (task.id)}
|
||||||
{@const title = typeof task.attributes?.title === 'string' ? task.attributes.title : task.name}
|
{@const title =
|
||||||
{@const outcome = typeof task.attributes?.outcome === 'string' ? task.attributes.outcome : undefined}
|
typeof task.attributes?.title === 'string' ? task.attributes.title : task.name}
|
||||||
<div class="flex items-center justify-between gap-2 border-b pb-1 text-xs last:border-0 last:pb-0">
|
{@const outcome =
|
||||||
|
typeof task.attributes?.outcome === 'string' ? task.attributes.outcome : undefined}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-2 border-b pb-1 text-xs last:border-0 last:pb-0"
|
||||||
|
>
|
||||||
{#if onSelectEntity}
|
{#if onSelectEntity}
|
||||||
<button type="button" class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground" title={title} onclick={() => onSelectEntity(task.slug)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-w-0 flex-1 truncate text-left hover:underline hover:text-foreground"
|
||||||
|
{title}
|
||||||
|
onclick={() => onSelectEntity(task.slug)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="min-w-0 flex-1 truncate" title={title}>{title}</span>
|
<span class="min-w-0 flex-1 truncate" {title}>{title}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex shrink-0 items-center gap-1">
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
{#if outcome}
|
{#if outcome}
|
||||||
<Badge variant={outcome === 'success' ? 'default' : 'destructive'}>{outcome}</Badge>
|
<Badge variant={outcome === 'success' ? 'default' : 'destructive'}>{outcome}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
<Badge variant="outline">{executionCount} action{executionCount === 1 ? '' : 's'}</Badge>
|
<Badge variant="outline"
|
||||||
|
>{executionCount} action{executionCount === 1 ? '' : 's'}</Badge
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -545,8 +617,12 @@
|
|||||||
{#each agentActivity as activity (activity.id)}
|
{#each agentActivity as activity (activity.id)}
|
||||||
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
|
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="font-mono text-muted-foreground">{new Date(activity.ts).toLocaleString()}</span>
|
<span class="font-mono text-muted-foreground"
|
||||||
<Badge variant={activity.success === false ? 'destructive' : 'outline'}>{activity.activity_type}</Badge>
|
>{new Date(activity.ts).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
<Badge variant={activity.success === false ? 'destructive' : 'outline'}
|
||||||
|
>{activity.activity_type}</Badge
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="truncate text-muted-foreground"
|
<span class="truncate text-muted-foreground"
|
||||||
>{activity.agent_id}{activity.tool_name ? ` · ${activity.tool_name}` : ''}</span
|
>{activity.agent_id}{activity.tool_name ? ` · ${activity.tool_name}` : ''}</span
|
||||||
@@ -563,10 +639,14 @@
|
|||||||
{#each auditEntries as entry (entry.id)}
|
{#each auditEntries as entry (entry.id)}
|
||||||
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
|
<div class="flex flex-col gap-0.5 border-b pb-1 text-xs last:border-0 last:pb-0">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="font-mono text-muted-foreground">{new Date(entry.ts).toLocaleString()}</span>
|
<span class="font-mono text-muted-foreground"
|
||||||
|
>{new Date(entry.ts).toLocaleString()}</span
|
||||||
|
>
|
||||||
<Badge variant="outline">{entry.actor_type}</Badge>
|
<Badge variant="outline">{entry.actor_type}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<span class="truncate text-muted-foreground">{entry.actor_id ?? '—'} · {entry.action}</span>
|
<span class="truncate text-muted-foreground"
|
||||||
|
>{entry.actor_id ?? '—'} · {entry.action}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-xs text-muted-foreground">No audit entries.</p>
|
<p class="text-xs text-muted-foreground">No audit entries.</p>
|
||||||
@@ -575,17 +655,34 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{@const sections = [
|
{@const sections = [
|
||||||
...(ownContent ? [{ key: 'content', title: 'Content', count: 1, content: contentContent }] : []),
|
...(ownContent
|
||||||
|
? [{ key: 'content', title: 'Content', count: 1, content: contentContent }]
|
||||||
|
: []),
|
||||||
{ key: 'details', title: 'Details', count: 1, content: detailsContent },
|
{ key: 'details', title: 'Details', count: 1, content: detailsContent },
|
||||||
{ key: 'monitoring', title: 'Monitoring', count: checks.length, content: monitoringContent },
|
{ key: 'monitoring', title: 'Monitoring', count: checks.length, content: monitoringContent },
|
||||||
{ key: 'attributes', title: 'Attributes', count: Object.keys(entity.attributes ?? {}).length, content: attributesContent },
|
{
|
||||||
{ key: 'relations', title: 'Relations', count: outgoingRelations.length + incomingRelations.length, content: relationsContent },
|
key: 'attributes',
|
||||||
|
title: 'Attributes',
|
||||||
|
count: Object.keys(entity.attributes ?? {}).length,
|
||||||
|
content: attributesContent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'relations',
|
||||||
|
title: 'Relations',
|
||||||
|
count: outgoingRelations.length + incomingRelations.length,
|
||||||
|
content: relationsContent
|
||||||
|
},
|
||||||
{ key: 'metrics', title: 'Metrics', count: metrics.length, content: metricsContent },
|
{ key: 'metrics', title: 'Metrics', count: metrics.length, content: metricsContent },
|
||||||
{ key: 'signals', title: 'Signals', count: signals.length, content: signalsContent },
|
{ key: 'signals', title: 'Signals', count: signals.length, content: signalsContent },
|
||||||
{ key: 'tasks', title: 'Tasks', count: tasks.length, content: tasksContent },
|
{ key: 'tasks', title: 'Tasks', count: tasks.length, content: tasksContent },
|
||||||
{ key: 'knowledge', title: 'Knowledge', count: knowledge.length, content: knowledgeContent },
|
{ key: 'knowledge', title: 'Knowledge', count: knowledge.length, content: knowledgeContent },
|
||||||
{ key: 'events', title: 'Recent events', count: events.length, content: eventsContent },
|
{ key: 'events', title: 'Recent events', count: events.length, content: eventsContent },
|
||||||
{ key: 'agentActivity', title: 'Agent activity', count: agentActivity.length, content: agentActivityContent },
|
{
|
||||||
|
key: 'agentActivity',
|
||||||
|
title: 'Agent activity',
|
||||||
|
count: agentActivity.length,
|
||||||
|
content: agentActivityContent
|
||||||
|
},
|
||||||
{ key: 'audit', title: 'Audit trail', count: auditEntries.length, content: auditContent }
|
{ key: 'audit', title: 'Audit trail', count: auditEntries.length, content: auditContent }
|
||||||
].sort((a, b) => (b.count > 0 ? 1 : 0) - (a.count > 0 ? 1 : 0))}
|
].sort((a, b) => (b.count > 0 ? 1 : 0) - (a.count > 0 ? 1 : 0))}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,13 @@
|
|||||||
return { sorted: true, direction: sortDir }
|
return { sorted: true, direction: sortDir }
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthRank: Record<string, number> = { down: 0, degraded: 1, stale: 2, unknown: 3, healthy: 4 }
|
const healthRank: Record<string, number> = {
|
||||||
|
down: 0,
|
||||||
|
degraded: 1,
|
||||||
|
stale: 2,
|
||||||
|
unknown: 3,
|
||||||
|
healthy: 4
|
||||||
|
}
|
||||||
|
|
||||||
function sortValue(entity: Entity, key: SortKey): string | number {
|
function sortValue(entity: Entity, key: SortKey): string | number {
|
||||||
if (key === 'health') return entity.health ? (healthRank[entity.health] ?? -1) : -1
|
if (key === 'health') return entity.health ? (healthRank[entity.health] ?? -1) : -1
|
||||||
@@ -131,7 +137,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#snippet row(entity: Entity, level: number, ancestors: Set<string>)}
|
{#snippet row(entity: Entity, level: number, ancestors: Set<string>)}
|
||||||
{@const ancestorsWithSelf = new Set(ancestors).add(entity.slug)}
|
{@const ancestorsWithSelf = new Set(ancestors).add(entity.slug)}
|
||||||
{@const children = (childrenByParent.get(entity.slug) ?? []).filter((c) => !ancestorsWithSelf.has(c.slug))}
|
{@const children = (childrenByParent.get(entity.slug) ?? []).filter(
|
||||||
|
(c) => !ancestorsWithSelf.has(c.slug)
|
||||||
|
)}
|
||||||
<Table.Row
|
<Table.Row
|
||||||
class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}"
|
class="cursor-pointer {entity.slug === selectedSlug ? 'bg-muted' : ''}"
|
||||||
role="row"
|
role="row"
|
||||||
@@ -139,7 +147,12 @@
|
|||||||
aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined}
|
aria-expanded={children.length > 0 ? !collapsedNodes.has(entity.slug) : undefined}
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
onclick={() => onSelect(entity.slug)}
|
onclick={() => onSelect(entity.slug)}
|
||||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(entity.slug) } }}
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
onSelect(entity.slug)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Table.Cell class="font-mono text-xs">
|
<Table.Cell class="font-mono text-xs">
|
||||||
<span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem">
|
<span class="flex items-center gap-1" style="padding-left: {(level - 1) * 1.25}rem">
|
||||||
@@ -149,7 +162,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="rounded text-muted-foreground hover:text-foreground"
|
class="rounded text-muted-foreground hover:text-foreground"
|
||||||
onclick={(e) => toggleNode(entity.slug, e)}
|
onclick={(e) => toggleNode(entity.slug, e)}
|
||||||
aria-label={collapsedNodes.has(entity.slug) ? `Expand ${entity.slug}` : `Collapse ${entity.slug}`}
|
aria-label={collapsedNodes.has(entity.slug)
|
||||||
|
? `Expand ${entity.slug}`
|
||||||
|
: `Collapse ${entity.slug}`}
|
||||||
>
|
>
|
||||||
{#if collapsedNodes.has(entity.slug)}
|
{#if collapsedNodes.has(entity.slug)}
|
||||||
<ChevronRightIcon class="size-3.5" />
|
<ChevronRightIcon class="size-3.5" />
|
||||||
@@ -186,23 +201,48 @@
|
|||||||
<Table.Row>
|
<Table.Row>
|
||||||
{@const ssSlug = getSortState('slug')}
|
{@const ssSlug = getSortState('slug')}
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<SortHeader label="Slug" sorted={ssSlug.sorted} direction={ssSlug.direction} onclick={() => sortBy('slug')} />
|
<SortHeader
|
||||||
|
label="Slug"
|
||||||
|
sorted={ssSlug.sorted}
|
||||||
|
direction={ssSlug.direction}
|
||||||
|
onclick={() => sortBy('slug')}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
{@const ssType = getSortState('type')}
|
{@const ssType = getSortState('type')}
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<SortHeader label="Type" sorted={ssType.sorted} direction={ssType.direction} onclick={() => sortBy('type')} />
|
<SortHeader
|
||||||
|
label="Type"
|
||||||
|
sorted={ssType.sorted}
|
||||||
|
direction={ssType.direction}
|
||||||
|
onclick={() => sortBy('type')}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
{@const ssName = getSortState('name')}
|
{@const ssName = getSortState('name')}
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<SortHeader label="Name" sorted={ssName.sorted} direction={ssName.direction} onclick={() => sortBy('name')} />
|
<SortHeader
|
||||||
|
label="Name"
|
||||||
|
sorted={ssName.sorted}
|
||||||
|
direction={ssName.direction}
|
||||||
|
onclick={() => sortBy('name')}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
{@const ssState = getSortState('state')}
|
{@const ssState = getSortState('state')}
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<SortHeader label="State" sorted={ssState.sorted} direction={ssState.direction} onclick={() => sortBy('state')} />
|
<SortHeader
|
||||||
|
label="State"
|
||||||
|
sorted={ssState.sorted}
|
||||||
|
direction={ssState.direction}
|
||||||
|
onclick={() => sortBy('state')}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
{@const ssHealth = getSortState('health')}
|
{@const ssHealth = getSortState('health')}
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<SortHeader label="Health" sorted={ssHealth.sorted} direction={ssHealth.direction} onclick={() => sortBy('health')} />
|
<SortHeader
|
||||||
|
label="Health"
|
||||||
|
sorted={ssHealth.sorted}
|
||||||
|
direction={ssHealth.direction}
|
||||||
|
onclick={() => sortBy('health')}
|
||||||
|
/>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
|
|||||||
@@ -8,14 +8,22 @@
|
|||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
value?: string
|
value?: string
|
||||||
tabs: { value: string; label: string; count?: number; variant?: 'destructive' | 'default' | 'secondary' | 'outline' }[]
|
tabs: {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
count?: number
|
||||||
|
variant?: 'destructive' | 'default' | 'secondary' | 'outline'
|
||||||
|
}[]
|
||||||
class?: string
|
class?: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
children?: any
|
children?: any
|
||||||
} = $props()
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tabs.Root bind:value class={['flex flex-1 flex-col overflow-hidden', className].filter(Boolean).join(' ')}>
|
<Tabs.Root
|
||||||
|
bind:value
|
||||||
|
class={['flex flex-1 flex-col overflow-hidden', className].filter(Boolean).join(' ')}
|
||||||
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
<Tabs.Trigger value={tab.value}>
|
<Tabs.Trigger value={tab.value}>
|
||||||
|
|||||||
@@ -60,7 +60,11 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const ev = $liveEvents[0]
|
const ev = $liveEvents[0]
|
||||||
if (!ev) return
|
if (!ev) return
|
||||||
if (ev.type.startsWith('entity.') || ev.type.startsWith('relationship.') || ev.type === 'health.changed') {
|
if (
|
||||||
|
ev.type.startsWith('entity.') ||
|
||||||
|
ev.type.startsWith('relationship.') ||
|
||||||
|
ev.type === 'health.changed'
|
||||||
|
) {
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -276,9 +280,11 @@
|
|||||||
const svcRaw = new Map<string, number>()
|
const svcRaw = new Map<string, number>()
|
||||||
for (const s of services) {
|
for (const s of services) {
|
||||||
const p = serviceProvider.get(s.slug)
|
const p = serviceProvider.get(s.slug)
|
||||||
svcRaw.set(s.slug, (p && yById.has(p) ? yById.get(p)! : TOP0))
|
svcRaw.set(s.slug, p && yById.has(p) ? yById.get(p)! : TOP0)
|
||||||
}
|
}
|
||||||
services.sort((a, b) => (svcRaw.get(a.slug)! - svcRaw.get(b.slug)!) || a.name.localeCompare(b.name))
|
services.sort(
|
||||||
|
(a, b) => svcRaw.get(a.slug)! - svcRaw.get(b.slug)! || a.name.localeCompare(b.name)
|
||||||
|
)
|
||||||
let prevY = TOP0 - ROW_H
|
let prevY = TOP0 - ROW_H
|
||||||
for (const s of services) {
|
for (const s of services) {
|
||||||
const y = Math.max(svcRaw.get(s.slug)!, prevY + ROW_H)
|
const y = Math.max(svcRaw.get(s.slug)!, prevY + ROW_H)
|
||||||
@@ -308,7 +314,9 @@
|
|||||||
counts[h]++
|
counts[h]++
|
||||||
const ingress = routesTo.get(n.slug)
|
const ingress = routesTo.get(n.slug)
|
||||||
const ingressEntity = ingress ? bySlugEntity.get(ingress) : undefined
|
const ingressEntity = ingress ? bySlugEntity.get(ingress) : undefined
|
||||||
const publicHost = ingress ? (ingressEntity?.name ?? ingress.replace(/^ingress:/, '')) : undefined
|
const publicHost = ingress
|
||||||
|
? (ingressEntity?.name ?? ingress.replace(/^ingress:/, ''))
|
||||||
|
: undefined
|
||||||
const fauth = ingressEntity ? boolAttr(ingressEntity, 'forward_auth') : undefined
|
const fauth = ingressEntity ? boolAttr(ingressEntity, 'forward_auth') : undefined
|
||||||
const provider = kind === 'service' ? serviceProvider.get(n.slug) : containerHost.get(n.slug)
|
const provider = kind === 'service' ? serviceProvider.get(n.slug) : containerHost.get(n.slug)
|
||||||
const mnt = (kind === 'container' ? mountsOf.get(n.slug) : mountsOf.get(provider ?? '')) ?? []
|
const mnt = (kind === 'container' ? mountsOf.get(n.slug) : mountsOf.get(provider ?? '')) ?? []
|
||||||
@@ -361,25 +369,41 @@
|
|||||||
const n = bySlug.get(slug)!
|
const n = bySlug.get(slug)!
|
||||||
return LANE_X[n.lane] + (side === 'r' ? LANE_W[n.lane] : 0)
|
return LANE_X[n.lane] + (side === 'r' ? LANE_W[n.lane] : 0)
|
||||||
}
|
}
|
||||||
const cy = (slug: string) => (bySlug.get(slug)!.y) + NODE_H / 2
|
const cy = (slug: string) => bySlug.get(slug)!.y + NODE_H / 2
|
||||||
const edges: LaidEdge[] = []
|
const edges: LaidEdge[] = []
|
||||||
const provChildren = new Map<string, string[]>() // provider -> [child]
|
const provChildren = new Map<string, string[]>() // provider -> [child]
|
||||||
// provision edges: host->container, provider->service
|
// provision edges: host->container, provider->service
|
||||||
for (const c of containers) {
|
for (const c of containers) {
|
||||||
const host = containerHost.get(c.slug)
|
const host = containerHost.get(c.slug)
|
||||||
if (host && bySlug.has(host)) {
|
if (host && bySlug.has(host)) {
|
||||||
const x1 = cx(host, 'r'), y1 = cy(host), x2 = cx(c.slug, 'l'), y2 = cy(c.slug)
|
const x1 = cx(host, 'r'),
|
||||||
|
y1 = cy(host),
|
||||||
|
x2 = cx(c.slug, 'l'),
|
||||||
|
y2 = cy(c.slug)
|
||||||
const mx = (x1 + x2) / 2
|
const mx = (x1 + x2) / 2
|
||||||
edges.push({ d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, kind: 'prov', s: host, t: c.slug })
|
edges.push({
|
||||||
|
d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
|
||||||
|
kind: 'prov',
|
||||||
|
s: host,
|
||||||
|
t: c.slug
|
||||||
|
})
|
||||||
pushMap(provChildren, host, c.slug)
|
pushMap(provChildren, host, c.slug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const s of services) {
|
for (const s of services) {
|
||||||
const p = serviceProvider.get(s.slug)
|
const p = serviceProvider.get(s.slug)
|
||||||
if (p && bySlug.has(p)) {
|
if (p && bySlug.has(p)) {
|
||||||
const x1 = cx(p, 'r'), y1 = cy(p), x2 = cx(s.slug, 'l'), y2 = cy(s.slug)
|
const x1 = cx(p, 'r'),
|
||||||
|
y1 = cy(p),
|
||||||
|
x2 = cx(s.slug, 'l'),
|
||||||
|
y2 = cy(s.slug)
|
||||||
const mx = (x1 + x2) / 2
|
const mx = (x1 + x2) / 2
|
||||||
edges.push({ d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, kind: 'prov', s: p, t: s.slug })
|
edges.push({
|
||||||
|
d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
|
||||||
|
kind: 'prov',
|
||||||
|
s: p,
|
||||||
|
t: s.slug
|
||||||
|
})
|
||||||
pushMap(provChildren, p, s.slug)
|
pushMap(provChildren, p, s.slug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,9 +412,17 @@
|
|||||||
if (!bySlug.has(srcSlug)) continue
|
if (!bySlug.has(srcSlug)) continue
|
||||||
for (const t of targets) {
|
for (const t of targets) {
|
||||||
if (!bySlug.has(t)) continue
|
if (!bySlug.has(t)) continue
|
||||||
const x1 = cx(srcSlug, 'r'), y1 = cy(srcSlug), x2 = cx(t, 'r'), y2 = cy(t)
|
const x1 = cx(srcSlug, 'r'),
|
||||||
|
y1 = cy(srcSlug),
|
||||||
|
x2 = cx(t, 'r'),
|
||||||
|
y2 = cy(t)
|
||||||
const bulge = Math.max(x1, x2) + 34 + Math.min(70, Math.abs(y1 - y2) * 0.32)
|
const bulge = Math.max(x1, x2) + 34 + Math.min(70, Math.abs(y1 - y2) * 0.32)
|
||||||
edges.push({ d: `M${x1},${y1} C${bulge},${y1} ${bulge},${y2} ${x2},${y2}`, kind: 'dep', s: srcSlug, t })
|
edges.push({
|
||||||
|
d: `M${x1},${y1} C${bulge},${y1} ${bulge},${y2} ${x2},${y2}`,
|
||||||
|
kind: 'dep',
|
||||||
|
s: srcSlug,
|
||||||
|
t
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +540,8 @@
|
|||||||
const q = search.trim().toLowerCase()
|
const q = search.trim().toLowerCase()
|
||||||
if (q) {
|
if (q) {
|
||||||
const s = new Set<string>()
|
const s = new Set<string>()
|
||||||
for (const n of m.nodes) if (n.slug.toLowerCase().includes(q) || n.name.toLowerCase().includes(q)) s.add(n.slug)
|
for (const n of m.nodes)
|
||||||
|
if (n.slug.toLowerCase().includes(q) || n.name.toLowerCase().includes(q)) s.add(n.slug)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -589,7 +622,9 @@
|
|||||||
if (!m || !healthFilter) return [] as LaidNode[]
|
if (!m || !healthFilter) return [] as LaidNode[]
|
||||||
return m.nodes.filter((n) => n.health === healthFilter)
|
return m.nodes.filter((n) => n.health === healthFilter)
|
||||||
})
|
})
|
||||||
const problemCount = $derived(model ? model.nodes.filter((n) => n.health !== 'healthy').length : 0)
|
const problemCount = $derived(
|
||||||
|
model ? model.nodes.filter((n) => n.health !== 'healthy').length : 0
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading && !model}
|
{#if loading && !model}
|
||||||
@@ -636,7 +671,8 @@
|
|||||||
<div class="stage relative min-w-0 flex-1 overflow-auto">
|
<div class="stage relative min-w-0 flex-1 overflow-auto">
|
||||||
<div class="relative" style="width:{model.width}px;height:{model.height}px">
|
<div class="relative" style="width:{model.width}px;height:{model.height}px">
|
||||||
<!-- background click target: clears selection -->
|
<!-- background click target: clears selection -->
|
||||||
<button type="button" class="reset-layer" aria-label="Clear selection" onclick={reset}></button>
|
<button type="button" class="reset-layer" aria-label="Clear selection" onclick={reset}
|
||||||
|
></button>
|
||||||
<!-- lane headers -->
|
<!-- lane headers -->
|
||||||
<div class="pointer-events-none absolute inset-x-0 top-0 z-10">
|
<div class="pointer-events-none absolute inset-x-0 top-0 z-10">
|
||||||
{#each [['Hosts', model.nodes.filter((n) => n.kind === 'host').length], ['Containers', model.nodes.filter((n) => n.kind === 'container').length], ['Services', model.nodes.filter((n) => n.kind === 'service').length]] as [label, count], i}
|
{#each [['Hosts', model.nodes.filter((n) => n.kind === 'host').length], ['Containers', model.nodes.filter((n) => n.kind === 'container').length], ['Services', model.nodes.filter((n) => n.kind === 'service').length]] as [label, count], i}
|
||||||
@@ -647,7 +683,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- edges -->
|
<!-- edges -->
|
||||||
<svg class="pointer-events-none absolute inset-0" width={model.width} height={model.height}>
|
<svg
|
||||||
|
class="pointer-events-none absolute inset-0"
|
||||||
|
width={model.width}
|
||||||
|
height={model.height}
|
||||||
|
>
|
||||||
{#each model.edges as e}
|
{#each model.edges as e}
|
||||||
<path d={e.d} class="edge {e.kind} {edgeState(e)}" />
|
<path d={e.d} class="edge {e.kind} {edgeState(e)}" />
|
||||||
{/each}
|
{/each}
|
||||||
@@ -661,7 +701,9 @@
|
|||||||
class:svc={n.kind === 'service'}
|
class:svc={n.kind === 'service'}
|
||||||
class:is-open={selectedSlug === n.slug}
|
class:is-open={selectedSlug === n.slug}
|
||||||
class:pulse={n.health === 'down'}
|
class:pulse={n.health === 'down'}
|
||||||
style="left:{n.x}px;top:{n.y}px;width:{n.w}px;height:{n.h}px;--nc:{HEALTH_COLOR[n.health]}"
|
style="left:{n.x}px;top:{n.y}px;width:{n.w}px;height:{n.h}px;--nc:{HEALTH_COLOR[
|
||||||
|
n.health
|
||||||
|
]}"
|
||||||
onmouseenter={() => setFocus(n.slug)}
|
onmouseenter={() => setFocus(n.slug)}
|
||||||
onmouseleave={scheduleClear}
|
onmouseleave={scheduleClear}
|
||||||
onfocus={() => setFocus(n.slug)}
|
onfocus={() => setFocus(n.slug)}
|
||||||
@@ -673,7 +715,11 @@
|
|||||||
title={n.slug}
|
title={n.slug}
|
||||||
>
|
>
|
||||||
{#if n.publicHost}
|
{#if n.publicHost}
|
||||||
<span class="badge" title={n.publicHost + (n.fauth ? ' · forward-auth' : n.fauth === false ? ' · no auth gate' : '')}>
|
<span
|
||||||
|
class="badge"
|
||||||
|
title={n.publicHost +
|
||||||
|
(n.fauth ? ' · forward-auth' : n.fauth === false ? ' · no auth gate' : '')}
|
||||||
|
>
|
||||||
{#if n.fauth}
|
{#if n.fauth}
|
||||||
<LockIcon class="size-3" />
|
<LockIcon class="size-3" />
|
||||||
{:else if n.fauth === false}
|
{:else if n.fauth === false}
|
||||||
@@ -748,7 +794,9 @@
|
|||||||
<div class="blast">
|
<div class="blast">
|
||||||
<div class="n" class:warn={detailBlast.length > 0}>{detailBlast.length}</div>
|
<div class="n" class:warn={detailBlast.length > 0}>{detailBlast.length}</div>
|
||||||
<div class="lbl">
|
<div class="lbl">
|
||||||
{detailNode.kind === 'service' ? 'downstream service' : 'service'}{detailBlast.length === 1 ? '' : 's'}
|
{detailNode.kind === 'service'
|
||||||
|
? 'downstream service'
|
||||||
|
: 'service'}{detailBlast.length === 1 ? '' : 's'}
|
||||||
affected if this goes down
|
affected if this goes down
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -756,18 +804,40 @@
|
|||||||
<div class="det-sec">
|
<div class="det-sec">
|
||||||
<h4>Attributes</h4>
|
<h4>Attributes</h4>
|
||||||
{#if detailNode.kind === 'container'}
|
{#if detailNode.kind === 'container'}
|
||||||
<div class="row"><span class="k">type</span><span class="v">{detailNode.type}</span></div>
|
<div class="row">
|
||||||
|
<span class="k">type</span><span class="v">{detailNode.type}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if detailNode.role}<div class="row"><span class="k">role</span><span class="v">{detailNode.role}</span></div>{/if}
|
{#if detailNode.role}<div class="row">
|
||||||
{#if detailNode.ip}<div class="row"><span class="k">ip</span><span class="v">{detailNode.ip}</span></div>{/if}
|
<span class="k">role</span><span class="v">{detailNode.role}</span>
|
||||||
|
</div>{/if}
|
||||||
|
{#if detailNode.ip}<div class="row">
|
||||||
|
<span class="k">ip</span><span class="v">{detailNode.ip}</span>
|
||||||
|
</div>{/if}
|
||||||
{#if detailNode.publicHost}
|
{#if detailNode.publicHost}
|
||||||
<div class="row"><span class="k">url</span><span class="v">{detailNode.publicHost}</span></div>
|
<div class="row">
|
||||||
<div class="row"><span class="k">exposure</span><span class="v">{detailNode.fauth ? 'forward-auth gated' : detailNode.fauth === false ? 'no auth gate' : 'public'}</span></div>
|
<span class="k">url</span><span class="v">{detailNode.publicHost}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="k">exposure</span><span class="v"
|
||||||
|
>{detailNode.fauth
|
||||||
|
? 'forward-auth gated'
|
||||||
|
: detailNode.fauth === false
|
||||||
|
? 'no auth gate'
|
||||||
|
: 'public'}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
{:else if detailNode.url}
|
{:else if detailNode.url}
|
||||||
<div class="row"><span class="k">url</span><span class="v">{detailNode.url}</span></div>
|
<div class="row">
|
||||||
|
<span class="k">url</span><span class="v">{detailNode.url}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if detailNode.mounts.length}<div class="row"><span class="k">mounts</span><span class="v">{detailNode.mounts.join(', ')}</span></div>{/if}
|
{#if detailNode.mounts.length}<div class="row">
|
||||||
{#if detailNode.repo}<div class="row"><span class="k">config</span><span class="v">{detailNode.repo}</span></div>{/if}
|
<span class="k">mounts</span><span class="v">{detailNode.mounts.join(', ')}</span>
|
||||||
|
</div>{/if}
|
||||||
|
{#if detailNode.repo}<div class="row">
|
||||||
|
<span class="k">config</span><span class="v">{detailNode.repo}</span>
|
||||||
|
</div>{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if detailRunsOn.length}
|
{#if detailRunsOn.length}
|
||||||
@@ -824,18 +894,24 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h3 class="legend-h">Health</h3>
|
<h3 class="legend-h">Health</h3>
|
||||||
{#each HEALTH_ORDER as h}
|
{#each HEALTH_ORDER as h}
|
||||||
<div class="legend-row"><span class="sw" style="background:{HEALTH_COLOR[h]}"></span>{HEALTH_LABEL[h]}</div>
|
<div class="legend-row">
|
||||||
|
<span class="sw" style="background:{HEALTH_COLOR[h]}"></span>{HEALTH_LABEL[h]}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="sep"></div>
|
<div class="sep"></div>
|
||||||
<h3 class="legend-h">Lanes</h3>
|
<h3 class="legend-h">Lanes</h3>
|
||||||
<div class="legend-row"><span class="lane-swatch"></span>Hosts — physical machines</div>
|
<div class="legend-row"><span class="lane-swatch"></span>Hosts — physical machines</div>
|
||||||
<div class="legend-row"><span class="lane-swatch"></span>Containers — LXCs & VMs</div>
|
<div class="legend-row">
|
||||||
<div class="legend-row"><span class="lane-swatch"></span>Services — what you actually use</div>
|
<span class="lane-swatch"></span>Containers — LXCs & VMs
|
||||||
|
</div>
|
||||||
|
<div class="legend-row">
|
||||||
|
<span class="lane-swatch"></span>Services — what you actually use
|
||||||
|
</div>
|
||||||
<div class="sep"></div>
|
<div class="sep"></div>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
Every service flows right from the machine that runs it. Hover any node to trace its chain
|
Every service flows right from the machine that runs it. Hover any node to trace its
|
||||||
and blast radius — what breaks if it goes down. Click to open it in a window. Dashed arcs are
|
chain and blast radius — what breaks if it goes down. Click to open it in a window.
|
||||||
service-to-service dependencies.
|
Dashed arcs are service-to-service dependencies.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -843,7 +919,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-full items-center justify-center text-xs text-muted-foreground">Failed to load graph</div>
|
<div class="flex h-full items-center justify-center text-xs text-muted-foreground">
|
||||||
|
Failed to load graph
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -883,7 +961,9 @@
|
|||||||
stroke: var(--border);
|
stroke: var(--border);
|
||||||
stroke-width: 1.4;
|
stroke-width: 1.4;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: opacity 0.16s, stroke 0.16s;
|
transition:
|
||||||
|
opacity 0.16s,
|
||||||
|
stroke 0.16s;
|
||||||
}
|
}
|
||||||
.edge.dep {
|
.edge.dep {
|
||||||
stroke-dasharray: 4 3;
|
stroke-dasharray: 4 3;
|
||||||
@@ -919,7 +999,11 @@
|
|||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.16s, border-color 0.14s, transform 0.12s, box-shadow 0.14s;
|
transition:
|
||||||
|
opacity 0.16s,
|
||||||
|
border-color 0.14s,
|
||||||
|
transform 0.12s,
|
||||||
|
box-shadow 0.14s;
|
||||||
}
|
}
|
||||||
.node::before {
|
.node::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -1001,7 +1085,10 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
transition:
|
||||||
|
color 0.12s,
|
||||||
|
border-color 0.12s,
|
||||||
|
background 0.12s;
|
||||||
}
|
}
|
||||||
.chip:hover {
|
.chip:hover {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
|
import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
|
||||||
|
|
||||||
// Prop-driven (not store-imported) — see SessionGraph.svelte for why.
|
// Prop-driven (not store-imported) — see SessionGraph.svelte for why.
|
||||||
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } = $props()
|
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } =
|
||||||
|
$props()
|
||||||
|
|
||||||
let freeText = $state('')
|
let freeText = $state('')
|
||||||
let submitting = $state(false)
|
let submitting = $state(false)
|
||||||
@@ -48,7 +49,13 @@
|
|||||||
{#if q.context.options?.length}
|
{#if q.context.options?.length}
|
||||||
<div class="ml-6 flex flex-wrap gap-1.5">
|
<div class="ml-6 flex flex-wrap gap-1.5">
|
||||||
{#each q.context.options as opt}
|
{#each q.context.options as opt}
|
||||||
<Button size="sm" variant="outline" class="h-7 px-2.5 text-xs" disabled={submitting} onclick={() => submit(opt)}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="h-7 px-2.5 text-xs"
|
||||||
|
disabled={submitting}
|
||||||
|
onclick={() => submit(opt)}
|
||||||
|
>
|
||||||
{opt}
|
{opt}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -69,7 +76,12 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" class="h-7 px-2.5 text-xs" disabled={!freeText.trim() || submitting} onclick={() => submit(freeText)}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
class="h-7 px-2.5 text-xs"
|
||||||
|
disabled={!freeText.trim() || submitting}
|
||||||
|
onclick={() => submit(freeText)}
|
||||||
|
>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,15 @@
|
|||||||
// these can be open (and independently live) at once.
|
// these can be open (and independently live) at once.
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||||
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
|
import {
|
||||||
|
chatFor,
|
||||||
|
loadSessionChat,
|
||||||
|
sendSessionMessage,
|
||||||
|
cancelSessionStream,
|
||||||
|
stopSessionPolling,
|
||||||
|
dismissError,
|
||||||
|
chatErrors
|
||||||
|
} from '$lib/stores/chat'
|
||||||
import { activityLogFor } from '$lib/stores/activity'
|
import { activityLogFor } from '$lib/stores/activity'
|
||||||
import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace'
|
import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace'
|
||||||
import ChatThread from '$lib/components/ChatThread.svelte'
|
import ChatThread from '$lib/components/ChatThread.svelte'
|
||||||
@@ -77,7 +85,9 @@
|
|||||||
|
|
||||||
<div class="flex h-full min-h-0">
|
<div class="flex h-full min-h-0">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">Loading…</div>
|
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
|
||||||
|
Loading…
|
||||||
|
</div>
|
||||||
{:else if $chatNotFound}
|
{:else if $chatNotFound}
|
||||||
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center">
|
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center">
|
||||||
<p class="text-sm text-muted-foreground">Task not found.</p>
|
<p class="text-sm text-muted-foreground">Task not found.</p>
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
// Prop-driven (not store-imported) so this can render either the main
|
// Prop-driven (not store-imported) so this can render either the main
|
||||||
// page's global "current session" data or a floating task window's own
|
// page's global "current session" data or a floating task window's own
|
||||||
// per-session data — see TaskContextPanel.svelte, which supplies both.
|
// per-session data — see TaskContextPanel.svelte, which supplies both.
|
||||||
let { messages, touched, healthDiffs }: { messages: ChatMessage[]; touched: TouchedEntity[]; healthDiffs: HealthDiff[] } = $props()
|
let {
|
||||||
|
messages,
|
||||||
|
touched,
|
||||||
|
healthDiffs
|
||||||
|
}: { messages: ChatMessage[]; touched: TouchedEntity[]; healthDiffs: HealthDiff[] } = $props()
|
||||||
|
|
||||||
// SVG ids are document-global, not scoped to this <svg> — several task
|
// SVG ids are document-global, not scoped to this <svg> — several task
|
||||||
// windows can each have their own Scope graph open at once, and without a
|
// windows can each have their own Scope graph open at once, and without a
|
||||||
@@ -138,7 +142,12 @@
|
|||||||
const curSlugs = new Set(current.map((n) => n.slug))
|
const curSlugs = new Set(current.map((n) => n.slug))
|
||||||
|
|
||||||
let changed = desiredSlugs.size !== curSlugs.size
|
let changed = desiredSlugs.size !== curSlugs.size
|
||||||
if (!changed) for (const s of desiredSlugs) if (!curSlugs.has(s)) { changed = true; break }
|
if (!changed)
|
||||||
|
for (const s of desiredSlugs)
|
||||||
|
if (!curSlugs.has(s)) {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
if (!changed) return
|
if (!changed) return
|
||||||
|
|
||||||
const bySlug = new Map(current.map((n) => [n.slug, n]))
|
const bySlug = new Map(current.map((n) => [n.slug, n]))
|
||||||
@@ -178,10 +187,19 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
sim = forceSimulation(nodes)
|
sim = forceSimulation(nodes)
|
||||||
.force('link', forceLink<Node, Edge>(links).id((n) => n.slug).distance(48).strength(0.5))
|
.force(
|
||||||
|
'link',
|
||||||
|
forceLink<Node, Edge>(links)
|
||||||
|
.id((n) => n.slug)
|
||||||
|
.distance(48)
|
||||||
|
.strength(0.5)
|
||||||
|
)
|
||||||
.force('charge', forceManyBody().strength(-150).distanceMax(240))
|
.force('charge', forceManyBody().strength(-150).distanceMax(240))
|
||||||
.force('center', forceCenter(cw / 2, ch / 2))
|
.force('center', forceCenter(cw / 2, ch / 2))
|
||||||
.force('collide', forceCollide<Node>((n) => nodeRadius(n) + 6))
|
.force(
|
||||||
|
'collide',
|
||||||
|
forceCollide<Node>((n) => nodeRadius(n) + 6)
|
||||||
|
)
|
||||||
.force('x', forceX(cw / 2).strength(0.06))
|
.force('x', forceX(cw / 2).strength(0.06))
|
||||||
.force('y', forceY(ch / 2).strength(0.06))
|
.force('y', forceY(ch / 2).strength(0.06))
|
||||||
.velocityDecay(0.34)
|
.velocityDecay(0.34)
|
||||||
@@ -232,7 +250,9 @@
|
|||||||
unknown: 'var(--muted-foreground)'
|
unknown: 'var(--muted-foreground)'
|
||||||
}
|
}
|
||||||
function nodeColor(n: Node): string {
|
function nodeColor(n: Node): string {
|
||||||
return n.health ? healthColor[n.health] ?? 'var(--muted-foreground)' : 'var(--muted-foreground)'
|
return n.health
|
||||||
|
? (healthColor[n.health] ?? 'var(--muted-foreground)')
|
||||||
|
: 'var(--muted-foreground)'
|
||||||
}
|
}
|
||||||
function nodeRadius(n: Node): number {
|
function nodeRadius(n: Node): number {
|
||||||
return 6 + Math.min(Math.sqrt(n.degree) * 1.5, 6)
|
return 6 + Math.min(Math.sqrt(n.degree) * 1.5, 6)
|
||||||
@@ -306,10 +326,17 @@
|
|||||||
const selectedRelations = $derived(
|
const selectedRelations = $derived(
|
||||||
selected
|
selected
|
||||||
? links
|
? links
|
||||||
.filter((l) => endpointSlug(l.source) === selected!.slug || endpointSlug(l.target) === selected!.slug)
|
.filter(
|
||||||
|
(l) =>
|
||||||
|
endpointSlug(l.source) === selected!.slug || endpointSlug(l.target) === selected!.slug
|
||||||
|
)
|
||||||
.map((l) => {
|
.map((l) => {
|
||||||
const outgoing = endpointSlug(l.source) === selected!.slug
|
const outgoing = endpointSlug(l.source) === selected!.slug
|
||||||
return { dir: outgoing ? '→' : '←', type: l.type, other: outgoing ? endpointSlug(l.target) : endpointSlug(l.source) }
|
return {
|
||||||
|
dir: outgoing ? '→' : '←',
|
||||||
|
type: l.type,
|
||||||
|
other: outgoing ? endpointSlug(l.target) : endpointSlug(l.source)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
@@ -317,7 +344,9 @@
|
|||||||
|
|
||||||
<aside class="flex h-full min-h-0 flex-col bg-card/40">
|
<aside class="flex h-full min-h-0 flex-col bg-card/40">
|
||||||
{#if nowTouching}
|
{#if nowTouching}
|
||||||
<div class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary">
|
<div
|
||||||
|
class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary"
|
||||||
|
>
|
||||||
<span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
|
<span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
|
||||||
Now touching <code class="font-mono">{nowTouching.slug}</code>
|
Now touching <code class="font-mono">{nowTouching.slug}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,22 +354,85 @@
|
|||||||
|
|
||||||
<div bind:this={container} class="relative min-h-0 flex-1 overflow-hidden">
|
<div bind:this={container} class="relative min-h-0 flex-1 overflow-hidden">
|
||||||
{#if nodes.length === 0}
|
{#if nodes.length === 0}
|
||||||
<div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-4 px-6 text-center">
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-4 px-6 text-center"
|
||||||
|
>
|
||||||
<svg viewBox="0 0 120 120" class="size-24 text-muted-foreground/40" fill="none">
|
<svg viewBox="0 0 120 120" class="size-24 text-muted-foreground/40" fill="none">
|
||||||
<circle cx="60" cy="60" r="6" fill="currentColor">
|
<circle cx="60" cy="60" r="6" fill="currentColor">
|
||||||
<animate attributeName="opacity" values="0.4;1;0.4" dur="2.4s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.4;1;0.4"
|
||||||
|
dur="2.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
<g stroke="currentColor" stroke-width="1" opacity="0.5">
|
<g stroke="currentColor" stroke-width="1" opacity="0.5">
|
||||||
<line x1="60" y1="60" x2="26" y2="34"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3s" repeatCount="indefinite" /></line>
|
<line x1="60" y1="60" x2="26" y2="34"
|
||||||
<line x1="60" y1="60" x2="96" y2="40"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3.4s" repeatCount="indefinite" /></line>
|
><animate
|
||||||
<line x1="60" y1="60" x2="34" y2="92"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="2.8s" repeatCount="indefinite" /></line>
|
attributeName="opacity"
|
||||||
<line x1="60" y1="60" x2="92" y2="90"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3.1s" repeatCount="indefinite" /></line>
|
values="0.1;0.5;0.1"
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></line
|
||||||
|
>
|
||||||
|
<line x1="60" y1="60" x2="96" y2="40"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.1;0.5;0.1"
|
||||||
|
dur="3.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></line
|
||||||
|
>
|
||||||
|
<line x1="60" y1="60" x2="34" y2="92"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.1;0.5;0.1"
|
||||||
|
dur="2.8s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></line
|
||||||
|
>
|
||||||
|
<line x1="60" y1="60" x2="92" y2="90"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.1;0.5;0.1"
|
||||||
|
dur="3.1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></line
|
||||||
|
>
|
||||||
</g>
|
</g>
|
||||||
<g fill="currentColor">
|
<g fill="currentColor">
|
||||||
<circle cx="26" cy="34" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3s" repeatCount="indefinite" /></circle>
|
<circle cx="26" cy="34" r="3.5"
|
||||||
<circle cx="96" cy="40" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.4s" repeatCount="indefinite" /></circle>
|
><animate
|
||||||
<circle cx="34" cy="92" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="2.8s" repeatCount="indefinite" /></circle>
|
attributeName="opacity"
|
||||||
<circle cx="92" cy="90" r="3.5"><animate attributeName="opacity" values="0.2;0.7;0.2" dur="3.1s" repeatCount="indefinite" /></circle>
|
values="0.2;0.7;0.2"
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></circle
|
||||||
|
>
|
||||||
|
<circle cx="96" cy="40" r="3.5"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.2;0.7;0.2"
|
||||||
|
dur="3.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></circle
|
||||||
|
>
|
||||||
|
<circle cx="34" cy="92" r="3.5"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.2;0.7;0.2"
|
||||||
|
dur="2.8s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></circle
|
||||||
|
>
|
||||||
|
<circle cx="92" cy="90" r="3.5"
|
||||||
|
><animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.2;0.7;0.2"
|
||||||
|
dur="3.1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/></circle
|
||||||
|
>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="max-w-[16rem] text-xs leading-relaxed text-muted-foreground">
|
<p class="max-w-[16rem] text-xs leading-relaxed text-muted-foreground">
|
||||||
@@ -394,7 +486,8 @@
|
|||||||
{#if node.x != null && node.y != null}
|
{#if node.x != null && node.y != null}
|
||||||
{@const r = nodeRadius(node)}
|
{@const r = nodeRadius(node)}
|
||||||
{@const isSel = selected?.slug === node.slug}
|
{@const isSel = selected?.slug === node.slug}
|
||||||
{@const dim = selected && !isSel && !selectedRelations.some((rel) => rel.other === node.slug)}
|
{@const dim =
|
||||||
|
selected && !isSel && !selectedRelations.some((rel) => rel.other === node.slug)}
|
||||||
{@const isTouched = node.slug in touchedBySlug}
|
{@const isTouched = node.slug in touchedBySlug}
|
||||||
{@const diff = diffBySlug[node.slug]}
|
{@const diff = diffBySlug[node.slug]}
|
||||||
<g
|
<g
|
||||||
@@ -410,12 +503,33 @@
|
|||||||
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
|
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isTouched}
|
{#if isTouched}
|
||||||
<circle r={r + 4} fill="none" stroke="var(--primary)" stroke-width="1.5" opacity="0.8">
|
<circle
|
||||||
<animate attributeName="r" values="{r + 3};{r + 8};{r + 3}" dur="1.6s" repeatCount="indefinite" />
|
r={r + 4}
|
||||||
<animate attributeName="opacity" values="0.8;0.1;0.8" dur="1.6s" repeatCount="indefinite" />
|
fill="none"
|
||||||
|
stroke="var(--primary)"
|
||||||
|
stroke-width="1.5"
|
||||||
|
opacity="0.8"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
values="{r + 3};{r + 8};{r + 3}"
|
||||||
|
dur="1.6s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.8;0.1;0.8"
|
||||||
|
dur="1.6s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
{/if}
|
{/if}
|
||||||
<circle r={r} fill={nodeColor(node)} stroke={isSel ? 'var(--foreground)' : 'var(--background)'} stroke-width={isSel ? 2 : 1.5} />
|
<circle
|
||||||
|
{r}
|
||||||
|
fill={nodeColor(node)}
|
||||||
|
stroke={isSel ? 'var(--foreground)' : 'var(--background)'}
|
||||||
|
stroke-width={isSel ? 2 : 1.5}
|
||||||
|
/>
|
||||||
<text
|
<text
|
||||||
y={r + 10}
|
y={r + 10}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
<svg viewBox="0 0 24 24" class={className} fill="none" aria-hidden="true">
|
<svg viewBox="0 0 24 24" class={className} fill="none" aria-hidden="true">
|
||||||
{#each Array.from({ length: TICKS }) as _, i (i)}
|
{#each Array.from({ length: TICKS }) as _, i (i)}
|
||||||
<rect
|
<rect
|
||||||
x="11" y="1.5" width="2" height="6" rx="1"
|
x="11"
|
||||||
|
y="1.5"
|
||||||
|
width="2"
|
||||||
|
height="6"
|
||||||
|
rx="1"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
opacity="0.15"
|
opacity="0.15"
|
||||||
transform="rotate({i * (360 / TICKS)} 12 12)"
|
transform="rotate({i * (360 / TICKS)} 12 12)"
|
||||||
|
|||||||
@@ -13,15 +13,18 @@
|
|||||||
class?: string
|
class?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
const variantMap: Record<StatusKind, Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>> = {
|
const variantMap: Record<
|
||||||
|
StatusKind,
|
||||||
|
Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>
|
||||||
|
> = {
|
||||||
risk: {
|
risk: {
|
||||||
destructive: 'destructive',
|
destructive: 'destructive',
|
||||||
config_mutation: 'secondary',
|
config_mutation: 'secondary'
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
critical: 'destructive',
|
critical: 'destructive',
|
||||||
warning: 'secondary',
|
warning: 'secondary',
|
||||||
info: 'default',
|
info: 'default'
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
failed: 'destructive',
|
failed: 'destructive',
|
||||||
@@ -30,13 +33,13 @@
|
|||||||
cancelled: 'destructive',
|
cancelled: 'destructive',
|
||||||
completed: 'default',
|
completed: 'default',
|
||||||
running: 'secondary',
|
running: 'secondary',
|
||||||
approved: 'secondary',
|
approved: 'secondary'
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
runbook: 'secondary',
|
runbook: 'secondary',
|
||||||
investigation: 'default',
|
investigation: 'default'
|
||||||
},
|
},
|
||||||
default: {},
|
default: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function variant(): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function variant(): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||||
import { startWorkspace, planSteps, currentTask, touched, healthDiffs, workspaceFor, taskFor } from '$lib/stores/workspace'
|
import {
|
||||||
|
startWorkspace,
|
||||||
|
planSteps,
|
||||||
|
currentTask,
|
||||||
|
touched,
|
||||||
|
healthDiffs,
|
||||||
|
workspaceFor,
|
||||||
|
taskFor
|
||||||
|
} from '$lib/stores/workspace'
|
||||||
import { streaming, messages, chatFor } from '$lib/stores/chat'
|
import { streaming, messages, chatFor } from '$lib/stores/chat'
|
||||||
import { activityLog, activityLogFor } from '$lib/stores/activity'
|
import { activityLog, activityLogFor } from '$lib/stores/activity'
|
||||||
import SessionGraph from './SessionGraph.svelte'
|
import SessionGraph from './SessionGraph.svelte'
|
||||||
@@ -70,7 +78,12 @@
|
|||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
|
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1">
|
||||||
<!-- Scope -->
|
<!-- Scope -->
|
||||||
<Pane bind:size={sizes[0]} minSize={scopeOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={scopeOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
|
<Pane
|
||||||
|
bind:size={sizes[0]}
|
||||||
|
minSize={scopeOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE}
|
||||||
|
maxSize={scopeOpen ? 100 : COLLAPSED_SIZE}
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
@@ -79,21 +92,36 @@
|
|||||||
scopeOpen = !scopeOpen
|
scopeOpen = !scopeOpen
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
|
||||||
|
class="size-3"
|
||||||
|
/>{/if}
|
||||||
<span>Scope</span>
|
<span>Scope</span>
|
||||||
{#if !scopeOpen}
|
{#if !scopeOpen}
|
||||||
<span class="ml-auto font-normal normal-case">{$touchedStore.length ? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}` : 'Graph'}</span>
|
<span class="ml-auto font-normal normal-case"
|
||||||
|
>{$touchedStore.length
|
||||||
|
? `${$touchedStore.length} entit${$touchedStore.length === 1 ? 'y' : 'ies'}`
|
||||||
|
: 'Graph'}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if scopeOpen}
|
{#if scopeOpen}
|
||||||
<div class="min-h-0 flex-1">
|
<div class="min-h-0 flex-1">
|
||||||
<SessionGraph messages={$messagesStore} touched={$touchedStore} healthDiffs={$healthDiffsStore} />
|
<SessionGraph
|
||||||
|
messages={$messagesStore}
|
||||||
|
touched={$touchedStore}
|
||||||
|
healthDiffs={$healthDiffsStore}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
||||||
<!-- Activity (merged plan + event log) -->
|
<!-- Activity (merged plan + event log) -->
|
||||||
<Pane bind:size={sizes[1]} minSize={activityOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE} maxSize={activityOpen ? 100 : COLLAPSED_SIZE} class="flex flex-col">
|
<Pane
|
||||||
|
bind:size={sizes[1]}
|
||||||
|
minSize={activityOpen ? OPEN_MIN_SIZE : COLLAPSED_SIZE}
|
||||||
|
maxSize={activityOpen ? 100 : COLLAPSED_SIZE}
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
@@ -102,25 +130,39 @@
|
|||||||
activityOpen = !activityOpen
|
activityOpen = !activityOpen
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
|
||||||
|
class="size-3"
|
||||||
|
/>{/if}
|
||||||
<span>Activity</span>
|
<span>Activity</span>
|
||||||
{#if $streamingStore && activityRunning > 0}
|
{#if $streamingStore && activityRunning > 0}
|
||||||
<Spinner class="size-3 text-primary" />
|
<Spinner class="size-3 text-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if planTotal > 0}
|
{#if planTotal > 0}
|
||||||
<span class="font-normal normal-case tabular-nums {planDone === planTotal ? 'text-muted-foreground' : 'text-primary'}">{planDone}/{planTotal}</span>
|
<span
|
||||||
|
class="font-normal normal-case tabular-nums {planDone === planTotal
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: 'text-primary'}">{planDone}/{planTotal}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !activityOpen && planTotal === 0}
|
{#if !activityOpen && planTotal === 0}
|
||||||
{#if $taskStore?.goal}
|
{#if $taskStore?.goal}
|
||||||
<span class="ml-auto max-w-[120px] truncate font-normal normal-case">{$taskStore.goal}</span>
|
<span class="ml-auto max-w-[120px] truncate font-normal normal-case"
|
||||||
|
>{$taskStore.goal}</span
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-auto font-normal normal-case text-muted-foreground">No activity yet</span>
|
<span class="ml-auto font-normal normal-case text-muted-foreground"
|
||||||
|
>No activity yet</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if activityOpen}
|
{#if activityOpen}
|
||||||
<div class="min-h-0 flex-1 overflow-hidden">
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
<UnifiedTimeline entries={$activityLogStore} planSteps={$planStepsStore} streaming={$streamingStore} />
|
<UnifiedTimeline
|
||||||
|
entries={$activityLogStore}
|
||||||
|
planSteps={$planStepsStore}
|
||||||
|
streaming={$streamingStore}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|||||||
@@ -51,13 +51,17 @@
|
|||||||
<span class="min-w-0 flex-1">
|
<span class="min-w-0 flex-1">
|
||||||
<span class="block truncate text-xs text-foreground/90">{label}</span>
|
<span class="block truncate text-xs text-foreground/90">{label}</span>
|
||||||
{#if argsSummary}
|
{#if argsSummary}
|
||||||
<span class="block truncate font-mono text-[10px] text-muted-foreground/60">{argsSummary}</span>
|
<span class="block truncate font-mono text-[10px] text-muted-foreground/60"
|
||||||
|
>{argsSummary}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span>
|
<span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span>
|
||||||
{#if hasDetail}
|
{#if hasDetail}
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
class="mt-px size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded ? 'rotate-90' : ''}"
|
class="mt-px size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded
|
||||||
|
? 'rotate-90'
|
||||||
|
: ''}"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -66,20 +70,41 @@
|
|||||||
<div class="space-y-2 px-2 pb-2 pl-7">
|
<div class="space-y-2 px-2 pb-2 pl-7">
|
||||||
{#if tool.args}
|
{#if tool.args}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Args</div>
|
<div
|
||||||
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.args, null, 2)}</pre>
|
class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"
|
||||||
|
>
|
||||||
|
Args
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(
|
||||||
|
tool.args,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tool.result !== undefined && tool.result !== null}
|
{#if tool.result !== undefined && tool.result !== null}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Result</div>
|
<div
|
||||||
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.result, null, 2)}</pre>
|
class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"
|
||||||
|
>
|
||||||
|
Result
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(
|
||||||
|
tool.result,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tool.error}
|
{#if tool.error}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">Error</div>
|
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">
|
||||||
<pre class="max-h-48 overflow-x-auto rounded-md border border-destructive/20 bg-destructive/5 p-2 text-[11px] text-destructive">{tool.error}</pre>
|
Error
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="max-h-48 overflow-x-auto rounded-md border border-destructive/20 bg-destructive/5 p-2 text-[11px] text-destructive">{tool.error}</pre>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,11 @@
|
|||||||
// current step visible while the agent works (follow mode disengages if
|
// current step visible while the agent works (follow mode disengages if
|
||||||
// the operator scrolls down into history, re-engages when streaming
|
// the operator scrolls down into history, re-engages when streaming
|
||||||
// starts again)
|
// starts again)
|
||||||
let { entries, planSteps: steps, streaming = false }: {
|
let {
|
||||||
|
entries,
|
||||||
|
planSteps: steps,
|
||||||
|
streaming = false
|
||||||
|
}: {
|
||||||
entries: ActivityEntry[]
|
entries: ActivityEntry[]
|
||||||
planSteps: PlanStep[]
|
planSteps: PlanStep[]
|
||||||
streaming?: boolean
|
streaming?: boolean
|
||||||
@@ -75,7 +79,9 @@
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const tools = entries.filter(
|
const tools = entries.filter(
|
||||||
(e) => e.stepSeq === s.seq && (e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
|
(e) =>
|
||||||
|
e.stepSeq === s.seq &&
|
||||||
|
(e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
|
||||||
)
|
)
|
||||||
const stepEntry = entries.find((e) => e.id === s.id)
|
const stepEntry = entries.find((e) => e.id === s.id)
|
||||||
// Timed from the step's own entry, else its earliest tool — so a step
|
// Timed from the step's own entry, else its earliest tool — so a step
|
||||||
@@ -134,7 +140,8 @@
|
|||||||
// entries land while following (instant, to avoid scroll-queue jank).
|
// entries land while following (instant, to avoid scroll-queue jank).
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!currentId || !follow || !container) return
|
if (!currentId || !follow || !container) return
|
||||||
container.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
|
container
|
||||||
|
.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
|
||||||
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
})
|
})
|
||||||
let lastEntryCount = 0
|
let lastEntryCount = 0
|
||||||
@@ -157,7 +164,12 @@
|
|||||||
// height so tools inside an expanded step stay on the line; first/last
|
// height so tools inside an expanded step stay on the line; first/last
|
||||||
// items clip theirs to their node/tool centers so the line never dangles
|
// items clip theirs to their node/tool centers so the line never dangles
|
||||||
// past the timeline's ends.
|
// past the timeline's ends.
|
||||||
function segClass(status: string, isFirst: boolean, isLast: boolean, expandedWithTools: boolean): string {
|
function segClass(
|
||||||
|
status: string,
|
||||||
|
isFirst: boolean,
|
||||||
|
isLast: boolean,
|
||||||
|
expandedWithTools: boolean
|
||||||
|
): string {
|
||||||
let color = 'bg-border'
|
let color = 'bg-border'
|
||||||
if (status === 'done') color = 'bg-primary/60'
|
if (status === 'done') color = 'bg-primary/60'
|
||||||
else if (status === 'running') color = 'bg-primary/40'
|
else if (status === 'running') color = 'bg-primary/40'
|
||||||
@@ -172,11 +184,16 @@
|
|||||||
|
|
||||||
function entryIcon(entry: ActivityEntry) {
|
function entryIcon(entry: ActivityEntry) {
|
||||||
switch (entry.type) {
|
switch (entry.type) {
|
||||||
case 'goal': return MilestoneIcon
|
case 'goal':
|
||||||
case 'knowledge': return SparklesIcon
|
return MilestoneIcon
|
||||||
case 'complete': return FlagIcon
|
case 'knowledge':
|
||||||
case 'question': return HelpCircleIcon
|
return SparklesIcon
|
||||||
default: return WrenchIcon
|
case 'complete':
|
||||||
|
return FlagIcon
|
||||||
|
case 'question':
|
||||||
|
return HelpCircleIcon
|
||||||
|
default:
|
||||||
|
return WrenchIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hhmm(ts: number): string {
|
function hhmm(ts: number): string {
|
||||||
@@ -185,10 +202,18 @@
|
|||||||
}
|
}
|
||||||
function hhmmss(ts: number): string {
|
function hhmmss(ts: number): string {
|
||||||
if (!ts) return ''
|
if (!ts) return ''
|
||||||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
return new Date(ts).toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
function prettyPrint(raw: string): string {
|
function prettyPrint(raw: string): string {
|
||||||
try { return JSON.stringify(JSON.parse(raw), null, 2) } catch { return raw }
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(raw), null, 2)
|
||||||
|
} catch {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -197,19 +222,57 @@
|
|||||||
{#if items.length === 0}
|
{#if items.length === 0}
|
||||||
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
|
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
|
||||||
<svg viewBox="0 0 64 110" class="h-14 w-auto text-muted-foreground/40" fill="none">
|
<svg viewBox="0 0 64 110" class="h-14 w-auto text-muted-foreground/40" fill="none">
|
||||||
<line x1="32" y1="8" x2="32" y2="102" stroke="currentColor" stroke-width="1" stroke-dasharray="2.5 4" opacity="0.35" />
|
<line
|
||||||
|
x1="32"
|
||||||
|
y1="8"
|
||||||
|
x2="32"
|
||||||
|
y2="102"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke-dasharray="2.5 4"
|
||||||
|
opacity="0.35"
|
||||||
|
/>
|
||||||
<circle cx="32" cy="22" r="4" fill="currentColor">
|
<circle cx="32" cy="22" r="4" fill="currentColor">
|
||||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.25;0.9;0.25"
|
||||||
|
dur="2.4s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="32" cy="55" r="4" fill="currentColor">
|
<circle cx="32" cy="55" r="4" fill="currentColor">
|
||||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.25;0.9;0.25"
|
||||||
|
dur="2.4s"
|
||||||
|
begin="0.6s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5">
|
<circle cx="32" cy="55" r="4" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<animate attributeName="r" values="4;11;4" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
<animate
|
||||||
<animate attributeName="opacity" values="0.6;0;0.6" dur="2.4s" begin="0.6s" repeatCount="indefinite" />
|
attributeName="r"
|
||||||
|
values="4;11;4"
|
||||||
|
dur="2.4s"
|
||||||
|
begin="0.6s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.6;0;0.6"
|
||||||
|
dur="2.4s"
|
||||||
|
begin="0.6s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="32" cy="88" r="4" fill="currentColor">
|
<circle cx="32" cy="88" r="4" fill="currentColor">
|
||||||
<animate attributeName="opacity" values="0.25;0.9;0.25" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.25;0.9;0.25"
|
||||||
|
dur="2.4s"
|
||||||
|
begin="1.2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
</circle>
|
</circle>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p>
|
<p class="text-[11px] leading-relaxed text-muted-foreground">Waiting for activity…</p>
|
||||||
@@ -227,24 +290,42 @@
|
|||||||
{@const expandedWithTools = open && item.tools.length > 0}
|
{@const expandedWithTools = open && item.tools.length > 0}
|
||||||
<!-- Step node on the backbone -->
|
<!-- Step node on the backbone -->
|
||||||
<div class="relative" data-tl-id={item.step.id}>
|
<div class="relative" data-tl-id={item.step.id}>
|
||||||
<span class="pointer-events-none absolute left-[17px] w-px {segClass(st, isFirst, isLast, expandedWithTools)}" aria-hidden="true"></span>
|
<span
|
||||||
|
class="pointer-events-none absolute left-[17px] w-px {segClass(
|
||||||
|
st,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
expandedWithTools
|
||||||
|
)}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs {expandable ? 'cursor-pointer hover:bg-muted/30' : 'cursor-default'} {st === 'running' ? 'bg-primary/5' : ''}"
|
class="relative flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs {expandable
|
||||||
|
? 'cursor-pointer hover:bg-muted/30'
|
||||||
|
: 'cursor-default'} {st === 'running' ? 'bg-primary/5' : ''}"
|
||||||
onclick={() => expandable && toggleStep(item.step)}
|
onclick={() => expandable && toggleStep(item.step)}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
disabled={!expandable}
|
disabled={!expandable}
|
||||||
>
|
>
|
||||||
<!-- Filled status node -->
|
<!-- Filled status node -->
|
||||||
<span class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
|
<span
|
||||||
{st === 'done' ? 'bg-primary'
|
class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full
|
||||||
: st === 'running' ? 'bg-background'
|
{st === 'done'
|
||||||
: st === 'failed' ? 'bg-destructive'
|
? 'bg-primary'
|
||||||
: st === 'blocked' ? 'bg-warning/25 border border-warning'
|
: st === 'running'
|
||||||
: st === 'skipped' || st === 'replaced' ? 'bg-muted'
|
? 'bg-background'
|
||||||
: 'bg-background border border-muted-foreground/40'}">
|
: st === 'failed'
|
||||||
|
? 'bg-destructive'
|
||||||
|
: st === 'blocked'
|
||||||
|
? 'bg-warning/25 border border-warning'
|
||||||
|
: st === 'skipped' || st === 'replaced'
|
||||||
|
? 'bg-muted'
|
||||||
|
: 'bg-background border border-muted-foreground/40'}"
|
||||||
|
>
|
||||||
{#if st === 'running'}
|
{#if st === 'running'}
|
||||||
<span class="absolute -inset-0.5 animate-ping rounded-full bg-primary/30"></span>
|
<span class="absolute -inset-0.5 animate-ping rounded-full bg-primary/30"
|
||||||
|
></span>
|
||||||
<Spinner class="relative size-3.5 text-primary" />
|
<Spinner class="relative size-3.5 text-primary" />
|
||||||
{:else if st === 'done'}
|
{:else if st === 'done'}
|
||||||
<CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} />
|
<CheckIcon class="size-2.5 text-primary-foreground" strokeWidth={3.5} />
|
||||||
@@ -256,15 +337,28 @@
|
|||||||
<SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} />
|
<SlashIcon class="size-2 text-muted-foreground" strokeWidth={3} />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span title={item.step.title} class="min-w-0 flex-1 leading-snug {open ? 'whitespace-normal' : 'truncate'} {st === 'done' ? 'text-muted-foreground' : st === 'running' ? 'font-medium text-foreground' : 'text-muted-foreground'}">
|
<span
|
||||||
|
title={item.step.title}
|
||||||
|
class="min-w-0 flex-1 leading-snug {open
|
||||||
|
? 'whitespace-normal'
|
||||||
|
: 'truncate'} {st === 'done'
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: st === 'running'
|
||||||
|
? 'font-medium text-foreground'
|
||||||
|
: 'text-muted-foreground'}"
|
||||||
|
>
|
||||||
{item.step.title}
|
{item.step.title}
|
||||||
</span>
|
</span>
|
||||||
{#if hhmm(item.ts)}
|
{#if hhmm(item.ts)}
|
||||||
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60">{hhmm(item.ts)}</span>
|
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
|
||||||
|
>{hhmm(item.ts)}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.tools.length > 0}
|
{#if item.tools.length > 0}
|
||||||
<span class="shrink-0 text-muted-foreground/60">
|
<span class="shrink-0 text-muted-foreground/60">
|
||||||
{#if open}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
{#if open}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon
|
||||||
|
class="size-3"
|
||||||
|
/>{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -275,10 +369,19 @@
|
|||||||
{@const tOpen = expandedTools.has(tool.id)}
|
{@const tOpen = expandedTools.has(tool.id)}
|
||||||
<div class="relative" data-tl-id={tool.id}>
|
<div class="relative" data-tl-id={tool.id}>
|
||||||
<!-- Branch stub: backbone → tool -->
|
<!-- Branch stub: backbone → tool -->
|
||||||
<span class="pointer-events-none absolute left-[17px] top-[9.5px] h-px w-[17px] {tool.status === 'failed' ? 'bg-destructive/40' : 'bg-border'}" aria-hidden="true"></span>
|
<span
|
||||||
|
class="pointer-events-none absolute left-[17px] top-[9.5px] h-px w-[17px] {tool.status ===
|
||||||
|
'failed'
|
||||||
|
? 'bg-destructive/40'
|
||||||
|
: 'bg-border'}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center gap-1.5 py-1 pl-9 pr-3 text-left text-[11px] {(tool.args || tool.detail) ? 'cursor-pointer hover:bg-muted/20' : 'cursor-default'}"
|
class="flex w-full items-center gap-1.5 py-1 pl-9 pr-3 text-left text-[11px] {tool.args ||
|
||||||
|
tool.detail
|
||||||
|
? 'cursor-pointer hover:bg-muted/20'
|
||||||
|
: 'cursor-default'}"
|
||||||
onclick={() => (tool.args || tool.detail) && toggleTool(tool.id)}
|
onclick={() => (tool.args || tool.detail) && toggleTool(tool.id)}
|
||||||
>
|
>
|
||||||
<span class="flex size-3 shrink-0 items-center justify-center">
|
<span class="flex size-3 shrink-0 items-center justify-center">
|
||||||
@@ -290,24 +393,47 @@
|
|||||||
<CheckIcon class="size-2.5 text-primary/70" strokeWidth={3.5} />
|
<CheckIcon class="size-2.5 text-primary/70" strokeWidth={3.5} />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span title={tool.description} class="min-w-0 flex-1 truncate leading-snug {tool.status === 'done' ? 'text-muted-foreground' : tool.status === 'failed' ? 'text-destructive' : 'text-foreground/80'}">
|
<span
|
||||||
|
title={tool.description}
|
||||||
|
class="min-w-0 flex-1 truncate leading-snug {tool.status === 'done'
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: tool.status === 'failed'
|
||||||
|
? 'text-destructive'
|
||||||
|
: 'text-foreground/80'}"
|
||||||
|
>
|
||||||
{tool.description}
|
{tool.description}
|
||||||
</span>
|
</span>
|
||||||
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/50">{hhmm(tool.timestamp)}</span>
|
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/50"
|
||||||
|
>{hhmm(tool.timestamp)}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
{#if tOpen}
|
{#if tOpen}
|
||||||
<div transition:slide={{ duration: 120 }} class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3">
|
<div
|
||||||
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
|
transition:slide={{ duration: 120 }}
|
||||||
|
class="flex flex-col gap-1 pb-1.5 pl-[52px] pr-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70"
|
||||||
|
>
|
||||||
<span class="capitalize">{tool.status}</span>
|
<span class="capitalize">{tool.status}</span>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<span>{hhmmss(tool.timestamp)}</span>
|
<span>{hhmmss(tool.timestamp)}</span>
|
||||||
{#if tool.toolName}<span aria-hidden="true">·</span><code class="font-mono">{tool.toolName}</code>{/if}
|
{#if tool.toolName}<span aria-hidden="true">·</span><code
|
||||||
|
class="font-mono">{tool.toolName}</code
|
||||||
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if tool.args}
|
{#if tool.args}
|
||||||
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(tool.args)}</pre>
|
<pre
|
||||||
|
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
|
||||||
|
tool.args
|
||||||
|
)}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tool.detail}
|
{#if tool.detail}
|
||||||
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {tool.status === 'failed' ? 'text-destructive' : 'text-muted-foreground'}">{prettyPrint(tool.detail)}</pre>
|
<pre
|
||||||
|
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {tool.status ===
|
||||||
|
'failed'
|
||||||
|
? 'text-destructive'
|
||||||
|
: 'text-muted-foreground'}">{prettyPrint(tool.detail)}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -322,14 +448,31 @@
|
|||||||
{@const Icon = entryIcon(e)}
|
{@const Icon = entryIcon(e)}
|
||||||
{@const eOpen = expandedTools.has(e.id)}
|
{@const eOpen = expandedTools.has(e.id)}
|
||||||
<div class="relative" data-tl-id={e.id}>
|
<div class="relative" data-tl-id={e.id}>
|
||||||
<span class="pointer-events-none absolute left-[17px] w-px {segClass(e.status, isFirst, isLast, false)}" aria-hidden="true"></span>
|
<span
|
||||||
|
class="pointer-events-none absolute left-[17px] w-px {segClass(
|
||||||
|
e.status,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
false
|
||||||
|
)}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] {(e.args || e.detail) ? 'cursor-pointer hover:bg-muted/30' : 'cursor-default'}"
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] {e.args ||
|
||||||
|
e.detail
|
||||||
|
? 'cursor-pointer hover:bg-muted/30'
|
||||||
|
: 'cursor-default'}"
|
||||||
onclick={() => (e.args || e.detail) && toggleTool(e.id)}
|
onclick={() => (e.args || e.detail) && toggleTool(e.id)}
|
||||||
>
|
>
|
||||||
<span class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background
|
<span
|
||||||
{e.status === 'failed' ? 'border-destructive text-destructive' : e.status === 'running' ? 'border-primary text-primary' : 'border-border text-primary'}">
|
class="relative z-10 flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background
|
||||||
|
{e.status === 'failed'
|
||||||
|
? 'border-destructive text-destructive'
|
||||||
|
: e.status === 'running'
|
||||||
|
? 'border-primary text-primary'
|
||||||
|
: 'border-border text-primary'}"
|
||||||
|
>
|
||||||
{#if e.status === 'running'}
|
{#if e.status === 'running'}
|
||||||
<Spinner class="size-2.5" />
|
<Spinner class="size-2.5" />
|
||||||
{:else if e.status === 'failed'}
|
{:else if e.status === 'failed'}
|
||||||
@@ -338,24 +481,43 @@
|
|||||||
<Icon class="size-2" strokeWidth={2.5} />
|
<Icon class="size-2" strokeWidth={2.5} />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span title={e.description} class="min-w-0 flex-1 truncate leading-snug {e.status === 'done' ? 'text-muted-foreground' : 'text-foreground/80'}">
|
<span
|
||||||
|
title={e.description}
|
||||||
|
class="min-w-0 flex-1 truncate leading-snug {e.status === 'done'
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: 'text-foreground/80'}"
|
||||||
|
>
|
||||||
{e.description}
|
{e.description}
|
||||||
</span>
|
</span>
|
||||||
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60">{hhmm(e.timestamp)}</span>
|
<span class="shrink-0 text-[9px] tabular-nums text-muted-foreground/60"
|
||||||
|
>{hhmm(e.timestamp)}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
{#if eOpen}
|
{#if eOpen}
|
||||||
<div transition:slide={{ duration: 120 }} class="flex flex-col gap-1 pb-1.5 pl-9 pr-3">
|
<div
|
||||||
|
transition:slide={{ duration: 120 }}
|
||||||
|
class="flex flex-col gap-1 pb-1.5 pl-9 pr-3"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
|
<div class="flex items-center gap-1.5 text-[9px] text-muted-foreground/70">
|
||||||
<span class="capitalize">{e.status}</span>
|
<span class="capitalize">{e.status}</span>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<span>{hhmmss(e.timestamp)}</span>
|
<span>{hhmmss(e.timestamp)}</span>
|
||||||
{#if e.toolName}<span aria-hidden="true">·</span><code class="font-mono">{e.toolName}</code>{/if}
|
{#if e.toolName}<span aria-hidden="true">·</span><code class="font-mono"
|
||||||
|
>{e.toolName}</code
|
||||||
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if e.args}
|
{#if e.args}
|
||||||
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(e.args)}</pre>
|
<pre
|
||||||
|
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed text-muted-foreground">{prettyPrint(
|
||||||
|
e.args
|
||||||
|
)}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
{#if e.detail}
|
{#if e.detail}
|
||||||
<pre class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {e.status === 'failed' ? 'text-destructive' : 'text-muted-foreground'}">{prettyPrint(e.detail)}</pre>
|
<pre
|
||||||
|
class="max-h-36 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-relaxed {e.status ===
|
||||||
|
'failed'
|
||||||
|
? 'text-destructive'
|
||||||
|
: 'text-muted-foreground'}">{prettyPrint(e.detail)}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
type Row = Record<string, any>
|
type Row = Record<string, any>
|
||||||
|
|
||||||
const renderers: Record<string, unknown> = {
|
const renderers: Record<string, unknown> = {
|
||||||
'badge': BadgeRenderer,
|
badge: BadgeRenderer,
|
||||||
'health-dot': HealthDotRenderer,
|
'health-dot': HealthDotRenderer,
|
||||||
'relative-time': RelativeTimeRenderer,
|
'relative-time': RelativeTimeRenderer,
|
||||||
'date': DateRenderer,
|
date: DateRenderer,
|
||||||
'status-badge': StatusBadgeRenderer,
|
'status-badge': StatusBadgeRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -119,7 +119,11 @@
|
|||||||
<div class={['flex flex-col h-full min-h-0', className].filter(Boolean).join(' ')}>
|
<div class={['flex flex-col h-full min-h-0', className].filter(Boolean).join(' ')}>
|
||||||
<Toolbar {table} {searchable} {paginated} onSearchChange={handleSearch} {children} />
|
<Toolbar {table} {searchable} {paginated} onSearchChange={handleSearch} {children} />
|
||||||
|
|
||||||
<div class={['flex flex-col min-h-0 flex-1', bordered ? 'rounded-xl border' : ''].filter(Boolean).join(' ')}>
|
<div
|
||||||
|
class={['flex flex-col min-h-0 flex-1', bordered ? 'rounded-xl border' : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
<table class="w-full caption-bottom text-sm table-fixed">
|
<table class="w-full caption-bottom text-sm table-fixed">
|
||||||
<thead class="[&_tr]:border-b">
|
<thead class="[&_tr]:border-b">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -130,7 +134,9 @@
|
|||||||
'bg-card/95',
|
'bg-card/95',
|
||||||
col.headerClass,
|
col.headerClass,
|
||||||
colAlignClass(col)
|
colAlignClass(col)
|
||||||
].filter(Boolean).join(' ')}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
style={colStyle(col)}
|
style={colStyle(col)}
|
||||||
>
|
>
|
||||||
{#if col.sortable !== false}
|
{#if col.sortable !== false}
|
||||||
@@ -156,8 +162,17 @@
|
|||||||
{#each skeletonWidths as w, i}
|
{#each skeletonWidths as w, i}
|
||||||
<tr class="border-b transition-colors hover:bg-transparent">
|
<tr class="border-b transition-colors hover:bg-transparent">
|
||||||
{#each visibleCols as col (col.key)}
|
{#each visibleCols as col (col.key)}
|
||||||
<td class={[col.class, colAlignClass(col), colTruncateClass(col)].filter(Boolean).join(' ')} style={colStyle(col)}>
|
<td
|
||||||
<Skeleton class="h-4 {skeletonWidths[(i + visibleCols.indexOf(col)) % skeletonWidths.length]}" />
|
class={[col.class, colAlignClass(col), colTruncateClass(col)]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={colStyle(col)}
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
class="h-4 {skeletonWidths[
|
||||||
|
(i + visibleCols.indexOf(col)) % skeletonWidths.length
|
||||||
|
]}"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -171,11 +186,18 @@
|
|||||||
'border-b transition-colors hover:bg-muted/50',
|
'border-b transition-colors hover:bg-muted/50',
|
||||||
onRowClick ? 'cursor-pointer' : '',
|
onRowClick ? 'cursor-pointer' : '',
|
||||||
selected === (row.id ?? row.slug) ? 'bg-muted' : ''
|
selected === (row.id ?? row.slug) ? 'bg-muted' : ''
|
||||||
].filter(Boolean).join(' ')}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
tabindex={onRowClick ? 0 : undefined}
|
tabindex={onRowClick ? 0 : undefined}
|
||||||
onclick={onRowClick ? () => onRowClick(row) : undefined}
|
onclick={onRowClick ? () => onRowClick(row) : undefined}
|
||||||
onkeydown={onRowClick
|
onkeydown={onRowClick
|
||||||
? (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRowClick(row) } }
|
? (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
onRowClick(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
>
|
>
|
||||||
{#each visibleCols as col (col.key)}
|
{#each visibleCols as col (col.key)}
|
||||||
@@ -186,19 +208,21 @@
|
|||||||
col.class,
|
col.class,
|
||||||
colAlignClass(col),
|
colAlignClass(col),
|
||||||
colTruncateClass(col)
|
colTruncateClass(col)
|
||||||
].filter(Boolean).join(' ')}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
style={colStyle(col)}
|
style={colStyle(col)}
|
||||||
>
|
>
|
||||||
{#if typeof col.render === 'string'}
|
{#if typeof col.render === 'string'}
|
||||||
{@const R = renderers[col.render]}
|
{@const R = renderers[col.render]}
|
||||||
{#if R}
|
{#if R}
|
||||||
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
||||||
<R value={val} {row} {...(col.renderProps ?? {})} />
|
<R value={val} {row} {...col.renderProps ?? {}} />
|
||||||
{:else}
|
{:else}
|
||||||
{String(val ?? '—')}
|
{String(val ?? '—')}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if typeof col.render === 'function'}
|
{:else if typeof col.render === 'function'}
|
||||||
<col.render {row} value={val} {...(col.renderProps ?? {})} />
|
<col.render {row} value={val} {...col.renderProps ?? {}} />
|
||||||
{:else}
|
{:else}
|
||||||
{String(val ?? '—')}
|
{String(val ?? '—')}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
import * as Select from '$lib/components/ui/select'
|
import * as Select from '$lib/components/ui/select'
|
||||||
import type { TableHandler } from '@vincjo/datatables'
|
import type { TableHandler } from '@vincjo/datatables'
|
||||||
|
|
||||||
let { table, class: className }: { table: TableHandler<Record<string, unknown>>; class?: string } = $props()
|
let {
|
||||||
|
table,
|
||||||
|
class: className
|
||||||
|
}: { table: TableHandler<Record<string, unknown>>; class?: string } = $props()
|
||||||
|
|
||||||
const options = [10, 20, 50, 100]
|
const options = [10, 20, 50, 100]
|
||||||
let value = $state('20')
|
let value = $state('20')
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button size="sm" disabled={deciding === row.id} onclick={() => onApprove?.(row.id)}>Approve</Button>
|
<Button size="sm" disabled={deciding === row.id} onclick={() => onApprove?.(row.id)}
|
||||||
<Button size="sm" variant="destructive" disabled={deciding === row.id} onclick={() => onDeny?.(row.id)}>Deny</Button>
|
>Approve</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deciding === row.id}
|
||||||
|
onclick={() => onDeny?.(row.id)}>Deny</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Badge, type BadgeVariant } from '$lib/components/ui/badge'
|
import { Badge, type BadgeVariant } from '$lib/components/ui/badge'
|
||||||
|
|
||||||
let { value, variant = 'outline' as BadgeVariant }: { value: unknown; variant?: BadgeVariant } = $props()
|
let { value, variant = 'outline' as BadgeVariant }: { value: unknown; variant?: BadgeVariant } =
|
||||||
|
$props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Badge {variant}>{String(value ?? '—')}</Badge>
|
<Badge {variant}>{String(value ?? '—')}</Badge>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if health}
|
{#if health}
|
||||||
<span class="flex items-center gap-1.5 text-xs" title={title}>
|
<span class="flex items-center gap-1.5 text-xs" {title}>
|
||||||
<span class="size-2 shrink-0 rounded-full {dot[row.health ?? ''] ?? ''}"></span>
|
<span class="size-2 shrink-0 rounded-full {dot[row.health ?? ''] ?? ''}"></span>
|
||||||
<span class="text-muted-foreground">{relativeTime(lastCheck)}</span>
|
<span class="text-muted-foreground">{relativeTime(lastCheck)}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,8 +19,13 @@
|
|||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
{#if row.state === 'raised'}
|
{#if row.state === 'raised'}
|
||||||
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onAck?.(row.id)}>Ack</Button>
|
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onAck?.(row.id)}
|
||||||
|
>Ack</Button
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onMute?.(row.id)}>Mute 1h</Button>
|
<Button size="sm" variant="outline" disabled={acting === row.id} onclick={() => onMute?.(row.id)}
|
||||||
<Button size="sm" disabled={acting === row.id} onclick={() => onResolve?.(row.id)}>Resolve</Button>
|
>Mute 1h</Button
|
||||||
|
>
|
||||||
|
<Button size="sm" disabled={acting === row.id} onclick={() => onResolve?.(row.id)}>Resolve</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Badge } from '$lib/components/ui/badge'
|
import { Badge } from '$lib/components/ui/badge'
|
||||||
|
|
||||||
let { value, kind = 'default' }: { value: unknown; kind?: 'risk' | 'severity' | 'execution' | 'state' | 'type' | 'default' } = $props()
|
let {
|
||||||
|
value,
|
||||||
|
kind = 'default'
|
||||||
|
}: { value: unknown; kind?: 'risk' | 'severity' | 'execution' | 'state' | 'type' | 'default' } =
|
||||||
|
$props()
|
||||||
|
|
||||||
const v = $derived(String(value ?? ''))
|
const v = $derived(String(value ?? ''))
|
||||||
|
|
||||||
const variantMap: Record<string, Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>> = {
|
const variantMap: Record<
|
||||||
|
string,
|
||||||
|
Record<string, 'default' | 'secondary' | 'destructive' | 'outline'>
|
||||||
|
> = {
|
||||||
risk: {
|
risk: {
|
||||||
destructive: 'destructive',
|
destructive: 'destructive',
|
||||||
config_mutation: 'secondary',
|
config_mutation: 'secondary',
|
||||||
default: 'default',
|
default: 'default'
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
critical: 'destructive',
|
critical: 'destructive',
|
||||||
warning: 'secondary',
|
warning: 'secondary',
|
||||||
info: 'default',
|
info: 'default',
|
||||||
default: 'default',
|
default: 'default'
|
||||||
},
|
},
|
||||||
execution: {
|
execution: {
|
||||||
failed: 'destructive',
|
failed: 'destructive',
|
||||||
@@ -25,19 +32,19 @@
|
|||||||
completed: 'default',
|
completed: 'default',
|
||||||
running: 'secondary',
|
running: 'secondary',
|
||||||
approved: 'secondary',
|
approved: 'secondary',
|
||||||
default: 'outline',
|
default: 'outline'
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
active: 'default',
|
active: 'default',
|
||||||
healthy: 'default',
|
healthy: 'default',
|
||||||
default: 'outline',
|
default: 'outline'
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
runbook: 'secondary',
|
runbook: 'secondary',
|
||||||
investigation: 'default',
|
investigation: 'default',
|
||||||
default: 'outline',
|
default: 'outline'
|
||||||
},
|
},
|
||||||
default: { default: 'default' },
|
default: { default: 'default' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const variant = $derived.by(() => {
|
const variant = $derived.by(() => {
|
||||||
|
|||||||
@@ -45,7 +45,9 @@
|
|||||||
// field.
|
// field.
|
||||||
function onWindowKeydown(e: KeyboardEvent) {
|
function onWindowKeydown(e: KeyboardEvent) {
|
||||||
const target = e.target as HTMLElement | null
|
const target = e.target as HTMLElement | null
|
||||||
const editable = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
const editable =
|
||||||
|
!!target &&
|
||||||
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||||
if (editable) return
|
if (editable) return
|
||||||
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z') return
|
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z') return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -84,7 +86,11 @@
|
|||||||
<div class="fixed inset-0 flex flex-col">
|
<div class="fixed inset-0 flex flex-col">
|
||||||
<div class="relative min-h-0 flex-1 overflow-hidden" role="presentation">
|
<div class="relative min-h-0 flex-1 overflow-hidden" role="presentation">
|
||||||
{#if bgActive}
|
{#if bgActive}
|
||||||
<div class="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden="true" style={bgOuterStyle}>
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 z-0 overflow-hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={bgOuterStyle}
|
||||||
|
>
|
||||||
<div class="absolute" style={bgInnerStyle}></div>
|
<div class="absolute" style={bgInnerStyle}></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -97,9 +103,7 @@
|
|||||||
old `currentTarget === target` event check. Left-click on bare
|
old `currentTarget === target` event check. Left-click on bare
|
||||||
desktop blurs the focused window (the familiar "click empty
|
desktop blurs the focused window (the familiar "click empty
|
||||||
desktop to deselect" affordance). -->
|
desktop to deselect" affordance). -->
|
||||||
<ContextMenu.Trigger
|
<ContextMenu.Trigger class="absolute inset-0 z-0" onclick={() => wm.blur()}
|
||||||
class="absolute inset-0 z-0"
|
|
||||||
onclick={() => wm.blur()}
|
|
||||||
></ContextMenu.Trigger>
|
></ContextMenu.Trigger>
|
||||||
<ContextMenu.Content class="min-w-48">
|
<ContextMenu.Content class="min-w-48">
|
||||||
<ContextMenu.Item onSelect={() => wm.arrange('cascade')}>
|
<ContextMenu.Item onSelect={() => wm.arrange('cascade')}>
|
||||||
|
|||||||
@@ -88,10 +88,14 @@
|
|||||||
onkeydown={onKeydown}
|
onkeydown={onKeydown}
|
||||||
title={app.title}
|
title={app.title}
|
||||||
>
|
>
|
||||||
<span class="relative flex size-10 items-center justify-center rounded-xl border bg-card/80 text-foreground shadow-sm backdrop-blur">
|
<span
|
||||||
|
class="relative flex size-10 items-center justify-center rounded-xl border bg-card/80 text-foreground shadow-sm backdrop-blur"
|
||||||
|
>
|
||||||
<app.icon class="size-5" />
|
<app.icon class="size-5" />
|
||||||
{#if badge > 0}
|
{#if badge > 0}
|
||||||
<span class="absolute -right-1.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold text-destructive-foreground">
|
<span
|
||||||
|
class="absolute -right-1.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold text-destructive-foreground"
|
||||||
|
>
|
||||||
{badge > 99 ? '99+' : badge}
|
{badge > 99 ? '99+' : badge}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
{@const C = mod.default}
|
{@const C = mod.default}
|
||||||
<C />
|
<C />
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<div class="flex h-full min-h-0 items-center justify-center p-4 text-center text-sm text-destructive">
|
<div
|
||||||
|
class="flex h-full min-h-0 items-center justify-center p-4 text-center text-sm text-destructive"
|
||||||
|
>
|
||||||
Failed to load app: {(error as Error).message}
|
Failed to load app: {(error as Error).message}
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
messages={[]}
|
messages={[]}
|
||||||
streaming={false}
|
streaming={false}
|
||||||
connectionState="connected"
|
connectionState="connected"
|
||||||
onSend={onSend}
|
{onSend}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
onReconnect={() => {}}
|
onReconnect={() => {}}
|
||||||
onDismissError={() => {}}
|
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"
|
class="max-h-52 min-h-24 resize-none field-sizing-fixed border-0 bg-transparent px-4 py-3.5 text-base shadow-none focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-between px-3 pb-3">
|
<div class="flex items-center justify-between px-3 pb-3">
|
||||||
<span class="text-[11px] text-muted-foreground">Enter to start · Shift+Enter for newline</span>
|
<span class="text-[11px] text-muted-foreground">Enter to start · Shift+Enter for newline</span
|
||||||
|
>
|
||||||
<Button type="submit" size="icon" disabled={!input.trim()} aria-label="Start task">
|
<Button type="submit" size="icon" disabled={!input.trim()} aria-label="Start task">
|
||||||
<ArrowUpIcon />
|
<ArrowUpIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
wm.focus(id)
|
wm.focus(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-11 shrink-0 items-center gap-1.5 border-t bg-muted/30 px-2">
|
<div class="flex h-11 shrink-0 items-center gap-1.5 border-t bg-muted/30 px-2">
|
||||||
@@ -77,14 +76,19 @@
|
|||||||
class="flex h-8 max-w-56 items-center gap-1.5 rounded-md border px-2 font-mono text-xs transition-colors {$wmState.focusedId ===
|
class="flex h-8 max-w-56 items-center gap-1.5 rounded-md border px-2 font-mono text-xs transition-colors {$wmState.focusedId ===
|
||||||
win.id && win.stage !== 'minimized'
|
win.id && win.stage !== 'minimized'
|
||||||
? 'border-primary/50 bg-primary/10 text-foreground'
|
? 'border-primary/50 bg-primary/10 text-foreground'
|
||||||
: 'border-transparent bg-card/60 text-muted-foreground hover:bg-muted'} {win.stage === 'minimized' ? 'opacity-60' : ''}"
|
: 'border-transparent bg-card/60 text-muted-foreground hover:bg-muted'} {win.stage ===
|
||||||
|
'minimized'
|
||||||
|
? 'opacity-60'
|
||||||
|
: ''}"
|
||||||
onclick={() => toggle(win.id, win)}
|
onclick={() => toggle(win.id, win)}
|
||||||
title={win.title}
|
title={win.title}
|
||||||
>
|
>
|
||||||
{#if Icon}<Icon class="size-3.5 shrink-0" />{/if}
|
{#if Icon}<Icon class="size-3.5 shrink-0" />{/if}
|
||||||
<span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span>
|
<span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span>
|
||||||
{#if badge > 0}
|
{#if badge > 0}
|
||||||
<span class="flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold text-destructive-foreground">
|
<span
|
||||||
|
class="flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold text-destructive-foreground"
|
||||||
|
>
|
||||||
{badge > 99 ? '99+' : badge}
|
{badge > 99 ? '99+' : badge}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -9,7 +9,14 @@
|
|||||||
// session:<id> -> SessionChatWindow (windows.ts openTaskWindow)
|
// session:<id> -> SessionChatWindow (windows.ts openTaskWindow)
|
||||||
// new-task -> NewTaskChat (windows.ts openNewTaskWindow)
|
// new-task -> NewTaskChat (windows.ts openNewTaskWindow)
|
||||||
// anything else -> entity slug -> EntityDetailContent
|
// anything else -> entity slug -> EntityDetailContent
|
||||||
import { wm, dk, wmState, openEntityWindow, NEW_TASK_WINDOW_ID, SESSION_PREFIX } from '$lib/stores/windows'
|
import {
|
||||||
|
wm,
|
||||||
|
dk,
|
||||||
|
wmState,
|
||||||
|
openEntityWindow,
|
||||||
|
NEW_TASK_WINDOW_ID,
|
||||||
|
SESSION_PREFIX
|
||||||
|
} from '$lib/stores/windows'
|
||||||
import { appById, appIdFromWindowId } from '$lib/apps'
|
import { appById, appIdFromWindowId } from '$lib/apps'
|
||||||
import EntityDetailContent from '../EntityDetailContent.svelte'
|
import EntityDetailContent from '../EntityDetailContent.svelte'
|
||||||
import SessionChatWindow from '../SessionChatWindow.svelte'
|
import SessionChatWindow from '../SessionChatWindow.svelte'
|
||||||
@@ -57,8 +64,15 @@
|
|||||||
{@const app = appId ? $appById.get(appId) : undefined}
|
{@const app = appId ? $appById.get(appId) : undefined}
|
||||||
{#if win && (!appId || app)}
|
{#if win && (!appId || app)}
|
||||||
<section use:dk.window={{ id }} class="min-w-0" aria-label={win.title}>
|
<section use:dk.window={{ id }} class="min-w-0" aria-label={win.title}>
|
||||||
<header data-wm-drag class="flex shrink-0 cursor-move items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5">
|
<header
|
||||||
<span data-wm-title class="flex min-w-0 flex-1 items-center self-stretch truncate font-mono text-xs font-medium">{win.title}</span>
|
data-wm-drag
|
||||||
|
class="flex shrink-0 cursor-move items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-wm-title
|
||||||
|
class="flex min-w-0 flex-1 items-center self-stretch truncate font-mono text-xs font-medium"
|
||||||
|
>{win.title}</span
|
||||||
|
>
|
||||||
<div class="flex shrink-0 items-center gap-0.5">
|
<div class="flex shrink-0 items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from 'tailwind-variants'
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
export const badgeVariants = tv({
|
||||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
||||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
destructive:
|
||||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
|
||||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
||||||
},
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
},
|
}
|
||||||
defaultVariants: {
|
},
|
||||||
variant: "default",
|
defaultVariants: {
|
||||||
},
|
variant: 'default'
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant']
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
import type { HTMLAnchorAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
href,
|
href,
|
||||||
class: className,
|
class: className,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<svelte:element
|
||||||
this={href ? "a" : "span"}
|
this={href ? 'a' : 'span'}
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="badge"
|
data-slot="badge"
|
||||||
{href}
|
{href}
|
||||||
class={cn(badgeVariants({ variant }), className)}
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default as Badge } from "./badge.svelte";
|
export { default as Badge } from './badge.svelte'
|
||||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
export { badgeVariants, type BadgeVariant } from './badge.svelte'
|
||||||
|
|||||||
@@ -1,82 +1,89 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from 'tailwind-variants'
|
||||||
|
|
||||||
export const buttonVariants = tv({
|
export const buttonVariants = tv({
|
||||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
outline:
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
|
||||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
secondary:
|
||||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
ghost:
|
||||||
},
|
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
||||||
size: {
|
destructive:
|
||||||
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
|
||||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
},
|
||||||
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
size: {
|
||||||
icon: "size-9",
|
default:
|
||||||
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||||
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
"icon-lg": "size-10",
|
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
|
||||||
},
|
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||||
},
|
icon: 'size-9',
|
||||||
defaultVariants: {
|
'icon-xs':
|
||||||
variant: "default",
|
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
size: "default",
|
'icon-sm':
|
||||||
},
|
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
|
||||||
});
|
'icon-lg': 'size-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']
|
||||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size']
|
||||||
|
|
||||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
WithElementRef<HTMLAnchorAttributes> & {
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant
|
||||||
size?: ButtonSize;
|
size?: ButtonSize
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let {
|
let {
|
||||||
class: className,
|
class: className,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
size = "default",
|
size = 'default',
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
href = undefined,
|
href = undefined,
|
||||||
type = "button",
|
type = 'button',
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: ButtonProps = $props();
|
}: ButtonProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
href={disabled ? undefined : href}
|
href={disabled ? undefined : href}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
role={disabled ? "link" : undefined}
|
role={disabled ? 'link' : undefined}
|
||||||
tabindex={disabled ? -1 : undefined}
|
tabindex={disabled ? -1 : undefined}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
{type}
|
{type}
|
||||||
{disabled}
|
{disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import Root, {
|
import Root, {
|
||||||
type ButtonProps,
|
type ButtonProps,
|
||||||
type ButtonSize,
|
type ButtonSize,
|
||||||
type ButtonVariant,
|
type ButtonVariant,
|
||||||
buttonVariants,
|
buttonVariants
|
||||||
} from "./button.svelte";
|
} from './button.svelte'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
type ButtonProps as Props,
|
type ButtonProps as Props,
|
||||||
//
|
//
|
||||||
Root as Button,
|
Root as Button,
|
||||||
buttonVariants,
|
buttonVariants,
|
||||||
type ButtonProps,
|
type ButtonProps,
|
||||||
type ButtonSize,
|
type ButtonSize,
|
||||||
type ButtonVariant,
|
type ButtonVariant
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
class={cn(
|
class={cn(
|
||||||
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
'cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-content"
|
data-slot="card-content"
|
||||||
class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
class={cn('px-6 group-data-[size=sm]/card:px-4', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn('text-muted-foreground text-sm', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
|
class={cn(
|
||||||
{...restProps}
|
'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
class={cn(
|
class={cn(
|
||||||
"gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
'gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
class={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)}
|
class={cn('text-base leading-normal font-medium group-data-[size=sm]/card:text-sm', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
size = "default",
|
size = 'default',
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
class={cn(
|
||||||
{...restProps}
|
'ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import Root from "./card.svelte";
|
import Root from './card.svelte'
|
||||||
import Content from "./card-content.svelte";
|
import Content from './card-content.svelte'
|
||||||
import Description from "./card-description.svelte";
|
import Description from './card-description.svelte'
|
||||||
import Footer from "./card-footer.svelte";
|
import Footer from './card-footer.svelte'
|
||||||
import Header from "./card-header.svelte";
|
import Header from './card-header.svelte'
|
||||||
import Title from "./card-title.svelte";
|
import Title from './card-title.svelte'
|
||||||
import Action from "./card-action.svelte";
|
import Action from './card-action.svelte'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Content,
|
Content,
|
||||||
Description,
|
Description,
|
||||||
Footer,
|
Footer,
|
||||||
Header,
|
Header,
|
||||||
Title,
|
Title,
|
||||||
Action,
|
Action,
|
||||||
//
|
//
|
||||||
Root as Card,
|
Root as Card,
|
||||||
Content as CardContent,
|
Content as CardContent,
|
||||||
Description as CardDescription,
|
Description as CardDescription,
|
||||||
Footer as CardFooter,
|
Footer as CardFooter,
|
||||||
Header as CardHeader,
|
Header as CardHeader,
|
||||||
Title as CardTitle,
|
Title as CardTitle,
|
||||||
Action as CardAction,
|
Action as CardAction
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked, indeterminate })}
|
{#snippet children({ checked, indeterminate })}
|
||||||
<div data-slot="checkbox-indicator" class="flex items-center justify-center text-current transition-none">
|
<div
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
class="flex items-center justify-center text-current transition-none"
|
||||||
|
>
|
||||||
{#if indeterminate}
|
{#if indeterminate}
|
||||||
<MinusIcon class="size-3.5" />
|
<MinusIcon class="size-3.5" />
|
||||||
{:else if checked}
|
{:else if checked}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
import { Collapsible as CollapsiblePrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
open = $bindable(false),
|
open = $bindable(false),
|
||||||
...restProps
|
...restProps
|
||||||
}: CollapsiblePrimitive.RootProps = $props();
|
}: CollapsiblePrimitive.RootProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Root from "./collapsible.svelte";
|
import Root from './collapsible.svelte'
|
||||||
import Trigger from "./collapsible-trigger.svelte";
|
import Trigger from './collapsible-trigger.svelte'
|
||||||
import Content from "./collapsible-content.svelte";
|
import Content from './collapsible-content.svelte'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Content,
|
Content,
|
||||||
Trigger,
|
Trigger,
|
||||||
//
|
//
|
||||||
Root as Collapsible,
|
Root as Collapsible,
|
||||||
Content as CollapsibleContent,
|
Content as CollapsibleContent,
|
||||||
Trigger as CollapsibleTrigger,
|
Trigger as CollapsibleTrigger
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte'
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
checked = $bindable(false),
|
checked = $bindable(false),
|
||||||
indeterminate = $bindable(false),
|
indeterminate = $bindable(false),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
children?: Snippet;
|
children?: Snippet
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:checked
|
bind:checked
|
||||||
bind:indeterminate
|
bind:indeterminate
|
||||||
data-slot="context-menu-checkbox-item"
|
data-slot="context-menu-checkbox-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked })}
|
{#snippet children({ checked })}
|
||||||
<span class="absolute right-2 pointer-events-none">
|
<span class="absolute right-2 pointer-events-none">
|
||||||
{#if checked}
|
{#if checked}
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.()}
|
{@render childrenProp?.()}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
import ContextMenuPortal from "./context-menu-portal.svelte";
|
import ContextMenuPortal from './context-menu-portal.svelte'
|
||||||
import type { ComponentProps } from "svelte";
|
import type { ComponentProps } from 'svelte'
|
||||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
import type { WithoutChildrenOrChild } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
portalProps,
|
portalProps,
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.ContentProps & {
|
}: ContextMenuPrimitive.ContentProps & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPortal {...portalProps}>
|
<ContextMenuPortal {...portalProps}>
|
||||||
<ContextMenuPrimitive.Content
|
<ContextMenuPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-content"
|
data-slot="context-menu-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none",
|
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-md p-1 shadow-md ring-1 duration-100 z-50 overflow-x-hidden overflow-y-auto outline-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
</ContextMenuPortal>
|
</ContextMenuPortal>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.GroupHeadingProps & {
|
}: ContextMenuPrimitive.GroupHeadingProps & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.GroupHeading
|
<ContextMenuPrimitive.GroupHeading
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-group-heading"
|
data-slot="context-menu-group-heading"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8", className)}
|
class={cn('text-foreground px-2 py-1.5 text-sm font-medium data-inset:ps-8', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props();
|
let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />
|
<ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.ItemProps & {
|
}: ContextMenuPrimitive.ItemProps & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
variant?: "default" | "destructive";
|
variant?: 'default' | 'destructive'
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Item
|
<ContextMenuPrimitive.Item
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-item"
|
data-slot="context-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="context-menu-label"
|
data-slot="context-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8", className)}
|
class={cn(
|
||||||
{...restProps}
|
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-inset:pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
|
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Portal {...restProps} />
|
<ContextMenuPrimitive.Portal {...restProps} />
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(""),
|
value = $bindable(''),
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.RadioGroupProps = $props();
|
}: ContextMenuPrimitive.RadioGroupProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.RadioGroup
|
<ContextMenuPrimitive.RadioGroup
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
data-slot="context-menu-radio-group"
|
data-slot="context-menu-radio-group"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
|
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
<ContextMenuPrimitive.RadioItem
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-radio-item"
|
data-slot="context-menu-radio-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked })}
|
{#snippet children({ checked })}
|
||||||
<span class="absolute right-2 pointer-events-none">
|
<span class="absolute right-2 pointer-events-none">
|
||||||
{#if checked}
|
{#if checked}
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.({ checked })}
|
{@render childrenProp?.({ checked })}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ContextMenuPrimitive.RadioItem>
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.SeparatorProps = $props();
|
}: ContextMenuPrimitive.SeparatorProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Separator
|
<ContextMenuPrimitive.Separator
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-separator"
|
data-slot="context-menu-separator"
|
||||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="context-menu-shortcut"
|
data-slot="context-menu-shortcut"
|
||||||
class={cn("text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
class={cn(
|
||||||
{...restProps}
|
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.SubContentProps = $props();
|
}: ContextMenuPrimitive.SubContentProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.SubContent
|
<ContextMenuPrimitive.SubContent
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-sub-content"
|
data-slot="context-menu-sub-content"
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100", className)}
|
class={cn(
|
||||||
{...restProps}
|
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-md border p-1 shadow-lg duration-100',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
<ContextMenuPrimitive.SubTrigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-sub-trigger"
|
data-slot="context-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
<ChevronRightIcon class="ml-auto" />
|
<ChevronRightIcon class="ml-auto" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props();
|
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Sub bind:open {...restProps} />
|
<ContextMenuPrimitive.Sub bind:open {...restProps} />
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ContextMenuPrimitive.TriggerProps = $props();
|
}: ContextMenuPrimitive.TriggerProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Trigger
|
<ContextMenuPrimitive.Trigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="context-menu-trigger"
|
data-slot="context-menu-trigger"
|
||||||
class={cn("cn-context-menu-trigger select-none", className)}
|
class={cn('cn-context-menu-trigger select-none', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props();
|
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenuPrimitive.Root bind:open {...restProps} />
|
<ContextMenuPrimitive.Root bind:open {...restProps} />
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
import Root from "./context-menu.svelte";
|
import Root from './context-menu.svelte'
|
||||||
import Sub from "./context-menu-sub.svelte";
|
import Sub from './context-menu-sub.svelte'
|
||||||
import Portal from "./context-menu-portal.svelte";
|
import Portal from './context-menu-portal.svelte'
|
||||||
import Trigger from "./context-menu-trigger.svelte";
|
import Trigger from './context-menu-trigger.svelte'
|
||||||
import Group from "./context-menu-group.svelte";
|
import Group from './context-menu-group.svelte'
|
||||||
import RadioGroup from "./context-menu-radio-group.svelte";
|
import RadioGroup from './context-menu-radio-group.svelte'
|
||||||
import Item from "./context-menu-item.svelte";
|
import Item from './context-menu-item.svelte'
|
||||||
import GroupHeading from "./context-menu-group-heading.svelte";
|
import GroupHeading from './context-menu-group-heading.svelte'
|
||||||
import Content from "./context-menu-content.svelte";
|
import Content from './context-menu-content.svelte'
|
||||||
import Shortcut from "./context-menu-shortcut.svelte";
|
import Shortcut from './context-menu-shortcut.svelte'
|
||||||
import RadioItem from "./context-menu-radio-item.svelte";
|
import RadioItem from './context-menu-radio-item.svelte'
|
||||||
import Separator from "./context-menu-separator.svelte";
|
import Separator from './context-menu-separator.svelte'
|
||||||
import SubContent from "./context-menu-sub-content.svelte";
|
import SubContent from './context-menu-sub-content.svelte'
|
||||||
import SubTrigger from "./context-menu-sub-trigger.svelte";
|
import SubTrigger from './context-menu-sub-trigger.svelte'
|
||||||
import CheckboxItem from "./context-menu-checkbox-item.svelte";
|
import CheckboxItem from './context-menu-checkbox-item.svelte'
|
||||||
import Label from "./context-menu-label.svelte";
|
import Label from './context-menu-label.svelte'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Sub,
|
Sub,
|
||||||
Portal,
|
Portal,
|
||||||
Item,
|
Item,
|
||||||
GroupHeading,
|
GroupHeading,
|
||||||
Label,
|
Label,
|
||||||
Group,
|
Group,
|
||||||
Trigger,
|
Trigger,
|
||||||
Content,
|
Content,
|
||||||
Shortcut,
|
Shortcut,
|
||||||
Separator,
|
Separator,
|
||||||
RadioItem,
|
RadioItem,
|
||||||
SubContent,
|
SubContent,
|
||||||
SubTrigger,
|
SubTrigger,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
CheckboxItem,
|
CheckboxItem,
|
||||||
//
|
//
|
||||||
Root as ContextMenu,
|
Root as ContextMenu,
|
||||||
Sub as ContextMenuSub,
|
Sub as ContextMenuSub,
|
||||||
Portal as ContextMenuPortal,
|
Portal as ContextMenuPortal,
|
||||||
Item as ContextMenuItem,
|
Item as ContextMenuItem,
|
||||||
GroupHeading as ContextMenuGroupHeading,
|
GroupHeading as ContextMenuGroupHeading,
|
||||||
Group as ContextMenuGroup,
|
Group as ContextMenuGroup,
|
||||||
Content as ContextMenuContent,
|
Content as ContextMenuContent,
|
||||||
Trigger as ContextMenuTrigger,
|
Trigger as ContextMenuTrigger,
|
||||||
Shortcut as ContextMenuShortcut,
|
Shortcut as ContextMenuShortcut,
|
||||||
RadioItem as ContextMenuRadioItem,
|
RadioItem as ContextMenuRadioItem,
|
||||||
Separator as ContextMenuSeparator,
|
Separator as ContextMenuSeparator,
|
||||||
RadioGroup as ContextMenuRadioGroup,
|
RadioGroup as ContextMenuRadioGroup,
|
||||||
SubContent as ContextMenuSubContent,
|
SubContent as ContextMenuSubContent,
|
||||||
SubTrigger as ContextMenuSubTrigger,
|
SubTrigger as ContextMenuSubTrigger,
|
||||||
CheckboxItem as ContextMenuCheckboxItem,
|
CheckboxItem as ContextMenuCheckboxItem,
|
||||||
Label as ContextMenuLabel,
|
Label as ContextMenuLabel
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
type = "button",
|
type = 'button',
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.CloseProps = $props();
|
}: DialogPrimitive.CloseProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
import DialogPortal from "./dialog-portal.svelte";
|
import DialogPortal from './dialog-portal.svelte'
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte'
|
||||||
import * as Dialog from "./index.js";
|
import * as Dialog from './index.js'
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||||
import type { ComponentProps } from "svelte";
|
import type { ComponentProps } from 'svelte'
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
portalProps,
|
portalProps,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>
|
||||||
children: Snippet;
|
children: Snippet
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPortal {...portalProps}>
|
<DialogPortal {...portalProps}>
|
||||||
<Dialog.Overlay />
|
<Dialog.Overlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<DialogPrimitive.Close data-slot="dialog-close">
|
<DialogPrimitive.Close data-slot="dialog-close">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
{/if}
|
{/if}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.DescriptionProps = $props();
|
}: DialogPrimitive.DescriptionProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
class={cn(
|
||||||
{...restProps}
|
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<DialogPrimitive.Close>
|
<DialogPrimitive.Close>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button variant="outline" {...props}>Close</Button>
|
<Button variant="outline" {...props}>Close</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
class={cn("gap-2 flex flex-col", className)}
|
class={cn('gap-2 flex flex-col', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.OverlayProps = $props();
|
}: DialogPrimitive.OverlayProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
class={cn(
|
||||||
{...restProps}
|
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
let { ...restProps }: DialogPrimitive.PortalProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Portal {...restProps} />
|
<DialogPrimitive.Portal {...restProps} />
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.TitleProps = $props();
|
}: DialogPrimitive.TitleProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
class={cn("leading-none font-medium", className)}
|
class={cn('leading-none font-medium', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
type = "button",
|
type = 'button',
|
||||||
...restProps
|
...restProps
|
||||||
}: DialogPrimitive.TriggerProps = $props();
|
}: DialogPrimitive.TriggerProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
import { Dialog as DialogPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Root bind:open {...restProps} />
|
<DialogPrimitive.Root bind:open {...restProps} />
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import Root from "./dialog.svelte";
|
import Root from './dialog.svelte'
|
||||||
import Portal from "./dialog-portal.svelte";
|
import Portal from './dialog-portal.svelte'
|
||||||
import Title from "./dialog-title.svelte";
|
import Title from './dialog-title.svelte'
|
||||||
import Footer from "./dialog-footer.svelte";
|
import Footer from './dialog-footer.svelte'
|
||||||
import Header from "./dialog-header.svelte";
|
import Header from './dialog-header.svelte'
|
||||||
import Overlay from "./dialog-overlay.svelte";
|
import Overlay from './dialog-overlay.svelte'
|
||||||
import Content from "./dialog-content.svelte";
|
import Content from './dialog-content.svelte'
|
||||||
import Description from "./dialog-description.svelte";
|
import Description from './dialog-description.svelte'
|
||||||
import Trigger from "./dialog-trigger.svelte";
|
import Trigger from './dialog-trigger.svelte'
|
||||||
import Close from "./dialog-close.svelte";
|
import Close from './dialog-close.svelte'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
Title,
|
Title,
|
||||||
Portal,
|
Portal,
|
||||||
Footer,
|
Footer,
|
||||||
Header,
|
Header,
|
||||||
Trigger,
|
Trigger,
|
||||||
Overlay,
|
Overlay,
|
||||||
Content,
|
Content,
|
||||||
Description,
|
Description,
|
||||||
Close,
|
Close,
|
||||||
//
|
//
|
||||||
Root as Dialog,
|
Root as Dialog,
|
||||||
Title as DialogTitle,
|
Title as DialogTitle,
|
||||||
Portal as DialogPortal,
|
Portal as DialogPortal,
|
||||||
Footer as DialogFooter,
|
Footer as DialogFooter,
|
||||||
Header as DialogHeader,
|
Header as DialogHeader,
|
||||||
Trigger as DialogTrigger,
|
Trigger as DialogTrigger,
|
||||||
Overlay as DialogOverlay,
|
Overlay as DialogOverlay,
|
||||||
Content as DialogContent,
|
Content as DialogContent,
|
||||||
Description as DialogDescription,
|
Description as DialogDescription,
|
||||||
Close as DialogClose,
|
Close as DialogClose
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable([]),
|
value = $bindable([]),
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
}: DropdownMenuPrimitive.CheckboxGroupProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxGroup
|
<DropdownMenuPrimitive.CheckboxGroup
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
data-slot="dropdown-menu-checkbox-group"
|
data-slot="dropdown-menu-checkbox-group"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
import MinusIcon from '@lucide/svelte/icons/minus'
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check'
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
checked = $bindable(false),
|
checked = $bindable(false),
|
||||||
indeterminate = $bindable(false),
|
indeterminate = $bindable(false),
|
||||||
class: className,
|
class: className,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
children?: Snippet;
|
children?: Snippet
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:checked
|
bind:checked
|
||||||
bind:indeterminate
|
bind:indeterminate
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked, indeterminate })}
|
{#snippet children({ checked, indeterminate })}
|
||||||
<span
|
<span
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
>
|
>
|
||||||
{#if indeterminate}
|
{#if indeterminate}
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
{:else if checked}
|
{:else if checked}
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.()}
|
{@render childrenProp?.()}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js'
|
||||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
import DropdownMenuPortal from './dropdown-menu-portal.svelte'
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import type { ComponentProps } from "svelte";
|
import type { ComponentProps } from 'svelte'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
align = "start",
|
align = 'start',
|
||||||
portalProps,
|
portalProps,
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.ContentProps & {
|
}: DropdownMenuPrimitive.ContentProps & {
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPortal {...portalProps}>
|
<DropdownMenuPortal {...portalProps}>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
{align}
|
{align}
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
import type { ComponentProps } from "svelte";
|
import type { ComponentProps } from 'svelte'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
...restProps
|
...restProps
|
||||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.GroupHeading
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-group-heading"
|
data-slot="dropdown-menu-group-heading"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.ItemProps & {
|
}: DropdownMenuPrimitive.ItemProps & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
variant?: "default" | "destructive";
|
variant?: 'default' | 'destructive'
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-item"
|
data-slot="dropdown-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8", className)}
|
class={cn(
|
||||||
{...restProps}
|
'text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8 data-[inset]:pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
}: DropdownMenuPrimitive.RadioGroupProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check'
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
import { cn, type WithoutChild } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children: childrenProp,
|
children: childrenProp,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{#snippet children({ checked })}
|
{#snippet children({ checked })}
|
||||||
<span
|
<span
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
data-slot="dropdown-menu-radio-item-indicator"
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
>
|
>
|
||||||
{#if checked}
|
{#if checked}
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{@render childrenProp?.({ checked })}
|
{@render childrenProp?.({ checked })}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
}: DropdownMenuPrimitive.SeparatorProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
class={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
class={cn(
|
||||||
{...restProps}
|
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
}: DropdownMenuPrimitive.SubContentProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto", className)}
|
class={cn(
|
||||||
{...restProps}
|
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 w-auto',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
class: className,
|
class: className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
} = $props();
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
class={cn(
|
class={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
<ChevronRightIcon class="ml-auto" />
|
<ChevronRightIcon class="ml-auto" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user