Files
accounted/app/globals.css
T
Jakob Wennberg 1ccf7ab81f feat(transactions): concept shell and row language (UI migration PR 5) (#1124)
* feat(transactions): concept shell for scene 10 (header, toolbar, footer)

- Header: title + Importera SplitButton (import guide / manual entry,
  last-used mode persisted via ui_state.create_mode.transactions),
  replacing the three-button row
- Toolbar in concept order: [Att bokföra/Alla seg with count chip]
  [search] [Välj flera as quiet toggle] ... [source ContextPicker far
  right merging the old in-list source dropdown (bank/Skatteverket)]
- Bank sync status + sync-now + Bankavstämning move to the concept's
  footer status line together with the honest visible-row counter
  ("n att hantera")
- skv-reconnect banner and all dialogs/actions untouched

Row cards -> concept dry-table conversion follows separately.

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

* feat(transactions): concept row language for the inbox cards

Rows read like the scene 10 table: date as the first tabular column,
description on one line, amount right (+income in sage), auto-detected
invoice matches as beige suggestion chips instead of primary pills,
tighter hairline rows at 13px. Every action unchanged: Bokför pill,
match/assistant/overflow menu, batch checkboxes, exit animations,
extraction status.

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

* refactor(transactions): de-bloat rows and chrome to match the concept

- Rows carry only the essentials: date, description, amount, Bokför and
  one overflow menu. The inline match and ask-assistant icon buttons
  fold into the overflow menu (all actions still one click away)
- The "n nya banktransaktioner sen ditt senaste besök" banner is removed;
  the footer sync line covers it
- The Skatteverket reconnect banner becomes the one-ochre-sentence
  AttnLine with an inline action (convention 6)

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

* refactor(ui): extract dry-table primitives into components/ui/dry-table

The concept table styles (TH/TD/VTH/VTD classes, quiet links, RowFoldout)
move out of JournalEntryList so the transaction inbox can render the exact
same table language.

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

* feat(transactions): Synka bank nu in the Importera menu

Extracts the per-connection sync/reconnect logic into a shared useBankSync()
hook (BankSyncNowButton keeps using it) and adds the concept's first menu row
to the Importera split button: sync all active connections, with the
last-synced age as the row description. Hidden without a connection or the
bank_sync capability; the footer button stays the gated conversion surface.

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

* feat(transactions): per-account source picker with balances

The context chip becomes the concept's account chooser: always visible in
inbox mode, one row per enabled cash account (PSD2 balance annotation),
an Övriga bucket for bank rows without a registered account, and a
Skattekonto row annotated with the cached saldo. The trigger reads
'Alla källor · <sum>' where the sum covers SEK ledgers + skattekonto.

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

* feat(transactions): concept dry-table for the inbox

The inbox list becomes the exact Bokforing table: borderless dry-table with
hover-revealed checkboxes and chevrons, one-line rows (date, description,
amount, quiet primary pill + overflow + foldout), and a RowFoldout expansion
carrying the row detail and the full action set as quiet links. Skattekonto
rows render as chip-marked table rows with inline actions. The 'Valj flera'
toggle and the floating batch bar are replaced by the concept bulkbar that
pops in above the table once a row is selected.

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

* fix(i18n): singular form for bulkbar selection count

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

* feat(transactions): suggestion-first Bokfor flow (concept scene 10)

Bokfor on a row with a suggestion now goes straight to the compact
kontering confirm (QuickReview) showing the proposed verifikat; the full
template picker becomes the fallback and the 'Byt mall' path. Rows carry
the concept's 'Forslag: <mall>' chip (with D/K in the tooltip and the
foldout meta). The picker dialog is de-bloated: alternate paths (manuellt,
matcha, ignorera) collapse into quiet links. Modal scroll containers
(DialogContent, SlideOverBody) keep a visible scrollbar thumb: with the
app-wide auto-hide, a long dialog read as cut off at the fold.

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

* refactor(transactions): drop suggestion UI and foldout action links

Founder direction: no booking suggestions in the list for now (logic comes
later), and the foldout duplicated the overflow menu: the three dots are
the clearer surface. The foldout keeps row detail only (FX conversion,
original bank name, 1930/1630 hint, extraction status) and rows without
any detail no longer expand. The Skattekonto picker row drops its saldo
annotation; the Alla kallor sum covers bank ledgers only.

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

* feat(transactions): dry-table for the Alla view + pill chips app-wide

The history list joins the inbox's table language: Datum, Beskrivning,
Belopp, Status columns with Bokford as muted text + Visa verifikat quiet
link (normal state) and Ej bokford as the beige exception chip with an
inline Bokfor pill; the business/private tabs become the house seg and the
KALLA header row becomes the standard chip-picker. Skattekonto rows get
the same treatment. SEK conversion moves to a tooltip (one-line rows).

Badge itself becomes the concept chip: pill radius (99px), 11.5px, quiet
padding, replacing the boxy rounded-md look the founder flagged on the
Underlag saknas badge.

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

* fix(ui): audit round: viewer-disabled split options, touch fallback

Regression-audit fixes: SplitButton options support disabled+disabledTitle
so viewer-gated create paths render inert with the viewer tooltip instead
of silently no-opping (wired on Ny transaktion); the history list gets its
Kopplad till faktura indicator back; dead transactionsWithMatches memo
removed; and coarse-pointer devices now always show hover-revealed
controls (checkboxes, chevrons, quiet row actions) since touch has no
hover.

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

---------

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

441 lines
12 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%;
/* 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;
}
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;
}
}
/* 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;
}
}