style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)

* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup

Multi-agent scan of all 404 UI files against the locked design system, then
141 verified surgical fixes across 109 files (net -32 lines):

- Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale
  bounce, hover:shadow on list items, transition-all -> transition-colors.
- Drop font-medium from single-weight Hedvig display headings/numerals.
- Replace raw rainbow Tailwind status colors with Badge variants / brand tokens /
  neutral surfaces (achromatic chrome, semantic colors stay data-only).
- Route raw dates through formatDate(), hand-rolled currency through formatCurrency(),
  add tabular-nums to financial figures; text-gray-* -> text-foreground tokens.
- Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale.
- Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total,
  collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake),
  correct mismatch-password toast, correct supplier currency-field label.
- Remove PII-leaking debug console.log on register, stray console.logs.

Verified: tsc clean on all changed files, eslint clean, production build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(auth): sanitize residual error logs in register flow

Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the
remaining console.error calls in the register flow passed raw error
objects, which Supabase may populate with PII (email) in nested fields.
Log only sanitized message strings instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-06-30 13:14:13 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a8072f6423
commit b800dcd403
109 changed files with 270 additions and 302 deletions
+3 -3
View File
@@ -276,7 +276,7 @@ function LoginPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-4">
<div className="rounded-lg border bg-card p-4">
<p className="text-sm text-muted-foreground text-center leading-relaxed">
{showResetPassword ? tAuth('email_sent_hint_reset') : tAuth('email_sent_hint_login')}
</p>
@@ -327,7 +327,7 @@ function LoginPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
<form onSubmit={handleResetPassword} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email">{tAuth('email_label')}</Label>
@@ -382,7 +382,7 @@ function LoginPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
{callbackError === 'auth_error' && (
<div className="mb-5 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
<p className="text-sm font-medium text-destructive">
+2 -2
View File
@@ -188,7 +188,7 @@ function MfaEnrollContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
<div className="space-y-4">
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm text-muted-foreground leading-relaxed">
@@ -242,7 +242,7 @@ function MfaEnrollContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6 space-y-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6 space-y-6">
{/* QR Code */}
<div className="flex justify-center">
<div
+1 -1
View File
@@ -171,7 +171,7 @@ function MfaVerifyContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
<form onSubmit={handleVerify} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="code">{t('verify_code_label')}</Label>
+8 -48
View File
@@ -140,7 +140,7 @@ function RegisterPageContent() {
})
if (error) {
console.error('[register] BankID verifyOtp failed', error)
console.error('[register] BankID verifyOtp failed', error.message)
toast({
title: t('register_failed_complete'),
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
@@ -152,7 +152,7 @@ function RegisterPageContent() {
router.push('/select-company')
router.refresh()
} catch (error) {
console.error('[register] BankID signup error', error)
console.error('[register] BankID signup error', error instanceof Error ? error.message : String(error))
toast({
title: t('register_failed_title'),
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
@@ -201,14 +201,6 @@ function RegisterPageContent() {
}
try {
console.log('[register] attempting signUp', {
email: emailValue,
hasPassword: !!passwordValue,
passwordLength: passwordValue.length,
redirectTo: `${window.location.origin}/auth/callback`,
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
})
const { data, error } = await supabase.auth.signUp({
email: emailValue,
password: passwordValue,
@@ -218,15 +210,7 @@ function RegisterPageContent() {
})
if (error) {
console.error('[register] signUp error', {
message: error.message,
code: error.code,
status: error.status,
name: error.name,
stack: error.stack,
cause: error.cause,
fullError: JSON.stringify(error, Object.getOwnPropertyNames(error)),
})
console.error('[register] signUp error', error.message)
toast({
title: t('register_failed_title'),
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
@@ -235,16 +219,6 @@ function RegisterPageContent() {
return
}
console.log('[register] signUp response', {
userId: data.user?.id,
email: data.user?.email,
isAnonymous: data.user?.is_anonymous,
identities: data.user?.identities?.length,
hasSession: !!data.session,
confirmationSentAt: data.user?.confirmation_sent_at,
provider: data.user?.app_metadata?.provider,
})
// If auto-confirmed (local dev), process invite immediately and redirect
if (data.session) {
const cookieMatch = document.cookie.match(/gnubok-invite-token=([^;]+)/)
@@ -260,19 +234,11 @@ function RegisterPageContent() {
if (res.ok) {
document.cookie = 'gnubok-invite-token=; path=/; max-age=0'
console.log('[register] invite accepted after auto-confirm — redirecting')
window.location.href = '/'
return
}
// Log the error response so we can diagnose invite failures
const errBody = await res.json().catch(() => ({}))
console.error('[register] invite acceptance returned non-ok', {
status: res.status,
error: errBody.error,
})
} catch (err) {
console.error('[register] invite acceptance failed:', err)
console.error('[register] invite acceptance failed:', err instanceof Error ? err.message : String(err))
}
}
@@ -294,13 +260,7 @@ function RegisterPageContent() {
setEmail(emailValue)
setIsRegistered(true)
} catch (error) {
console.error('[register] unexpected exception', {
error,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
type: typeof error,
constructor: error?.constructor?.name,
})
console.error('[register] unexpected exception', error instanceof Error ? error.message : String(error))
toast({
title: t('register_failed_title'),
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
@@ -329,7 +289,7 @@ function RegisterPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-4">
<div className="rounded-lg border bg-card p-4">
<p className="text-sm text-muted-foreground text-center leading-relaxed">
{t('duplicate_hint')}
</p>
@@ -377,7 +337,7 @@ function RegisterPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-4">
<div className="rounded-lg border bg-card p-4">
<p className="text-sm text-muted-foreground text-center leading-relaxed">
{t('confirm_email_hint')}
</p>
@@ -416,7 +376,7 @@ function RegisterPageContent() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
{bankIdEnabled && !bankIdUser && (
<>
<div className="mb-5">
+2 -2
View File
@@ -40,7 +40,7 @@ export default function ResetPasswordPage() {
if (password !== confirmPassword) {
toast({
title: t('mismatch_title'),
description: t('weak_description'),
description: t('mismatch_description'),
variant: 'destructive',
})
setIsLoading(false)
@@ -102,7 +102,7 @@ export default function ResetPasswordPage() {
</p>
</div>
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
<div className="rounded-lg border bg-card p-6">
<form onSubmit={handleResetPassword} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="password">{t('new_password_label')}</Label>
+1 -1
View File
@@ -189,7 +189,7 @@ export default function ArticleDetailPage({
<Icon className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{article.name}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{article.name}</h1>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary">{t(ARTICLE_TYPE_KEY[article.type])}</Badge>
{article.article_number && (
+2 -2
View File
@@ -255,7 +255,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight font-mono">
<h1 className="font-display text-2xl md:text-3xl tracking-tight font-mono">
{formatVoucher(entry)}
</h1>
<JournalEntryStatusBadge entry={entry} />
@@ -366,7 +366,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
{entry.committed_at && (
<div className="flex justify-between">
<span className="text-muted-foreground">{t('field_posted_at')}</span>
<span>{new Date(entry.committed_at).toLocaleDateString('sv-SE')}</span>
<span>{formatDate(entry.committed_at)}</span>
</div>
)}
<div className="flex justify-between">
@@ -808,7 +808,7 @@ function ManualEntryEditor({
<div className="rounded-md border border-border p-3 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-medium">{template.name}</p>
<Button variant="ghost" size="sm" onClick={onRemove} className="h-7 px-2">
<Button variant="ghost" size="sm" onClick={onRemove} className="h-7 px-2" aria-label="Ta bort">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
+1 -1
View File
@@ -203,7 +203,7 @@ export default function CustomerDetailPage({
<Icon className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{customer.name}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{customer.name}</h1>
<Badge variant="secondary">{t(CUSTOMER_TYPE_KEY[customer.customer_type])}</Badge>
</div>
</div>
+4 -3
View File
@@ -10,6 +10,7 @@ import { DeadlineList } from '@/components/deadlines/DeadlineList'
import { PageHeader } from '@/components/ui/page-header'
import { AlertTriangle, ArrowRight } from 'lucide-react'
import { useCompany } from '@/contexts/CompanyContext'
import { formatCurrency } from '@/lib/utils'
import type { Deadline } from '@/types'
const supabase = createClient()
@@ -224,7 +225,7 @@ export default function DeadlinesPage() {
}
return (
<div className="space-y-6">
<div className="space-y-8">
<PageHeader title={t('title')} />
{/* Overdue invoices alert */}
@@ -235,8 +236,8 @@ export default function DeadlinesPage() {
<AlertTriangle className="h-4 w-4 text-destructive flex-shrink-0" />
<p className="text-sm">
<span className="font-medium">{t('overdue_invoices', { count: overdueInvoices.count })}</span>
<span className="text-muted-foreground ml-1.5">
{overdueInvoices.total.toLocaleString('sv-SE')} kr
<span className="text-muted-foreground ml-1.5 tabular-nums">
{formatCurrency(overdueInvoices.total)}
</span>
</p>
</div>
+1 -1
View File
@@ -17,7 +17,7 @@ export default function DashboardError({
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
<h2 className="text-xl font-semibold">Nagot gick fel</h2>
<h2 className="text-xl font-semibold">Något gick fel</h2>
<p className="text-muted-foreground text-sm max-w-md text-center">
Ett oväntat fel uppstod. Försök igen eller{' '}
<SupportLink variant="inline" subject="Oväntat fel">kontakta support</SupportLink>{' '}
+7 -7
View File
@@ -210,12 +210,12 @@ const glossaryTerms: GlossaryTerm[] = [
]
const categoryConfig = {
skatt: { labelKey: 'category_skatt', icon: Calculator, color: 'bg-orange-500/10 text-orange-600' },
moms: { labelKey: 'category_moms', icon: Receipt, color: 'bg-blue-500/10 text-blue-600' },
faktura: { labelKey: 'category_faktura', icon: FileText, color: 'bg-success/10 text-success' },
bokföring: { labelKey: 'category_bokforing', icon: BookOpen, color: 'bg-purple-500/10 text-purple-600' },
bank: { labelKey: 'category_bank', icon: Building2, color: 'bg-pink-500/10 text-pink-600' },
företag: { labelKey: 'category_foretag', icon: Building2, color: 'bg-cyan-500/10 text-cyan-600' },
skatt: { labelKey: 'category_skatt', icon: Calculator, color: 'bg-secondary text-muted-foreground' },
moms: { labelKey: 'category_moms', icon: Receipt, color: 'bg-secondary text-muted-foreground' },
faktura: { labelKey: 'category_faktura', icon: FileText, color: 'bg-secondary text-muted-foreground' },
bokföring: { labelKey: 'category_bokforing', icon: BookOpen, color: 'bg-secondary text-muted-foreground' },
bank: { labelKey: 'category_bank', icon: Building2, color: 'bg-secondary text-muted-foreground' },
företag: { labelKey: 'category_foretag', icon: Building2, color: 'bg-secondary text-muted-foreground' },
}
function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpanded: boolean; onToggle: () => void }) {
@@ -224,7 +224,7 @@ function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpand
const CategoryIcon = config.icon
return (
<Card className={cn('transition-all', isExpanded && 'ring-2 ring-primary/20')}>
<Card className={cn('transition-colors', isExpanded && 'ring-2 ring-primary/20')}>
<CardContent className="pt-4">
<button
onClick={onToggle}
+11 -11
View File
@@ -2164,7 +2164,7 @@ export default function ImportPage() {
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
{view === 'export' ? t('export_title') : t('title')}
</h1>
<p className="text-muted-foreground">
@@ -2198,10 +2198,10 @@ export default function ImportPage() {
tabIndex={isSandbox ? -1 : 0}
aria-disabled={isSandbox}
className={cn(
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
isSandbox
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
: 'cursor-pointer hover:border-foreground/15'
)}
onClick={() => { if (!isSandbox) setMode('psd2') }}
onKeyDown={(e) => { if (!isSandbox && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setMode('psd2') } }}
@@ -2231,10 +2231,10 @@ export default function ImportPage() {
tabIndex={isSandbox ? -1 : 0}
aria-disabled={isSandbox}
className={cn(
'group rounded-lg border bg-card p-5 transition-all',
'group rounded-lg border bg-card p-5 transition-colors',
isSandbox
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
: 'cursor-pointer hover:border-foreground/15'
)}
onClick={() => { if (!isSandbox) setMode('migration') }}
onKeyDown={(e) => { if (!isSandbox && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setMode('migration') } }}
@@ -2277,8 +2277,8 @@ export default function ImportPage() {
role="button"
tabIndex={0}
className={cn(
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
'cursor-pointer hover:border-foreground/15'
)}
onClick={() => setMode('bank')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('bank') } }}
@@ -2307,8 +2307,8 @@ export default function ImportPage() {
role="button"
tabIndex={0}
className={cn(
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
'cursor-pointer hover:border-foreground/15'
)}
onClick={() => setMode('csv_data')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('csv_data') } }}
@@ -2344,8 +2344,8 @@ export default function ImportPage() {
role="button"
tabIndex={0}
className={cn(
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
'cursor-pointer hover:border-foreground/15'
)}
onClick={() => setMode('sie')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('sie') } }}
@@ -155,7 +155,7 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
{t('subtitle', { number: invoice.invoice_number ?? '' })}
</p>
+1 -1
View File
@@ -82,7 +82,7 @@ export default function KpiPage() {
return (
<div className="space-y-8">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
<div className="flex gap-2">
<KPISettingsDialog
preferences={preferences}
+4 -4
View File
@@ -6,7 +6,7 @@ export default function DashboardLoading() {
{/* Metrics row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-xl border border-border/60 bg-card p-4 space-y-2">
<div key={i} className="rounded-lg border border-border bg-card p-4 space-y-2">
<Skeleton className="h-3.5 w-20" />
<Skeleton className="h-7 w-28" />
<Skeleton className="h-3 w-16" />
@@ -17,7 +17,7 @@ export default function DashboardLoading() {
{/* Income / Expenses */}
<div className="grid md:grid-cols-2 gap-4">
{[1, 2].map((i) => (
<div key={i} className="rounded-xl border border-border/60 bg-card p-6 space-y-3">
<div key={i} className="rounded-lg border border-border bg-card p-6 space-y-3">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-8 w-32" />
<Skeleton className="h-3 w-24" />
@@ -27,12 +27,12 @@ export default function DashboardLoading() {
{/* Alerts + deadlines */}
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-border/60 bg-card p-4 space-y-3">
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className="rounded-xl border border-border/60 bg-card p-4 space-y-3">
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
<Skeleton className="h-5 w-32" />
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center justify-between">
@@ -144,7 +144,7 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
<Link href="/salary/employees" aria-label="Tillbaka till anställda"><ArrowLeft className="h-4 w-4" /></Link>
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
{employee.first_name} {employee.last_name}
</h1>
<p className="text-sm text-muted-foreground mt-1">
@@ -92,7 +92,7 @@ export default function NewEmployeePage() {
<Button variant="ghost" size="icon" asChild>
<Link href="/salary/employees" aria-label="Tillbaka till anställda"><ArrowLeft className="h-4 w-4" /></Link>
</Button>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny anställd</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">Ny anställd</h1>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
+1 -1
View File
@@ -45,7 +45,7 @@ export default function EmployeesPage() {
<Link href="/salary" aria-label={t('back_to_payroll')}><ArrowLeft className="h-4 w-4" /></Link>
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
<p className="text-sm text-muted-foreground mt-1">{t('registered_count', { count: employees.length })}</p>
</div>
</div>
@@ -168,7 +168,7 @@ export default function SalaryRunEmployeeDetailPage({
</Link>
<div className="flex flex-wrap items-baseline justify-between gap-2">
<div>
<h1 className="font-serif text-2xl font-medium tracking-tight">
<h1 className="font-display text-2xl tracking-tight">
{employee.first_name} {employee.last_name}
</h1>
<p className="text-sm text-muted-foreground tabular-nums">
@@ -312,7 +312,7 @@ function SummaryCard({ label, value, accent, overridden }: { label: string; valu
function AbsenceCount({ label, days }: { label: string; days: number }) {
return (
<div className="rounded-md border bg-muted/30 px-2.5 py-1.5">
<div className="rounded-md border bg-muted/30 px-3 py-2">
<div className="text-[11px] text-muted-foreground">{label}</div>
<div className="text-sm font-medium tabular-nums">{days} dagar</div>
</div>
+2 -2
View File
@@ -387,7 +387,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
<Link href="/salary" aria-label="Tillbaka till löner"><ArrowLeft className="h-4 w-4" /></Link>
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
Lönekörning {periodLabel}
</h1>
<p className="text-sm text-muted-foreground mt-1">
@@ -426,7 +426,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
<Card key={label}>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground mb-2">{label}</p>
<p className={`font-display text-xl font-medium tabular-nums leading-tight ${accent ? 'text-success' : ''}`}>
<p className={`font-display text-xl tabular-nums leading-tight ${accent ? 'text-success' : ''}`}>
{formatCurrency(value)}
</p>
</CardContent>
+1 -1
View File
@@ -61,7 +61,7 @@ export default function NewSalaryRunPage() {
<Button variant="ghost" size="icon" asChild>
<Link href="/salary" aria-label="Tillbaka till löner"><ArrowLeft className="h-4 w-4" /></Link>
</Button>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny lönekörning</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">Ny lönekörning</h1>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
@@ -581,7 +581,7 @@ export default function SupplierInvoiceDetailPage() {
{invoice.delivery_date && (
<div className="flex justify-between">
<span className="text-muted-foreground">{t('delivery_date_label')}</span>
<span>{invoice.delivery_date}</span>
<span className="tabular-nums">{formatDate(invoice.delivery_date)}</span>
</div>
)}
{invoice.payment_reference && (
@@ -1439,7 +1439,7 @@ export default function NewSupplierInvoicePage() {
{/* Valuta & moms — kept inline with the line items because they
drive how each row is interpreted. Hidden defaults (SEK +
normal moms) collapse to nothing so most users don't see this. */}
<div className="mb-5 pb-5 border-b grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="mb-4 pb-4 border-b grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-1.5">
<Label className="text-xs">{t('currency_label')}</Label>
<Controller
+4 -4
View File
@@ -152,7 +152,7 @@ export default function SupplierDetailPage() {
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{supplier.name}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{supplier.name}</h1>
<p className="text-muted-foreground">
{supplierTypeLabels[supplier.supplier_type]}
{supplier.org_number && t('org_number_inline', { number: supplier.org_number })}
@@ -189,7 +189,7 @@ export default function SupplierDetailPage() {
<CardTitle className="text-sm text-muted-foreground">{t('outstanding')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
<p className="font-display text-2xl tabular-nums">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
</CardContent>
</Card>
<Card>
@@ -197,7 +197,7 @@ export default function SupplierDetailPage() {
<CardTitle className="text-sm text-muted-foreground">{t('total_paid')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
<p className="font-display text-2xl tabular-nums">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
</CardContent>
</Card>
<Card>
@@ -205,7 +205,7 @@ export default function SupplierDetailPage() {
<CardTitle className="text-sm text-muted-foreground">{t('invoice_count')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-display text-2xl font-medium tabular-nums">{supplier.stats?.invoice_count || 0}</p>
<p className="font-display text-2xl tabular-nums">{supplier.stats?.invoice_count || 0}</p>
</CardContent>
</Card>
</div>
+9 -8
View File
@@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { EmptyState } from '@/components/ui/empty-state'
import { Skeleton } from '@/components/ui/skeleton'
import { ReportExportMenu } from '@/components/reports/ReportExportMenu'
import { useToast } from '@/components/ui/use-toast'
import { Plus, Search, Building2, Lock } from 'lucide-react'
@@ -114,7 +115,7 @@ export default function SuppliersPage() {
<div className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
{t('subtitle')}
</p>
@@ -168,13 +169,13 @@ export default function SuppliersPage() {
{isLoading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<Card key={i} className="animate-pulse">
<Card key={i}>
<CardContent className="p-6 space-y-3">
<div className="h-5 bg-muted rounded w-1/2" />
<div className="h-3 bg-muted rounded w-2/3" />
<Skeleton className="h-5 w-1/2" />
<Skeleton className="h-3 w-2/3" />
<div className="h-px bg-muted" />
<div className="h-3 bg-muted rounded w-1/2" />
<div className="h-3 bg-muted rounded w-1/3" />
<Skeleton className="h-3 w-1/2" />
<Skeleton className="h-3 w-1/3" />
</CardContent>
</Card>
))}
@@ -206,7 +207,7 @@ export default function SuppliersPage() {
const location = formatLocation(supplier)
return (
<Link key={supplier.id} href={`/suppliers/${supplier.id}`} className="group">
<Card className="h-full cursor-pointer transition-all duration-150 hover:border-foreground/20 hover:shadow-sm motion-safe:active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<Card className="h-full cursor-pointer transition-colors duration-150 hover:border-foreground/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<CardContent className="p-6 flex flex-col h-full">
<div className="space-y-1 mb-4">
<h3 className="text-[15px] font-semibold tracking-tight leading-tight truncate group-hover:text-primary transition-colors">
@@ -217,7 +218,7 @@ export default function SuppliersPage() {
</p>
</div>
<dl className="mt-auto space-y-1.5 text-sm border-t pt-3">
<dl className="mt-auto space-y-2 text-sm border-t pt-3">
{supplier.org_number && (
<div className="flex items-baseline justify-between gap-3">
<dt className="text-xs text-muted-foreground shrink-0">{t('label_org_number')}</dt>
+3 -3
View File
@@ -2061,7 +2061,7 @@ export default function TransactionsPage() {
}
return (
<div className="space-y-6">
<div className="space-y-8">
{/* Status bar */}
<TransactionStatusBar
uncategorizedCount={totalUncategorizedCount ?? uncategorizedTransactions.length}
@@ -2091,7 +2091,7 @@ export default function TransactionsPage() {
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-9 gap-1.5 px-3 text-sm">
<Button variant="outline" size="sm" className="h-9 gap-2 px-3 text-sm">
{mode === 'inbox'
? `Att bokföra${(totalUncategorizedCount ?? uncategorizedTransactions.length) > 0 ? ` (${totalUncategorizedCount ?? uncategorizedTransactions.length})` : ''}`
: 'Alla transaktioner'}
@@ -2138,7 +2138,7 @@ export default function TransactionsPage() {
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 gap-1.5 px-2 text-xs">
<Button variant="outline" size="sm" className="h-7 gap-2 px-2 text-xs">
{sourceFilter === 'all'
? t('source_all', { count: uncategorizedTransactions.length + skvUnmatched.length })
: sourceFilter === 'bank'
+1 -1
View File
@@ -46,7 +46,7 @@ export default async function OnboardingLayout({
href="/settings/account"
aria-label="Kontoinställningar"
title="Kontoinställningar"
className="fixed bottom-5 right-5 z-50 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-background/80 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:border-foreground/40 hover:text-foreground"
className="fixed bottom-6 right-6 z-50 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-background/80 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:border-foreground/40 hover:text-foreground"
>
<Settings className="h-4 w-4" />
</Link>
+4 -4
View File
@@ -5,7 +5,7 @@ import { getBranding } from '@/lib/branding/service'
export function generateMetadata(): Metadata {
return {
title: `Personuppgiftsbitradesavtal - ${getBranding().appName}`,
title: `Personuppgiftsbiträdesavtal - ${getBranding().appName}`,
}
}
@@ -16,7 +16,7 @@ export default function DPAPage() {
<div className="max-w-3xl mx-auto space-y-8">
<div className="text-center space-y-2">
<h1 className="font-display text-3xl md:text-4xl tracking-tight text-foreground">
Personuppgiftsbitradesavtal (DPA)
Personuppgiftsbiträdesavtal (DPA)
</h1>
<p className="text-sm text-muted-foreground">
Enligt GDPR Art. 28 &middot; Senast uppdaterad: 2026-06-03
@@ -112,7 +112,7 @@ export default function DPAPage() {
åtkomst</li>
<li><strong>Integritetskontroll:</strong> SHA-256 checksummor för alla dokument, med
regelbunden verifiering</li>
<li><strong>Revisionslogg:</strong> Alla ändringshandelser loggas automatiskt av databasen
<li><strong>Revisionslogg:</strong> Alla ändringshändelser loggas automatiskt av databasen
(ej redigerbara)</li>
<li><strong>Oföränderlig bokföring:</strong> Bokförda verifikationer kan inte ändras eller
raderas (databasutlösare)</li>
@@ -243,7 +243,7 @@ export default function DPAPage() {
<Card>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground text-center">
Detta personuppgiftsbitradesavtal träder i kraft när du skapar ett konto på
Detta personuppgiftsbiträdesavtal träder i kraft när du skapar ett konto på
{' '}{appName} och gäller så länge du använder tjänsten. För frågor, kontakta oss
på {privacyEmail}.
</p>
+2 -2
View File
@@ -156,7 +156,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
<div className="max-w-lg mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
<h1 className="text-2xl font-bold text-foreground mb-2">
Betalningspåminnelse
</h1>
<p className="text-muted-foreground">
@@ -268,7 +268,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
onClick={() => handleAction('disputed')}
disabled={isSubmitting}
>
<MessageSquare className="h-5 w-5 mr-3 text-orange-600" />
<MessageSquare className="h-5 w-5 mr-3 text-muted-foreground" />
<div className="text-left">
<p className="font-medium">Kontakta avsändaren</p>
<p className="text-sm text-muted-foreground font-normal">
+2 -2
View File
@@ -15,7 +15,7 @@ export default function PrivacyPolicyPage() {
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white py-12 px-4">
<div className="max-w-3xl mx-auto space-y-6">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
<h1 className="text-3xl font-bold text-foreground mb-2">
Integritetspolicy
</h1>
<p className="text-muted-foreground">
@@ -69,7 +69,7 @@ export default function PrivacyPolicyPage() {
</li>
<li>
<strong>Berättigat intresse (Art. 6.1f):</strong> Produktförbättringar, säkerhet och
bedrägeriforbud.
bedrägeribekämpning.
</li>
<li>
<strong>Samtycke (Art. 6.1a):</strong> För AI-baserade funktioner som skickar data
+1 -2
View File
@@ -181,7 +181,6 @@ function NewCompanyContent() {
return
}
console.log(LOG, 'created company', result.companyId)
toast({
title: t('toast_company_created'),
description: t('toast_switched_to_new'),
@@ -263,7 +262,7 @@ function NewCompanyContent() {
</div>
<div key={`title-${currentStep}`} className="animate-fade-in">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
<h1 className="font-display text-2xl md:text-3xl tracking-tight leading-[1.1]">
{stepInfo.title}
</h1>
<p className="text-white/40 mt-1.5 text-sm max-w-sm leading-relaxed">
+1 -1
View File
@@ -172,7 +172,7 @@ export default function InvitePage() {
</span>
</div>
<div className="animate-fade-in">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
<h1 className="font-display text-2xl md:text-3xl tracking-tight leading-[1.1]">
{error ? t('header_invalid') : t('header_invited')}
</h1>
</div>