Files
accounted/lib/worklist/__tests__/resume.test.ts
T
Jakob Wennberg be9d630347 feat(home): concept Hem with Att göra + Fortsätt (UI migration PR 11) (#1132)
* 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>
2026-07-23 22:36:18 +02:00

84 lines
2.5 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
mergeResumeItems,
isSalaryRunLate,
RESUME_MAX_ROWS,
type ResumeItem,
} from '../resume'
function item(overrides: Partial<ResumeItem>): ResumeItem {
return {
kind: 'journal_draft',
ref: `journal:${Math.abs(overrides.updated_at?.length ?? 1)}`,
href: '/bookkeeping/x',
context: null,
updated_at: '2026-07-01T10:00:00Z',
...overrides,
}
}
describe('mergeResumeItems', () => {
it('orders by updated_at desc', () => {
const merged = mergeResumeItems([
item({ ref: 'a', updated_at: '2026-07-01T10:00:00Z' }),
item({ ref: 'b', updated_at: '2026-07-03T10:00:00Z' }),
item({ ref: 'c', updated_at: '2026-07-02T10:00:00Z' }),
])
expect(merged.map((i) => i.ref)).toEqual(['b', 'c', 'a'])
})
it('boosts late items above newer non-late items', () => {
const merged = mergeResumeItems([
item({ ref: 'fresh', updated_at: '2026-07-20T10:00:00Z' }),
item({ ref: 'late-old', late: true, updated_at: '2026-05-01T10:00:00Z' }),
])
expect(merged[0].ref).toBe('late-old')
})
it('caps at RESUME_MAX_ROWS', () => {
const many = Array.from({ length: 7 }, (_, i) =>
item({ ref: `r${i}`, updated_at: `2026-07-0${i + 1}T10:00:00Z` }),
)
expect(mergeResumeItems(many)).toHaveLength(RESUME_MAX_ROWS)
})
it('keeps late-first ordering stable within groups', () => {
const merged = mergeResumeItems([
item({ ref: 'late-new', late: true, updated_at: '2026-07-10T10:00:00Z' }),
item({ ref: 'late-old', late: true, updated_at: '2026-06-10T10:00:00Z' }),
item({ ref: 'plain', updated_at: '2026-07-22T10:00:00Z' }),
])
expect(merged.map((i) => i.ref)).toEqual(['late-new', 'late-old', 'plain'])
})
it('does not mutate the input array', () => {
const input = [
item({ ref: 'a', updated_at: '2026-07-01T10:00:00Z' }),
item({ ref: 'b', updated_at: '2026-07-02T10:00:00Z' }),
]
const before = [...input]
mergeResumeItems(input)
expect(input).toEqual(before)
})
})
describe('isSalaryRunLate', () => {
const now = new Date('2026-07-23T12:00:00Z')
it('flags a run two months past its period', () => {
expect(isSalaryRunLate(2026, 5, now)).toBe(true)
})
it('does not flag last month', () => {
expect(isSalaryRunLate(2026, 6, now)).toBe(false)
})
it('does not flag the current period', () => {
expect(isSalaryRunLate(2026, 7, now)).toBe(false)
})
it('handles year boundaries', () => {
expect(isSalaryRunLate(2025, 12, now)).toBe(true)
})
})