.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>
153 lines
6.5 KiB
Svelte
153 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
// The desktop shell: full-viewport surface (background + icons + the
|
|
// centered task launcher + the floating window layer) with the taskbar
|
|
// docked below it as a real flex sibling, not an overlay — so a maximized
|
|
// or dragged window can never end up underneath the taskbar. This replaces
|
|
// the old sidebar + hash-routed page shell in App.svelte entirely; apps are
|
|
// desktop icons now (see $lib/apps.ts), not nav items.
|
|
import { apps } from '$lib/apps'
|
|
import { iconPositions, resetIconLayout } from '$lib/stores/icons'
|
|
import { wm, openAppWindow, toggleShowDesktop } from '$lib/stores/windows'
|
|
import { summary } from '$lib/stores/context'
|
|
import { getBackground } from '$lib/stores/background.svelte'
|
|
import { patternCss } from '$lib/desktop-patterns'
|
|
import DesktopIcon from './DesktopIcon.svelte'
|
|
import TaskLauncher from './TaskLauncher.svelte'
|
|
import WindowLayer from './WindowLayer.svelte'
|
|
import DockedLayer from './DockedLayer.svelte'
|
|
import Taskbar from './Taskbar.svelte'
|
|
import * as ContextMenu from '$lib/components/ui/context-menu'
|
|
import LayersIcon from '@lucide/svelte/icons/layers'
|
|
import Rows3Icon from '@lucide/svelte/icons/rows-3'
|
|
import MonitorIcon from '@lucide/svelte/icons/monitor'
|
|
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'
|
|
import Undo2Icon from '@lucide/svelte/icons/undo-2'
|
|
import Redo2Icon from '@lucide/svelte/icons/redo-2'
|
|
|
|
// canUndo/canRedo are plain wmkit method calls (not stores), so they're
|
|
// snapshotted once when the menu opens (onOpenChange) rather than read
|
|
// reactively in the template. bits-ui auto-dismisses on item select and
|
|
// on Escape / click-away, so the old manual menuPos/closeMenu/runMenuAction
|
|
// machinery is gone.
|
|
let menuCanUndo = $state(false)
|
|
let menuCanRedo = $state(false)
|
|
|
|
function onOpenChange(open: boolean) {
|
|
if (!open) return
|
|
menuCanUndo = wm.canUndo()
|
|
menuCanRedo = wm.canRedo()
|
|
}
|
|
|
|
// Cmd/Ctrl+Z / Shift+Z for window-arrangement undo/redo (move, resize,
|
|
// close, ...) — wmkit tracks this history but ships no default keybinding.
|
|
// Skipped entirely while an editable element has focus so it never
|
|
// fights the browser's own text-undo inside the task input or a form
|
|
// field.
|
|
function onWindowKeydown(e: KeyboardEvent) {
|
|
const target = e.target as HTMLElement | null
|
|
const editable =
|
|
!!target &&
|
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
|
if (editable) return
|
|
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z') return
|
|
e.preventDefault()
|
|
if (e.shiftKey) wm.redo()
|
|
else wm.undo()
|
|
}
|
|
|
|
// Configurable in Settings → Appearance (see background.svelte.ts). Two
|
|
// layers, not one, because rotation and the fade mask need different
|
|
// geometry:
|
|
// - outer: exactly the viewport box. Carries the fade mask, since a
|
|
// vignette has to be centered on what's actually visible.
|
|
// - inner: oversized (200%) and centered before rotating, so turning the
|
|
// pattern doesn't pull its straight edges into view at the corners —
|
|
// a viewport-sized box rotated in place would do exactly that.
|
|
const bgActive = $derived.by(() => {
|
|
const bg = getBackground()
|
|
return bg.pattern !== 'none' || bg.fillColor !== null
|
|
})
|
|
const bgOuterStyle = $derived.by(() => {
|
|
const bg = getBackground()
|
|
if (bg.fade <= 0) return ''
|
|
const stop = Math.round(100 - bg.fade * 70)
|
|
const mask = `radial-gradient(circle at 50% 50%, black 0%, black ${stop}%, transparent 100%)`
|
|
return `mask-image:${mask};-webkit-mask-image:${mask};`
|
|
})
|
|
const bgInnerStyle = $derived.by(() => {
|
|
const bg = getBackground()
|
|
const css = patternCss(bg.pattern, bg.color, bg.scale)
|
|
return `inset:-50%;width:200%;height:200%;opacity:${bg.opacity};background-color:${bg.fillColor ?? 'transparent'};transform:rotate(${bg.rotation}deg);${css}`
|
|
})
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onWindowKeydown} />
|
|
|
|
<div class="fixed inset-0 flex flex-col">
|
|
<div class="relative min-h-0 flex-1 overflow-hidden" role="presentation">
|
|
{#if bgActive}
|
|
<div
|
|
class="pointer-events-none absolute inset-0 z-0 overflow-hidden"
|
|
aria-hidden="true"
|
|
style={bgOuterStyle}
|
|
>
|
|
<div class="absolute" style={bgInnerStyle}></div>
|
|
</div>
|
|
{/if}
|
|
|
|
<ContextMenu.Root {onOpenChange}>
|
|
<!-- The bare-desktop hit area. Placed before the icons/windows layers
|
|
so they (pointer-events-auto, later in DOM → paint on top) catch
|
|
their own right-clicks — the trigger only sees right-clicks that
|
|
fall through to bare desktop. This DOM-structure gate replaces the
|
|
old `currentTarget === target` event check. Left-click on bare
|
|
desktop blurs the focused window (the familiar "click empty
|
|
desktop to deselect" affordance). -->
|
|
<ContextMenu.Trigger class="absolute inset-0 z-0" onclick={() => wm.blur()}
|
|
></ContextMenu.Trigger>
|
|
<ContextMenu.Content class="min-w-48">
|
|
<ContextMenu.Item onSelect={() => wm.arrange('cascade')}>
|
|
<LayersIcon class="size-4" /> Cascade windows
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item onSelect={() => wm.arrange('tile')}>
|
|
<Rows3Icon class="size-4" /> Tile windows
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item onSelect={toggleShowDesktop}>
|
|
<MonitorIcon class="size-4" /> Show desktop
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Separator />
|
|
<ContextMenu.Item onSelect={resetIconLayout}>
|
|
<RotateCcwIcon class="size-4" /> Reset icon layout
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Separator />
|
|
<ContextMenu.Item disabled={!menuCanUndo} onSelect={() => wm.undo()}>
|
|
<Undo2Icon class="size-4" /> Undo
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item disabled={!menuCanRedo} onSelect={() => wm.redo()}>
|
|
<Redo2Icon class="size-4" /> Redo
|
|
</ContextMenu.Item>
|
|
</ContextMenu.Content>
|
|
</ContextMenu.Root>
|
|
|
|
<div class="pointer-events-none absolute inset-0 z-0">
|
|
{#each $apps as app (app.id)}
|
|
{@const pos = $iconPositions[app.id] ?? { col: 0, row: 0 }}
|
|
{@const badge = app.badge?.($summary) ?? 0}
|
|
<DesktopIcon {app} {pos} {badge} onOpen={() => openAppWindow(app.id)} />
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6">
|
|
<div class="pointer-events-auto">
|
|
<TaskLauncher />
|
|
</div>
|
|
</div>
|
|
|
|
<WindowLayer />
|
|
|
|
<DockedLayer />
|
|
</div>
|
|
|
|
<Taskbar />
|
|
</div>
|