Rebrand light/dark themes to the cyberspace.online look: warm cream-on-black palette (light/dark are exact inverses), self-hosted JetBrains Mono + VT323, square corners, border-driven surfaces with no soft shadows. Adds a terminal design-system CSS layer (DOS double-border modals with hatched corner, fg focus, inversion-on-hover), a theme-aware <RasterImage> (Atkinson-dithered canvas with img fallback), and unifies desktop icons, taskbar, window controls, pills and links under one idiom. Pins window titlebars to a fixed height and switches chat auto-scroll off scrollIntoView to avoid titlebar reflow. VERSION 0.15.1 -> 0.16.0
134 lines
5.2 KiB
Svelte
134 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
// Bottom taskbar: a real flex row in the page layout (not an overlay), so
|
|
// windows can never be dragged/maximized underneath it — see Desktop.svelte
|
|
// for how the window layer's bounds are scoped to the surface above this.
|
|
// Shows a button for every open window (not just minimized ones — compare
|
|
// to the old MinimizedWindowsBar, which only ever showed minimized windows
|
|
// and gave no way to see/switch between windows that were merely
|
|
// unfocused), plus a system tray for theme/connection/version.
|
|
import { wm, wmState, toggleShowDesktop, openAppWindow } from '$lib/stores/windows'
|
|
import { appById, appIdFromWindowId } from '$lib/apps'
|
|
import { summary } from '$lib/stores/context'
|
|
import { truncateMiddle } from '$lib/utils'
|
|
import { getTheme, toggleTheme, THEME_LABELS } from '$lib/stores/theme.svelte'
|
|
import { VERSION } from '$lib/version'
|
|
import MessageSquareIcon from '@lucide/svelte/icons/message-square'
|
|
import DatabaseIcon from '@lucide/svelte/icons/database'
|
|
import PaletteIcon from '@lucide/svelte/icons/palette'
|
|
import SettingsIcon from '@lucide/svelte/icons/settings'
|
|
import LayoutGridIcon from '@lucide/svelte/icons/layout-grid'
|
|
import XIcon from '@lucide/svelte/icons/x'
|
|
|
|
const SESSION_PREFIX = 'session:'
|
|
|
|
const buttons = $derived(
|
|
[...$wmState.order]
|
|
.map((id) => $wmState.windows[id])
|
|
.filter((w): w is NonNullable<typeof w> => !!w)
|
|
.sort((a, b) => a.openedSeq - b.openedSeq)
|
|
)
|
|
|
|
function iconFor(id: string) {
|
|
const appId = appIdFromWindowId(id)
|
|
if (appId) return $appById.get(appId)?.icon
|
|
if (id.startsWith(SESSION_PREFIX)) return MessageSquareIcon
|
|
return DatabaseIcon
|
|
}
|
|
|
|
function badgeFor(id: string): number {
|
|
const appId = appIdFromWindowId(id)
|
|
const app = appId ? $appById.get(appId) : undefined
|
|
return app?.badge?.($summary) ?? 0
|
|
}
|
|
|
|
function toggle(id: string, win: (typeof buttons)[number]) {
|
|
if (win.stage === 'minimized') {
|
|
wm.restore(id)
|
|
wm.focus(id)
|
|
} else if ($wmState.focusedId === id) {
|
|
wm.minimize(id)
|
|
} else {
|
|
wm.focus(id)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex h-11 shrink-0 items-center gap-1.5 border-t border-border bg-background px-2">
|
|
<button
|
|
type="button"
|
|
class="flex shrink-0 items-center justify-center border border-border p-1.5 text-muted-foreground transition-colors hover:border-foreground hover:bg-foreground hover:text-background"
|
|
onclick={toggleShowDesktop}
|
|
title="Show desktop"
|
|
>
|
|
<LayoutGridIcon class="size-4" />
|
|
</button>
|
|
|
|
<div class="h-6 w-px shrink-0 bg-border"></div>
|
|
|
|
<div class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto py-1.5">
|
|
{#each buttons as win (win.id)}
|
|
{@const Icon = iconFor(win.id)}
|
|
{@const badge = badgeFor(win.id)}
|
|
<div class="group/tb relative flex shrink-0">
|
|
<button
|
|
type="button"
|
|
data-taskbar-btn={win.id}
|
|
class="flex h-8 max-w-56 items-center gap-1.5 border px-2 font-mono text-xs transition-colors {$wmState.focusedId ===
|
|
win.id && win.stage !== 'minimized'
|
|
? 'border-foreground bg-foreground text-background'
|
|
: 'border-border bg-background text-muted-foreground hover:border-foreground hover:bg-foreground hover:text-background'} {win.stage ===
|
|
'minimized'
|
|
? 'opacity-50'
|
|
: ''}"
|
|
onclick={() => toggle(win.id, win)}
|
|
title={win.title}
|
|
>
|
|
{#if Icon}<Icon class="size-3.5 shrink-0" />{/if}
|
|
<span class="min-w-0 truncate">{truncateMiddle(win.title, 26)}</span>
|
|
{#if badge > 0}
|
|
<span
|
|
class="flex h-3.5 min-w-3.5 shrink-0 items-center justify-center border border-background bg-destructive px-1 text-[9px] font-semibold text-destructive-foreground"
|
|
>
|
|
{badge > 99 ? '99+' : badge}
|
|
</span>
|
|
{/if}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="win-ctrl absolute -top-1.5 -right-1.5 hidden size-4 group-hover/tb:flex"
|
|
onclick={(e) => {
|
|
e.stopPropagation()
|
|
wm.close(win.id)
|
|
}}
|
|
aria-label="Close {win.title}"
|
|
>
|
|
<XIcon class="size-2.5" />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="h-6 w-px shrink-0 bg-border"></div>
|
|
|
|
<div class="flex shrink-0 items-center gap-0.5">
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center border border-border p-1.5 text-muted-foreground transition-colors hover:border-foreground hover:bg-foreground hover:text-background"
|
|
onclick={() => toggleTheme()}
|
|
title="Cycle theme ({THEME_LABELS[getTheme()]})"
|
|
aria-label="Cycle theme, currently {THEME_LABELS[getTheme()]}"
|
|
>
|
|
<PaletteIcon class="size-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center border border-border p-1.5 text-muted-foreground transition-colors hover:border-foreground hover:bg-foreground hover:text-background"
|
|
onclick={() => openAppWindow('settings')}
|
|
title="Settings"
|
|
>
|
|
<SettingsIcon class="size-4" />
|
|
</button>
|
|
<span class="px-1.5 font-mono text-[11px] text-muted-foreground select-none">{VERSION}</span>
|
|
</div>
|
|
</div>
|