refactor(design): lock the border-radius ladder, one radius per role (#1607)
Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for which went where; one toolbar row on /transactions mixed four shape languages. This locks a 4-tier ladder (design.md convention 16): - pill: interactive toolbar controls (buttons, chips, pickers, segmented controls, toolbar search, count nubs) - rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs - rounded-lg (8px): cards, form fields, popover/menu content, boxes - rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs) Changes: - New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the hand-rolled bg-muted/70 tablist copied across 11 files - New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars; dialog/picker searches keep the rounded-lg Input - dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette - ContextPicker chips at the shared h-8 toolbar height - ~300 rounded-md / bare rounded call sites remapped by role; auth icon tiles and the mobile nav sheet come down from 16px to 12px - rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead vocabulary, enforced by a new off-ladder-radius check in check:guards Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc clean on all changed files, sandbox screenshots of transactions/ bookkeeping/granskning toolbars and the Ny verifikation dialog. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
18c20e68e6
commit
9686b54b41
@@ -371,7 +371,7 @@ export function LoginClient({ initialMethod }: { initialMethod: LoginMethod | nu
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<Mail className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -432,7 +432,7 @@ export function LoginClient({ initialMethod }: { initialMethod: LoginMethod | nu
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<KeyRound className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ function MfaEnrollContent() {
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<ShieldCheck className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,7 +233,7 @@ function MfaEnrollContent() {
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<ShieldCheck className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,7 +258,7 @@ function MfaEnrollContent() {
|
||||
Kan du inte skanna? Ange denna nyckel manuellt:
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded-md border bg-muted/50 px-3 py-2 text-xs font-mono text-center break-all select-all">
|
||||
<code className="flex-1 rounded-sm border bg-muted/50 px-3 py-2 text-xs font-mono text-center break-all select-all">
|
||||
{secret}
|
||||
</code>
|
||||
<Button
|
||||
|
||||
@@ -174,7 +174,7 @@ function MfaVerifyContent() {
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<ShieldCheck className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -387,7 +387,7 @@ function RegisterPageContent() {
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<Mail className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,7 +440,7 @@ function RegisterPageContent() {
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-frame p-4">
|
||||
<div className="w-full max-w-sm animate-slide-up space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<Mail className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -212,7 +212,7 @@ function ResetPasswordInner() {
|
||||
<div className="w-full max-w-sm animate-slide-up">
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-primary/8 flex items-center justify-center">
|
||||
<div className="h-14 w-14 rounded-xl bg-primary/8 flex items-center justify-center">
|
||||
<KeyRound className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,13 @@ import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
|
||||
import { Plus, Search, Package, Lock, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
|
||||
import { Plus, Package, Lock, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
|
||||
import { ActivateAccountsDialog } from '@/components/bookkeeping/ActivateAccountsDialog'
|
||||
import {
|
||||
useSubmitWithAccountActivation,
|
||||
@@ -315,18 +315,15 @@ function ArticlesPageInner() {
|
||||
|
||||
{/* Toolbar: search, plus the currency scope far right (convention 8) */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-[190px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<ToolbarSearch
|
||||
containerClassName="min-w-[190px]"
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
/>
|
||||
{/* A single-currency register needs no scope: the chip would be a
|
||||
control with one meaningful position. */}
|
||||
{availableCurrencies.length > 1 && (
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function DisposeAssetPage({ params }: { params: Promise<{ id: str
|
||||
<SelectContent>{VAT_TREATMENTS.map((value) => <SelectItem key={value} value={value}>{t(`vat_${value}`)}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<div className="rounded-md bg-secondary/40 p-3 text-xs">
|
||||
<div className="rounded-lg bg-secondary/40 p-3 text-xs">
|
||||
<SummaryRow label={t('gross')} value={formatCurrency(proceedsNumber)} />
|
||||
<SummaryRow label={t('vat')} value={formatCurrency(vatAmount)} />
|
||||
<SummaryRow label={t('net')} value={formatCurrency(netProceeds)} strong />
|
||||
@@ -351,20 +351,20 @@ export default function DisposeAssetPage({ params }: { params: Promise<{ id: str
|
||||
</div>
|
||||
)}
|
||||
{jamkningAssessment && (
|
||||
<div className="rounded-md border border-border bg-secondary/40 p-3 text-sm">
|
||||
<div className="rounded-lg border border-border bg-secondary/40 p-3 text-sm">
|
||||
<SummaryRow label={t('adjustment_direction')} value={t(`direction_${jamkningAssessment.direction}`)} />
|
||||
<SummaryRow label={t('adjustment_amount')} value={formatCurrency(jamkningAssessment.amount)} strong />
|
||||
{jamkningAssessment.capped && <p className="mt-2 text-xs text-muted-foreground">{t('adjustment_capped')}</p>}
|
||||
</div>
|
||||
)}
|
||||
{disposalType === 'business_transfer' && (
|
||||
<div className="flex items-center gap-3 rounded-md border border-border p-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border p-3">
|
||||
<Switch id="businessTransfer" checked={businessTransferConfirmed} onCheckedChange={setBusinessTransferConfirmed} />
|
||||
<Label htmlFor="businessTransfer" className="cursor-pointer">{t('business_transfer_confirm')}</Label>
|
||||
</div>
|
||||
)}
|
||||
{disposalType === 'business_transfer' && transferNeedsDocument && (
|
||||
<div className="flex items-center gap-3 rounded-md border border-border p-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border p-3">
|
||||
<Switch id="adjustmentDocument" checked={adjustmentDocumentConfirmed} onCheckedChange={setAdjustmentDocumentConfirmed} />
|
||||
<Label htmlFor="adjustmentDocument" className="cursor-pointer">{t('adjustment_document_confirm')}</Label>
|
||||
</div>
|
||||
|
||||
@@ -842,7 +842,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRetagLine(line as unknown as RetagLine)}
|
||||
className="p-1 rounded text-muted-foreground/50 hover:text-foreground hover:bg-secondary/60 transition-colors"
|
||||
className="p-1 rounded-sm text-muted-foreground/50 hover:text-foreground hover:bg-secondary/60 transition-colors"
|
||||
aria-label="Ändra dimensioner"
|
||||
title="Ändra dimensioner (påverkar endast internredovisningen)"
|
||||
>
|
||||
@@ -932,7 +932,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRetagLine(line as unknown as RetagLine)}
|
||||
className="p-1 rounded text-muted-foreground/50 hover:text-foreground transition-colors"
|
||||
className="p-1 rounded-sm text-muted-foreground/50 hover:text-foreground transition-colors"
|
||||
aria-label="Ändra dimensioner"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
@@ -996,7 +996,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
<li key={`${ref.type}-${ref.id}`}>
|
||||
<Link
|
||||
href={ref.type === 'invoice' ? `/invoices/${ref.id}` : `/supplier-invoices/${ref.id}`}
|
||||
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50 hover:bg-secondary/60 transition-colors"
|
||||
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded-sm bg-muted/50 hover:bg-secondary/60 transition-colors"
|
||||
>
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
|
||||
import { SegmentedControl } from '@/components/ui/segmented-control'
|
||||
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS, RowFoldout } from '@/components/ui/dry-table'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { useCanWrite } from '@/lib/hooks/use-can-write'
|
||||
@@ -211,35 +212,15 @@ export default function AccrualSchedulesPage() {
|
||||
) : null}
|
||||
|
||||
{/* Toolbar: status seg (concept scene 33) */}
|
||||
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
|
||||
{(
|
||||
[
|
||||
{ key: 'active', label: 'Aktiva', count: activeCount },
|
||||
{ key: 'completed', label: 'Avslutade', count: null },
|
||||
{ key: 'all', label: 'Alla', count: null },
|
||||
] as const
|
||||
).map(({ key, label, count }) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={statusFilter === key}
|
||||
onClick={() => setStatusFilter(key)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150 ${
|
||||
statusFilter === key
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{count !== null && count > 0 && (
|
||||
<span className="rounded-full bg-secondary px-1.5 text-[10px] font-medium tabular-nums">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
options={[
|
||||
{ value: 'active', label: 'Aktiva', count: activeCount ?? undefined },
|
||||
{ value: 'completed', label: 'Avslutade' },
|
||||
{ value: 'all', label: 'Alla' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -522,7 +522,7 @@ function VacationStep({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{proposal ? (
|
||||
<div className="flex items-start justify-between gap-4 rounded-md border border-border p-4">
|
||||
<div className="flex items-start justify-between gap-4 rounded-lg border border-border p-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="text-sm font-medium">{proposal.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{proposal.description}</p>
|
||||
@@ -590,7 +590,7 @@ function AuditStep({
|
||||
</Label>
|
||||
</div>
|
||||
{state.enabled && (
|
||||
<div className="grid grid-cols-2 gap-4 rounded-md border border-border p-4">
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg border border-border p-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Belopp (kr)</Label>
|
||||
<Input
|
||||
@@ -605,7 +605,7 @@ function AuditStep({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Konto</Label>
|
||||
<select
|
||||
className="border border-border rounded-md h-9 text-sm px-2 w-full bg-background"
|
||||
className="border border-border rounded-lg h-9 text-sm px-2 w-full bg-background"
|
||||
value={state.liabilityAccount}
|
||||
onChange={(e) =>
|
||||
onChange({ ...state, liabilityAccount: e.target.value as '2991' | '2992' })
|
||||
@@ -667,7 +667,7 @@ function AutoStep({
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-start gap-3 rounded-md border border-border p-3"
|
||||
className="flex items-start gap-3 rounded-lg border border-border p-3"
|
||||
>
|
||||
<Checkbox
|
||||
id={`auto-${key}`}
|
||||
@@ -808,7 +808,7 @@ function ManualEntryEditor({
|
||||
template.side === 'deferred_revenue' ? 'Intäktskonto' : 'Kostnadskonto'
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border p-3 space-y-3">
|
||||
<div className="rounded-lg 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" aria-label="Ta bort">
|
||||
@@ -989,7 +989,7 @@ function ReviewStep({
|
||||
|
||||
function ReviewLine({ label, amount, note }: { label: string; amount: number; note?: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border border-border px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{label}</p>
|
||||
{note && <p className="text-xs text-muted-foreground">{note}</p>}
|
||||
|
||||
@@ -6,13 +6,13 @@ import { useLocale, useTranslations } from 'next-intl'
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
|
||||
import { Plus, Search, Users, Lock, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
|
||||
import { Plus, Users, Lock, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
|
||||
import { EmptyCustomers, EmptyState } from '@/components/ui/empty-state'
|
||||
import { ReportExportMenu } from '@/components/reports/ReportExportMenu'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -293,18 +293,14 @@ function CustomersPageInner() {
|
||||
|
||||
{/* Toolbar: search (concept) */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-[220px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<ToolbarSearch
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { HelpPopover } from '@/components/ui/help-popover'
|
||||
import { AttnLine } from '@/components/ui/attn-line'
|
||||
import { SegmentedControl } from '@/components/ui/segmented-control'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -2048,7 +2049,7 @@ function CSVDataImportWizard() {
|
||||
onClick={() => setEntity(opt.value)}
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
'relative h-9 rounded-md border px-4 text-sm font-medium transition-colors',
|
||||
'relative h-9 rounded-lg border px-4 text-sm font-medium transition-colors',
|
||||
selected
|
||||
? 'border-foreground bg-foreground text-background'
|
||||
: 'border-border bg-card text-foreground hover:border-foreground/30 hover:bg-muted',
|
||||
@@ -2203,29 +2204,14 @@ export default function ImportPage() {
|
||||
{isSandbox && <AttnLine>{t('sandbox_disabled')}</AttnLine>}
|
||||
|
||||
{/* Importera / Exportera as separate tabs (house seg), like before */}
|
||||
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
|
||||
{(
|
||||
[
|
||||
{ key: 'import', label: t('tab_import') },
|
||||
{ key: 'export', label: t('tab_export') },
|
||||
] as const
|
||||
).map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={view === key}
|
||||
onClick={() => handleViewChange(key)}
|
||||
className={`rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150 ${
|
||||
view === key
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={view}
|
||||
onChange={handleViewChange}
|
||||
options={[
|
||||
{ value: 'import', label: t('tab_import') },
|
||||
{ value: 'export', label: t('tab_export') },
|
||||
]}
|
||||
/>
|
||||
|
||||
{view === 'import' ? (
|
||||
<div>
|
||||
@@ -2370,7 +2356,7 @@ export default function ImportPage() {
|
||||
<label className="flex cursor-pointer items-start gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-4 w-4 rounded border-border"
|
||||
className="mt-0.5 h-4 w-4 rounded-sm border-border"
|
||||
checked={exportExcludeClosing}
|
||||
onChange={(e) => setExportExcludeClosing(e.target.checked)}
|
||||
/>
|
||||
@@ -2551,7 +2537,7 @@ function ImportRow({
|
||||
// with the inverse lift in dark mode.
|
||||
function LogoChip({ src, name, mono = false }: { src: string; name: string; mono?: boolean }) {
|
||||
return (
|
||||
<span className="flex items-center gap-2 rounded border border-border bg-muted/30 px-2 py-1">
|
||||
<span className="flex items-center gap-2 rounded-sm border border-border bg-muted/30 px-2 py-1">
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
|
||||
@@ -1580,7 +1580,7 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
return (
|
||||
<li
|
||||
key={p.id}
|
||||
className="flex items-center justify-between gap-3 px-2 py-2 text-sm transition-colors hover:bg-secondary/60 rounded"
|
||||
className="flex items-center justify-between gap-3 px-2 py-2 text-sm transition-colors hover:bg-secondary/60 rounded-sm"
|
||||
>
|
||||
<span className="tabular-nums text-muted-foreground">
|
||||
{formatDate(p.payment_date)}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { RowStatus, type RowStatusDescriptor } from '@/components/ui/row-status'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Dialog, DialogContent, DialogTitle, DialogVeil } from '@/components/ui/dialog'
|
||||
import { DataListEmpty } from '@/components/ui/data-list'
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
Plus,
|
||||
Search,
|
||||
ReceiptText,
|
||||
Repeat,
|
||||
FileInput,
|
||||
@@ -620,18 +619,15 @@ export default function InvoicesPage() {
|
||||
annotation: tabCounts[tab] > 0 ? String(tabCounts[tab]) : undefined,
|
||||
}))}
|
||||
/>
|
||||
<div className="relative min-w-[190px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
resetPaging()
|
||||
}}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<ToolbarSearch
|
||||
containerClassName="min-w-[190px]"
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
resetPaging()
|
||||
}}
|
||||
/>
|
||||
<div className="ml-auto">
|
||||
<FyPicker
|
||||
value={fyPeriodId}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function DashboardLoading() {
|
||||
full-width list, so that shape stays there. */}
|
||||
<aside className="hidden md:flex md:w-12 shrink-0 flex-col items-center border-r border-border bg-card/40 py-3 gap-2">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-md" />
|
||||
<Skeleton className="h-8 w-8" />
|
||||
</aside>
|
||||
<aside className="flex w-full flex-col border-r border-border bg-card/40 md:hidden shrink-0">
|
||||
<div className="space-y-3 border-b border-border px-5 py-4">
|
||||
@@ -42,7 +42,7 @@ export default function DashboardLoading() {
|
||||
<Skeleton className="h-2.5 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="h-8 w-full rounded-md" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
<div className="space-y-1 p-3">
|
||||
{['w-40', 'w-48', 'w-36', 'w-44'].map((w, i) => (
|
||||
@@ -80,7 +80,7 @@ export default function DashboardLoading() {
|
||||
key={i}
|
||||
className="flex items-start gap-3 border-b border-border px-1 py-3.5"
|
||||
>
|
||||
<Skeleton className="mt-px h-[15px] w-[15px] shrink-0 rounded" />
|
||||
<Skeleton className="mt-px h-[15px] w-[15px] shrink-0" />
|
||||
<Skeleton className={`h-3.5 ${w}`} />
|
||||
<Skeleton className="ml-auto h-5 w-7 shrink-0 rounded-full" />
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { DataListEmpty, DataListLoading } from '@/components/ui/data-list'
|
||||
import { ContextPicker } from '@/components/common/ContextPicker'
|
||||
import { SegmentedControl } from '@/components/ui/segmented-control'
|
||||
import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import {
|
||||
SlideOver,
|
||||
@@ -229,7 +230,7 @@ function useAccountNamesSource(): Record<string, string> {
|
||||
function PeriodLockBanner({ period }: { period: PeriodStatusShape }) {
|
||||
const lockedThrough = period.lock_date ? formatDate(period.lock_date) : null
|
||||
return (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm">
|
||||
<div className="flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm">
|
||||
<Lock className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-destructive">
|
||||
@@ -704,7 +705,7 @@ export default function PendingOperationsPage() {
|
||||
</div>
|
||||
|
||||
{conversationFilter && (
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/30 px-3 py-2 text-sm">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/30 px-3 py-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
<span>
|
||||
@@ -733,30 +734,15 @@ export default function PendingOperationsPage() {
|
||||
{/* Toolbar (concept): status seg left, source picker (convention 8)
|
||||
far right. The count chip rides only on Väntar: it is the queue. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
|
||||
{SEG_TABS.map(({ tab, labelKey }) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150',
|
||||
activeTab === tab
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{t(labelKey)}
|
||||
{tab === 'pending' && (pendingCount ?? 0) > 0 && (
|
||||
<span className="rounded-full bg-secondary px-1.5 text-[10px] font-medium tabular-nums">
|
||||
{pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
options={SEG_TABS.map(({ tab, labelKey }) => ({
|
||||
value: tab,
|
||||
label: t(labelKey),
|
||||
count: tab === 'pending' ? pendingCount ?? 0 : undefined,
|
||||
}))}
|
||||
/>
|
||||
<div className="ml-auto">
|
||||
<ContextPicker
|
||||
value={sourceFilter}
|
||||
@@ -1190,7 +1176,7 @@ export default function PendingOperationsPage() {
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>{t('bulk_confirm_intro')}</p>
|
||||
<ul className="space-y-1 rounded-md border bg-muted/30 px-3 py-2">
|
||||
<ul className="space-y-1 rounded-lg border bg-muted/30 px-3 py-2">
|
||||
{selectedBreakdown.map(({ type, count }) => (
|
||||
<li key={type} className="flex justify-between font-mono tabular-nums">
|
||||
<span className="font-sans">{bulkActionLabel(type, count, t)}</span>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function SalaryRunEmployeeDetailPage({
|
||||
>
|
||||
<ArrowLeft className="mr-1 h-3.5 w-3.5" /> {t('back_to_run')}
|
||||
</Link>
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error ?? t('error_load_employee')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,7 +321,7 @@ export default function SalaryRunEmployeeDetailPage({
|
||||
function SummaryCard({ label, value, accent, overridden }: { label: string; value: number; accent?: boolean; overridden?: boolean }) {
|
||||
const t = useTranslations('salary_run_employee')
|
||||
return (
|
||||
<div className={cn('rounded-md border bg-card p-3', accent && 'ring-1 ring-primary/40')}>
|
||||
<div className={cn('rounded-lg border bg-card p-3', accent && 'ring-1 ring-primary/40')}>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{label}
|
||||
{/* The override is an exception, so it is a chip, not a second ring:
|
||||
@@ -337,7 +337,7 @@ function SummaryCard({ label, value, accent, overridden }: { label: string; valu
|
||||
function AbsenceCount({ label, days }: { label: string; days: number }) {
|
||||
const t = useTranslations('salary_run_employee')
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/30 px-3 py-2">
|
||||
<div className="rounded-lg 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">{t('days_count', { days })}</div>
|
||||
</div>
|
||||
|
||||
@@ -896,7 +896,7 @@ export default function SalaryRunPage({ params }: { params: Promise<{ id: string
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{approveOverride && approveOverride.length > 0 && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-border p-3 text-xs">
|
||||
<div className="flex items-start gap-2 rounded-lg border border-border p-3 text-xs">
|
||||
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning" />
|
||||
<ul className="space-y-1">
|
||||
{approveOverride.map((reason, i) => (
|
||||
|
||||
@@ -888,7 +888,7 @@ function MatchDialog({
|
||||
)}
|
||||
|
||||
{!loading && candidates && candidates.length > 0 && (
|
||||
<div className="max-h-[420px] overflow-y-auto rounded-md border">
|
||||
<div className="max-h-[420px] overflow-y-auto rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -1080,7 +1080,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
onChange={(e) => updateEditLine(i, { description: e.target.value })}
|
||||
placeholder="Beskrivning"
|
||||
/>
|
||||
<div className="inline-flex rounded-md border bg-background overflow-hidden h-9">
|
||||
<div className="inline-flex rounded-lg border bg-background overflow-hidden h-9">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateEditLine(i, { side: 'debit' })}
|
||||
@@ -1197,7 +1197,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
? t('duplicate_payment_description_one')
|
||||
: t('duplicate_payment_description_many')}
|
||||
</p>
|
||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3">
|
||||
<div className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
{duplicateCandidates?.map((c) => (
|
||||
<div key={c.id} className="flex items-center justify-between gap-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -8,13 +8,13 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { DataListEmpty } from '@/components/ui/data-list'
|
||||
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import { FyPicker } from '@/components/common/FyPicker'
|
||||
import { ContextPicker } from '@/components/common/ContextPicker'
|
||||
import { HelpPopover } from '@/components/ui/help-popover'
|
||||
import { Plus, FileInput, Lock, Search } from 'lucide-react'
|
||||
import { Plus, FileInput, Lock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { DialogLoadingSkeleton } from '@/components/ui/dialog-loading-skeleton'
|
||||
import { useCanWrite } from '@/lib/hooks/use-can-write'
|
||||
@@ -316,15 +316,12 @@ export default function SupplierInvoicesPage() {
|
||||
: undefined,
|
||||
}))}
|
||||
/>
|
||||
<div className="relative min-w-[190px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<ToolbarSearch
|
||||
containerClassName="min-w-[190px]"
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<div className="ml-auto flex items-center gap-4">
|
||||
<Link href="/supplier-invoices/payment-files" className={QUIET_LINK_CLASS}>
|
||||
{t('payment_files_link')}
|
||||
|
||||
@@ -6,14 +6,14 @@ import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table'
|
||||
import { ReportExportMenu } from '@/components/reports/ReportExportMenu'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Plus, Search, Building2, Lock } from 'lucide-react'
|
||||
import { Plus, Building2, Lock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
@@ -170,18 +170,14 @@ export default function SuppliersPage() {
|
||||
|
||||
{/* Toolbar: search (concept) */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-[220px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<ToolbarSearch
|
||||
placeholder={t('search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setVisibleCount(INITIAL_VISIBLE_ROWS)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
@@ -14,10 +14,11 @@ import { ToastAction } from '@/components/ui/toast'
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { DataList, DataListEmpty } from '@/components/ui/data-list'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { SegmentedControl } from '@/components/ui/segmented-control'
|
||||
import { ToolbarSearch } from '@/components/ui/toolbar-search'
|
||||
import { TH_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
||||
import { Loader2, Search } from 'lucide-react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import TransactionStatusBar from '@/components/transactions/TransactionStatusBar'
|
||||
import BankSyncStatusChip from '@/components/transactions/BankSyncStatusChip'
|
||||
import { ContextPicker, type ContextPickerItem } from '@/components/common/ContextPicker'
|
||||
@@ -3177,71 +3178,25 @@ export default function TransactionsPage() {
|
||||
{/* Toolbar (concept order): [Att bokföra/Alla-seg] [sök] [Välj flera]
|
||||
... [source ContextPicker far right] */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={mode === 'inbox'}
|
||||
onClick={() => setMode('inbox')}
|
||||
className={`inline-flex items-center gap-1.5 rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150 ${
|
||||
mode === 'inbox'
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('mode_inbox')}
|
||||
{inboxBadgeCount > 0 && (
|
||||
<span className="rounded-full bg-secondary px-1.5 text-[10px] font-medium tabular-nums">
|
||||
{inboxBadgeCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={mode === 'history'}
|
||||
onClick={() => setMode('history')}
|
||||
className={`rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150 ${
|
||||
mode === 'history'
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('mode_all')}
|
||||
</button>
|
||||
{/* Review tab exists only while suggestions do (or while the user is
|
||||
standing in it after emptying the list): a permanent third tab
|
||||
would advertise a migrator surface most companies never need. */}
|
||||
{(suggestionItems.length > 0 || mode === 'review') && (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={mode === 'review'}
|
||||
onClick={() => setMode('review')}
|
||||
className={`inline-flex items-center gap-1.5 rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150 ${
|
||||
mode === 'review'
|
||||
? 'border border-border bg-card font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('mode_review')}
|
||||
{suggestionItems.length > 0 && (
|
||||
<span className="rounded-full bg-secondary px-1.5 text-[10px] font-medium tabular-nums">
|
||||
{suggestionItems.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative min-w-[220px] max-w-xs flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Sök transaktioner…"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="h-9 pl-10"
|
||||
/>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
options={[
|
||||
{ value: 'inbox', label: t('mode_inbox'), count: inboxBadgeCount },
|
||||
{ value: 'history', label: t('mode_all') },
|
||||
// Review tab exists only while suggestions do (or while the user is
|
||||
// standing in it after emptying the list): a permanent third tab
|
||||
// would advertise a migrator surface most companies never need.
|
||||
...(suggestionItems.length > 0 || mode === 'review'
|
||||
? [{ value: 'review' as const, label: t('mode_review'), count: suggestionItems.length }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
<ToolbarSearch
|
||||
placeholder="Sök transaktioner…"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
{/* Far-right context group, shared by both view modes: period scope
|
||||
(rakenskapsar chip) and the account chooser (concept scene 10).
|
||||
Approved deviation from convention 8's single chip: booking is
|
||||
@@ -3276,12 +3231,12 @@ export default function TransactionsPage() {
|
||||
<DataList className="stagger-enter">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-4 py-3">
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
<Skeleton className="h-5 w-5" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-48 rounded" />
|
||||
<Skeleton className="h-3 w-24 rounded" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-20 rounded" />
|
||||
<Skeleton className="h-5 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</DataList>
|
||||
@@ -3857,7 +3812,7 @@ export default function TransactionsPage() {
|
||||
fakturan istället för att bokföra direkt på leverantörsskuldskontot, annars skapas en
|
||||
dubblerad verifikation som måste stornas (BFL 5 kap 5 §).
|
||||
</p>
|
||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3">
|
||||
<div className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
{siMatchSuggestion?.candidates.map((c) => (
|
||||
<div key={c.supplier_invoice_id} className="flex items-center justify-between gap-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
@@ -3915,7 +3870,7 @@ export default function TransactionsPage() {
|
||||
istället för att bokföra direkt mot kundfordringskontot, annars skapas en dubblerad
|
||||
verifikation som måste stornas (BFL 5 kap 5 §).
|
||||
</p>
|
||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3">
|
||||
<div className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
{ciMatchSuggestion?.candidates.map((c) => (
|
||||
<div key={c.invoice_id} className="flex items-center justify-between gap-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function GlobalError({
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-full bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Försök igen
|
||||
</button>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default async function PayslipPage({
|
||||
|
||||
<a
|
||||
href={`/api/payslip/${token}/pdf`}
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-full bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
|
||||
>
|
||||
Ladda ner PDF (lönespecifikation)
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user