* 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>
125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
import { X } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
/**
|
|
* Right slide-over for reviewing an object (UI-migration convention 13):
|
|
* a 480px panel inset 18px from the frame edge, rounded, with veil, Esc
|
|
* and click-outside. Create/confirm flows use the centered dialog instead;
|
|
* this is the review surface (e.g. the Granskning detail).
|
|
*/
|
|
const SlideOver = DialogPrimitive.Root
|
|
const SlideOverTrigger = DialogPrimitive.Trigger
|
|
const SlideOverClose = DialogPrimitive.Close
|
|
|
|
const SlideOverContent = React.forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<DialogPrimitive.Portal>
|
|
<DialogPrimitive.Overlay
|
|
className={cn(
|
|
'fixed inset-0 z-50 bg-black/30 dark:bg-black/50',
|
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
|
|
)}
|
|
/>
|
|
<DialogPrimitive.Content
|
|
ref={ref}
|
|
className={cn(
|
|
'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',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</DialogPrimitive.Content>
|
|
</DialogPrimitive.Portal>
|
|
))
|
|
SlideOverContent.displayName = 'SlideOverContent'
|
|
|
|
/**
|
|
* Header block: kicker line (actor · risk · time), serif title, close
|
|
* button. Body scrolls; header and footer stay put.
|
|
*/
|
|
function SlideOverHeader({
|
|
kicker,
|
|
title,
|
|
className,
|
|
}: {
|
|
kicker?: React.ReactNode
|
|
title: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div className={cn('flex-shrink-0 border-b border-border px-6 py-4', className)}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
{kicker && (
|
|
<div className="mb-1 text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
|
|
{kicker}
|
|
</div>
|
|
)}
|
|
<DialogPrimitive.Title className="font-display text-lg leading-6 tracking-tight">
|
|
{title}
|
|
</DialogPrimitive.Title>
|
|
</div>
|
|
<DialogPrimitive.Close
|
|
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors duration-150 hover:bg-secondary/60 hover:text-foreground"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</DialogPrimitive.Close>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SlideOverBody({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div className={cn('min-h-0 flex-1 overflow-y-auto scrollbar-visible px-6 py-4', className)}>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SlideOverFooter({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-shrink-0 items-center justify-end gap-2 border-t border-border px-6 py-3',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export {
|
|
SlideOver,
|
|
SlideOverTrigger,
|
|
SlideOverClose,
|
|
SlideOverContent,
|
|
SlideOverHeader,
|
|
SlideOverBody,
|
|
SlideOverFooter,
|
|
}
|