feat(web): replace 3D graph with fleet map, add desktop background patterns, rename Knowledge Base to Fleet
- FleetMap: service-centric host -> container -> service graph replacing the WebGL 3D force graph, with health coloring, hover-to-trace blast radius, and click-to-open - Desktop background: configurable CSS pattern picker in Settings -> Appearance (8 patterns, color/fill/opacity/fade/size/rotation), replacing the hardcoded ambient graph background - Fix missing data-orientation/data-disabled Tailwind custom variants so the shadcn Slider's track actually renders - Rename "Knowledge Base" app to "Fleet"; scope its table to the same fleet entities as the graph (compute-entity descendants + service) instead of all entities - Remove dead code: EntityGraph, GraphBackground, categories.ts, MultiSelectFilter (all superseded by the above) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
118
web/src/lib/stores/background.svelte.ts
Normal file
118
web/src/lib/stores/background.svelte.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { PATTERNS, type BackgroundPatternId } from '$lib/desktop-patterns'
|
||||
|
||||
export interface BackgroundConfig {
|
||||
pattern: BackgroundPatternId
|
||||
color: string // pattern foreground color, hex
|
||||
fillColor: string | null // fill behind the pattern (its "gaps"); null = transparent, i.e. the app's own theme background shows through
|
||||
opacity: number // 0..1
|
||||
fade: number // 0..1 — 0 disables the vignette mask entirely
|
||||
scale: number // pattern tile-size multiplier; 1 = natural size
|
||||
rotation: number // degrees, 0..359
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'oikos-desktop-bg'
|
||||
const VALID_IDS = new Set(PATTERNS.map((p) => p.id))
|
||||
|
||||
// Matches the previous hardcoded desktop background (bubbles, brand blue,
|
||||
// tuned low so it reads as texture rather than noise) so this feature ships
|
||||
// as "now configurable" rather than a visual regression on first load.
|
||||
const DEFAULTS: BackgroundConfig = {
|
||||
pattern: 'bubbles',
|
||||
color: '#444cf7',
|
||||
fillColor: null,
|
||||
opacity: 0.08,
|
||||
fade: 0,
|
||||
scale: 1,
|
||||
rotation: 0
|
||||
}
|
||||
|
||||
function clamp01(v: unknown, fallback: number): number {
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
if (!Number.isFinite(n)) return fallback
|
||||
return Math.min(1, Math.max(0, n))
|
||||
}
|
||||
|
||||
function clampScale(v: unknown): number {
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
if (!Number.isFinite(n)) return DEFAULTS.scale
|
||||
return Math.min(3, Math.max(0.4, n))
|
||||
}
|
||||
|
||||
function clampRotation(v: unknown): number {
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
if (!Number.isFinite(n)) return DEFAULTS.rotation
|
||||
return ((n % 360) + 360) % 360
|
||||
}
|
||||
|
||||
function isHexColor(c: unknown): c is string {
|
||||
return typeof c === 'string' && /^#[0-9a-fA-F]{6}$/.test(c)
|
||||
}
|
||||
|
||||
function storedConfig(): BackgroundConfig {
|
||||
if (typeof localStorage === 'undefined') return DEFAULTS
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return DEFAULTS
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return {
|
||||
pattern: VALID_IDS.has(parsed.pattern) ? parsed.pattern : DEFAULTS.pattern,
|
||||
color: isHexColor(parsed.color) ? parsed.color : DEFAULTS.color,
|
||||
fillColor: parsed.fillColor === null ? null : isHexColor(parsed.fillColor) ? parsed.fillColor : DEFAULTS.fillColor,
|
||||
opacity: clamp01(parsed.opacity, DEFAULTS.opacity),
|
||||
fade: clamp01(parsed.fade, DEFAULTS.fade),
|
||||
scale: clampScale(parsed.scale),
|
||||
rotation: clampRotation(parsed.rotation)
|
||||
}
|
||||
} catch {
|
||||
return DEFAULTS
|
||||
}
|
||||
}
|
||||
|
||||
let config: BackgroundConfig = $state(storedConfig())
|
||||
|
||||
function persist(): void {
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
||||
}
|
||||
|
||||
export function getBackground(): BackgroundConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
export function setBackgroundPattern(pattern: BackgroundPatternId): void {
|
||||
config = { ...config, pattern }
|
||||
persist()
|
||||
}
|
||||
|
||||
export function setPatternColor(color: string): void {
|
||||
if (!isHexColor(color)) return
|
||||
config = { ...config, color }
|
||||
persist()
|
||||
}
|
||||
|
||||
// null clears back to "auto" (transparent — the app's theme background
|
||||
// shows through the pattern's gaps).
|
||||
export function setFillColor(color: string | null): void {
|
||||
if (color !== null && !isHexColor(color)) return
|
||||
config = { ...config, fillColor: color }
|
||||
persist()
|
||||
}
|
||||
|
||||
export function setBackgroundOpacity(opacity: number): void {
|
||||
config = { ...config, opacity: clamp01(opacity, DEFAULTS.opacity) }
|
||||
persist()
|
||||
}
|
||||
|
||||
export function setBackgroundFade(fade: number): void {
|
||||
config = { ...config, fade: clamp01(fade, DEFAULTS.fade) }
|
||||
persist()
|
||||
}
|
||||
|
||||
export function setBackgroundScale(scale: number): void {
|
||||
config = { ...config, scale: clampScale(scale) }
|
||||
persist()
|
||||
}
|
||||
|
||||
export function setBackgroundRotation(rotation: number): void {
|
||||
config = { ...config, rotation: clampRotation(rotation) }
|
||||
persist()
|
||||
}
|
||||
Reference in New Issue
Block a user