be9d630347
* feat(home): concept scene 14: greeting + Att gora/Fortsatt panes Hem becomes the founder-approved two-panel layout: serif time-of-day greeting with date and company, the Att gora worklist restyled to the concept pane (eyebrow header, h-rows with count chips, hover chevrons) and a new Fortsatt pane listing in-progress work derived purely from draft state (lib/worklist/resume: journal drafts, invoice drafts/unsent, mid-lifecycle salary runs; deadline boost, cap 3, tested). A completed flow can never render as a resume row by construction: only draft-state rows are fetched. KPI tiles, revenue/expense cards and the deadline/tax widgets leave the page per dev_docs/last_session_resume.md section 8, which also prunes their fetches (journal-line YTD aggregation, unpaid totals, deadlines): the page got faster. Banners, checklist, build-assistant hero and the Skatteverket nudge survive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(home): serif pane titles for Att gora and Fortsatt Founder feedback: the uppercase eyebrow headers read as a stray font. Both pane titles are now the Hedvig display serif (text-lg) over the hairline, matching the page's heading language; the band headers inside Att gora keep their small uppercase form as grouping devices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(home): Geist pane titles for Att gora and Fortsatt Founder call: the pane titles use the body sans (14px medium), not the display serif. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): Geist section headers + drop stale-transactions chip Founder feedback: pane/section headers (Att gora, Fortsatt, the reports groups Lopande/Bokslut/Skatt & moms etc) render in Geist sentence case instead of uppercase eyebrows or serif. The global h1-h3 display-font rule moves into @layer base so utility classes like font-sans can actually override it (unlayered element rules beat Tailwind's layered utilities: this was silently eating the override). Also removes the 'N aldre an 14 dagar' chip from the Bokfora transaktioner row and its stale-count plumbing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): continuous nav crossfade + floating slide-over entrance The rail/full nav states now stay mounted and crossfade past each other (the inactive layer absolute, faded, nudged sideways, inert) while the aside width animates: the switch reads as one continuous motion instead of a DOM swap. The detail slide-over floats in from the right edge (slide-in-from-right-full, 300ms decelerating curve) per the concept, with a quicker ease-in exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): make transitions and enter/exit animations actually run Two silent app-wide animation killers found while chasing 'the nav still is not smooth': 1. The codebase uses the shadcn animate-in/out vocabulary everywhere but no animate plugin was ever installed: Tailwind v4 silently dropped every such class, so popovers, dialogs, menus and the slide-over all appeared instantly. globals.css now defines the exact subset in use (accEnter/accExit keyframes + var-driven utilities), plugin-free, composing with duration/ease via --tw-duration/--tw-ease and collapsing under prefers-reduced-motion. Dialog drops its bracket-variant slide classes (zoom+fade carries the entrance). 2. The scrollbar auto-hide block's universal '* { transition: scrollbar-color ... }' was unlayered, and unlayered rules beat Tailwind's layered transition-* utilities regardless of specificity: every width/margin/color transition in the app was dead. The rule now lives in @layer base. Verified: the aside animates 248->64 over 300ms and the slide-over runs accEnter at 0.3s with the decelerating curve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): finish the rr-mask session-replay masking sweep Main's #1105 switched one amount cell from the no-op sensitive-field class to rr-mask (rrweb's built-in text-masking class). The reskinned tables introduced more sensitive-field cells; all 12 occurrences now use rr-mask so financial amounts are masked in session replays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { cn, formatCurrency } from '@/lib/utils'
|
|
import { BookOpen, ChevronRight, FileText, HandCoins } from 'lucide-react'
|
|
import type { ResumeItem } from '@/lib/worklist/resume'
|
|
|
|
/**
|
|
* "Fortsätt" (concept scene 14, right pane): in-progress work you can jump
|
|
* back into, derived purely from draft state (lib/worklist/resume). Renders
|
|
* null when empty: never a blank-but-present card.
|
|
*/
|
|
export default function ResumePane({ items }: { items: ResumeItem[] }) {
|
|
const t = useTranslations('dashboard')
|
|
|
|
// Captured once: render must stay pure and the labels stable per mount.
|
|
const [nowMs] = useState(() => Date.now())
|
|
|
|
if (items.length === 0) return null
|
|
|
|
const relativeLabel = (iso: string): string => {
|
|
const days = Math.floor((nowMs - new Date(iso).getTime()) / 86_400_000)
|
|
if (days <= 0) return t('rel_today')
|
|
if (days === 1) return t('rel_yesterday')
|
|
return t('rel_days', { count: days })
|
|
}
|
|
|
|
const rowFor = (item: ResumeItem) => {
|
|
switch (item.kind) {
|
|
case 'invoice_draft':
|
|
return {
|
|
icon: FileText,
|
|
title: t('resume_invoice_draft', { customer: item.context ?? '' }),
|
|
sub: [
|
|
item.amount != null ? formatCurrency(item.amount, item.currency ?? 'SEK') : null,
|
|
t('resume_edited', { when: relativeLabel(item.updated_at) }),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
}
|
|
case 'invoice_unsent':
|
|
return {
|
|
icon: FileText,
|
|
title: t('resume_invoice_unsent', { number: item.number ?? '' }),
|
|
sub: [
|
|
item.context,
|
|
item.amount != null ? formatCurrency(item.amount, item.currency ?? 'SEK') : null,
|
|
t('resume_edited', { when: relativeLabel(item.updated_at) }),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
}
|
|
case 'salary_run': {
|
|
const key =
|
|
item.salaryStatus === 'review'
|
|
? 'resume_salary_review'
|
|
: item.salaryStatus === 'approved'
|
|
? 'resume_salary_approved'
|
|
: item.salaryStatus === 'paid'
|
|
? 'resume_salary_paid'
|
|
: 'resume_salary_draft'
|
|
return {
|
|
icon: HandCoins,
|
|
title: t(key, { period: item.context ?? '' }),
|
|
sub: t('resume_edited', { when: relativeLabel(item.updated_at) }),
|
|
}
|
|
}
|
|
case 'journal_draft':
|
|
default:
|
|
return {
|
|
icon: BookOpen,
|
|
title: t('resume_journal_draft'),
|
|
sub: [item.context, t('resume_edited', { when: relativeLabel(item.updated_at) })]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section aria-label={t('resume_title')}>
|
|
{/* Pane header: Geist title over a hairline */}
|
|
<div className="flex items-baseline justify-between border-b border-border px-1 pb-2.5">
|
|
<h2 className="font-sans text-sm font-medium">{t('resume_title')}</h2>
|
|
</div>
|
|
<div>
|
|
{items.map((item) => {
|
|
const row = rowFor(item)
|
|
const Icon = row.icon
|
|
return (
|
|
<Link
|
|
key={item.ref}
|
|
href={item.href}
|
|
className="group flex w-full items-start gap-3 border-b border-border px-1 py-3.5 transition-colors duration-150 hover:bg-secondary/30"
|
|
>
|
|
<span className="mt-px w-[18px] shrink-0 text-muted-foreground" aria-hidden>
|
|
<Icon className="h-[15px] w-[15px]" />
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate text-[13.5px]">{row.title}</span>
|
|
{row.sub && (
|
|
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
{row.sub}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className="ml-auto flex shrink-0 items-center gap-2.5 pt-px">
|
|
{item.late && (
|
|
<Badge variant="warning" className="font-normal">
|
|
{t('resume_late')}
|
|
</Badge>
|
|
)}
|
|
<ChevronRight
|
|
className={cn(
|
|
'h-3.5 w-3.5 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover:opacity-100',
|
|
)}
|
|
/>
|
|
</span>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|