refactor(ts): convert all UI primitives + ThemeContext to TSX (B2)
Foundational for phase B: the ui/* primitives were untyped .jsx, and TS infers React-19 ref-as-prop components as *requiring* a ref, so every consumer errored. Converted all 22 ui primitives (button, input, card, dialog, select, table, dropdown-menu, tabs, badge, checkbox, switch, tooltip, separator, label, Skeleton, collapsible, alert-dialog, sonner, save-status, theme-toggle, input-dialog, confirm-dialog) using ComponentProps<'el'> / ComponentProps<typeof Radix.X> + VariantProps for cva components — refs now optional, event params typed. Also converted ThemeContext.tsx (createContext(null) made useContext's post-guard return `never`, so setTheme was uncallable — fixed with a typed context value) and three tracker cells (EditableCell/NotesCell/ LowerThisMonthButton) that consume the branded TrackerRow. Added a shared errMessage(unknown) helper to utils (strict catch → unknown); usePaymentActions reuses it. TrackerRow gains monthly_notes + amount_suggestion (real server fields). typecheck 0, lint 0 errors (42 warns), build green, 48 client tests. NOTE: the e2e auth-setup probe is currently red on an unrelated, pre-existing release-notes-modal dismissal race (reproduces on the last known-good commit; the a11y snapshot shows these converted primitives rendering correctly) — investigating separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dc675fbecd
commit
1f18a720c4
|
|
@ -1,14 +1,24 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn, fmt, fmtDate } from '@/lib/utils';
|
||||
import { cn, fmt, fmtDate, errMessage } from '@/lib/utils';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { Dollars } from '@/lib/money';
|
||||
import type { TrackerRow } from '@/types';
|
||||
|
||||
// `threshold` = actual_amount ?? expected_amount for this bill/month
|
||||
export function EditableCell({ row, field, threshold, defaultPaymentDate, refresh }) {
|
||||
interface EditableCellProps {
|
||||
row: TrackerRow;
|
||||
field: 'amount' | 'date';
|
||||
threshold: Dollars;
|
||||
defaultPaymentDate: string;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export function EditableCell({ row, field, threshold, defaultPaymentDate, refresh }: EditableCellProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const displayVal = field === 'amount'
|
||||
? (row.total_paid > 0 ? fmt(row.total_paid) : '—')
|
||||
|
|
@ -33,10 +43,10 @@ export function EditableCell({ row, field, threshold, defaultPaymentDate, refres
|
|||
if (!val) return;
|
||||
try {
|
||||
if (row.payments && row.payments.length > 0) {
|
||||
const update = {};
|
||||
const update: { amount?: number; paid_date?: string } = {};
|
||||
if (field === 'amount') update.amount = parseFloat(val);
|
||||
if (field === 'date') update.paid_date = val;
|
||||
await api.updatePayment(row.payments[0].id, update);
|
||||
await api.updatePayment(row.payments[0]!.id, update);
|
||||
} else {
|
||||
await api.createPayment({
|
||||
bill_id: row.id,
|
||||
|
|
@ -47,11 +57,11 @@ export function EditableCell({ row, field, threshold, defaultPaymentDate, refres
|
|||
toast.success('Saved');
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
toast.error(errMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e) {
|
||||
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === 'Enter') inputRef.current?.blur();
|
||||
if (e.key === 'Escape') { setValue(''); setEditing(false); }
|
||||
}
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn, fmt } from '@/lib/utils';
|
||||
import { cn, fmt, errMessage } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MONTHS, rowThreshold, paymentSummary } from '@/lib/trackerUtils';
|
||||
import type { TrackerRow } from '@/types';
|
||||
|
||||
export function LowerThisMonthButton({ row, year, month, refresh, compact = false }) {
|
||||
interface LowerThisMonthButtonProps {
|
||||
row: TrackerRow;
|
||||
year: number;
|
||||
month: number;
|
||||
refresh?: () => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function LowerThisMonthButton({ row, year, month, refresh, compact = false }: LowerThisMonthButtonProps) {
|
||||
const threshold = rowThreshold(row);
|
||||
const summary = paymentSummary(row, threshold);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -25,7 +34,7 @@ export function LowerThisMonthButton({ row, year, month, refresh, compact = fals
|
|||
toast.success(`${MONTHS[month - 1]} amount set to ${fmt(summary.paid)}`);
|
||||
refresh?.();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to update monthly amount');
|
||||
toast.error(errMessage(err, 'Failed to update monthly amount'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn, errMessage } from '@/lib/utils';
|
||||
import type { TrackerRow } from '@/types';
|
||||
|
||||
// Monthly notes stored in monthly_bill_state — per-month, not per-bill.
|
||||
export function NotesCell({ row, refresh }) {
|
||||
// Monthly notes stored in monthly_bill_state — per-month, not per-bill. The
|
||||
// tracker page injects year/month onto the row for this cell's save.
|
||||
interface NotesCellProps {
|
||||
row: TrackerRow & { year?: number; month?: number };
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export function NotesCell({ row, refresh }: NotesCellProps) {
|
||||
const savedNote = row.monthly_notes || '';
|
||||
const [value, setValue] = useState(savedNote);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -33,7 +40,7 @@ export function NotesCell({ row, refresh }) {
|
|||
});
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
toast.error(errMessage(err));
|
||||
setValue(savedNote);
|
||||
} finally { setSaving(false); }
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({ className, variant = 'line', ref, ...props }) {
|
||||
const variants = {
|
||||
type SkeletonVariant = 'line' | 'circle' | 'card' | 'button' | 'input';
|
||||
|
||||
function Skeleton({ className, variant = 'line', ref, ...props }: ComponentProps<'div'> & { variant?: SkeletonVariant }) {
|
||||
const variants: Record<SkeletonVariant, string> = {
|
||||
line: 'h-4 w-full rounded-md',
|
||||
circle: 'h-10 w-10 rounded-full',
|
||||
card: 'h-24 w-full rounded-xl',
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
|
@ -8,7 +8,7 @@ const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|||
const AlertDialogCancel = AlertDialogPrimitive.Cancel;
|
||||
const AlertDialogAction = AlertDialogPrimitive.Action;
|
||||
|
||||
function AlertDialogOverlay({ className, ...props }) {
|
||||
function AlertDialogOverlay({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
|
|
@ -20,7 +20,7 @@ function AlertDialogOverlay({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({ className, children, ...props }) {
|
||||
function AlertDialogContent({ className, children, ...props }: ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
|
|
@ -42,17 +42,17 @@ function AlertDialogContent({ className, children, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({ className, ...props }) {
|
||||
function AlertDialogHeader({ className, ...props }: ComponentProps<'div'>) {
|
||||
return <div className={cn('flex flex-col gap-2', className)} {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogFooter({ className, ...props }) {
|
||||
function AlertDialogFooter({ className, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({ className, ...props }) {
|
||||
function AlertDialogTitle({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
className={cn('text-base font-semibold text-foreground', className)}
|
||||
|
|
@ -61,7 +61,7 @@ function AlertDialogTitle({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({ className, ...props }) {
|
||||
function AlertDialogDescription({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
className={cn('text-sm text-muted-foreground leading-relaxed', className)}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
|
|
@ -25,7 +26,7 @@ const badgeVariants = cva(
|
|||
}
|
||||
);
|
||||
|
||||
function Badge({ className, variant, ...props }) {
|
||||
function Badge({ className, variant, ...props }: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type { ComponentProps, ElementType } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
|
|
@ -28,8 +29,10 @@ const buttonVariants = cva(
|
|||
}
|
||||
);
|
||||
|
||||
function Button({ className, variant, size, asChild = false, ref, ...props }) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
type ButtonProps = ComponentProps<'button'> & VariantProps<typeof buttonVariants> & { asChild?: boolean };
|
||||
|
||||
function Button({ className, variant, size, asChild = false, ref, ...props }: ButtonProps) {
|
||||
const Comp: ElementType = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Card({ className, ref, ...props }) {
|
||||
function Card({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -10,7 +11,7 @@ function Card({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ref, ...props }) {
|
||||
function CardHeader({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -20,7 +21,7 @@ function CardHeader({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ref, ...props }) {
|
||||
function CardTitle({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -30,7 +31,7 @@ function CardTitle({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ref, ...props }) {
|
||||
function CardDescription({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -40,7 +41,7 @@ function CardDescription({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ref, ...props }) {
|
||||
function CardContent({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -50,7 +51,7 @@ function CardContent({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ref, ...props }) {
|
||||
function CardFooter({ className, ref, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Checkbox({ className, ref, ...props }) {
|
||||
function Checkbox({ className, ref, ...props }: ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
function CollapsibleContent({ ref, ...props }) {
|
||||
function CollapsibleContent({ ref, ...props }: ComponentProps<typeof CollapsiblePrimitive.Content>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.Content
|
||||
ref={ref}
|
||||
|
|
@ -11,20 +11,22 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/* ── ConfirmDialog ──────────────────────────────────────────────
|
||||
Reusable confirmation dialog that replaces window.confirm.
|
||||
type ConfirmVariant = 'default' | 'destructive';
|
||||
|
||||
Props:
|
||||
open boolean
|
||||
onOpenChange (open: boolean) => void
|
||||
title string
|
||||
description string | ReactNode
|
||||
confirmLabel string (default: 'Confirm')
|
||||
cancelLabel string (default: 'Cancel')
|
||||
variant 'default' | 'destructive'
|
||||
onConfirm () => void | Promise<void>
|
||||
loading boolean
|
||||
─────────────────────────────────────────────────────────────── */
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: ConfirmVariant;
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/* ── ConfirmDialog ──────────────────────────────────────────────
|
||||
Reusable confirmation dialog that replaces window.confirm. */
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
|
|
@ -36,7 +38,7 @@ export function ConfirmDialog({
|
|||
variant = 'default',
|
||||
onConfirm,
|
||||
loading = false,
|
||||
}) {
|
||||
}: ConfirmDialogProps) {
|
||||
const handleConfirm = async () => {
|
||||
await onConfirm?.();
|
||||
};
|
||||
|
|
@ -84,14 +86,29 @@ export function ConfirmDialog({
|
|||
|
||||
Usage:
|
||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||
// ...
|
||||
const ok = await confirm({ title, description, confirmLabel, variant });
|
||||
// Returns true if confirmed, false if cancelled.
|
||||
// Render <ConfirmDialogComponent /> anywhere in the tree.
|
||||
─────────────────────────────────────────────────────────────── */
|
||||
// Render <ConfirmDialogComponent /> anywhere in the tree. */
|
||||
|
||||
interface ConfirmState {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: React.ReactNode;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
variant: ConfirmVariant;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
interface ConfirmOptions {
|
||||
title?: string;
|
||||
description?: React.ReactNode;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: ConfirmVariant;
|
||||
}
|
||||
|
||||
export function useConfirm() {
|
||||
const [state, setState] = React.useState({
|
||||
const [state, setState] = React.useState<ConfirmState>({
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
|
|
@ -102,7 +119,7 @@ export function useConfirm() {
|
|||
});
|
||||
|
||||
// Keep a stable ref to the resolve callback so it survives re-renders
|
||||
const resolveRef = React.useRef(null);
|
||||
const resolveRef = React.useRef<((value: boolean) => void) | null>(null);
|
||||
|
||||
const confirm = React.useCallback(
|
||||
({
|
||||
|
|
@ -111,7 +128,7 @@ export function useConfirm() {
|
|||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'default',
|
||||
} = {}) => {
|
||||
}: ConfirmOptions = {}): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
resolveRef.current = resolve;
|
||||
setState({
|
||||
|
|
@ -135,7 +152,7 @@ export function useConfirm() {
|
|||
setState((s) => ({ ...s, open: false, loading: false }));
|
||||
}, []);
|
||||
|
||||
const handleOpenChange = React.useCallback((open) => {
|
||||
const handleOpenChange = React.useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
resolveRef.current?.(false);
|
||||
resolveRef.current = null;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { X } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
|
@ -8,7 +9,7 @@ const DialogTrigger = DialogPrimitive.Trigger;
|
|||
const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
function DialogOverlay({ className, ref, ...props }) {
|
||||
function DialogOverlay({ className, ref, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
|
|
@ -21,7 +22,7 @@ function DialogOverlay({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DialogContent({ className, children, ref, ...props }) {
|
||||
function DialogContent({ className, children, ref, ...props }: ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
|
|
@ -53,7 +54,7 @@ function DialogContent({ className, children, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }) {
|
||||
function DialogHeader({ className, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
|
||||
|
|
@ -62,7 +63,7 @@ function DialogHeader({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }) {
|
||||
function DialogFooter({ className, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
|
|
@ -71,7 +72,7 @@ function DialogFooter({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ref, ...props }) {
|
||||
function DialogTitle({ className, ref, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
|
|
@ -81,7 +82,7 @@ function DialogTitle({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DialogDescription({ className, ref, ...props }) {
|
||||
function DialogDescription({ className, ref, ...props }: ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
|
@ -9,7 +10,7 @@ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
|
||||
function DropdownMenuSubTrigger({ className, inset, children, ...props }: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
className={cn(
|
||||
|
|
@ -26,7 +27,7 @@ function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({ className, ...props }) {
|
||||
function DropdownMenuSubContent({ className, ...props }: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
className={cn(
|
||||
|
|
@ -40,7 +41,7 @@ function DropdownMenuSubContent({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
||||
function DropdownMenuContent({ className, sideOffset = 4, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
|
|
@ -57,7 +58,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({ className, inset, destructive, ...props }) {
|
||||
function DropdownMenuItem({ className, inset, destructive, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Item> & { inset?: boolean; destructive?: boolean }) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
className={cn(
|
||||
|
|
@ -72,7 +73,7 @@ function DropdownMenuItem({ className, inset, destructive, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
|
||||
function DropdownMenuCheckboxItem({ className, children, checked, ...props }: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
className={cn(
|
||||
|
|
@ -93,7 +94,7 @@ function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({ className, children, ...props }) {
|
||||
function DropdownMenuRadioItem({ className, children, ...props }: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
className={cn(
|
||||
|
|
@ -113,7 +114,7 @@ function DropdownMenuRadioItem({ className, children, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({ className, inset, ...props }) {
|
||||
function DropdownMenuLabel({ className, inset, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
className={cn('px-2 py-1.5 text-xs font-medium text-muted-foreground', inset && 'pl-8', className)}
|
||||
|
|
@ -122,7 +123,7 @@ function DropdownMenuLabel({ className, inset, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({ className, ...props }) {
|
||||
function DropdownMenuSeparator({ className, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
|
|
@ -132,7 +133,7 @@ function DropdownMenuSeparator({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }) {
|
||||
function DropdownMenuShortcut({ className, ...props }: ComponentProps<'span'>) {
|
||||
return (
|
||||
<span className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)} {...props} />
|
||||
);
|
||||
|
|
@ -17,6 +17,21 @@ import { Button } from '@/components/ui/button';
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface InputDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
label?: string;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
validate?: (value: string) => string | null;
|
||||
onConfirm?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function InputDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
@ -30,10 +45,10 @@ export function InputDialog({
|
|||
validate, // optional (value: string) => string | null (null = valid)
|
||||
onConfirm,
|
||||
loading = false,
|
||||
}) {
|
||||
}: InputDialogProps) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Reset value when dialog opens
|
||||
useEffect(() => {
|
||||
|
|
@ -58,7 +73,7 @@ export function InputDialog({
|
|||
onConfirm?.(trimmed);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); handleConfirm(); }
|
||||
if (e.key === 'Escape') onOpenChange?.(false);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Input({ className, type, ref, ...props }) {
|
||||
function Input({ className, type, ref, ...props }: ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
);
|
||||
|
||||
function Label({ className, ref, ...props }) {
|
||||
function Label({ className, ref, ...props }: ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Check, CloudUpload, AlertCircle } from 'lucide-react';
|
||||
import { Check, CloudUpload, AlertCircle, type LucideIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const STATES = {
|
||||
interface SaveState {
|
||||
icon: LucideIcon;
|
||||
text: string;
|
||||
cls: string;
|
||||
}
|
||||
|
||||
const STATES: Record<string, SaveState> = {
|
||||
saving: { icon: CloudUpload, text: 'Saving…', cls: 'text-muted-foreground border-border/70 bg-muted/40' },
|
||||
saved: { icon: Check, text: 'Saved', cls: 'text-emerald-600 dark:text-emerald-300 border-emerald-500/30 bg-emerald-500/10' },
|
||||
error: { icon: AlertCircle, text: 'Save failed', cls: 'text-rose-500 dark:text-rose-300 border-rose-500/35 bg-rose-500/10' },
|
||||
|
|
@ -12,8 +18,8 @@ const STATES = {
|
|||
* Tiny animated pill that reflects auto-save state. Renders nothing while idle —
|
||||
* the page communicates "changes save automatically" once, statically.
|
||||
*/
|
||||
export function SaveStatus({ status, className }) {
|
||||
const state = STATES[status];
|
||||
export function SaveStatus({ status, className }: { status?: string | null; className?: string }) {
|
||||
const state = status ? STATES[status] : undefined;
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{state && (
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
function SelectTrigger({ className, children, ref, ...props }) {
|
||||
function SelectTrigger({ className, children, ref, ...props }: ComponentProps<typeof SelectPrimitive.Trigger>) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
|
|
@ -26,7 +27,7 @@ function SelectTrigger({ className, children, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({ className, ref, ...props }) {
|
||||
function SelectScrollUpButton({ className, ref, ...props }: ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
|
|
@ -38,7 +39,7 @@ function SelectScrollUpButton({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({ className, ref, ...props }) {
|
||||
function SelectScrollDownButton({ className, ref, ...props }: ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
|
|
@ -50,7 +51,7 @@ function SelectScrollDownButton({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectContent({ className, children, position = 'popper', ref, ...props }) {
|
||||
function SelectContent({ className, children, position = 'popper', ref, ...props }: ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
|
|
@ -82,7 +83,7 @@ function SelectContent({ className, children, position = 'popper', ref, ...props
|
|||
);
|
||||
}
|
||||
|
||||
function SelectLabel({ className, ref, ...props }) {
|
||||
function SelectLabel({ className, ref, ...props }: ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
|
|
@ -92,7 +93,7 @@ function SelectLabel({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectItem({ className, children, ref, ...props }) {
|
||||
function SelectItem({ className, children, ref, ...props }: ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
|
|
@ -113,7 +114,7 @@ function SelectItem({ className, children, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({ className, ref, ...props }) {
|
||||
function SelectSeparator({ className, ref, ...props }: ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Separator({ className, orientation = 'horizontal', decorative = true, ref, ...props }) {
|
||||
function Separator({ className, orientation = 'horizontal', decorative = true, ref, ...props }: ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Switch({ className, ...props }) {
|
||||
function Switch({ className, ...props }: ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
className={cn(
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Table({ className, ref, ...props }) {
|
||||
function Table({ className, ref, ...props }: ComponentProps<'table'>) {
|
||||
return (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
|
|
@ -17,7 +18,7 @@ function Table({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ref, ...props }) {
|
||||
function TableHeader({ className, ref, ...props }: ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
|
|
@ -30,7 +31,7 @@ function TableHeader({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ref, ...props }) {
|
||||
function TableBody({ className, ref, ...props }: ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
ref={ref}
|
||||
|
|
@ -43,7 +44,7 @@ function TableBody({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ref, ...props }) {
|
||||
function TableFooter({ className, ref, ...props }: ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
|
|
@ -57,7 +58,7 @@ function TableFooter({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ref, ...props }) {
|
||||
function TableRow({ className, ref, ...props }: ComponentProps<typeof motion.tr>) {
|
||||
return (
|
||||
<motion.tr
|
||||
ref={ref}
|
||||
|
|
@ -73,7 +74,7 @@ function TableRow({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ref, ...props }) {
|
||||
function TableHead({ className, ref, ...props }: ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
ref={ref}
|
||||
|
|
@ -90,7 +91,7 @@ function TableHead({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ref, ...props }) {
|
||||
function TableCell({ className, ref, ...props }: ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
ref={ref}
|
||||
|
|
@ -106,7 +107,7 @@ function TableCell({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TableCaption({ className, ref, ...props }) {
|
||||
function TableCaption({ className, ref, ...props }: ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
ref={ref}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
function TabsList({ className, ref, ...props }) {
|
||||
function TabsList({ className, ref, ...props }: ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
|
|
@ -16,7 +17,7 @@ function TabsList({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ref, ...props }) {
|
||||
function TabsTrigger({ className, ref, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
|
|
@ -29,7 +30,7 @@ function TabsTrigger({ className, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TabsContent({ className, ref, ...props }) {
|
||||
function TabsContent({ className, ref, ...props }: ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
import { Sun, Moon, type LucideIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ import { useTheme } from '@/contexts/ThemeContext';
|
|||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
function DropdownMenuContent({ className, sideOffset = 6, ref, ...props }) {
|
||||
function DropdownMenuContent({ className, sideOffset = 6, ref, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
|
|
@ -28,7 +29,7 @@ function DropdownMenuContent({ className, sideOffset = 6, ref, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({ className, inset, ref, ...props }) {
|
||||
function DropdownMenuItem({ className, inset, ref, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Item> & { inset?: boolean }) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
|
|
@ -46,17 +47,23 @@ function DropdownMenuItem({ className, inset, ref, ...props }) {
|
|||
|
||||
/* ── Theme options config ───────────────────────────────────── */
|
||||
|
||||
const THEMES = [
|
||||
interface ThemeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
Icon: LucideIcon;
|
||||
}
|
||||
|
||||
const THEMES: ThemeOption[] = [
|
||||
{ value: 'light', label: 'Light', Icon: Sun },
|
||||
{ value: 'dark', label: 'Dark', Icon: Moon },
|
||||
];
|
||||
|
||||
/* ── ThemeToggle ────────────────────────────────────────────── */
|
||||
|
||||
export function ThemeToggle({ className }) {
|
||||
export function ThemeToggle({ className }: { className?: string }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const current = THEMES.find((t) => t.value === theme) ?? THEMES[1];
|
||||
const current = THEMES.find((t) => t.value === theme) ?? THEMES[1]!;
|
||||
const CurrentIcon = current.Icon;
|
||||
|
||||
return (
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
function TooltipContent({ className, sideOffset = 4, ref, ...props }) {
|
||||
function TooltipContent({ className, sideOffset = 4, ref, ...props }: ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* Themes: 'light' | 'dark'
|
||||
|
|
@ -9,11 +9,13 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
|||
* dark → 'dark'
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'bt-theme';
|
||||
const VALID_THEMES = ['light', 'dark'];
|
||||
const DEFAULT_THEME = 'dark';
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
function applyTheme(theme) {
|
||||
const STORAGE_KEY = 'bt-theme';
|
||||
const VALID_THEMES: Theme[] = ['light', 'dark'];
|
||||
const DEFAULT_THEME: Theme = 'dark';
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('dark');
|
||||
if (theme === 'dark') {
|
||||
|
|
@ -22,31 +24,36 @@ function applyTheme(theme) {
|
|||
// 'light' → no classes
|
||||
}
|
||||
|
||||
function loadStoredTheme() {
|
||||
function loadStoredTheme(): Theme {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'dark-purple') return 'dark';
|
||||
if (stored && VALID_THEMES.includes(stored)) return stored;
|
||||
if (stored && (VALID_THEMES as string[]).includes(stored)) return stored as Theme;
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext(null);
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
setTheme: (theme: string) => void;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setThemeState] = useState(() => {
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
const stored = loadStoredTheme();
|
||||
// Apply immediately (before first paint) to avoid flash
|
||||
applyTheme(stored);
|
||||
return stored;
|
||||
});
|
||||
|
||||
const setTheme = (newTheme) => {
|
||||
if (!VALID_THEMES.includes(newTheme)) return;
|
||||
applyTheme(newTheme);
|
||||
setThemeState(newTheme);
|
||||
const setTheme = (newTheme: string) => {
|
||||
if (!(VALID_THEMES as string[]).includes(newTheme)) return;
|
||||
applyTheme(newTheme as Theme);
|
||||
setThemeState(newTheme as Theme);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, newTheme);
|
||||
} catch {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { fmt } from '@/lib/utils';
|
||||
import { fmt, errMessage } from '@/lib/utils';
|
||||
import type { Dollars } from '@/lib/money';
|
||||
import { useInvalidateTrackerData } from '@/hooks/useQueries';
|
||||
|
||||
|
|
@ -11,11 +11,6 @@ interface PayableRow {
|
|||
name: string;
|
||||
}
|
||||
|
||||
/** Narrow an unknown error (strict catch clauses) to a message string. */
|
||||
function errMessage(err: unknown, fallback: string): string {
|
||||
return err instanceof Error && err.message ? err.message : fallback;
|
||||
}
|
||||
|
||||
// Quick-pay a tracker row (record a payment for `val` on `paidDate`) with a
|
||||
// reversible Undo toast. A React Query mutation: `isPending` comes for free and
|
||||
// the tracker/badge caches are invalidated on settle. Shared by the desktop and
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export function fmt(amount: Parameters<typeof formatUSD>[0]): string {
|
|||
return formatUSD(amount);
|
||||
}
|
||||
|
||||
// Narrow an unknown error (strict catch clauses give `unknown`) to a message
|
||||
// string, falling back when the thrown value isn't an Error with a message.
|
||||
export function errMessage(err: unknown, fallback = 'Something went wrong'): string {
|
||||
return err instanceof Error && err.message ? err.message : fallback;
|
||||
}
|
||||
|
||||
export function fmtDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '—';
|
||||
const [y = '', m = '', d = ''] = dateStr.split('-');
|
||||
|
|
|
|||
|
|
@ -152,7 +152,9 @@ export interface TrackerRow {
|
|||
actual_amount: Dollars | null;
|
||||
is_skipped: boolean;
|
||||
snoozed_until: string | null;
|
||||
monthly_notes?: string | null;
|
||||
autopay_suggestion?: unknown;
|
||||
amount_suggestion?: unknown;
|
||||
previous_month_paid: Dollars;
|
||||
// Bank-mode augmentation (present only when bank_tracking.enabled):
|
||||
pending_cleared?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue