refactor(ui): system hygiene sweep from the UI craft audit (#1281)
Raw palette: SalaryCalendar was the only file in the salary cluster still on bg-red-100 / text-amber-800 style classes, with zero dark: variants, so every absence pill rendered paper-white on a near-black page. All 30 are now alpha-over-token on the semantic scale, which needs no dark: variant (the approach badge.tsx already takes). Eight absence types share four tones, so fill carries a second axis: filled vs outlined separates the types whose Lucide icon is identical (Heart is parental, pregnancy and care_relative; Activity is study and other_leave). Primitives: - select and dropdown-menu now scale from their trigger via the Radix transform-origin variables instead of from their own middle. Dialog stays centred: modals are not anchored to a trigger. - toast used fade-out-80, the one animate-* class with no @utility in globals.css, so --tw-exit-opacity fell back to 1 and the toast slid away at full opacity. Enter and exit now also share one path per breakpoint (top on mobile where the viewport is a full-width bar, right on desktop where it is a corner card) using max-sm:/sm: rather than stacking both, which would have produced a diagonal. - slide-over exited on ease-in, which delays the moment the user has already decided to leave; it enters on the drawer curve and now leaves on it too. - progress animated transition-all where only transform changes. - One app-wide TooltipProvider in the root layout. skipDelayDuration is provider-scoped, so a provider per instance meant the grace window could never fire and every account number in a huvudbok re-paid the full 200ms. Also: nine hand-rolled animate-pulse placeholders in three different greys to the Skeleton primitive, 25 CardTitle text-lg to the locked text-base, two always-on chips to muted text (a chip on every row is not an exception), the three extensions titles onto the display face, the help filter chips onto pill geometry, and the dead border-border/60 on the /import row. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bc5c673f7d
commit
3d02a74147
@@ -242,9 +242,13 @@ export default function ArticleDetailPage({
|
||||
<div>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{article.name}</h1>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant={article.active ? 'success' : 'secondary'}>
|
||||
{article.active ? t('status_active') : t('status_inactive')}
|
||||
</Badge>
|
||||
{article.active ? (
|
||||
<span className="text-sm text-muted-foreground">{t('status_active')}</span>
|
||||
) : (
|
||||
<Badge variant="outline" className="font-normal">
|
||||
{t('status_inactive')}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground tabular-nums">
|
||||
{t(ARTICLE_TYPE_KEY[article.type])}
|
||||
{article.article_number ? ` · #${article.article_number}` : ''}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default async function ExtensionDetailPage({
|
||||
<Icon className="h-7 w-7 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold tracking-tight">{extensionName}</h1>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{extensionName}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{extensionDescription}</p>
|
||||
<div className="mt-2">
|
||||
<CategoryBadge category={definition.category} />
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function SectorExtensionsPage({
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold tracking-tight">{sectorName}</h1>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{sectorName}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{sectorDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function ExtensionsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-semibold tracking-tight">{t('page_title')}</h1>
|
||||
<h1 className="font-display text-2xl leading-8 tracking-tight">{t('page_title')}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('page_description')}
|
||||
</p>
|
||||
|
||||
@@ -350,10 +350,10 @@ export default function HelpPage() {
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
|
||||
'rounded-full px-4 py-[7px] text-[13px] transition-colors duration-150',
|
||||
selectedCategory === null
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground hover:text-foreground'
|
||||
: 'bg-secondary/60 text-muted-foreground hover:bg-secondary hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{t('filter_all')}
|
||||
@@ -363,10 +363,10 @@ export default function HelpPage() {
|
||||
key={key}
|
||||
onClick={() => setSelectedCategory(selectedCategory === key ? null : key)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
|
||||
'rounded-full px-4 py-[7px] text-[13px] transition-colors duration-150',
|
||||
selectedCategory === key
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground hover:text-foreground'
|
||||
: 'bg-secondary/60 text-muted-foreground hover:bg-secondary hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{t(config.labelKey)}
|
||||
@@ -400,7 +400,7 @@ export default function HelpPage() {
|
||||
{/* Document templates */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('templates_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('templates_title')}</CardTitle>
|
||||
<CardDescription>{t('templates_subtitle')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -438,7 +438,7 @@ export default function HelpPage() {
|
||||
{/* External resources */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('external_resources_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('external_resources_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
|
||||
@@ -2298,7 +2298,7 @@ function ImportRow({
|
||||
disabled={disabled}
|
||||
aria-expanded={expanded}
|
||||
className={cn(
|
||||
'group flex w-full items-center justify-between gap-4 border-b border-border/60 px-1 py-3 text-left',
|
||||
'group flex w-full items-center justify-between gap-4 border-b border-border px-1 py-3 text-left',
|
||||
'transition-colors duration-150 hover:bg-secondary/35',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent',
|
||||
)}
|
||||
|
||||
@@ -261,9 +261,9 @@ export default function RecurringInvoicesPage() {
|
||||
<TableCell className="tabular-nums">{formatDate(s.next_run_date)}</TableCell>
|
||||
<TableCell>
|
||||
{s.status === 'active' ? (
|
||||
<Badge variant="success">{t('status_active')}</Badge>
|
||||
<span className="text-xs text-muted-foreground">{t('status_active')}</span>
|
||||
) : (
|
||||
<Badge variant="secondary">{t('status_paused')}</Badge>
|
||||
<Badge variant="outline" className="font-normal">{t('status_paused')}</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums text-right">
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/info-tooltip'
|
||||
import {
|
||||
@@ -193,7 +192,6 @@ export function KassaflodesanalysClient() {
|
||||
)}
|
||||
Ladda ner PDF
|
||||
</Button>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
@@ -205,7 +203,6 @@ export function KassaflodesanalysClient() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Snart tillgänglig</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('invoice_info_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('invoice_info_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
@@ -629,7 +629,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('amounts_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('amounts_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
@@ -666,7 +666,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{invoice.supplier && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('supplier_section_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('supplier_section_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm">
|
||||
<Link href={`/suppliers/${invoice.supplier.id}`} className="text-primary hover:underline font-medium">
|
||||
@@ -683,7 +683,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{/* Line items */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('rows_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('rows_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Desktop table */}
|
||||
@@ -766,7 +766,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{payments.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('payment_history_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('payment_history_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Desktop table */}
|
||||
@@ -824,7 +824,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{invoice.document_id && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('document_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('document_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -844,7 +844,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{/* Journal entries (sambandskrav) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('vouchers_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('vouchers_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{invoice.registration_journal_entry_id ? (
|
||||
@@ -895,7 +895,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{invoice.notes && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('notes_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('notes_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{invoice.notes}</p>
|
||||
|
||||
@@ -126,9 +126,7 @@ export default function SupplierDetailPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Card className="animate-pulse">
|
||||
<CardContent className="h-48" />
|
||||
</Card>
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -240,7 +238,7 @@ export default function SupplierDetailPage() {
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('contact_section_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('contact_section_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{supplier.email && <p>{t('email_inline', { email: supplier.email })}</p>}
|
||||
@@ -252,7 +250,7 @@ export default function SupplierDetailPage() {
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('payment_section_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('payment_section_title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{supplier.bankgiro && <p>{t('bankgiro_inline', { value: supplier.bankgiro })}</p>}
|
||||
@@ -269,7 +267,7 @@ export default function SupplierDetailPage() {
|
||||
{/* Invoices */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg">{t('invoices_section_title')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('invoices_section_title')}</CardTitle>
|
||||
<Link href="/supplier-invoices?new=1">
|
||||
<Button size="sm">
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
|
||||
+7
-4
@@ -6,6 +6,7 @@ import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip-provider";
|
||||
import { DeployReloadPrompt } from "@/components/system/DeployReloadPrompt";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { SWRProvider } from "@/components/providers/SWRProvider";
|
||||
@@ -89,10 +90,12 @@ export default async function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SWRProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
<DeployReloadPrompt />
|
||||
<ScrollbarReveal />
|
||||
<TooltipProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
<DeployReloadPrompt />
|
||||
<ScrollbarReveal />
|
||||
</TooltipProvider>
|
||||
</SWRProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-m
|
||||
import type { StoredStagedOperation } from '@/types'
|
||||
import type { AgentStatusEvent } from './agent-status'
|
||||
import { sendFeedback, type FeedbackSentiment } from './feedback-client'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
// Markdown parser loads separately from the chat surface: react-markdown +
|
||||
// remark-gfm pull in the whole unified/remark tree.
|
||||
@@ -1197,9 +1198,9 @@ function SkeletonBubble() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-start animate-fade-in">
|
||||
<div className="max-w-[85%] rounded-lg border border-border bg-card px-4 py-3 space-y-2 w-72">
|
||||
<div className="h-3 rounded bg-muted-foreground/15 animate-pulse w-full" />
|
||||
<div className="h-3 rounded bg-muted-foreground/15 animate-pulse w-[85%]" />
|
||||
<div className="h-3 rounded bg-muted-foreground/15 animate-pulse w-[60%]" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-[85%]" />
|
||||
<Skeleton className="h-3 w-[60%]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type AgentStatus,
|
||||
type AgentStatusEvent,
|
||||
} from './agent-status'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
// The sheet is a lazy chunk, and it used to have no loading state at all: a
|
||||
// click on the launcher produced NOTHING until the chunk arrived, then the
|
||||
@@ -34,8 +35,8 @@ function AgentSheetSkeleton() {
|
||||
style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}
|
||||
>
|
||||
<div className="flex items-center gap-2 border-b border-border px-4 py-4">
|
||||
<div className="h-8 w-8 shrink-0 animate-pulse rounded-full bg-secondary" />
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-secondary" />
|
||||
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PaymentCalendar } from '@/extensions/general/calendar/components/Paymen
|
||||
import type { DeadlineFormValues } from '@/components/deadlines/DeadlineForm'
|
||||
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
|
||||
import type { Invoice, Deadline } from '@/types'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export default function CalendarWorkspace({ userId }: WorkspaceComponentProps) {
|
||||
const [invoices, setInvoices] = useState<Invoice[]>([])
|
||||
@@ -107,9 +108,9 @@ export default function CalendarWorkspace({ userId }: WorkspaceComponentProps) {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-10 bg-muted rounded w-48 mb-4" />
|
||||
<div className="h-96 bg-muted rounded" />
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-48" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1019,7 +1019,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
|
||||
{dryRunResults && dryRunResults.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
<CardTitle className="text-base">
|
||||
Förhandsgranskning: {dryRunResults.length}{' '}
|
||||
{dryRunResults.length === 1 ? 'matchning hittad' : 'matchningar hittade'}
|
||||
</CardTitle>
|
||||
@@ -1280,7 +1280,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
|
||||
{unmatchedGlLines.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
<CardTitle className="text-base">
|
||||
Omatchade verifikationer på <AccountNumber number={accountNumber} /> ({unmatchedGlLines.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -1362,7 +1362,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
<CardTitle className="text-lg">
|
||||
<CardTitle className="text-base">
|
||||
Ignorerade transaktioner ({ignoredTx.length})
|
||||
</CardTitle>
|
||||
</div>
|
||||
@@ -1427,7 +1427,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
<CardTitle className="text-lg">
|
||||
<CardTitle className="text-base">
|
||||
Matchade transaktioner ({matchedTx.length})
|
||||
</CardTitle>
|
||||
</div>
|
||||
|
||||
@@ -510,7 +510,7 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
|
||||
{/* Revenue */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Rörelseintäkter</CardTitle>
|
||||
<CardTitle className="text-base">Rörelseintäkter</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReportSectionTable sections={data.revenue_sections} onNavigateToAccount={onNavigateToAccount} />
|
||||
@@ -524,7 +524,7 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
|
||||
{/* Expenses */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Rörelsekostnader</CardTitle>
|
||||
<CardTitle className="text-base">Rörelsekostnader</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReportSectionTable sections={data.expense_sections} negate onNavigateToAccount={onNavigateToAccount} />
|
||||
@@ -551,7 +551,7 @@ export function IncomeStatementView({ periodId, dateRange, dimensionFilter = nul
|
||||
{data.financial_sections.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Finansiella poster</CardTitle>
|
||||
<CardTitle className="text-base">Finansiella poster</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReportSectionTable sections={data.financial_sections} onNavigateToAccount={onNavigateToAccount} />
|
||||
@@ -648,7 +648,7 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: {
|
||||
{/* Assets */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Tillgångar</CardTitle>
|
||||
<CardTitle className="text-base">Tillgångar</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReportSectionTable sections={data.asset_sections} onNavigateToAccount={onNavigateToAccount} />
|
||||
@@ -662,7 +662,7 @@ export function BalanceSheetView({ periodId, dateRange, onNavigateToAccount }: {
|
||||
{/* Equity and liabilities */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Eget kapital och skulder</CardTitle>
|
||||
<CardTitle className="text-base">Eget kapital och skulder</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReportSectionTable sections={data.equity_liability_sections} onNavigateToAccount={onNavigateToAccount} />
|
||||
|
||||
@@ -81,15 +81,27 @@ interface AbsenceTypeMeta {
|
||||
pillClass: string
|
||||
}
|
||||
|
||||
// Absence tone comes from the semantic scale, never the raw Tailwind palette.
|
||||
// Every value is alpha-over-token, which is why none of these need a `dark:`
|
||||
// variant: the same class reads correctly on both grounds (the approach
|
||||
// components/ui/badge.tsx already takes). The eight types share four tones, so
|
||||
// fill carries a second axis: filled vs outlined separates the two types
|
||||
// inside a tone whose Lucide icon is identical (Heart is parental, pregnancy
|
||||
// and care_relative; Activity is study and other_leave).
|
||||
//
|
||||
// terracotta = sick (karens, employer cost)
|
||||
// ochre = caring for someone else (vab, närstående)
|
||||
// sage = parental and pregnancy leave
|
||||
// neutral = the rest (study, unpaid, other)
|
||||
const TYPE_META: Record<AbsenceType, AbsenceTypeMeta> = {
|
||||
sick: { labelKey: 'type_sick', shortLabelKey: 'type_sick_short', icon: HeartPulse, pillClass: 'bg-red-100 text-red-800' },
|
||||
vab: { labelKey: 'type_vab', shortLabelKey: 'type_vab_short', icon: Baby, pillClass: 'bg-amber-100 text-amber-800' },
|
||||
parental: { labelKey: 'type_parental', shortLabelKey: 'type_parental_short', icon: Heart, pillClass: 'bg-emerald-100 text-emerald-800' },
|
||||
pregnancy: { labelKey: 'type_pregnancy', shortLabelKey: 'type_pregnancy_short', icon: Heart, pillClass: 'bg-pink-100 text-pink-800' },
|
||||
care_relative: { labelKey: 'type_care_relative', shortLabelKey: 'type_care_relative_short', icon: Heart, pillClass: 'bg-blue-100 text-blue-800' },
|
||||
study: { labelKey: 'type_study', shortLabelKey: 'type_study_short', icon: Activity, pillClass: 'bg-indigo-100 text-indigo-800' },
|
||||
unpaid_leave: { labelKey: 'type_unpaid_leave', shortLabelKey: 'type_unpaid_leave_short', icon: MinusCircle, pillClass: 'bg-slate-100 text-slate-800' },
|
||||
other_leave: { labelKey: 'type_other_leave', shortLabelKey: 'type_other_leave_short', icon: Activity, pillClass: 'bg-zinc-100 text-zinc-800' },
|
||||
sick: { labelKey: 'type_sick', shortLabelKey: 'type_sick_short', icon: HeartPulse, pillClass: 'bg-destructive/10 text-destructive' },
|
||||
vab: { labelKey: 'type_vab', shortLabelKey: 'type_vab_short', icon: Baby, pillClass: 'bg-warning/15 text-warning-foreground' },
|
||||
care_relative: { labelKey: 'type_care_relative', shortLabelKey: 'type_care_relative_short', icon: Heart, pillClass: 'border border-warning/40 text-warning-foreground' },
|
||||
parental: { labelKey: 'type_parental', shortLabelKey: 'type_parental_short', icon: Heart, pillClass: 'bg-success/10 text-success' },
|
||||
pregnancy: { labelKey: 'type_pregnancy', shortLabelKey: 'type_pregnancy_short', icon: Heart, pillClass: 'border border-success/40 text-success' },
|
||||
study: { labelKey: 'type_study', shortLabelKey: 'type_study_short', icon: Activity, pillClass: 'bg-secondary text-secondary-foreground' },
|
||||
unpaid_leave: { labelKey: 'type_unpaid_leave', shortLabelKey: 'type_unpaid_leave_short', icon: MinusCircle, pillClass: 'bg-muted text-muted-foreground' },
|
||||
other_leave: { labelKey: 'type_other_leave', shortLabelKey: 'type_other_leave_short', icon: Activity, pillClass: 'border border-border text-muted-foreground' },
|
||||
}
|
||||
|
||||
const TYPE_ORDER: AbsenceType[] = ['sick', 'vab', 'parental', 'pregnancy', 'care_relative', 'study', 'unpaid_leave', 'other_leave']
|
||||
@@ -422,7 +434,7 @@ export function SalaryCalendar({
|
||||
</span>
|
||||
<div className="mt-auto flex flex-col items-start gap-0.5">
|
||||
{w && (
|
||||
<span className="inline-flex items-center gap-0.5 rounded-full bg-emerald-100 px-1.5 py-px text-[10px] font-medium text-emerald-800">
|
||||
<span className="inline-flex items-center gap-0.5 rounded-full bg-secondary px-1.5 py-px text-[10px] font-medium text-secondary-foreground">
|
||||
<Clock className="h-2.5 w-2.5" aria-hidden />
|
||||
<span className="tabular-nums">{w.hours}h</span>
|
||||
</span>
|
||||
@@ -472,8 +484,8 @@ export function SalaryCalendar({
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 border-t px-3 py-2 text-[11px] text-muted-foreground">
|
||||
{isHourly && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-flex h-3 w-3 items-center justify-center rounded-full bg-emerald-100">
|
||||
<Clock className="h-2 w-2 text-emerald-800" aria-hidden />
|
||||
<span className="inline-flex h-3 w-3 items-center justify-center rounded-full bg-secondary">
|
||||
<Clock className="h-2 w-2 text-secondary-foreground" aria-hidden />
|
||||
</span>
|
||||
<span>{t('worked_time')}</span>
|
||||
</span>
|
||||
@@ -713,11 +725,11 @@ function BulkWorkedDialog({
|
||||
</div>
|
||||
|
||||
{conflicts.length > 0 && (
|
||||
<div className="space-y-1 rounded-md border border-amber-200 bg-amber-50 p-2 text-xs">
|
||||
<div className="font-medium text-amber-900">
|
||||
<div className="space-y-1 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs">
|
||||
<div className="font-medium text-warning-foreground">
|
||||
{t('conflicts_worked', { count: conflicts.length })}
|
||||
</div>
|
||||
<ul className="list-disc space-y-0.5 pl-4 text-amber-800 tabular-nums">
|
||||
<ul className="list-disc space-y-0.5 pl-4 text-warning-foreground tabular-nums">
|
||||
{conflicts.map(c => <li key={c.date}>{c.date}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -866,11 +878,11 @@ function BulkAbsenceDialog({
|
||||
</div>
|
||||
|
||||
{conflicts.length > 0 && (
|
||||
<div className="space-y-1 rounded-md border border-amber-200 bg-amber-50 p-2 text-xs">
|
||||
<div className="font-medium text-amber-900">
|
||||
<div className="space-y-1 rounded-md border border-warning/40 bg-warning/10 p-2 text-xs">
|
||||
<div className="font-medium text-warning-foreground">
|
||||
{t('conflicts_absence', { count: conflicts.length })}
|
||||
</div>
|
||||
<ul className="list-disc space-y-0.5 pl-4 text-amber-800 tabular-nums">
|
||||
<ul className="list-disc space-y-0.5 pl-4 text-warning-foreground tabular-nums">
|
||||
{conflicts.map(c => <li key={c.date}>{c.date}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -981,9 +993,9 @@ function DayInspectorDialog({
|
||||
|
||||
<div className="space-y-2">
|
||||
{isHourly && worked && (
|
||||
<div className="flex items-center justify-between rounded-md border bg-emerald-50 px-3 py-2">
|
||||
<div className="flex items-center justify-between rounded-md border bg-secondary/50 px-3 py-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-emerald-700" />
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium">{t('worked_time')}</div>
|
||||
<div className="text-xs text-muted-foreground tabular-nums">{t('hours_count', { hours: String(worked.hours) })}</div>
|
||||
|
||||
@@ -1552,7 +1552,7 @@ export default function NewSupplierInvoiceForm({
|
||||
{/* Section 1: Faktura */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('section_invoice')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('section_invoice')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Eget utlägg-toggle. När den är på bokas verifikatet direkt mot
|
||||
@@ -1718,7 +1718,7 @@ export default function NewSupplierInvoiceForm({
|
||||
{/* Section 2: Kontering */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<CardTitle className="text-lg">{t('section_accounting')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('section_accounting')}</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -2187,7 +2187,7 @@ export default function NewSupplierInvoiceForm({
|
||||
onClick={() => setAdvancedOpen(!advancedOpen)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{t('section_other')}</CardTitle>
|
||||
<CardTitle className="text-base">{t('section_other')}</CardTitle>
|
||||
<ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${advancedOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useCompany } from '@/contexts/CompanyContext'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/info-tooltip'
|
||||
|
||||
@@ -140,7 +139,6 @@ export default function BankSyncStatusChip() {
|
||||
|
||||
// healthy
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex cursor-help items-center gap-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-1 text-xs text-muted-foreground">
|
||||
@@ -160,6 +158,5 @@ export default function BankSyncStatusChip() {
|
||||
<div className="text-sm leading-relaxed">{t('bank_sync_latency_hint')}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} from '@/components/ui/info-tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -69,7 +68,6 @@ export function AccountNumber({
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
@@ -112,6 +110,5 @@ export function AccountNumber({
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[10rem] overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"z-50 min-w-[10rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -67,7 +67,6 @@ function InfoTooltip({
|
||||
const Icon = variant === 'help' ? HelpCircle : Info
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{children ? (
|
||||
@@ -102,7 +101,6 @@ function InfoTooltip({
|
||||
<div className="text-sm leading-relaxed">{content}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className="h-full w-full flex-1 bg-primary transition-transform duration-300 ease-linear"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
@@ -74,7 +74,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 max-h-96 min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
|
||||
@@ -33,7 +33,7 @@ const SlideOverContent = React.forwardRef<
|
||||
'fixed top-[18px] right-[18px] bottom-[18px] z-50 flex w-[480px] max-w-[calc(100vw-36px)] flex-col',
|
||||
'rounded-xl border border-border bg-background shadow-[var(--shadow-lg)]',
|
||||
'data-[state=open]:animate-in data-[state=open]:slide-in-from-right-full data-[state=open]:fade-in-0 data-[state=open]:duration-300 data-[state=open]:ease-[cubic-bezier(0.32,0.72,0,1)]',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right-full data-[state=closed]:fade-out-0 data-[state=closed]:duration-200 data-[state=closed]:ease-in',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right-full data-[state=closed]:fade-out-0 data-[state=closed]:duration-200 data-[state=closed]:ease-[cubic-bezier(0.32,0.72,0,1)]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -24,7 +24,7 @@ const ToastViewport = React.forwardRef<
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-0 max-sm:data-[state=open]:slide-in-from-top-full max-sm:data-[state=closed]:slide-out-to-top-full sm:data-[state=open]:slide-in-from-right-full sm:data-[state=closed]:slide-out-to-right-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
/**
|
||||
* The single app-wide tooltip provider, mounted once in the root layout.
|
||||
*
|
||||
* `skipDelayDuration` is provider-scoped: it is the grace window during which
|
||||
* a second tooltip opens instantly because one is already open. Mounting a
|
||||
* provider per tooltip instance (which is what AccountNumber, InfoTooltip,
|
||||
* BankSyncStatusChip and KassaflodesanalysClient each used to do) means every
|
||||
* instance sits in its own scope and the grace window can never fire, so
|
||||
* scanning a huvudbok re-pays the full open delay on every single account
|
||||
* number. One provider makes the second and later tooltips instant.
|
||||
*/
|
||||
export function TooltipProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider delayDuration={200} skipDelayDuration={300}>
|
||||
{children}
|
||||
</TooltipPrimitive.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user