The taskbar's gear icon reopened the full-page "Connect to Oikos" screen even once already connected. Split that: Config.svelte stays as the first-run/unconfigured screen; a new Settings app (windowed, like Tasks or Operations) now handles in-session changes, with a section list (Connection, Appearance) built to grow — future settings are one more entry, not a new screen. - pages/Settings.svelte: Connection (server URL/token/Authentik, reusing config.ts + oidc.ts) and Appearance (Terracotta/Carbon picker) sections. - apps.ts: registered as a normal desktop app. - Taskbar's gear button now opens the Settings window; removed the onOpenConnection prop threaded through App -> Desktop -> Taskbar, since Settings' "Forget saved connection" (clear config + reload) replaces it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
162 lines
6.1 KiB
Svelte
162 lines
6.1 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 GraphBackground from '../GraphBackground.svelte'
|
|
import DesktopIcon from './DesktopIcon.svelte'
|
|
import TaskLauncher from './TaskLauncher.svelte'
|
|
import WindowLayer from './WindowLayer.svelte'
|
|
import Taskbar from './Taskbar.svelte'
|
|
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'
|
|
|
|
// Clicking the bare desktop (not an icon, not a window) blurs the focused
|
|
// window — the familiar "click empty desktop to deselect" affordance.
|
|
function onSurfaceClick(e: MouseEvent) {
|
|
if (e.currentTarget === e.target) wm.blur()
|
|
}
|
|
|
|
// Right-click menu, bare desktop only (same currentTarget===target gate as
|
|
// onSurfaceClick above — icons and windows sit on pointer-events-auto
|
|
// layers above the otherwise pointer-events-none surface, so a right-click
|
|
// that lands on either of them never reaches here). canUndo/canRedo are
|
|
// plain wmkit method calls (not stores), so they're snapshotted once at
|
|
// open time rather than read reactively in the template.
|
|
let menuPos = $state<{ x: number; y: number } | null>(null)
|
|
let menuCanUndo = $state(false)
|
|
let menuCanRedo = $state(false)
|
|
|
|
function onSurfaceContextMenu(e: MouseEvent) {
|
|
if (e.currentTarget !== e.target) return
|
|
e.preventDefault()
|
|
menuCanUndo = wm.canUndo()
|
|
menuCanRedo = wm.canRedo()
|
|
menuPos = { x: e.clientX, y: e.clientY }
|
|
}
|
|
|
|
function closeMenu() {
|
|
menuPos = null
|
|
}
|
|
|
|
function runMenuAction(fn: () => void) {
|
|
fn()
|
|
closeMenu()
|
|
}
|
|
|
|
// 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) {
|
|
if (e.key === 'Escape' && menuPos) {
|
|
closeMenu()
|
|
return
|
|
}
|
|
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()
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onWindowKeydown} onclick={closeMenu} />
|
|
|
|
<div class="fixed inset-0 flex flex-col">
|
|
<div
|
|
class="relative min-h-0 flex-1 overflow-hidden"
|
|
role="presentation"
|
|
onclick={onSurfaceClick}
|
|
oncontextmenu={onSurfaceContextMenu}
|
|
>
|
|
<GraphBackground />
|
|
|
|
<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 />
|
|
</div>
|
|
|
|
<Taskbar />
|
|
</div>
|
|
|
|
{#if menuPos}
|
|
<div
|
|
class="fixed z-50 min-w-48 rounded-md border bg-popover p-1 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10"
|
|
style="left: {menuPos.x}px; top: {menuPos.y}px"
|
|
role="menu"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground"
|
|
onclick={() => runMenuAction(() => wm.arrange('cascade'))}
|
|
>
|
|
<LayersIcon class="size-4" /> Cascade windows
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground"
|
|
onclick={() => runMenuAction(() => wm.arrange('tile'))}
|
|
>
|
|
<Rows3Icon class="size-4" /> Tile windows
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground"
|
|
onclick={() => runMenuAction(toggleShowDesktop)}
|
|
>
|
|
<MonitorIcon class="size-4" /> Show desktop
|
|
</button>
|
|
<div class="my-1 h-px bg-border"></div>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground"
|
|
onclick={() => runMenuAction(resetIconLayout)}
|
|
>
|
|
<RotateCcwIcon class="size-4" /> Reset icon layout
|
|
</button>
|
|
<div class="my-1 h-px bg-border"></div>
|
|
<button
|
|
type="button"
|
|
disabled={!menuCanUndo}
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
onclick={() => runMenuAction(() => wm.undo())}
|
|
>
|
|
<Undo2Icon class="size-4" /> Undo
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={!menuCanRedo}
|
|
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
onclick={() => runMenuAction(() => wm.redo())}
|
|
>
|
|
<Redo2Icon class="size-4" /> Redo
|
|
</button>
|
|
</div>
|
|
{/if}
|