diff --git a/VERSION b/VERSION index e815b86..04a373e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.15.1 +0.16.0 diff --git a/web/index.html b/web/index.html index 63aff8c..578fefc 100644 --- a/web/index.html +++ b/web/index.html @@ -4,12 +4,6 @@ Oikos - - - diff --git a/web/public/fonts/JetBrainsMono-Bold.woff2 b/web/public/fonts/JetBrainsMono-Bold.woff2 new file mode 100644 index 0000000..3a4e333 Binary files /dev/null and b/web/public/fonts/JetBrainsMono-Bold.woff2 differ diff --git a/web/public/fonts/JetBrainsMono-Regular.woff2 b/web/public/fonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 0000000..5858873 Binary files /dev/null and b/web/public/fonts/JetBrainsMono-Regular.woff2 differ diff --git a/web/public/fonts/VT323-Regular.woff2 b/web/public/fonts/VT323-Regular.woff2 new file mode 100644 index 0000000..fd760b5 Binary files /dev/null and b/web/public/fonts/VT323-Regular.woff2 differ diff --git a/web/src/app.css b/web/src/app.css index b11dd96..00d8b23 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -2,6 +2,29 @@ @custom-variant dark (&:is(.dark *)); +/* ── Self-hosted type (cyberspace terminal aesthetic) ── */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/JetBrainsMono-Regular.woff2') format('woff2'); +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/fonts/JetBrainsMono-Bold.woff2') format('woff2'); +} +@font-face { + font-family: 'VT323'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/VT323-Regular.woff2') format('woff2'); +} + /* bits-ui components (Slider, and any future orientation/disabled-aware primitive) style themselves via shorthand data-* variants that Tailwind v4 doesn't ship — it only auto-generates variants for bare boolean data @@ -14,13 +37,16 @@ @custom-variant data-disabled (&[data-disabled]); @theme inline { - --font-sans: 'DM Sans', system-ui, sans-serif; - --font-mono: 'DM Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; - --font-heading: 'Inknut Antiqua', Georgia, serif; - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); + --font-sans: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + --font-heading: 'JetBrains Mono', ui-monospace, Menlo, monospace; + /* Square corners across the whole radius scale (--radius is pinned to 0 + by both themes below). Kept as a 4-step scale so any future softer theme + can relax just --radius and get graded corners back for free. */ + --radius-sm: var(--radius); + --radius-md: var(--radius); --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); + --radius-xl: var(--radius); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -57,103 +83,107 @@ --color-chart-5: var(--chart-5); } -/* ── Terracotta Light Theme ── */ +/* ── Cyberspace Light (black ink on warm cream paper) ── + Ported from cyberspace.online's 3-color model (fg/bg/fgDim). Light and + dark are exact inverses of the same cream (#efe5c0). Emphasis is by + inversion (primary = fg ink), borders are fg-derived hairlines, and the + radius is 0 so every surface is square. */ :root { - --radius: 0.625rem; - --background: oklch(0.94 0.02 55); - --foreground: oklch(0.18 0.03 45); - --card: oklch(0.91 0.025 55); - --card-foreground: oklch(0.18 0.03 45); - --popover: oklch(0.91 0.025 55); - --popover-foreground: oklch(0.18 0.03 45); - --primary: oklch(0.55 0.14 45); - --primary-foreground: oklch(0.95 0.02 55); - --secondary: oklch(0.86 0.03 55); - --secondary-foreground: oklch(0.18 0.03 45); - --muted: oklch(0.86 0.025 55); - --muted-foreground: oklch(0.45 0.04 45); - --accent: oklch(0.84 0.035 55); - --accent-foreground: oklch(0.18 0.03 45); - --destructive: oklch(0.5 0.2 25); - --destructive-foreground: oklch(0.95 0.02 55); - --border: oklch(0.6 0.08 45); - --input: oklch(0.78 0.04 55); - --ring: oklch(0.55 0.14 45); - --chart-1: oklch(0.55 0.14 45); - --chart-2: oklch(0.65 0.1 70); - --chart-3: oklch(0.5 0.08 30); - --chart-4: oklch(0.6 0.06 90); - --chart-5: oklch(0.4 0.04 45); - --sidebar: oklch(0.9 0.025 55); - --sidebar-foreground: oklch(0.18 0.03 45); - --sidebar-primary: oklch(0.55 0.14 45); - --sidebar-primary-foreground: oklch(0.95 0.02 55); - --sidebar-accent: oklch(0.84 0.035 55); - --sidebar-accent-foreground: oklch(0.18 0.03 45); - --sidebar-border: oklch(0.6 0.08 45); - --sidebar-ring: oklch(0.55 0.14 45); - --success: #3fb950; - --warning: #d29922; + --radius: 0px; + --background: #efe5c0; + --foreground: #000000; + --card: #efe5c0; + --card-foreground: #000000; + --popover: #efe5c0; + --popover-foreground: #000000; + --primary: #000000; + --primary-foreground: #efe5c0; + --secondary: #e0d6b0; + --secondary-foreground: #000000; + --muted: #e6dcc0; + --muted-foreground: #3a3a3a; + --accent: #000000; + --accent-foreground: #efe5c0; + --destructive: #9d0006; + --destructive-foreground: #efe5c0; + --border: color-mix(in oklab, #000000 22%, transparent); + --input: color-mix(in oklab, #000000 30%, transparent); + --ring: #000000; + --chart-1: #000000; + --chart-2: #3a3a3a; + --chart-3: #b57614; + --chart-4: #79740e; + --chart-5: #076678; + --sidebar: #efe5c0; + --sidebar-foreground: #000000; + --sidebar-primary: #000000; + --sidebar-primary-foreground: #efe5c0; + --sidebar-accent: #e0d6b0; + --sidebar-accent-foreground: #000000; + --sidebar-border: color-mix(in oklab, #000000 22%, transparent); + --sidebar-ring: #000000; + --success: #79740e; + --warning: #b57614; --bg: var(--background); --bg-surface: var(--card); - --bg-deeper: oklch(0.98 0.01 55); + --bg-deeper: #e6dcc0; --bg-hover: var(--secondary); --bg-active: var(--accent); --text: var(--foreground); --text-muted: var(--muted-foreground); - --accent-blue: var(--primary); + --accent-blue: #076678; --accent-green: var(--success); --accent-red: var(--destructive); --accent-orange: var(--warning); } -/* ── Carbon Dark Theme ── */ +/* ── Cyberspace Dark (warm cream on black) — exact inverse of Light ── */ .dark { - --radius: 0.625rem; - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.371 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.985 0 0); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.6 0.15 220); - --chart-3: oklch(0.5 0.1 160); - --chart-4: oklch(0.7 0.08 60); - --chart-5: oklch(0.45 0.05 320); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.439 0 0); - --success: #3fb950; - --warning: #d29922; + --radius: 0px; + --background: #000000; + --foreground: #efe5c0; + --card: #000000; + --card-foreground: #efe5c0; + --popover: #000000; + --popover-foreground: #efe5c0; + --primary: #efe5c0; + --primary-foreground: #000000; + --secondary: #1a1a1a; + --secondary-foreground: #efe5c0; + --muted: #141414; + --muted-foreground: #a89984; + --accent: #efe5c0; + --accent-foreground: #000000; + --destructive: #cc241d; + --destructive-foreground: #efe5c0; + --border: color-mix(in oklab, #efe5c0 22%, transparent); + --input: color-mix(in oklab, #efe5c0 30%, transparent); + --ring: #efe5c0; + --chart-1: #efe5c0; + --chart-2: #a89984; + --chart-3: #fabd2f; + --chart-4: #b8bb26; + --chart-5: #83a598; + --sidebar: #000000; + --sidebar-foreground: #efe5c0; + --sidebar-primary: #efe5c0; + --sidebar-primary-foreground: #000000; + --sidebar-accent: #1a1a1a; + --sidebar-accent-foreground: #efe5c0; + --sidebar-border: color-mix(in oklab, #efe5c0 22%, transparent); + --sidebar-ring: #efe5c0; + --success: #b8bb26; + --warning: #fabd2f; --bg: var(--background); --bg-surface: var(--card); - --bg-deeper: oklch(0.11 0 0); + --bg-deeper: #050505; --bg-hover: var(--secondary); --bg-active: var(--accent); --text: var(--foreground); --text-muted: var(--muted-foreground); - --accent-blue: var(--sidebar-primary); + --accent-blue: #83a598; --accent-green: var(--success); --accent-red: var(--destructive); --accent-orange: var(--warning); @@ -273,14 +303,17 @@ color: var(--card-foreground); border: 1px solid var(--border); border-radius: var(--radius-lg); - box-shadow: 0 8px 24px oklch(0 0 0 / 0.18); + /* Hard offset shadow (DOS-style), not a soft drop shadow — keeps the + border-driven system and still separates stacked windows, which matters + because --card now equals the desktop background. */ + box-shadow: 3px 3px 0 0 var(--border); overflow: hidden; outline: none; } [data-wm-window][data-wm-focused] { border-color: var(--ring); - box-shadow: 0 12px 32px oklch(0 0 0 / 0.28); + box-shadow: 3px 3px 0 0 var(--ring); } [data-wm-window][data-wm-dragging], @@ -417,8 +450,177 @@ a:hover { } .markdown-body a { color: var(--primary); - text-decoration: none; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; } .markdown-body a:hover { - text-decoration: underline; + text-decoration-style: solid; +} + +/* ── cyberspace idioms (work in any theme; idiomatic for the terminal look) ── */ + +/* Pixel/terminal display face for stylized wordmarks & hero titles. Maps to + VT323 when available, JetBrains Mono fallback. Headings use --font-heading + (JetBrains Mono) by default; opt into this per-element for the "de-imagined" + title voice. */ +.font-vt { + font-family: 'VT323', var(--font-mono); + letter-spacing: 0.02em; +} + +/* Bordered square card with a 2px fg focus ring — the universal cyberspace + surface. Use on any container that wants the terminal-box look. */ +.terminal-box { + border: 1px solid var(--border); + border-radius: 0; +} +.terminal-box:focus-within { + box-shadow: 0 0 0 2px var(--ring); +} + +/* ════════════════════════════════════════════════════════════════════════ + TERMINAL DESIGN SYSTEM — cyberspace.online adoption + These rules are UNLAYERED (written after Tailwind's @layer utilities), + so they override utility classes — shadow-*, ring-*, rounded-* — on the + shadcn data-slot primitives regardless of specificity. The system is: + • border-driven — hairline borders separate surfaces; no soft shadows + • square — every corner is 0 (also enforced via --radius tokens) + • focus by color — :focus signals via border/text color, not glow rings + • DOS modals — dialogs get a double fg-line frame + hatched corner + ════════════════════════════════════════════════════════════════════════ */ +:root { + --dos-border: 1px; /* DOS frame line width (used doubled for the modal edge) */ + --dos-dither: 4px; /* hatch tile size for the modal corner shadow */ + --dos-offset: 7px; /* how far the hatched corner sits out from the frame */ +} + +/* ── Containers → terminal-box: solid hairline border, square, no shadow. + Replaces shadcn's `shadow-xs ring-1 ring-foreground/10 rounded-xl/md`. ── */ +[data-slot='card'], +[data-slot='popover-content'], +[data-slot='hover-card-content'], +[data-slot='dropdown-menu-content'], +[data-slot='select-content'], +[data-slot='tooltip-content'], +[data-slot='sheet-content'], +[data-slot='menubar-content'], +[data-slot='alert-dialog-content'] { + border: 1px solid var(--border); + border-radius: 0; + box-shadow: none; +} + +/* ── Dialogs → the DOS frame. A 1px fg border, then a bg gap, then a 1px fg + ring (two stacked box-shadows) = the double-line terminal window edge. ── */ +[data-slot='dialog-content'] { + position: fixed; + border: var(--dos-border) solid var(--foreground); + border-radius: 0; + box-shadow: + 0 0 0 var(--dos-border) var(--background), + 0 0 0 calc(var(--dos-border) * 2) var(--foreground); +} +/* Hatched corner shadow (fg-dim 45° checks) bottom-right — the DOS window + tell. Clipped to a small square outside the frame. */ +[data-slot='dialog-content']::after { + content: ''; + position: absolute; + right: calc(-1 * var(--dos-offset)); + bottom: calc(-1 * var(--dos-offset)); + width: var(--dos-offset); + height: var(--dos-offset); + pointer-events: none; + background-color: var(--muted-foreground); + background-image: + linear-gradient( + 45deg, + var(--background) 25%, + transparent 25%, + transparent 75%, + var(--background) 75% + ), + linear-gradient( + 45deg, + var(--background) 25%, + transparent 25%, + transparent 75%, + var(--background) 75% + ); + background-size: var(--dos-dither) var(--dos-dither); + background-position: + 0 0, + calc(var(--dos-dither) / 2) calc(var(--dos-dither) / 2); +} + +/* ── Overlays: opaque-ish theme background, no blur (terminal, not glass). ── */ +[data-slot='dialog-overlay'], +[data-slot='alert-dialog-overlay'], +[data-slot='sheet-overlay'] { + background-color: color-mix(in oklab, var(--background) 82%, transparent); + backdrop-filter: none; +} + +/* ── Inputs: square, no resting shadow; focus = solid fg border (cyberspace + signals focus by border color, not a glow ring). ── */ +[data-slot='input'], +[data-slot='textarea'] { + border-radius: 0; + box-shadow: none; +} +[data-slot='input']:focus, +[data-slot='input']:focus-visible, +[data-slot='textarea']:focus, +[data-slot='textarea']:focus-visible { + border-color: var(--foreground); + box-shadow: none; + outline: none; +} + +/* ── Buttons: square, no resting shadow. Focus keeps the cva border-color + shift to fg (focus-visible:border-ring) rather than a ring. ── */ +[data-slot='button'] { + border-radius: 0; + box-shadow: none; +} + +/* ── Badges & tabs: square. ── */ +[data-slot='badge'] { + border-radius: 0; +} +[data-slot='tabs-trigger'], +[data-slot='tabs-list'] { + border-radius: 0; +} + +/* ── Window controls (titlebar minimize/maximize/close + taskbar item close). + ONE style across all window chrome: bordered square, invert on hover — the + same idiom as desktop icons and taskbar buttons. Close is intentionally not + "danger"-colored so every control reads as the same component. ── */ +.win-ctrl { + align-items: center; + justify-content: center; + border: 1px solid var(--border); + background: var(--background); + color: var(--muted-foreground); + transition: + color 0.12s, + border-color 0.12s, + background 0.12s; +} +.win-ctrl:hover { + border-color: var(--foreground); + background: var(--foreground); + color: var(--background); +} + +/* Pin every window titlebar to one exact height, targeted by attribute so it + holds for every window regardless of its titlebar classes — including + already-open wmkit windows whose titlebar markup can lag behind on HMR + (component HMR is unreliable for wmkit windows; CSS HMR is not). + `flex: 0 0 2.25rem` is the strongest guarantee a flex item won't grow or + shrink — it stops a scrolling/growing content pane from compressing the + titlebar (the chat-window symptom). */ +[data-wm-drag] { + flex: 0 0 2.25rem !important; } diff --git a/web/src/lib/components/ChatThread.svelte b/web/src/lib/components/ChatThread.svelte index 5fdf343..c5221a0 100644 --- a/web/src/lib/components/ChatThread.svelte +++ b/web/src/lib/components/ChatThread.svelte @@ -59,7 +59,6 @@ } = $props() let input = $state(typeof initialDraft === 'string' ? initialDraft : '') - let messagesEnd = $state(null) let scrolledUp = $state(false) let container = $state(null) @@ -150,12 +149,17 @@ } // Auto-scroll to bottom on new messages (or a freshly-raised question) — - // unless user scrolled up to read. + // unless user scrolled up to read. Sets scrollTop on the messages container + // directly instead of `scrollIntoView`, which walks ancestors and forces a + // reflow that can momentarily perturb the window titlebar height. $effect(() => { void messages void question if (streaming || !scrolledUp) { - setTimeout(() => messagesEnd?.scrollIntoView({ behavior: 'smooth' }), 50) + setTimeout( + () => container?.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }), + 50 + ) } }) @@ -326,7 +330,6 @@ {#if question} {/if} -
diff --git a/web/src/lib/components/ConfigBackground.svelte b/web/src/lib/components/ConfigBackground.svelte index 55fb7e1..6835210 100644 --- a/web/src/lib/components/ConfigBackground.svelte +++ b/web/src/lib/components/ConfigBackground.svelte @@ -106,7 +106,7 @@ // pulse brightness const alpha = p.pulse * (0.35 + 0.15 * Math.sin(t * 1.2 + p.phase)) - ctx.fillStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})` + ctx.fillStyle = dark ? `rgba(168,153,132,${alpha})` : `rgba(58,58,58,${alpha})` ctx.beginPath() ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2) ctx.fill() @@ -123,7 +123,7 @@ const dist = dx * dx + dy * dy if (dist < CONNECT_DIST * CONNECT_DIST) { const alpha = (1 - Math.sqrt(dist) / CONNECT_DIST) * 0.18 - ctx.strokeStyle = dark ? `rgba(140,175,230,${alpha})` : `rgba(60,90,140,${alpha})` + ctx.strokeStyle = dark ? `rgba(168,153,132,${alpha})` : `rgba(58,58,58,${alpha})` ctx.beginPath() ctx.moveTo(a.x, a.y) ctx.lineTo(b.x, b.y) @@ -136,7 +136,7 @@ const cx = w / 2, cy = h / 2 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 ? '0,0,0' : '239,229,192' scrim.addColorStop(0, `rgba(${base},0.72)`) scrim.addColorStop(0.35, `rgba(${base},0.40)`) scrim.addColorStop(0.65, `rgba(${base},0.08)`) diff --git a/web/src/lib/components/FleetMap.svelte b/web/src/lib/components/FleetMap.svelte index b2e8422..fb069ec 100644 --- a/web/src/lib/components/FleetMap.svelte +++ b/web/src/lib/components/FleetMap.svelte @@ -1101,7 +1101,7 @@ font-size: 12px; font-weight: 500; border: 1px solid var(--border); - border-radius: 999px; + border-radius: 0; background: transparent; color: var(--muted-foreground); cursor: pointer; @@ -1114,9 +1114,12 @@ border-color: var(--primary); } .chip.on { - color: var(--foreground); - background: var(--secondary); - border-color: var(--primary); + color: var(--background); + background: var(--foreground); + border-color: var(--foreground); + } + .chip.on b { + color: var(--background); } .chip .dot { width: 8px; diff --git a/web/src/lib/components/RasterImage.svelte b/web/src/lib/components/RasterImage.svelte new file mode 100644 index 0000000..964b1ff --- /dev/null +++ b/web/src/lib/components/RasterImage.svelte @@ -0,0 +1,206 @@ + + + + +
+ {#if showSkeleton} + + {/if} + {#if !plain} + + {/if} + { + tainted = true + loaded = true + }} + /> +
+ + diff --git a/web/src/lib/components/desktop-shell/DesktopIcon.svelte b/web/src/lib/components/desktop-shell/DesktopIcon.svelte index fa6be89..e0fc510 100644 --- a/web/src/lib/components/desktop-shell/DesktopIcon.svelte +++ b/web/src/lib/components/desktop-shell/DesktopIcon.svelte @@ -80,25 +80,30 @@ diff --git a/web/src/lib/components/desktop-shell/Taskbar.svelte b/web/src/lib/components/desktop-shell/Taskbar.svelte index 285033d..3fda2a8 100644 --- a/web/src/lib/components/desktop-shell/Taskbar.svelte +++ b/web/src/lib/components/desktop-shell/Taskbar.svelte @@ -53,10 +53,10 @@ } -
+
- {VERSION} + {VERSION}
diff --git a/web/src/lib/components/desktop-shell/WindowLayer.svelte b/web/src/lib/components/desktop-shell/WindowLayer.svelte index 9255f07..4eeb28c 100644 --- a/web/src/lib/components/desktop-shell/WindowLayer.svelte +++ b/web/src/lib/components/desktop-shell/WindowLayer.svelte @@ -67,18 +67,18 @@
{win.title} -
+