Files
accounted/app/globals.css
T
Jakob Wennberg 60cc51fe21 feat(assistant): dock the panel into the frame, give the trigger a status channel (#1220)
* feat(assistant): dock the panel into the frame and give the trigger a status channel

Two things the panel could not do.

It covered the page it was talking about. Opening it on /invoices laid a 480px
curtain over the invoice, so verifying an answer meant closing the thing that
gave it. The page panel now gives up that width plus the frame's own gutter, and
the two float side by side. Docking applies at the compact width only: expanded
is a deliberate focus mode, where there is no page left to read anyway, so it
goes back to overlaying. Driven by a --agent-dock-w custom property because the
frame layout is a server component; globals.css seeds the default so the first
paint is not a jump, and below md nothing changes.

And a minimized session was silent. The agent could be three tool calls into a
booking, or finished ten minutes ago, and the pill said "Fortsätt med Anna"
either way, so the only way to find out was to reopen it. There is now one
status channel: the trigger spins with the current step while work runs and
shows an unread dot when a turn landed behind a hidden panel.

The channel is a reducer in a React-free module rather than a pair of booleans,
because a durable background run has to publish to the same one later. Its
'detached' state (working somewhere the user cannot see) is built and rendered
now even though nothing dispatches it in v1, so adding runs is a publisher and
not a redesign. Turn boundaries derive from the streaming flag rather than being
published per call site: a turn can end by completing, erroring, aborting or
being stopped, and missing one would leave the trigger claiming the agent is
still working forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record the Sonnet 5 ceiling, dock and status-channel decisions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:57:22 +02:00

518 lines
16 KiB
CSS

@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
/* ─────────────────────────────────────────────────────────────────────────
* Spacing scale: locked.
* Card padding: p-6. Section gap: space-y-8. Form/control gap: gap-4.
* Allowed Tailwind values: 1, 2, 3, 4, 6, 8, 10, 12. No 2.5 / 5.
* Sidebar width is md:w-64 (256px); main offset matches md:pl-64.
* ───────────────────────────────────────────────────────────────────────── */
:root {
color-scheme: light;
/* Sidebar column width (frame layout). Default for shells that don't
override it; the dashboard shell (#dash-shell) sets it inline from
user_preferences.ui_state (248px expanded / 64px collapsed rail) and
the nav toggle updates it client-side. aside and <main> both read the
variable so they stay in lockstep. */
--nav-w: 248px;
/* Width the docked assistant panel claims from the page panel. Default is
the plain frame gutter, so the layout is correct before any JS runs; the
agent provider raises it while the panel is docked and clears it again. */
--agent-dock-w: 10px;
/* Grayscale Chrome Palette: paper white + warm beige */
--background: 0 0% 100%;
--foreground: 0 0% 9%;
/* Warm-toned frame behind the floating page panel (frame layout) */
--frame: 40 18% 96%;
--card: 0 0% 100%;
--card-foreground: 0 0% 9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 100%;
--secondary: 40 11% 89%; /* Warm beige paper chip */
--secondary-foreground: 0 0% 9%;
--muted: 40 8% 93%;
--muted-foreground: 0 0% 40%;
--accent: 40 8% 93%;
--accent-foreground: 0 0% 9%;
--destructive: 6 36% 48%; /* Muted terracotta */
--destructive-foreground: 0 0% 100%;
--border: 45 5% 85%;
--input: 45 5% 85%;
--ring: 0 0% 9%;
/* Radius */
--radius: 0.5rem;
/* Success: Sage green */
--success: 155 25% 40%;
--success-foreground: 0 0% 100%;
/* Warning: Muted ochre */
--warning: 38 55% 50%;
--warning-foreground: 0 0% 9%;
/* Attn: darker ochre for the one-sentence attention line (.attn / AttnLine),
derived from --warning but AA-safe as small text on the page background. */
--attn: 38 50% 34%;
--warm-accent: 38 45% 52%;
/* Chart colors */
--chart-1: 155 25% 40%;
--chart-2: 6 36% 48%;
--chart-3: 38 55% 50%;
--chart-4: 0 0% 50%;
/* Custom tokens */
--font-display: var(--font-hedvig-serif), Georgia, serif;
--font-body: var(--font-geist-sans), system-ui, sans-serif;
/* Shadows: barely-there; chrome relies on hairline borders */
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.03), 0 1px 1px rgba(15, 23, 42, 0.02);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.03);
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.05);
/* Animation */
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-base: 300ms;
--duration-slow: 500ms;
}
.dark {
color-scheme: dark;
--background: 0 0% 7%;
--foreground: 0 0% 87%;
--frame: 0 0% 5%;
--card: 0 0% 11%;
--card-foreground: 0 0% 87%;
--popover: 0 0% 11%;
--popover-foreground: 0 0% 87%;
--primary: 0 0% 87%;
--primary-foreground: 0 0% 7%;
--secondary: 0 0% 14%;
--secondary-foreground: 0 0% 87%;
--muted: 0 0% 16%;
--muted-foreground: 0 0% 50%;
--accent: 0 0% 16%;
--accent-foreground: 0 0% 87%;
--destructive: 6 32% 56%;
--destructive-foreground: 0 0% 100%;
--border: 45 4% 18%;
--input: 45 4% 18%;
--ring: 0 0% 87%;
--success: 155 22% 48%;
--success-foreground: 0 0% 100%;
--warning: 38 50% 55%;
--warning-foreground: 38 50% 90%;
--attn: 38 45% 62%;
--warm-accent: 38 42% 58%;
--chart-1: 155 22% 48%;
--chart-2: 6 32% 56%;
--chart-3: 38 50% 55%;
--chart-4: 0 0% 55%;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.45);
}
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-frame: hsl(var(--frame));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-success: hsl(var(--success));
--color-success-foreground: hsl(var(--success-foreground));
--color-warning: hsl(var(--warning));
--color-warning-foreground: hsl(var(--warning-foreground));
--color-attn: hsl(var(--attn));
--color-warm-accent: hsl(var(--warm-accent));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--font-sans: var(--font-body);
--font-mono: var(--font-geist-mono), monospace;
--font-serif: var(--font-display);
}
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: var(--font-body);
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Display typography - Fraunces */
.font-display {
font-family: var(--font-display);
font-optical-sizing: auto;
}
/* Layered so utility classes (e.g. font-sans on a pane title) can override:
unlayered element rules would beat Tailwind's layered utilities. */
@layer base {
h1, h2, h3 {
font-family: var(--font-display);
font-weight: 500;
letter-spacing: -0.015em;
font-optical-sizing: auto;
}
}
/* Number styling - tabular for alignment */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Text balance for better line wrapping on descriptions */
.text-balance {
text-wrap: balance;
}
/* Safe area padding for mobile devices with notch/home indicator */
.safe-area-bottom {
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
}
/* Main content padding: clears bottom nav + safe area on mobile */
.safe-area-main-padding {
padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
}
/* Animation Keyframes */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes gentlePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Three-dot "typing" indicator used by the agent chat pre-token bubble.
Each dot rides the same wave but with staggered animation-delay so the
bounce reads left-to-right. */
@keyframes typingDot {
0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }
30% { transform: translateY(-3px); opacity: 1; }
}
.animate-typing-dot {
animation: typingDot 1.1s ease-in-out infinite;
}
/* Marching-ants dashed border used by the import entity selector. */
@keyframes marchingAnts {
to { stroke-dashoffset: -14; }
}
.animate-marching-ants {
animation: marchingAnts 1.2s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-typing-dot {
animation: gentlePulse 1.6s ease-in-out infinite;
transform: none;
}
.animate-marching-ants {
animation: none;
}
}
/* ─────────────────────────────────────────────────────────────────────────
* Enter/exit animation utilities (animate-in / animate-out vocabulary).
* The codebase uses the shadcn/tailwindcss-animate classes but no animate
* plugin was ever installed, so Tailwind v4 silently dropped them all:
* every popover, dialog and slide-over appeared instantly. These implement
* the exact subset in use, plugin-free. The duration and ease utilities
* compose via Tailwind v4's --tw-duration and --tw-ease variables; the
* global prefers-reduced-motion block collapses them like everything else.
* ───────────────────────────────────────────────────────────────────────── */
@keyframes accEnter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0)
scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), 1);
}
}
@keyframes accExit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0)
scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), 1);
}
}
@utility animate-in {
animation-name: accEnter;
animation-duration: var(--tw-duration, 150ms);
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
animation-fill-mode: both;
}
@utility animate-out {
animation-name: accExit;
animation-duration: var(--tw-duration, 150ms);
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
animation-fill-mode: both;
}
@utility fade-in { --tw-enter-opacity: 0; }
@utility fade-in-0 { --tw-enter-opacity: 0; }
@utility fade-out { --tw-exit-opacity: 0; }
@utility fade-out-0 { --tw-exit-opacity: 0; }
@utility zoom-in-95 { --tw-enter-scale: 0.95; }
@utility zoom-out-95 { --tw-exit-scale: 0.95; }
@utility zoom-in-98 { --tw-enter-scale: 0.98; }
@utility zoom-out-98 { --tw-exit-scale: 0.98; }
@utility slide-in-from-top { --tw-enter-translate-y: -100%; }
@utility slide-in-from-top-1 { --tw-enter-translate-y: -0.25rem; }
@utility slide-in-from-top-2 { --tw-enter-translate-y: -0.5rem; }
@utility slide-in-from-top-full { --tw-enter-translate-y: -100%; }
@utility slide-in-from-bottom { --tw-enter-translate-y: 100%; }
@utility slide-in-from-bottom-1 { --tw-enter-translate-y: 0.25rem; }
@utility slide-in-from-bottom-2 { --tw-enter-translate-y: 0.5rem; }
@utility slide-in-from-left { --tw-enter-translate-x: -100%; }
@utility slide-in-from-left-1 { --tw-enter-translate-x: -0.25rem; }
@utility slide-in-from-left-2 { --tw-enter-translate-x: -0.5rem; }
@utility slide-in-from-right { --tw-enter-translate-x: 100%; }
@utility slide-in-from-right-2 { --tw-enter-translate-x: 0.5rem; }
@utility slide-in-from-right-full { --tw-enter-translate-x: 100%; }
@utility slide-out-to-top { --tw-exit-translate-y: -100%; }
@utility slide-out-to-bottom { --tw-exit-translate-y: 100%; }
@utility slide-out-to-left { --tw-exit-translate-x: -100%; }
@utility slide-out-to-right { --tw-exit-translate-x: 100%; }
@utility slide-out-to-right-full { --tw-exit-translate-x: 100%; }
/* Staggered entrance animation */
.stagger-enter > * {
animation: slideUp var(--duration-slow) var(--ease-out) both;
}
.stagger-enter > *:nth-child(1) { animation-delay: 0ms; }
.stagger-enter > *:nth-child(2) { animation-delay: 80ms; }
.stagger-enter > *:nth-child(3) { animation-delay: 160ms; }
.stagger-enter > *:nth-child(4) { animation-delay: 240ms; }
.stagger-enter > *:nth-child(5) { animation-delay: 320ms; }
.stagger-enter > *:nth-child(6) { animation-delay: 400ms; }
.stagger-enter > *:nth-child(7) { animation-delay: 480ms; }
.stagger-enter > *:nth-child(8) { animation-delay: 560ms; }
.stagger-enter > *:nth-child(9) { animation-delay: 640ms; }
.stagger-enter > *:nth-child(10) { animation-delay: 720ms; }
/* Utility animations */
.animate-fade-in {
animation: fadeIn var(--duration-base) var(--ease-out);
}
.animate-slide-up {
animation: slideUp var(--duration-slow) var(--ease-out);
}
.animate-scale-in {
animation: scaleIn var(--duration-base) var(--ease-out);
}
/* Soft focus states */
.focus-ring:focus-visible {
outline: none;
box-shadow: 0 0 0 2px hsl(var(--background)),
0 0 0 4px hsl(var(--primary) / 0.35);
}
/* Overlay-style scrollbars: invisible at rest, revealed only while the
container is actually scrolling. ScrollbarReveal (mounted in the root
layout) toggles .is-scrolling on the scrolled element; the thumb fades
back out when scrolling stops. The gutter is reserved either way, so
revealing never shifts layout. */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
}
.is-scrolling::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.25);
}
@supports (scrollbar-color: auto) {
/* Layered: the universal transition must not beat Tailwind's layered
transition-* utilities (unlayered rules win over layers regardless of
specificity: this silently killed every width/margin/color transition
in the app). scrollbar-color still animates via the base default. */
@layer base {
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 300ms ease-out;
}
}
.is-scrolling {
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
}
/* Hover-reveal fallback for touch: coarse pointers have no hover, so the
controls that desktop reveals on hover (row checkboxes, chevrons, quiet
row actions) stay visible. Targets exactly the opacity-0 +
group-hover:opacity-100 pairing and nothing else. */
@media (pointer: coarse) {
.group .opacity-0.group-hover\:opacity-100 {
opacity: 1;
}
}
/* Always-visible thumb for modal scroll containers (DialogContent,
SlideOverBody): inside an overlay the scrollbar is the only cue that
content continues below the fold, so the at-rest auto-hide would read
as "the dialog is cut off". */
.scrollbar-visible::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.25);
}
@supports (scrollbar-color: auto) {
.scrollbar-visible {
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
}
/* Hide scrollbar utility */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide number input spinners */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Selection color */
::selection {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--foreground));
}
.dark ::selection {
background: hsl(var(--primary) / 0.3);
color: hsl(var(--foreground));
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.stagger-enter > * {
animation: none;
opacity: 1;
}
}