Files
accounted/components/ui/slide-over.tsx
T
Jakob WennbergandClaude Fable 5 5b5ee8e429 feat(ui): shared migration primitives (UI migration PR 3) (#1122)
* feat(ui): shared migration primitives (UI migration PR 3)

The component kit every page migration (PR 4-8) builds on:

- ContextPicker: the one-per-page chip-dropdown context scope (convention
  8), right-aligned popover with checks and muted annotations
- FyPicker: fiscal-year picker on ContextPicker with the same controlled
  API and per-company localStorage key as FiscalYearSelector, which it
  replaces page by page from PR 4
- SplitButton: primary + caret menu, last-used mode persisted per user
  via ui_state.create_mode (lib/ui-state/client, unit-tested); nav
  persistence refactored onto the same helper
- ConfirmDialog: centered min-460px confirm-up-front dialog (convention
  10) with pending state on an awaitable onConfirm
- HelpPopover: 17px "?" after the H1 opening an anchored popover
  (convention 7); PageHeader gets a `help` slot
- AttnLine: the one-ochre-sentence attention pattern (convention 6) with
  optional inline action; new AA-safe --attn token pair
- RowStatus: chips-mark-exceptions helper (convention 5)
- SlideOver: right review panel, 480px, 18px inset, rounded, veil + Esc
  (convention 13), with header kicker / body / footer slots
- Stagger: .stagger-enter applied to the five target pages' list
  containers (bookkeeping, transactions, pending, invoices,
  supplier-invoices); structural loading.tsx added for supplier-invoices,
  customers, kpi, pending, deadlines

No page adopts the new pickers/dialogs yet: that is PR 4-8, one page per
PR against this kit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): FyPicker chip must not double the Rakenskapsar label

Real fiscal periods are often named "Rakenskapsar 2026" already; only
prefix the label when the period name lacks it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:29:36 +02:00

125 lines
3.5 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)]',
'duration-200 data-[state=open]:animate-in data-[state=open]:slide-in-from-right-8 data-[state=open]:fade-in-0',
'data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right-8 data-[state=closed]:fade-out-0',
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 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,
}