refactor(ui): design-system consistency pass over dense pages + i18n de-bloat (#961)
* refactor(ui): normalize dense pages to the locked design system Sweep of the info-dense surfaces against .claude/rules/design.md; no behavior changes, classNames and primitive adoption only. - Replace hand-rolled h1s with PageHeader (import, suppliers, kpi, salary/employees, skattekonto, settings layout) and drop the one double title (SalarySettingsContent under the settings h1) - Replace hand-rolled empty states with EmptyState (skattekonto, banking/api-keys/oauth/counterparty settings) and hand-rolled pulse divs with Skeleton (deadlines, report view loaders) - Remove semantic colors used as chrome: amber/emerald banners in AGIPanel and SkatteverketPanel, success/warning tints in kassaflodesanalys, arsredovisning and import become neutral surfaces with the tint kept on the icon only - Full-opacity borders everywhere (border-border/30-60, border-destructive/20-40, border-foreground/30, text-destructive/80) - Snap off-scale spacing (p-5 to p-6, p-2.5 to p-3, gap/mt-x.5 to scale values); KPI metric tiles p-6 to p-4 per the tile rule - Remove the mobile Select that duplicated the invoices status Tabs (TabsList already scrolls horizontally); single Tabs now serves both breakpoints - supplier-invoices: shared formatCurrency instead of a local formatAmount helper; skattekonto: formatDate/formatDateLong/ formatDateTime instead of raw dates and toLocaleString - arsredovisning flerarsoversikt converted to the Table primitive with right-aligned tabular-nums cells - Settings: CardTitle text-base on section cards, one heading idiom in AccountSettingsContent, h3 to h2 in CompanyProfileView Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(i18n): trim text bloat and fix an untranslated sv string - Fix invoice_credit.create_failed_fallback: sv catalog carried the English "Failed to create credit note"; now "Kunde inte skapa kreditfaktura". Translate new_user_checklist.step3_title in en - Drop descriptions that paraphrase their own title (design.md forbidden pattern): invoice_detail.credited_description, invoice_credit.original_card_description, invoice_editor customer/notes card descriptions (keys deleted from both catalogs, zero remaining usages); the transaction booking DialogDescription becomes sr-only so screen readers keep it - Trim redundant sentences from settings_salary.info_payroll_scope, settings_backup.intro, ext_cloud_backup_long_description, settings.name_description, salary_payments.open_payments_note and shorten invoice_credit.reason_card_description; statutory BFL/tax prose untouched - Normalize toast punctuation (dimensions/self_billing created_description lose the trailing period like their siblings) - common.delete "Radera" to "Ta bort" (zero live call sites; Radera stays reserved for irreversible account/company deletion) Catalogs verified key-identical (4795 keys each) and JSON-parseable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ca8a89324c
commit
da859d7236
@@ -1292,7 +1292,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{isSelfBilled ? <>{ts('customer_label')}<RequiredMark /></> : <>{t('customer_card_title')}<RequiredMark /></>}</CardTitle>
|
||||
<CardDescription>{isSelfBilled ? ts('issuer_card_description') : t('customer_card_description')}</CardDescription>
|
||||
{isSelfBilled && <CardDescription>{ts('issuer_card_description')}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Controller
|
||||
@@ -1960,7 +1960,6 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('notes_card_title')}</CardTitle>
|
||||
<CardDescription>{t('notes_card_description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
|
||||
@@ -137,7 +137,7 @@ export function KPIHeroCards({ report, preferences }: KPIHeroCardsProps) {
|
||||
|
||||
return (
|
||||
<Card key={def.id}>
|
||||
<CardContent className="p-6">
|
||||
<CardContent className="p-4">
|
||||
<InfoTooltip
|
||||
content={tooltipContent}
|
||||
side="top"
|
||||
|
||||
@@ -266,7 +266,7 @@ function ExpansionContent({
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines.map((line) => (
|
||||
<tr key={`${line.voucher_series}-${line.voucher_number}-${line.journal_entry_id}`} className="border-t border-border/40">
|
||||
<tr key={`${line.voucher_series}-${line.voucher_number}-${line.journal_entry_id}`} className="border-t border-border">
|
||||
<td className="py-1.5">
|
||||
{line.journal_entry_id ? (
|
||||
<Link
|
||||
|
||||
@@ -630,8 +630,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
|
||||
</div>
|
||||
)}
|
||||
{success && !error && (
|
||||
<div className="flex items-start gap-2 text-sm text-success bg-success/5 rounded-lg p-3">
|
||||
<CheckCircle2 className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<div className="flex items-start gap-2 text-sm rounded-lg border border-border bg-muted/30 p-3">
|
||||
<CheckCircle2 className="h-4 w-4 mt-0.5 shrink-0 text-success" />
|
||||
<span>{success}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -650,13 +650,13 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
|
||||
className={`flex items-start gap-2 text-sm rounded-lg p-3 ${
|
||||
c.status === 'ERROR'
|
||||
? 'bg-destructive/5 text-destructive'
|
||||
: 'bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200'
|
||||
: 'border border-border bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
{c.status === 'ERROR' ? (
|
||||
<ShieldAlert className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0 text-warning" />
|
||||
)}
|
||||
<div>
|
||||
<span className="font-mono text-xs mr-1.5">{c.code}</span>
|
||||
@@ -802,13 +802,13 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
|
||||
className={`flex items-start gap-2 text-sm rounded-lg p-3 ${
|
||||
k.status === 'ERROR'
|
||||
? 'bg-destructive/5 text-destructive'
|
||||
: 'bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200'
|
||||
: 'border border-border bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
{k.status === 'ERROR' ? (
|
||||
<ShieldAlert className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0 text-warning" />
|
||||
)}
|
||||
<div>
|
||||
<span className="font-mono text-xs mr-1.5">{k.kod}</span>
|
||||
@@ -841,7 +841,7 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
|
||||
|
||||
{/* Signing link */}
|
||||
{signeringslank && (
|
||||
<div className="rounded-lg border border-success/30 bg-success/5 p-3 space-y-2">
|
||||
<div className="rounded-lg border border-border bg-muted/30 p-3 space-y-2">
|
||||
<p className="text-sm font-medium">Utkastet är låst och redo att signeras</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Öppna länken nedan och signera med BankID på Skatteverkets sida.
|
||||
|
||||
@@ -117,8 +117,10 @@ export function TrialBalanceView({ periodId, onNavigateToAccount }: { periodId:
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center text-muted-foreground">
|
||||
Laddar saldobalans...
|
||||
<CardContent className="p-6 space-y-2">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<Skeleton key={i} className="h-4 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -451,8 +453,10 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center text-muted-foreground">
|
||||
Laddar resultaträkning...
|
||||
<CardContent className="p-6 space-y-2">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<Skeleton key={i} className="h-4 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -717,8 +721,10 @@ export function ResultatrapportView({ periodId, dateRange, dimensionFilter = nul
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center text-muted-foreground">
|
||||
Laddar resultatrapport...
|
||||
<CardContent className="p-6 space-y-2">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<Skeleton key={i} className="h-4 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -717,7 +717,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
refresh token or has burned through its 10-refresh budget. The
|
||||
only fix is a fresh BankID round-trip. */}
|
||||
{(status?.expired === true || status?.canRefresh === false) && !readOnly && (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 dark:border-amber-900/40 dark:bg-amber-900/20">
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<p className="text-sm font-medium">{t('expired_banner_title')}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t('expired_banner_description')}
|
||||
@@ -734,7 +734,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
after some users had already connected, so their stored token
|
||||
grants moms/skattekonto but not AGI. */}
|
||||
{missingAgdScope && !readOnly && (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 dark:border-amber-900/40 dark:bg-amber-900/20">
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<p className="text-sm font-medium">
|
||||
{t('missing_scope_title')}
|
||||
</p>
|
||||
@@ -784,7 +784,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
treatment so the user understands they must fix errors before
|
||||
BankID signing is even possible. */}
|
||||
{submission?.signeringslank && awaitingSigning && !draftIsStale && (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-900/40 dark:bg-amber-900/20">
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<p className="text-sm font-medium">{t('draft_locked_title')}</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t('draft_locked_description')}
|
||||
@@ -793,7 +793,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
href={submission.signeringslank}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-2 inline-flex items-center gap-1 text-sm font-medium text-amber-900 hover:underline dark:text-amber-200"
|
||||
className="mt-2 inline-flex items-center gap-1 text-sm font-medium hover:underline"
|
||||
>
|
||||
{t('open_signing_link')} <ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
@@ -806,7 +806,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
it would file the old amounts. The "Lås upp" button below releases
|
||||
the SKV lock; the user then re-submits the freshly generated XML. */}
|
||||
{awaitingSigning && draftIsStale && (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 dark:border-amber-900/40 dark:bg-amber-900/20">
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3">
|
||||
<p className="text-sm font-medium">{t('stale_draft_title')}</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t('stale_draft_description', {
|
||||
@@ -849,12 +849,12 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
)}
|
||||
|
||||
{kontroller.length > 0 && (
|
||||
<div className="space-y-1 rounded-md border bg-muted/30 p-2.5">
|
||||
<div className="space-y-1 rounded-md border bg-muted/30 p-3">
|
||||
{kontroller.map((k, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-start gap-2 text-xs ${
|
||||
k.status === 'STOPP' ? 'text-destructive' : 'text-amber-700 dark:text-amber-400'
|
||||
k.status === 'STOPP' ? 'text-destructive' : 'text-warning'
|
||||
}`}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
@@ -878,7 +878,7 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
status?.expired === true ||
|
||||
status?.canRefresh === false
|
||||
return (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 text-sm text-destructive">
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<AlertCircle className="mr-1 inline h-3.5 w-3.5" />
|
||||
{error}
|
||||
{sessionExpired && !readOnly && (
|
||||
@@ -893,8 +893,8 @@ export function AGIPanel(props: AGIPanelProps) {
|
||||
)
|
||||
})()}
|
||||
{success && !error && (
|
||||
<div className="rounded-md bg-emerald-50 p-2.5 text-sm text-emerald-900 dark:bg-emerald-900/20 dark:text-emerald-300">
|
||||
<CheckCircle2 className="mr-1 inline h-3.5 w-3.5" />
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
|
||||
<CheckCircle2 className="mr-1 inline h-3.5 w-3.5 text-success" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function RunJournalPreview({ preview, onRecalculate, recalculating }: Run
|
||||
</thead>
|
||||
<tbody>
|
||||
{entry.lines.map((line, li) => (
|
||||
<tr key={li} className="border-t border-border/30">
|
||||
<tr key={li} className="border-t border-border">
|
||||
<td className="py-1.5 tabular-nums font-mono">{line.account_number}</td>
|
||||
<td className="py-1.5 text-muted-foreground">{line.line_description}</td>
|
||||
<td className="py-1.5 text-right tabular-nums">
|
||||
|
||||
@@ -103,7 +103,7 @@ export function AccountDangerZone() {
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-4 border-t border-border pt-8">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-destructive/80">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-destructive">
|
||||
{t('heading')}
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const PROSE =
|
||||
'prose-table:my-2 prose-table:text-xs [&_table]:w-full ' +
|
||||
'[&_th]:border-b [&_th]:border-border [&_th]:py-1.5 [&_th]:px-2 [&_th]:text-left [&_th]:font-medium ' +
|
||||
'[&_th]:text-muted-foreground [&_th]:uppercase [&_th]:tracking-wider [&_th]:text-[10px] ' +
|
||||
'[&_td]:border-b [&_td]:border-border/60 [&_td]:py-1.5 [&_td]:px-2 [&_td]:align-top'
|
||||
'[&_td]:border-b [&_td]:border-border [&_td]:py-1.5 [&_td]:px-2 [&_td]:align-top'
|
||||
|
||||
// The API returns errors either as a plain string (legacy/validation) or as
|
||||
// the canonical { code, message } envelope — extract something renderable.
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2, Plus, Copy, Check, Trash2, Key, ChevronDown, AlertTriangle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -216,7 +217,7 @@ function ScopeCard({
|
||||
className={cn(
|
||||
'flex min-h-[68px] cursor-pointer flex-col gap-1 rounded-md border p-2 transition-colors',
|
||||
checked
|
||||
? 'border-foreground/30 bg-secondary'
|
||||
? 'border-border bg-secondary'
|
||||
: 'border-border hover:bg-secondary/60'
|
||||
)}
|
||||
>
|
||||
@@ -387,7 +388,7 @@ export function ApiKeysPanel() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
@@ -408,13 +409,11 @@ export function ApiKeysPanel() {
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : keys.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Key className="h-8 w-8 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{t('empty_title')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('empty_help')}
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Key}
|
||||
title={t('empty_title')}
|
||||
description={t('empty_help')}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{keys.map((key) => {
|
||||
|
||||
@@ -185,7 +185,7 @@ export function BackupDownloadForm() {
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('create_backup_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('create_backup_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -129,7 +129,7 @@ export function BookingTemplatesPanel() {
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function CompanyDangerZone() {
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-4 border-t border-border pt-8">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-destructive/80">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-destructive">
|
||||
{t('danger_heading')}
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ function cleanSignatory(raw: string): string[] {
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section>
|
||||
<h3 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
</h2>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslations } from 'next-intl'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2, Trash2, Users, ChevronDown } from 'lucide-react'
|
||||
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
|
||||
@@ -94,7 +95,7 @@ export function CounterpartyTemplatesPanel() {
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
@@ -105,13 +106,11 @@ export function CounterpartyTemplatesPanel() {
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Users className="h-8 w-8 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{t('empty_title')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('empty_help')}
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title={t('empty_title')}
|
||||
description={t('empty_help')}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{templates.map((tt) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2, Plus, Trash2, Globe } from 'lucide-react'
|
||||
|
||||
@@ -124,7 +125,7 @@ export function OAuthClientsPanel() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
@@ -141,13 +142,11 @@ export function OAuthClientsPanel() {
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : clients.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Globe className="h-8 w-8 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{t('empty_title')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('empty_help')}
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title={t('empty_title')}
|
||||
description={t('empty_help')}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{clients.map((c) => (
|
||||
|
||||
@@ -152,7 +152,7 @@ export function PdfPrintSettings({ settings, onUpdate }: PdfPrintSettingsProps)
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t('placement_aria_label')}
|
||||
className="inline-flex rounded-md border border-border/60 p-0.5"
|
||||
className="inline-flex rounded-md border border-border p-1"
|
||||
>
|
||||
{(['header', 'footer'] as const).map((pos) => {
|
||||
const active = (settings.invoice_company_name_position ?? 'header') === pos
|
||||
|
||||
@@ -105,7 +105,7 @@ export function SkatteverketConnectPanel() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('title')}</CardTitle>
|
||||
<EnvironmentBadge environment={status?.environment} disabled={status?.disabled} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -154,7 +154,7 @@ export function SkatteverketConnectPanel() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
{t('title')}
|
||||
{status.expired ? (
|
||||
<Badge variant="destructive">{t('expired')}</Badge>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -219,53 +218,47 @@ export function AccountSettingsContent() {
|
||||
)}
|
||||
|
||||
{/* Logout */}
|
||||
<section className="border-t border-border pt-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{tCommon('account_settings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">{tCommon('logout')}</p>
|
||||
<p className="text-sm text-muted-foreground">{tCommon('logout_description')}</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
{tCommon('logout')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section className="space-y-4 border-t border-border pt-8">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{tCommon('account_settings')}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">{tCommon('logout')}</p>
|
||||
<p className="text-sm text-muted-foreground">{tCommon('logout_description')}</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
{tCommon('logout')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Privacy & agreements: surface the otherwise-unlinked DPA + privacy policy */}
|
||||
<section className="border-t border-border pt-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{tSettings('legal_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Link
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border p-4 transition-colors hover:bg-secondary/60"
|
||||
>
|
||||
<span className="font-medium">{tSettings('legal_privacy')}</span>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/dpa"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border p-4 transition-colors hover:bg-secondary/60"
|
||||
>
|
||||
<span className="font-medium">{tSettings('legal_dpa')}</span>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section className="space-y-4 border-t border-border pt-8">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{tSettings('legal_title')}
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border p-4 transition-colors hover:bg-secondary/60"
|
||||
>
|
||||
<span className="font-medium">{tSettings('legal_privacy')}</span>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/dpa"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border p-4 transition-colors hover:bg-secondary/60"
|
||||
>
|
||||
<span className="font-medium">{tSettings('legal_dpa')}</span>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Delete account: only for non-sandbox */}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { AlertTriangle, CreditCard, ExternalLink } from 'lucide-react'
|
||||
import { getSettingsPanel } from '@/lib/extensions/settings-panel-registry'
|
||||
@@ -75,7 +76,7 @@ export function BankingSettingsContent() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{bankConnectionError && (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-destructive/20 bg-destructive/10 p-4">
|
||||
<div className="flex items-start gap-3 rounded-lg border border-destructive p-4">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-destructive" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-destructive">{bankConnectionError}</p>
|
||||
@@ -123,18 +124,19 @@ export function BankingSettingsContent() {
|
||||
</>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<CreditCard className="h-10 w-10 text-muted-foreground/40 mb-4" />
|
||||
<p className="font-medium mb-1">{t('not_enabled_title')}</p>
|
||||
<p className="text-sm text-muted-foreground mb-4 max-w-md">
|
||||
{t('not_enabled_description')}
|
||||
</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/extensions">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('go_to_extensions')}
|
||||
</Link>
|
||||
</Button>
|
||||
<CardContent className="p-0">
|
||||
<EmptyState
|
||||
icon={CreditCard}
|
||||
title={t('not_enabled_title')}
|
||||
description={t('not_enabled_description')}
|
||||
>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/extensions">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('go_to_extensions')}
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper'
|
||||
@@ -74,8 +73,6 @@ export function SalarySettingsContent() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader title={t('title')} />
|
||||
|
||||
<SettingsFormWrapper onSave={handleSave} className="space-y-8">
|
||||
{/* Payment */}
|
||||
<section className="space-y-4">
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function TransactionBookingDialog({
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="sr-only">
|
||||
{t('description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
Reference in New Issue
Block a user