Files
accounted/app/globals.css
T
Jakob WennbergandClaude Fable 5 d59e4708cf feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2) (#1133)
* feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2)

The concept's navigation, exactly, with all current functionality kept:

- Groups restructured per concept: top (Hem, Assistenten), ARBETA
  (Bokforing, Underlag, Transaktioner, Granskning, Kundfakturor,
  Leverantorsfakturor, Loner), ANALYS, DATA (Register fold +
  Importera/exportera), SKATT & BOKSLUT (Moms, Skattekonto, Viktiga
  datum, Bokslut fold). Entity/capability/dimension gating unchanged.
- Register and Bokslut are animated folds (grid-rows 0fr/1fr), children
  text-indented behind a hairline; closed by default, forced open by an
  active child route; state persists per user.
- Sidebar collapses to a 64px icon rail (toggle top of rail); width is
  one inline --nav-w CSS variable on #dash-shell that aside and <main>
  both read, so the panel follows in lockstep. Server-rendered from
  ui_state so first paint is right.
- Sticky bottom user block (avatar, name, active company) opening an
  upward user menu: identity, company-switcher flyout (search + building
  glyphs + roles + check, real switch mechanism via shared
  lib/company/switch-client), Installningar, Medlemmar och roller,
  Abonnemang, Hjalp, support, terracotta logout. Trial touchpoint kept.
- CompanySwitcher removed from desktop top (lives in the user menu now);
  mobile bottom nav + sheet unchanged.
- New migration 20260723120000: user_preferences.ui_state jsonb bag
  (founder-approved) + POST /api/user/ui-state (requireAuth, strict zod,
  merge semantics) with route tests.
- i18n: fold/collapse/menu keys added sv+en; deadlines -> "Viktiga
  datum", year_end -> "Arsbokslut" per concept.

Discord-community row deferred: no invite URL exists in the repo.
Badges stay the current two (Transaktioner, Granskning); an Underlag
count is a follow-up with lib/worklist.

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

* fix(nav): brand-mark sidebar header + auto-hiding scrollbars

Concept alignment feedback: the sidebar gets a header row (brand mark
left, collapse toggle right) hanging from the same top line as the
panel, instead of a lone right-aligned toggle.

Scrollbars go overlay-style app-wide: transparent at rest, revealed only
while their container scrolls (ScrollbarReveal stamps .is-scrolling via
one capture-phase document listener), fading out after 700ms idle. The
gutter stays reserved so revealing never shifts layout.

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

* fix(nav): company flyout opens downward + Discord community row

The flyout was bottom-anchored to its row and grew upward over the menu;
founder feedback: top-align with the row and grow downward. Adds the
Discord community row to the user menu (external invite link).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:28:07 +02:00

411 lines
10 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;
/* 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%;
--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%;
--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-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;
}
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;
}
}
/* 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) {
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 300ms ease-out;
}
.is-scrolling {
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;
}
}