d0fbc2b616
* refactor(ui): app-wide UI/UX consistency pass
Net: +1,159 / −1,373 LOC across 77 files. No new features, no behavior
changes. Locks in a uniform design system across every dashboard surface.
What changed:
- **Foundation**: sidebar width 232→256px (md:w-64), spacing scale locked
(Tailwind 1/2/3/4/6/8/10/12; 2.5/5 forbidden), card padding p-6 default
(p-4 for compact metric cards), space-y-8 between page sections.
- **Tables unified**: all 33 thead blocks now share the Resultatrapport
pattern via shadcn Table primitive (text-[11px] font-medium uppercase
tracking-wider text-muted-foreground). Hand-rolled <table> instances
converted where they were data tables; form/edit grids kept distinct.
- **Status badges unified**: every status indicator routes through
shadcn <Badge variant>. Eliminated raw Tailwind colors
(bg-amber-100, bg-emerald-500/10, bg-blue-100, bg-purple-100, etc.)
in favor of the gnubok semantic palette (success=sage, warning=ochre,
destructive=terracotta).
- **Empty states unified**: list pages migrated from hand-rolled
"flex flex-col items-center py-12" divs to the EmptyState primitive.
- **Loading skeletons unified**: hand-rolled bg-muted rounded animate-pulse
divs replaced with shadcn <Skeleton> across 15 files.
- **Touch targets**: 6 back-buttons + edit-pencil + inbox delete bumped
from 24/32/36px to shadcn's 40px icon default. Added aria-labels on
9 icon-only navigation buttons.
- **Date formatting**: formatDate() for accounting data (ISO yyyy-MM-dd,
table-friendly) vs formatDateLong() for metadata (Swedish long form).
Raw {x.invoice_date} renderings routed through formatDate() in 18 sites.
- **Toast titles**: eliminated 33 generic "Fel" titles. Each toast title
now carries the action ("Kunde inte skapa lönekörning" etc.) with
description carrying the error detail.
- **Page-level cleanups**:
- Dashboard: dropped greeting hero + Snabbåtgärder/Att hantera nav
duplicates + Visa detaljer collapsible.
- Reports: 5-col mega-menu replaced with left-rail layout
(new ReportsNav component).
- Bookkeeping: fixed layout jump between Verifikationer/Ny verifikation
tabs (moved FiscalYearSelector inside journal tab).
- Bookkeeping: added voucher sort (A1 first / latest first) alongside
existing date sort. Required matching API param sort_by.
- KPI page: FiscalYearSelector instead of raw <select>; InfoTooltip
instead of inline info-button toggle; bigger numbers.
- Salary section: enum values translated to Swedish labels, mobile
table collapses to Anställd+Netto on <md, KPI typography aligned
with dashboard.
- Invoice forms: styled RequiredMark + aria-required, tabular-nums
on amount inputs.
- **CLAUDE.md**: new "Design System Tokens" subsection documents the
locked spacing scale, primitives table, typography rules, date helpers,
and forbidden patterns so future contributors don't drift.
Tests: 2,906 passing (unchanged). Lint: unchanged from main baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback (Greptile + compliance bot)
- **formatDate / formatDateLong timezone fix**: switch from new Date() to
parseISO. Bare yyyy-MM-dd strings are now parsed as local midnight rather
than UTC midnight, eliminating the off-by-one display in west-of-UTC
timezones flagged by Greptile.
- **DashboardContentProps cleanup**: removed unused firstName and settings
fields from the interface, and the corresponding fetch (profiles table)
+ computation in app/(dashboard)/page.tsx. The greeting was dropped in
the dashboard cleanup; these props were dead weight.
- **Voucher sort behavior documented**: extended the comment in the journal
entries API route to explain why voucher sort intentionally uses strict
fiscal_period_id filtering (BFL 5 kap 6–7 §§ — voucher numbers are
series-scoped within a fiscal year). The row-count delta between date
sort and voucher sort is now a documented design choice.
- **delete_last_voucher migration + draft-delete test included**: the UI
already shipped the "Radera utkast" path in the previous commit; this
pulls in the backing RPC migration that allows draft deletes (with the
full safety logic — drafts skip series/period checks since they have
voucher_number=0, posted entries go through the existing unchanged
path). This was originally meant for a separate PR but the UI shipped
half the feature without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(migration): rename to match applied version
The function delete_last_voucher is already applied to the production DB
under version 20260509103736 (verified via pg_get_functiondef — exact
byte-for-byte match to file content). The previous file timestamp
20260509120000 would cause a fresh `supabase db push` to attempt re-applying
under a different version row. Renaming the file aligns local tracking
with what the database actually has.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address compliance bot findings (payroll label + VAT visibility)
- sick_karens label: drop "(första sjukdagen)" qualifier. Per sjuklönelagen
6 §, karensavdrag is a single calculated amount (20% of one week's
sjuklön) deducted from the first sick day's pay — not bounded to the
first day. The qualifier could mislead users when the first sick day
and return-to-work span a weekend. Swedish-payroll bot recommendation.
- Omvänd skattskyldighet badge: variant outline → warning. The reverse-
charge indicator is compliance-critical (ML 16 kap) — missing it leads
to incorrect input VAT deduction. Outline was too subtle; warning's
ochre fill matches its semantic weight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
365 lines
8.9 KiB
CSS
365 lines
8.9 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;
|
|
/* Grayscale Chrome Palette */
|
|
|
|
--background: 0 0% 97%;
|
|
--foreground: 0 0% 9%;
|
|
|
|
--card: 0 0% 99%;
|
|
--card-foreground: 0 0% 9%;
|
|
|
|
--popover: 0 0% 99%;
|
|
--popover-foreground: 0 0% 9%;
|
|
|
|
--primary: 210 5% 25%;
|
|
--primary-foreground: 0 0% 100%;
|
|
|
|
--secondary: 0 0% 94%;
|
|
--secondary-foreground: 0 0% 9%;
|
|
|
|
--muted: 0 0% 92%;
|
|
--muted-foreground: 0 0% 40%;
|
|
|
|
--accent: 0 0% 92%;
|
|
--accent-foreground: 0 0% 9%;
|
|
|
|
--destructive: 6 36% 48%; /* Muted terracotta */
|
|
--destructive-foreground: 0 0% 100%;
|
|
|
|
--border: 0 0% 90%;
|
|
--input: 0 0% 90%;
|
|
--ring: 210 5% 25%;
|
|
|
|
/* 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-fraunces), Georgia, serif;
|
|
--font-body: var(--font-geist-sans), system-ui, sans-serif;
|
|
|
|
/* Shadows — cool-toned */
|
|
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06);
|
|
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.08);
|
|
|
|
/* 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%;
|
|
|
|
--card: 0 0% 10%;
|
|
--card-foreground: 0 0% 87%;
|
|
|
|
--popover: 0 0% 10%;
|
|
--popover-foreground: 0 0% 87%;
|
|
|
|
--primary: 210 4% 82%;
|
|
--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: 0 0% 18%;
|
|
--input: 0 0% 18%;
|
|
--ring: 210 4% 82%;
|
|
|
|
--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-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; }
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Refined hover effects */
|
|
.hover-lift {
|
|
transition: transform var(--duration-fast) var(--ease-out),
|
|
box-shadow var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
.hover-lift:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Custom scrollbar - minimal */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: hsl(var(--muted-foreground) / 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: hsl(var(--muted-foreground) / 0.3);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.hover-lift:hover {
|
|
transform: none;
|
|
}
|
|
}
|