Files
website/src/index.css
mroxso cde5be0874 fix: keep a window in place when focus switches (#16)
Raising a window made it visibly jump to the top-left corner and animate
back. Two things combined: the enter keyframes animated `transform`, which
overrode the inline `translate3d` a window's position is applied with, and
the window layer rendered the z-sorted list, so focusing a window moved its
DOM node and made the browser replay that animation.

Animate the standalone `scale` property instead — it composes with the
inline transform rather than replacing it — scope the animation to
`.os-window` (it was hitting every child of the layer, including the snap
preview, which is positioned the same way), and render windows in their
stable creation order, since the inline `z-index` already handles stacking.

Fixes #13


Claude-Session: https://claude.ai/code/session_01Trku191Ww2a2YmDWQF3fTS

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:09:00 +02:00

351 lines
8.8 KiB
CSS

@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-os-desktop: var(--os-desktop);
--color-os-titlebar: var(--os-titlebar);
--color-os-titlebar-inactive: var(--os-titlebar-inactive);
--color-os-window-border: var(--os-window-border);
--color-success: var(--success);
--color-warning: var(--warning);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--radius-xl: calc(var(--radius) + 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
/* Container utility replacement for v4 (previously configured in tailwind.config) */
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= 1400px) {
max-width: 1400px;
}
}
:root {
--radius: 0.75rem;
/* Desktop surface: a warm off-white the windows can sit on without glare. */
--os-desktop: hsl(40 22% 95%);
--os-desktop-dot: hsl(30 12% 82%);
--os-menubar: hsl(40 25% 99% / 0.72);
--os-titlebar: hsl(40 20% 98%);
--os-titlebar-inactive: hsl(40 12% 96%);
--os-window-border: hsl(30 12% 86%);
--os-shadow-focused: 0 24px 60px -12px hsl(265 30% 20% / 0.22), 0 8px 20px -8px hsl(265 30% 20% / 0.14);
--os-shadow-idle: 0 8px 24px -10px hsl(265 20% 20% / 0.14);
--background: hsl(0 0% 100%);
--foreground: hsl(20 14% 12%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(20 14% 12%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(20 14% 12%);
--primary: hsl(265 85% 60%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(30 16% 95%);
--secondary-foreground: hsl(20 14% 18%);
--muted: hsl(30 16% 95%);
--muted-foreground: hsl(25 8% 45%);
--accent: hsl(265 60% 96%);
--accent-foreground: hsl(265 60% 30%);
--destructive: hsl(0 72% 51%);
--destructive-foreground: hsl(0 0% 100%);
--success: hsl(150 60% 38%);
--warning: hsl(38 92% 48%);
--border: hsl(30 12% 88%);
--input: hsl(30 12% 88%);
--ring: hsl(265 85% 60%);
--chart-1: hsl(265 85% 60%);
--chart-2: hsl(190 70% 45%);
--chart-3: hsl(20 90% 58%);
--chart-4: hsl(150 55% 42%);
--chart-5: hsl(330 75% 60%);
--sidebar: hsl(40 20% 97%);
--sidebar-foreground: hsl(20 12% 26%);
--sidebar-primary: hsl(265 85% 60%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(30 16% 93%);
--sidebar-accent-foreground: hsl(20 14% 18%);
--sidebar-border: hsl(30 12% 88%);
--sidebar-ring: hsl(265 85% 60%);
}
.dark {
--os-desktop: hsl(265 12% 8%);
--os-desktop-dot: hsl(265 8% 20%);
--os-menubar: hsl(265 12% 12% / 0.72);
--os-titlebar: hsl(265 10% 16%);
--os-titlebar-inactive: hsl(265 10% 13%);
--os-window-border: hsl(265 8% 24%);
--os-shadow-focused: 0 24px 60px -12px hsl(265 40% 2% / 0.7), 0 8px 20px -8px hsl(265 40% 2% / 0.5);
--os-shadow-idle: 0 8px 24px -10px hsl(265 40% 2% / 0.5);
--background: hsl(265 10% 12%);
--foreground: hsl(40 12% 94%);
--card: hsl(265 10% 12%);
--card-foreground: hsl(40 12% 94%);
--popover: hsl(265 10% 14%);
--popover-foreground: hsl(40 12% 94%);
--primary: hsl(265 85% 70%);
--primary-foreground: hsl(265 40% 12%);
--secondary: hsl(265 8% 19%);
--secondary-foreground: hsl(40 12% 94%);
--muted: hsl(265 8% 19%);
--muted-foreground: hsl(265 6% 64%);
--accent: hsl(265 30% 22%);
--accent-foreground: hsl(265 60% 88%);
--destructive: hsl(0 65% 55%);
--destructive-foreground: hsl(0 0% 100%);
--success: hsl(150 55% 50%);
--warning: hsl(38 90% 58%);
--border: hsl(265 8% 22%);
--input: hsl(265 8% 22%);
--ring: hsl(265 85% 70%);
--chart-1: hsl(265 85% 70%);
--chart-2: hsl(190 70% 58%);
--chart-3: hsl(20 90% 65%);
--chart-4: hsl(150 55% 55%);
--chart-5: hsl(330 75% 70%);
--sidebar: hsl(265 11% 10%);
--sidebar-foreground: hsl(40 10% 88%);
--sidebar-primary: hsl(265 85% 70%);
--sidebar-primary-foreground: hsl(265 40% 12%);
--sidebar-accent: hsl(265 8% 18%);
--sidebar-accent-foreground: hsl(40 12% 94%);
--sidebar-border: hsl(265 8% 22%);
--sidebar-ring: hsl(265 85% 70%);
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border, currentcolor);
}
html,
body,
#root {
height: 100%;
}
body {
@apply bg-background text-foreground;
/* The OS owns its own scroll regions; the page itself never scrolls. */
overflow: hidden;
overscroll-behavior: none;
font-family: "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI",
ui-sans-serif, system-ui, "Helvetica Neue", Arial, sans-serif;
font-feature-settings: "cv11", "ss01";
font-variation-settings: "opsz" 32;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
}
/* ---------------------------------------------------------------- OS shell */
/*
* The desktop dot grid. A single repeating radial-gradient rather than an
* image, so it recolours with the theme and costs nothing to load.
*/
.os-desktop-surface {
background-color: var(--os-desktop);
background-image: radial-gradient(var(--os-desktop-dot) 1px, transparent 1px);
background-size: 22px 22px;
background-position: -1px -1px;
}
/* While a window is being dragged or resized, nothing else should react. */
body.os-dragging {
cursor: inherit;
user-select: none;
}
body.os-dragging iframe,
body.os-dragging .os-window-content {
pointer-events: none;
}
.os-window {
box-shadow: var(--os-shadow-idle);
transition: box-shadow 160ms ease;
}
.os-window[data-focused="true"] {
box-shadow: var(--os-shadow-focused);
}
.os-menubar-surface {
background-color: var(--os-menubar);
backdrop-filter: blur(14px) saturate(180%);
-webkit-backdrop-filter: blur(14px) saturate(180%);
}
/* Slim scrollbars so dense app content does not get a chunky OS bar. */
.os-scroll {
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.os-scroll::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.os-scroll::-webkit-scrollbar-track {
background: transparent;
}
.os-scroll::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 9999px;
border: 3px solid transparent;
background-clip: content-box;
}
.os-scroll::-webkit-scrollbar-thumb:hover {
background-color: var(--muted-foreground);
}
/* ------------------------------------------------------------- Animations */
/*
* Windows carry their position in an inline `transform: translate3d(...)`, so
* the opening animation must not touch `transform` — animating it would pin
* the window at 0,0 for the duration. The standalone `scale` property
* composes with the inline transform instead of replacing it.
*/
@keyframes os-window-in {
from {
opacity: 0;
scale: 0.96;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes os-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.os-window {
animation: os-window-in 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.os-fade-in {
animation: os-fade-in 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.os-window {
transition: none;
animation: os-fade-in 120ms ease-out;
}
}