@campfire-interactive/ui 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +175 -18
  2. package/dist/index.js +300 -95
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -363,7 +363,11 @@ declare const Checkbox: react.ForwardRefExoticComponent<CheckboxProps & react.Re
363
363
  interface SwitchProps extends React.ComponentPropsWithoutRef<typeof RadixSwitch.Root> {
364
364
  label?: React.ReactNode;
365
365
  description?: React.ReactNode;
366
- /** Puts the label before the control — the shape a settings row uses. */
366
+ /**
367
+ * Where the CONTROL sits beside its label. `end` (the default) puts the
368
+ * label first and the switch after it — the shape a settings row uses.
369
+ * `start` puts the switch first.
370
+ */
367
371
  labelPosition?: 'start' | 'end';
368
372
  }
369
373
  /**
@@ -372,6 +376,12 @@ interface SwitchProps extends React.ComponentPropsWithoutRef<typeof RadixSwitch.
372
376
  * A switch takes effect immediately; a checkbox is a value you then save. That
373
377
  * distinction is the whole reason both exist, and mixing them is why some
374
378
  * settings screens in the fleet appear to save on toggle and others don't.
379
+ *
380
+ * NAMED WITHOUT AN `id`. The label is tied to the control by `htmlFor`, and
381
+ * that used to need the caller's `id` — without one, a screen reader announced
382
+ * an unnamed switch (found in pm's Settings → Gates, 2026-10-01). It now makes
383
+ * its own when none is passed, and the description is wired as the control's
384
+ * description the same way.
375
385
  */
376
386
  declare const Switch: react.ForwardRefExoticComponent<SwitchProps & react.RefAttributes<HTMLButtonElement>>;
377
387
 
@@ -423,7 +433,9 @@ declare function DialogBody({ className, ...rest }: React.HTMLAttributes<HTMLDiv
423
433
  declare function DialogFooter({ className, ...rest }: React.HTMLAttributes<HTMLDivElement>): react.JSX.Element;
424
434
  interface ModalProps {
425
435
  open: boolean;
426
- onOpenChange: (open: boolean) => void;
436
+ onOpenChange?: (open: boolean) => void;
437
+ /** Called when the dialog asks to close (Esc, ×, outside click). */
438
+ onClose?: () => void;
427
439
  title: React.ReactNode;
428
440
  /** Sub-heading under the title. Also becomes the dialog's accessible description. */
429
441
  description?: React.ReactNode;
@@ -453,7 +465,7 @@ interface ModalProps {
453
465
  * its Save button on screen. That single detail is missing from most of the
454
466
  * hand-built copies.
455
467
  */
456
- declare function Modal({ open, onOpenChange, title, description, size, side, footer, hideClose, dismissible, className, children, }: ModalProps): react.JSX.Element;
468
+ declare function Modal({ open, onOpenChange, onClose, title, description, size, side, footer, hideClose, dismissible, className, children, }: ModalProps): react.JSX.Element;
457
469
  /**
458
470
  * A `Modal` that slides in from the side.
459
471
  *
@@ -479,7 +491,9 @@ declare const SheetBody: typeof DialogBody;
479
491
  declare const SheetFooter: typeof DialogFooter;
480
492
  interface ConfirmDialogProps {
481
493
  open: boolean;
482
- onOpenChange: (open: boolean) => void;
494
+ onOpenChange?: (open: boolean) => void;
495
+ /** Called on Cancel, Esc, or any other request to close. */
496
+ onClose?: () => void;
483
497
  title: React.ReactNode;
484
498
  /** What is about to happen, and what cannot be undone. */
485
499
  description?: React.ReactNode;
@@ -504,7 +518,7 @@ interface ConfirmDialogProps {
504
518
  * confirm should not count as an answer either way. Esc still cancels, and
505
519
  * focus starts on Cancel, so a held Enter key cannot confirm a deletion.
506
520
  */
507
- declare function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive, loading, onConfirm, children, }: ConfirmDialogProps): react.JSX.Element;
521
+ declare function ConfirmDialog({ open, onOpenChange, onClose, title, description, confirmLabel, cancelLabel, destructive, loading, onConfirm, children, }: ConfirmDialogProps): react.JSX.Element;
508
522
 
509
523
  declare const PopoverRoot: react.FC<RadixPopover.PopoverProps>;
510
524
  declare const PopoverTrigger: react.ForwardRefExoticComponent<RadixPopover.PopoverTriggerProps & react.RefAttributes<HTMLButtonElement>>;
@@ -1345,28 +1359,40 @@ interface ToggleOption<T extends string = string> {
1345
1359
  /** Accessible name for an icon-only option. Required when there is no label. */
1346
1360
  ariaLabel?: string;
1347
1361
  }
1348
- interface ToggleGroupProps<T extends string = string> {
1362
+ interface ToggleGroupBaseProps<T extends string> {
1349
1363
  options: ToggleOption<T>[];
1364
+ size?: 'sm' | 'md';
1365
+ className?: string;
1366
+ 'aria-label': string;
1367
+ }
1368
+ interface ToggleGroupSingleProps<T extends string> extends ToggleGroupBaseProps<T> {
1369
+ type?: 'single';
1350
1370
  value?: T;
1351
1371
  onValueChange?: (value: T) => void;
1352
- size?: 'sm' | 'md';
1353
1372
  /**
1354
1373
  * Allows deselecting the active option, leaving nothing selected. Off by
1355
1374
  * default — a view switcher with no view selected shows nothing.
1356
1375
  */
1357
1376
  allowEmpty?: boolean;
1358
- className?: string;
1359
- 'aria-label': string;
1360
1377
  }
1378
+ interface ToggleGroupMultipleProps<T extends string> extends ToggleGroupBaseProps<T> {
1379
+ /** Any number of options on at once, including none — a set of filters. */
1380
+ type: 'multiple';
1381
+ value?: T[];
1382
+ onValueChange?: (value: T[]) => void;
1383
+ }
1384
+ type ToggleGroupProps<T extends string = string> = ToggleGroupSingleProps<T> | ToggleGroupMultipleProps<T>;
1361
1385
  /**
1362
- * A segmented single-choice control — a view switcher, a unit toggle, a
1363
- * density picker.
1364
- *
1365
- * Clicking the active option does nothing unless `allowEmpty` is set. Radix
1366
- * clears the value by default, which is how a chart switcher ends up rendering
1367
- * an empty panel.
1386
+ * A segmented control — a view switcher, a unit toggle, a density picker
1387
+ * (`type="single"`, the default), or a set of on/off filters drawn the same
1388
+ * way (`type="multiple"`).
1389
+ *
1390
+ * Single: clicking the active option does nothing unless `allowEmpty` is set.
1391
+ * Radix clears the value by default, which is how a chart switcher ends up
1392
+ * rendering an empty panel. Multiple: every click toggles, and an empty
1393
+ * selection is a normal state, so there is no `allowEmpty`.
1368
1394
  */
1369
- declare function ToggleGroup<T extends string = string>({ options, value, onValueChange, size, allowEmpty, className, ...aria }: ToggleGroupProps<T>): react.JSX.Element;
1395
+ declare function ToggleGroup<T extends string = string>(props: ToggleGroupProps<T>): react.JSX.Element;
1370
1396
 
1371
1397
  /**
1372
1398
  * The states every data-backed screen has to render.
@@ -1442,6 +1468,124 @@ interface ComingSoonProps {
1442
1468
  * invented figures is worse than an honest gap.
1443
1469
  */
1444
1470
  declare function ComingSoon({ title, detail, className }: ComingSoonProps): react.JSX.Element;
1471
+ interface BootScreenProps {
1472
+ /** `loading` while the app fetches its config; `error` when that failed. */
1473
+ state: 'loading' | 'error';
1474
+ error?: unknown;
1475
+ /** Defaults to "Loading…". */
1476
+ label?: string;
1477
+ /** Defaults to "Could not start the app". */
1478
+ title?: string;
1479
+ /** Defaults to reloading the page, which is the only retry a boot has. */
1480
+ onRetry?: () => void;
1481
+ retryLabel?: string;
1482
+ }
1483
+ /**
1484
+ * The whole viewport before the app exists: config loading, or config failed.
1485
+ *
1486
+ * Every adopted app's `main.tsx` hand-wrote both, with a raw `#dc2626` and an
1487
+ * unstyled `<button>`, because the page states above assume a layout to sit
1488
+ * in. This is them, centered on the screen. It renders before any provider,
1489
+ * so it takes no context and no translations.
1490
+ */
1491
+ declare function BootScreen({ state, error, label, title, onRetry, retryLabel, }: BootScreenProps): react.JSX.Element;
1492
+
1493
+ type StatTone = 'neutral' | 'success' | 'warning' | 'error';
1494
+ interface StatTileProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title' | 'onClick'> {
1495
+ label: ReactNode;
1496
+ /** Already formatted: `$1.3M`, `12/18`, `—`. A number is thousands-separated. */
1497
+ value: ReactNode;
1498
+ /** One muted line under the value: a breakdown, a date, a unit. Truncates. */
1499
+ hint?: ReactNode;
1500
+ /** Colors the value. `neutral` (default) leaves it at body color. */
1501
+ tone?: StatTone;
1502
+ /**
1503
+ * Dims the value when it is a zero, a dash, or otherwise "nothing to see".
1504
+ * A zero is a real answer worth showing, but at full weight a row of zeros
1505
+ * carries the same claim as the numbers that moved and the eye goes nowhere.
1506
+ */
1507
+ quiet?: boolean;
1508
+ /** Top-right corner: a confidence ring, an info button, a sparkline. */
1509
+ aside?: ReactNode;
1510
+ /** Under the hint: a trend delta, a volatility meter. */
1511
+ footer?: ReactNode;
1512
+ /** Makes the whole tile a button — a drill-down. */
1513
+ onClick?: () => void;
1514
+ }
1515
+ /**
1516
+ * A KPI: a label, one big number, and at most a line of context.
1517
+ *
1518
+ * Six copies existed across the three adopted apps (four in pm alone) and
1519
+ * agreed on everything that matters — muted label, tabular figure, optional
1520
+ * sub-line, a tone for the bad news — while disagreeing on type size,
1521
+ * padding and which of three palettes "amber" meant. Tabular figures are the
1522
+ * one non-negotiable: tiles sit in a row and their digits should line up.
1523
+ */
1524
+ declare function StatTile({ label, value, hint, tone, quiet, aside, footer, onClick, className, ...rest }: StatTileProps): react.JSX.Element;
1525
+ interface StatGridProps extends React.HTMLAttributes<HTMLDivElement> {
1526
+ /** Narrowest a tile gets before the row wraps. Default `11rem`. */
1527
+ minTileWidth?: string;
1528
+ }
1529
+ /** A row of `StatTile`s that wraps by width, not by breakpoint. */
1530
+ declare function StatGrid({ minTileWidth, className, style, ...rest }: StatGridProps): react.JSX.Element;
1531
+
1532
+ interface ExternalLinkProps {
1533
+ /**
1534
+ * Null when the id behind the label is missing. Renders the children and
1535
+ * nothing else: the honest rendering of "we have the number but not the
1536
+ * link" is the number, not underlined text that does nothing.
1537
+ */
1538
+ href: string | null | undefined;
1539
+ /** What the reader is about to open: "Open quote Q-2026-164 in Opportunity Management". */
1540
+ title?: string;
1541
+ className?: string;
1542
+ children: ReactNode;
1543
+ }
1544
+ /**
1545
+ * A link to a record in ANOTHER app, drawn the same way every time.
1546
+ *
1547
+ * pm and analytics each had one, and master-data built its URLs a third way.
1548
+ * A cross-app link says three things an in-app link does not: that it leaves
1549
+ * the app (the arrow), where it goes (the title), and when the id is missing,
1550
+ * that it is not a link at all (null `href`).
1551
+ *
1552
+ * New tab, deliberately: the reader is mid-task and the other app is a
1553
+ * reference. Build the href with shell-header's `getAppUrl`, never from an
1554
+ * env var, so it follows the environment the reader is in.
1555
+ */
1556
+ declare function ExternalLink({ href, title, className, children }: ExternalLinkProps): react.JSX.Element;
1557
+ interface FileButtonProps extends Omit<ButtonProps, 'onClick' | 'asChild'> {
1558
+ /** The input's `accept`: `.csv,.xlsx`, `image/*`. */
1559
+ accept?: string;
1560
+ /** Called with the chosen file. The input is reset after, so choosing the same file again fires again. */
1561
+ onFile: (file: File) => void;
1562
+ }
1563
+ /**
1564
+ * A button that opens the file picker.
1565
+ *
1566
+ * Five copies across pm and master-data, each a hidden `<input type="file">`
1567
+ * plus a ref plus a button that clicks it, and most forgot to reset the
1568
+ * input — so re-uploading the same file after a failed import did nothing.
1569
+ */
1570
+ declare function FileButton({ accept, onFile, children, ...button }: FileButtonProps): react.JSX.Element;
1571
+
1572
+ /**
1573
+ * `value`, once it has stopped changing for `delayMs`.
1574
+ *
1575
+ * For a value that drives a request (a search string into a query key).
1576
+ * Do not wrap a `SearchInput`'s value in this: `SearchInput` already
1577
+ * debounces its `onChange`, and master-data's seven list hooks waited twice.
1578
+ */
1579
+ declare function useDebouncedValue<T>(value: T, delayMs?: number): T;
1580
+ /**
1581
+ * `useState` that survives a reload, per viewer and per browser.
1582
+ *
1583
+ * For conveniences only — a collapsed panel, a chosen tab, column widths.
1584
+ * Storage can be absent or throw (a private window, blocked site data), so
1585
+ * every read and write is guarded and the state still works in memory; and a
1586
+ * stored value that no longer parses falls back to `initial`.
1587
+ */
1588
+ declare function usePersistentState<T>(key: string, initial: T): [T, (next: T | ((prev: T) => T)) => void];
1445
1589
 
1446
1590
  interface SortState<K extends string> {
1447
1591
  key: K;
@@ -1775,6 +1919,13 @@ declare function usePagedRows<T>(all: readonly T[], initialPageSize?: number): P
1775
1919
  * one that matches a browser setting.
1776
1920
  */
1777
1921
  declare function formatDate(value: Date | string | number | null | undefined): string;
1922
+ /**
1923
+ * A calendar date — an SOP, a due date, a birthday — that the API sends as a
1924
+ * UTC-midnight timestamp (`2028-01-01T00:00:00.000Z`, which is how a Prisma
1925
+ * `@db.Date` serializes). Reads the UTC day, so it is the same day in Detroit
1926
+ * and in Silao. Use `formatDate` for an instant (created, updated).
1927
+ */
1928
+ declare function formatCalendarDate(value: Date | string | number | null | undefined): string;
1778
1929
  /** `2026-09-11 14:30`. Same reasoning as `formatDate`, plus a 24-hour clock. */
1779
1930
  declare function formatDateTime(value: Date | string | number | null | undefined): string;
1780
1931
  interface MoneyOptions {
@@ -1788,8 +1939,14 @@ interface MoneyOptions {
1788
1939
  decimals?: number;
1789
1940
  /** Set for a per-piece figure to get 4 decimals without naming the number. */
1790
1941
  perUnit?: boolean;
1942
+ /**
1943
+ * Scale to the magnitude for a tile or an axis: `$1.3M`, `$52.0K`, `$640`.
1944
+ * `decimals` (default 1) applies to the scaled figure; under 1,000 rounds
1945
+ * to whole units. Never for a per-unit price, which needs its 4 places.
1946
+ */
1947
+ compact?: boolean;
1791
1948
  }
1792
- declare function formatMoney(value: number | null | undefined, { currency, decimals, perUnit }?: MoneyOptions): string;
1949
+ declare function formatMoney(value: number | null | undefined, { currency, decimals, perUnit, compact }?: MoneyOptions): string;
1793
1950
  /** Thousands-separated, no currency. `null` renders as an em dash, not `0`. */
1794
1951
  declare function formatNumber(value: number | null | undefined, decimals?: number): string;
1795
1952
  /**
@@ -1799,4 +1956,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
1799
1956
  */
1800
1957
  declare function formatPercent(value: number | null | undefined, decimals?: number): string;
1801
1958
 
1802
- export { Accordion, AccordionContent, AccordionItem, type AccordionProps, AccordionRoot, type AccordionSection, AccordionTrigger, type AccordionTriggerProps, Alert, type AlertProps, type AlertTone, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Badge, type BadgeProps, type BadgeTone, type Breadcrumb, Button, ButtonGroup, type ButtonGroupProps, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardBody, CardDescription, CardFooter, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ClassValue, Combobox, type ComboboxOption, type ComboboxProps, ComingSoon, type ComingSoonProps, ConfirmDialog, type ConfirmDialogProps, CountBadge, type CountBadgeProps, CreatableSelect, type CreatableSelectProps, DataList, DataListItem, type DataListItemProps, type DataListProps, Dialog, DialogBody, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogFooter, DialogHeader, DialogPortal, DialogRoot, type DialogSize, DialogTitle, DialogTrigger, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, Field, type FieldProps, Fieldset, type FieldsetProps, FilterBar, type FilterBarProps, type FilterSelect, type FilterToggle, type FilterValue, IconButton, type IconButtonProps, Input, type InputProps, type InputSize, Label, type LabelProps, ListToolbar, type ListToolbarProps, LoadingState, type LoadingStateProps, Menu, type MenuAction, MenuCheckboxItem, MenuContent, MenuGroup, MenuItem, type MenuItemProps, MenuLabel, type MenuProps, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, Modal, type ModalProps, type MoneyOptions, PageHeader, type PageHeaderProps, type PagedRows, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, type PopoverContentProps, type PopoverProps, PopoverRoot, PopoverTrigger, Progress, type ProgressProps, ScrollArea, type ScrollAreaProps, SearchInput, type SearchInputProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, type SelectOption, type SelectProps, SelectRoot, SelectSeparator, SelectTrigger, type SelectTriggerProps, SelectValue, SelectionBar, type SelectionBarProps, Separator, type SeparatorProps, Sheet, SheetBody, SheetClose, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetPortal, SheetTitle, SheetTrigger, SideModal, Skeleton, type SkeletonProps, Slider, type SliderProps, type SortDirection, type SortState, SplitButton, type SplitButtonProps, Switch, type SwitchProps, type TabDefinition, TabSet, type TabSetProps, Table, TableBody, TableCaption, TableCell, type TableCellProps, TableEmpty, TableFoot, TableFooter, TableHead, type TableHeadProps, TableHeader, type TableProps, TableRow, type TableRowProps, TableScroller, Tabs, TabsContent, TabsList, type TabsListProps, TabsRoot, TabsTrigger, type TabsTriggerProps, Textarea, type TextareaProps, type ToastOptions, ToastProvider, type ToastProviderProps, type ToastTone, ToggleGroup, type ToggleGroupProps, type ToggleOption, Tooltip, TooltipContent, type TooltipContentProps, type TooltipProps, TooltipProvider, TooltipRoot, TooltipTrigger, type UseSortStateResult, cn, formatDate, formatDateTime, formatMoney, formatNumber, formatPercent, useFieldControl, usePagedRows, useSortState, useToast };
1959
+ export { Accordion, AccordionContent, AccordionItem, type AccordionProps, AccordionRoot, type AccordionSection, AccordionTrigger, type AccordionTriggerProps, Alert, type AlertProps, type AlertTone, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Badge, type BadgeProps, type BadgeTone, BootScreen, type BootScreenProps, type Breadcrumb, Button, ButtonGroup, type ButtonGroupProps, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardBody, CardDescription, CardFooter, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type ClassValue, Combobox, type ComboboxOption, type ComboboxProps, ComingSoon, type ComingSoonProps, ConfirmDialog, type ConfirmDialogProps, CountBadge, type CountBadgeProps, CreatableSelect, type CreatableSelectProps, DataList, DataListItem, type DataListItemProps, type DataListProps, Dialog, DialogBody, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogFooter, DialogHeader, DialogPortal, DialogRoot, type DialogSize, DialogTitle, DialogTrigger, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, ExternalLink, type ExternalLinkProps, Field, type FieldProps, Fieldset, type FieldsetProps, FileButton, type FileButtonProps, FilterBar, type FilterBarProps, type FilterSelect, type FilterToggle, type FilterValue, IconButton, type IconButtonProps, Input, type InputProps, type InputSize, Label, type LabelProps, ListToolbar, type ListToolbarProps, LoadingState, type LoadingStateProps, Menu, type MenuAction, MenuCheckboxItem, MenuContent, MenuGroup, MenuItem, type MenuItemProps, MenuLabel, type MenuProps, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, Modal, type ModalProps, type MoneyOptions, PageHeader, type PageHeaderProps, type PagedRows, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, type PopoverContentProps, type PopoverProps, PopoverRoot, PopoverTrigger, Progress, type ProgressProps, ScrollArea, type ScrollAreaProps, SearchInput, type SearchInputProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, type SelectOption, type SelectProps, SelectRoot, SelectSeparator, SelectTrigger, type SelectTriggerProps, SelectValue, SelectionBar, type SelectionBarProps, Separator, type SeparatorProps, Sheet, SheetBody, SheetClose, SheetContent, type SheetContentProps, SheetDescription, SheetFooter, SheetHeader, SheetPortal, SheetTitle, SheetTrigger, SideModal, Skeleton, type SkeletonProps, Slider, type SliderProps, type SortDirection, type SortState, SplitButton, type SplitButtonProps, StatGrid, type StatGridProps, StatTile, type StatTileProps, type StatTone, Switch, type SwitchProps, type TabDefinition, TabSet, type TabSetProps, Table, TableBody, TableCaption, TableCell, type TableCellProps, TableEmpty, TableFoot, TableFooter, TableHead, type TableHeadProps, TableHeader, type TableProps, TableRow, type TableRowProps, TableScroller, Tabs, TabsContent, TabsList, type TabsListProps, TabsRoot, TabsTrigger, type TabsTriggerProps, Textarea, type TextareaProps, type ToastOptions, ToastProvider, type ToastProviderProps, type ToastTone, ToggleGroup, type ToggleGroupProps, type ToggleOption, Tooltip, TooltipContent, type TooltipContentProps, type TooltipProps, TooltipProvider, TooltipRoot, TooltipTrigger, type UseSortStateResult, cn, formatCalendarDate, formatDate, formatDateTime, formatMoney, formatNumber, formatPercent, useDebouncedValue, useFieldControl, usePagedRows, usePersistentState, useSortState, useToast };
package/dist/index.js CHANGED
@@ -124,7 +124,7 @@ import * as RadixMenu from "@radix-ui/react-dropdown-menu";
124
124
  import { Check } from "lucide-react";
125
125
 
126
126
  // src/Floating.css
127
- styleInject("@layer components {\n .cfi-ui-floating {\n z-index: var(--cfi-ui-z-popover);\n background: var(--cfi-ui-surface-raised);\n color: var(--cfi-ui-content);\n border: 1px solid var(--cfi-ui-border);\n border-radius: var(--cfi-ui-radius-md);\n box-shadow: var(--cfi-ui-shadow-lg);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n animation: cfi-ui-zoom-in var(--cfi-ui-transition-fast);\n max-height: var(--radix-popper-available-height, none);\n overflow: hidden auto;\n }\n .cfi-ui-floating__arrow {\n fill: var(--cfi-ui-surface-raised);\n filter: drop-shadow(0 -1px 0 var(--cfi-ui-border));\n }\n .cfi-ui-popover {\n padding: var(--cfi-ui-space-md);\n min-width: 12rem;\n }\n .cfi-ui-popover[data-flush] {\n padding: 0;\n }\n .cfi-ui-menu {\n z-index: var(--cfi-ui-z-dropdown);\n min-width: 11rem;\n padding: var(--cfi-ui-space-xs);\n }\n .cfi-ui-menu__item {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n padding: 0.375rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n font-size: var(--cfi-ui-text-sm);\n line-height: var(--cfi-ui-leading-normal);\n color: var(--cfi-ui-content);\n cursor: pointer;\n user-select: none;\n outline: none;\n }\n .cfi-ui-menu__item[data-highlighted] {\n background: var(--cfi-ui-surface-active);\n }\n .cfi-ui-menu__item[data-disabled] {\n color: var(--cfi-ui-content-disabled);\n cursor: not-allowed;\n }\n .cfi-ui-menu__item[data-variant=danger] {\n color: var(--cfi-ui-error-text);\n }\n .cfi-ui-menu__item[data-variant=danger][data-highlighted] {\n background: var(--cfi-ui-error-soft);\n }\n .cfi-ui-menu__item > svg,\n .cfi-ui-menu__item .cfi-ui-menu__icon > svg {\n width: 0.875rem;\n height: 0.875rem;\n flex: none;\n }\n .cfi-ui-menu__icon {\n display: inline-flex;\n flex: none;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-menu__item[data-variant=danger] .cfi-ui-menu__icon {\n color: inherit;\n }\n .cfi-ui-menu__shortcut {\n margin-left: auto;\n padding-left: var(--cfi-ui-space-md);\n font-family: var(--cfi-ui-font-mono);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-menu__label {\n padding: 0.375rem 0.5rem 0.25rem;\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n letter-spacing: var(--cfi-ui-tracking-caps);\n text-transform: uppercase;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-menu__separator {\n height: 1px;\n margin: var(--cfi-ui-space-xs) calc(-1 * var(--cfi-ui-space-xs));\n background: var(--cfi-ui-border);\n }\n .cfi-ui-select-content {\n z-index: var(--cfi-ui-z-dropdown);\n padding: var(--cfi-ui-space-xs);\n min-width: var(--radix-select-trigger-width, 11rem);\n }\n .cfi-ui-select-viewport {\n max-height: min(18rem, var(--radix-select-content-available-height, 18rem));\n }\n .cfi-ui-select-item {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n padding: 0.375rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content);\n cursor: pointer;\n user-select: none;\n outline: none;\n }\n .cfi-ui-select-item[data-highlighted] {\n background: var(--cfi-ui-surface-active);\n }\n .cfi-ui-select-item[data-state=checked] {\n font-weight: var(--cfi-ui-weight-medium);\n }\n .cfi-ui-select-item[data-disabled] {\n color: var(--cfi-ui-content-disabled);\n cursor: not-allowed;\n }\n .cfi-ui-select-item__indicator {\n display: inline-flex;\n flex: none;\n width: 0.875rem;\n color: var(--cfi-ui-accent);\n }\n .cfi-ui-select-item__indicator > svg {\n width: 0.875rem;\n height: 0.875rem;\n }\n .cfi-ui-select-empty {\n padding: var(--cfi-ui-space-sm);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n text-align: center;\n }\n .cfi-ui-tooltip {\n z-index: var(--cfi-ui-z-tooltip);\n max-width: 20rem;\n padding: 0.3125rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-content);\n color: var(--cfi-ui-surface);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-xs);\n line-height: var(--cfi-ui-leading-normal);\n box-shadow: var(--cfi-ui-shadow-md);\n animation: cfi-ui-zoom-in var(--cfi-ui-transition-fast);\n }\n .cfi-ui-tooltip__arrow {\n fill: var(--cfi-ui-content);\n }\n}\n");
127
+ styleInject("@layer components {\n .cfi-ui-floating {\n z-index: var(--cfi-ui-z-popover);\n background: var(--cfi-ui-surface-raised);\n color: var(--cfi-ui-content);\n border: 1px solid var(--cfi-ui-border);\n border-radius: var(--cfi-ui-radius-md);\n box-shadow: var(--cfi-ui-shadow-lg);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n animation: cfi-ui-zoom-in var(--cfi-ui-transition-fast);\n max-height: var(--radix-popper-available-height, none);\n overflow: hidden auto;\n }\n .cfi-ui-floating__arrow {\n fill: var(--cfi-ui-surface-raised);\n filter: drop-shadow(0 -1px 0 var(--cfi-ui-border));\n }\n .cfi-ui-popover {\n padding: var(--cfi-ui-space-md);\n min-width: 12rem;\n }\n .cfi-ui-popover[data-flush] {\n padding: 0;\n }\n .cfi-ui-menu {\n min-width: 11rem;\n padding: var(--cfi-ui-space-xs);\n }\n .cfi-ui-menu__item {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n padding: 0.375rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n font-size: var(--cfi-ui-text-sm);\n line-height: var(--cfi-ui-leading-normal);\n color: var(--cfi-ui-content);\n cursor: pointer;\n user-select: none;\n outline: none;\n }\n .cfi-ui-menu__item[data-highlighted] {\n background: var(--cfi-ui-surface-active);\n }\n .cfi-ui-menu__item[data-disabled] {\n color: var(--cfi-ui-content-disabled);\n cursor: not-allowed;\n }\n .cfi-ui-menu__item[data-variant=danger] {\n color: var(--cfi-ui-error-text);\n }\n .cfi-ui-menu__item[data-variant=danger][data-highlighted] {\n background: var(--cfi-ui-error-soft);\n }\n .cfi-ui-menu__item > svg,\n .cfi-ui-menu__item .cfi-ui-menu__icon > svg {\n width: 0.875rem;\n height: 0.875rem;\n flex: none;\n }\n .cfi-ui-menu__icon {\n display: inline-flex;\n flex: none;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-menu__item[data-variant=danger] .cfi-ui-menu__icon {\n color: inherit;\n }\n .cfi-ui-menu__shortcut {\n margin-left: auto;\n padding-left: var(--cfi-ui-space-md);\n font-family: var(--cfi-ui-font-mono);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-menu__label {\n padding: 0.375rem 0.5rem 0.25rem;\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n letter-spacing: var(--cfi-ui-tracking-caps);\n text-transform: uppercase;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-menu__separator {\n height: 1px;\n margin: var(--cfi-ui-space-xs) calc(-1 * var(--cfi-ui-space-xs));\n background: var(--cfi-ui-border);\n }\n .cfi-ui-select-content {\n padding: var(--cfi-ui-space-xs);\n min-width: var(--radix-select-trigger-width, 11rem);\n }\n .cfi-ui-select-viewport {\n max-height: min(18rem, var(--radix-select-content-available-height, 18rem));\n }\n .cfi-ui-select-item {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n padding: 0.375rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content);\n cursor: pointer;\n user-select: none;\n outline: none;\n }\n .cfi-ui-select-item[data-highlighted] {\n background: var(--cfi-ui-surface-active);\n }\n .cfi-ui-select-item[data-state=checked] {\n font-weight: var(--cfi-ui-weight-medium);\n }\n .cfi-ui-select-item[data-disabled] {\n color: var(--cfi-ui-content-disabled);\n cursor: not-allowed;\n }\n .cfi-ui-select-item__indicator {\n display: inline-flex;\n flex: none;\n width: 0.875rem;\n color: var(--cfi-ui-accent);\n }\n .cfi-ui-select-item__indicator > svg {\n width: 0.875rem;\n height: 0.875rem;\n }\n .cfi-ui-select-empty {\n padding: var(--cfi-ui-space-sm);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n text-align: center;\n }\n .cfi-ui-tooltip {\n z-index: var(--cfi-ui-z-tooltip);\n max-width: 20rem;\n padding: 0.3125rem 0.5rem;\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-content);\n color: var(--cfi-ui-surface);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-xs);\n line-height: var(--cfi-ui-leading-normal);\n box-shadow: var(--cfi-ui-shadow-md);\n animation: cfi-ui-zoom-in var(--cfi-ui-transition-fast);\n }\n .cfi-ui-tooltip__arrow {\n fill: var(--cfi-ui-content);\n }\n}\n");
128
128
 
129
129
  // src/Menu.tsx
130
130
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -481,7 +481,7 @@ function Select({
481
481
  }
482
482
 
483
483
  // src/Switch.tsx
484
- import { forwardRef as forwardRef7 } from "react";
484
+ import { forwardRef as forwardRef7, useId } from "react";
485
485
  import * as RadixSwitch from "@radix-ui/react-switch";
486
486
 
487
487
  // src/Switch.css
@@ -491,11 +491,15 @@ styleInject("@layer components {\n .cfi-ui-switch {\n flex: none;\n posit
491
491
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
492
492
  var Switch = forwardRef7(
493
493
  function Switch2({ label, description, labelPosition = "end", className, id, ...rest }, ref) {
494
+ const generated = useId();
495
+ const controlId = id ?? generated;
496
+ const descriptionId = description ? `${controlId}-description` : void 0;
494
497
  const control = /* @__PURE__ */ jsx7(
495
498
  RadixSwitch.Root,
496
499
  {
497
500
  ref,
498
- id,
501
+ id: controlId,
502
+ "aria-describedby": descriptionId,
499
503
  className: cn("cfi-ui", "cfi-ui-switch", !label && className),
500
504
  ...rest,
501
505
  children: /* @__PURE__ */ jsx7(RadixSwitch.Thumb, { className: "cfi-ui-switch__thumb" })
@@ -505,8 +509,8 @@ var Switch = forwardRef7(
505
509
  return /* @__PURE__ */ jsxs7("div", { "data-label-position": labelPosition, className: cn("cfi-ui", "cfi-ui-switch-row", className), children: [
506
510
  labelPosition === "start" && control,
507
511
  /* @__PURE__ */ jsxs7("div", { className: "cfi-ui-switch-row__text", children: [
508
- /* @__PURE__ */ jsx7("label", { htmlFor: id, className: "cfi-ui-switch-row__label", children: label }),
509
- description && /* @__PURE__ */ jsx7("span", { className: "cfi-ui-switch-row__description", children: description })
512
+ /* @__PURE__ */ jsx7("label", { htmlFor: controlId, className: "cfi-ui-switch-row__label", children: label }),
513
+ description && /* @__PURE__ */ jsx7("span", { id: descriptionId, className: "cfi-ui-switch-row__description", children: description })
510
514
  ] }),
511
515
  labelPosition === "end" && control
512
516
  ] });
@@ -659,7 +663,7 @@ var Textarea = forwardRef8(function Textarea2({ invalid, className, rows = 3, ..
659
663
  });
660
664
 
661
665
  // src/Field.tsx
662
- import { Children, cloneElement as cloneElement2, createContext, isValidElement as isValidElement2, useContext, useId } from "react";
666
+ import { Children, cloneElement as cloneElement2, createContext, isValidElement as isValidElement2, useContext, useId as useId2 } from "react";
663
667
 
664
668
  // src/Field.css
665
669
  styleInject('@layer components {\n .cfi-ui-label {\n display: inline-flex;\n align-items: center;\n gap: var(--cfi-ui-space-3xs);\n font-size: var(--cfi-ui-text-sm);\n font-weight: var(--cfi-ui-weight-medium);\n color: var(--cfi-ui-content);\n line-height: var(--cfi-ui-leading-normal);\n }\n .cfi-ui-label__required {\n color: var(--cfi-ui-error);\n font-weight: var(--cfi-ui-weight-semibold);\n }\n .cfi-ui-field {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: var(--cfi-ui-space-xs);\n }\n .cfi-ui-field__label {\n justify-self: start;\n }\n .cfi-ui-field__body {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: var(--cfi-ui-space-xs);\n min-width: 0;\n }\n .cfi-ui-field__hint,\n .cfi-ui-field__error {\n margin: 0;\n font-size: var(--cfi-ui-text-xs);\n line-height: var(--cfi-ui-leading-normal);\n }\n .cfi-ui-field__hint {\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-field__error {\n color: var(--cfi-ui-error-text);\n font-weight: var(--cfi-ui-weight-medium);\n }\n .cfi-ui-field[data-orientation=horizontal] {\n grid-template-columns: minmax(8rem, 12rem) 1fr;\n align-items: start;\n gap: var(--cfi-ui-space-md);\n }\n .cfi-ui-field[data-orientation=horizontal] .cfi-ui-field__label {\n padding-top: 0.4375rem;\n }\n @media (max-width: 640px) {\n .cfi-ui-field[data-orientation=horizontal] {\n grid-template-columns: 1fr;\n gap: var(--cfi-ui-space-xs);\n }\n .cfi-ui-field[data-orientation=horizontal] .cfi-ui-field__label {\n padding-top: 0;\n }\n }\n .cfi-ui-fieldset {\n min-width: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n .cfi-ui-fieldset__legend {\n padding: 0 0 var(--cfi-ui-space-sm);\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n letter-spacing: var(--cfi-ui-tracking-caps);\n text-transform: uppercase;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-fieldset__body {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: var(--cfi-ui-space-md);\n }\n .cfi-ui-fieldset[data-columns="2"] .cfi-ui-fieldset__body {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n .cfi-ui-fieldset[data-columns="3"] .cfi-ui-fieldset__body {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n @media (max-width: 720px) {\n .cfi-ui-fieldset[data-columns="2"] .cfi-ui-fieldset__body,\n .cfi-ui-fieldset[data-columns="3"] .cfi-ui-fieldset__body {\n grid-template-columns: 1fr;\n }\n }\n}\n');
@@ -686,7 +690,7 @@ function Field({
686
690
  children,
687
691
  id
688
692
  }) {
689
- const generatedId = useId();
693
+ const generatedId = useId2();
690
694
  const controlId = id ?? `cfi-field-${generatedId}`;
691
695
  const hintId = `${controlId}-hint`;
692
696
  const errorId = `${controlId}-error`;
@@ -842,9 +846,16 @@ function DialogBody({ className, ...rest }) {
842
846
  function DialogFooter({ className, ...rest }) {
843
847
  return /* @__PURE__ */ jsx13("div", { className: cn("cfi-ui-dialog__footer", className), ...rest });
844
848
  }
849
+ function closeHandler(onOpenChange, onClose) {
850
+ return (open) => {
851
+ onOpenChange?.(open);
852
+ if (!open) onClose?.();
853
+ };
854
+ }
845
855
  function Modal({
846
856
  open,
847
857
  onOpenChange,
858
+ onClose,
848
859
  title,
849
860
  description,
850
861
  size = "md",
@@ -856,7 +867,7 @@ function Modal({
856
867
  children
857
868
  }) {
858
869
  const block = dismissible ? void 0 : (e) => e.preventDefault();
859
- return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs13(
870
+ return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange: closeHandler(onOpenChange, onClose), children: /* @__PURE__ */ jsxs13(
860
871
  DialogContent,
861
872
  {
862
873
  size,
@@ -899,6 +910,7 @@ var SheetFooter = DialogFooter;
899
910
  function ConfirmDialog({
900
911
  open,
901
912
  onOpenChange,
913
+ onClose,
902
914
  title,
903
915
  description,
904
916
  confirmLabel = "Confirm",
@@ -908,7 +920,7 @@ function ConfirmDialog({
908
920
  onConfirm,
909
921
  children
910
922
  }) {
911
- return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs13(
923
+ return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange: closeHandler(onOpenChange, onClose), children: /* @__PURE__ */ jsxs13(
912
924
  DialogContent,
913
925
  {
914
926
  size: "sm",
@@ -1087,7 +1099,7 @@ function CreatableSelect({
1087
1099
  }
1088
1100
 
1089
1101
  // src/Combobox.tsx
1090
- import { Fragment as Fragment4, useEffect, useId as useId2, useMemo as useMemo3, useRef, useState as useState3 } from "react";
1102
+ import { Fragment as Fragment4, useEffect, useId as useId3, useMemo as useMemo3, useRef, useState as useState3 } from "react";
1091
1103
  import { Check as Check4, ChevronDown as ChevronDown3, Search, X as X3 } from "lucide-react";
1092
1104
 
1093
1105
  // src/Combobox.css
@@ -1123,7 +1135,7 @@ function Combobox({
1123
1135
  const [open, setOpen] = useState3(false);
1124
1136
  const [search, setSearch] = useState3("");
1125
1137
  const [highlighted, setHighlighted] = useState3(0);
1126
- const listId = useId2();
1138
+ const listId = useId3();
1127
1139
  const searchRef = useRef(null);
1128
1140
  const listRef = useRef(null);
1129
1141
  const filtered = useMemo3(() => {
@@ -1962,15 +1974,42 @@ function Accordion({
1962
1974
  /* @__PURE__ */ jsx24(AccordionContent, { children: section.content })
1963
1975
  ] }, section.value)) });
1964
1976
  }
1965
- function ToggleGroup({
1966
- options,
1967
- value,
1968
- onValueChange,
1969
- size = "md",
1970
- allowEmpty = false,
1971
- className,
1972
- ...aria
1973
- }) {
1977
+ function ToggleGroup(props) {
1978
+ const { options, size = "md", className, "aria-label": ariaLabel } = props;
1979
+ const shared = {
1980
+ "data-size": size,
1981
+ className: cn("cfi-ui", "cfi-ui-toggle-group", className),
1982
+ "aria-label": ariaLabel
1983
+ };
1984
+ const items = options.map((option) => /* @__PURE__ */ jsxs24(
1985
+ RadixToggleGroup.Item,
1986
+ {
1987
+ value: option.value,
1988
+ disabled: option.disabled,
1989
+ "aria-label": option.ariaLabel,
1990
+ title: option.ariaLabel,
1991
+ className: "cfi-ui-toggle-group__item",
1992
+ children: [
1993
+ option.icon && /* @__PURE__ */ jsx24("span", { className: "cfi-ui-toggle-group__icon", children: option.icon }),
1994
+ option.label
1995
+ ]
1996
+ },
1997
+ option.value
1998
+ ));
1999
+ if (props.type === "multiple") {
2000
+ const { value: value2, onValueChange: onValueChange2 } = props;
2001
+ return /* @__PURE__ */ jsx24(
2002
+ RadixToggleGroup.Root,
2003
+ {
2004
+ type: "multiple",
2005
+ value: value2,
2006
+ onValueChange: (next) => onValueChange2?.(next),
2007
+ ...shared,
2008
+ children: items
2009
+ }
2010
+ );
2011
+ }
2012
+ const { value, onValueChange, allowEmpty = false } = props;
1974
2013
  return /* @__PURE__ */ jsx24(
1975
2014
  RadixToggleGroup.Root,
1976
2015
  {
@@ -1980,24 +2019,8 @@ function ToggleGroup({
1980
2019
  if (!next && !allowEmpty) return;
1981
2020
  onValueChange?.(next);
1982
2021
  },
1983
- "data-size": size,
1984
- className: cn("cfi-ui", "cfi-ui-toggle-group", className),
1985
- ...aria,
1986
- children: options.map((option) => /* @__PURE__ */ jsxs24(
1987
- RadixToggleGroup.Item,
1988
- {
1989
- value: option.value,
1990
- disabled: option.disabled,
1991
- "aria-label": option.ariaLabel,
1992
- title: option.ariaLabel,
1993
- className: "cfi-ui-toggle-group__item",
1994
- children: [
1995
- option.icon && /* @__PURE__ */ jsx24("span", { className: "cfi-ui-toggle-group__icon", children: option.icon }),
1996
- option.label
1997
- ]
1998
- },
1999
- option.value
2000
- ))
2022
+ ...shared,
2023
+ children: items
2001
2024
  }
2002
2025
  );
2003
2026
  }
@@ -2006,7 +2029,7 @@ function ToggleGroup({
2006
2029
  import { AlertCircle as AlertCircle2, Loader2 } from "lucide-react";
2007
2030
 
2008
2031
  // src/PageState.css
2009
- styleInject("@layer components {\n :where(.cfi-ui-pagestate) {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--cfi-ui-space-sm);\n padding-block: var(--cfi-ui-space-2xl, 4rem);\n text-align: center;\n color: var(--cfi-ui-content);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n }\n .cfi-ui-pagestate[data-size=inline] {\n padding-block: var(--cfi-ui-space-md);\n gap: var(--cfi-ui-space-xs);\n }\n .cfi-ui-pagestate:has(> .cfi-ui-pagestate__spinner) {\n flex-direction: row;\n }\n .cfi-ui-pagestate__spinner {\n width: 1rem;\n height: 1rem;\n flex: none;\n color: var(--cfi-ui-content-muted);\n animation: cfi-ui-pagestate-spin 1s linear infinite;\n }\n @media (prefers-reduced-motion: reduce) {\n .cfi-ui-pagestate__spinner {\n animation-duration: 2.4s;\n }\n }\n .cfi-ui-pagestate__icon {\n display: inline-flex;\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-pagestate__icon > svg {\n width: 1.5rem;\n height: 1.5rem;\n }\n .cfi-ui-pagestate__icon[data-tone=error] {\n width: 1.5rem;\n height: 1.5rem;\n color: var(--cfi-ui-error-strong);\n }\n .cfi-ui-pagestate__title {\n margin: 0;\n font-weight: var(--cfi-ui-weight-medium);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-pagestate__muted {\n margin: 0;\n max-width: 34rem;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-pagestate__title + .cfi-ui-pagestate__muted {\n margin-top: var(--cfi-ui-space-3xs);\n }\n .cfi-ui-pagestate__action {\n margin-top: var(--cfi-ui-space-xs);\n }\n .cfi-ui-pagestate__retry {\n height: var(--cfi-ui-control-height-sm);\n padding-inline: var(--cfi-ui-space-sm);\n background: var(--cfi-ui-surface);\n color: var(--cfi-ui-content);\n border: 1px solid var(--cfi-ui-border-strong);\n border-radius: var(--cfi-ui-radius-md);\n font-family: inherit;\n font-size: var(--cfi-ui-text-sm);\n line-height: normal;\n cursor: pointer;\n }\n .cfi-ui-pagestate__retry:hover {\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-pagestate__retry:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n}\n@keyframes cfi-ui-pagestate-spin {\n to {\n transform: rotate(360deg);\n }\n}\n");
2032
+ styleInject("@layer components {\n :where(.cfi-ui-pagestate) {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--cfi-ui-space-sm);\n padding-block: var(--cfi-ui-space-2xl, 4rem);\n text-align: center;\n color: var(--cfi-ui-content);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n }\n .cfi-ui-pagestate[data-size=inline] {\n padding-block: var(--cfi-ui-space-md);\n gap: var(--cfi-ui-space-xs);\n }\n .cfi-ui-pagestate:has(> .cfi-ui-pagestate__spinner) {\n flex-direction: row;\n }\n .cfi-ui-pagestate__spinner {\n width: 1rem;\n height: 1rem;\n flex: none;\n color: var(--cfi-ui-content-muted);\n animation: cfi-ui-pagestate-spin 1s linear infinite;\n }\n @media (prefers-reduced-motion: reduce) {\n .cfi-ui-pagestate__spinner {\n animation-duration: 2.4s;\n }\n }\n .cfi-ui-pagestate__icon {\n display: inline-flex;\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-pagestate__icon > svg {\n width: 1.5rem;\n height: 1.5rem;\n }\n .cfi-ui-pagestate__icon[data-tone=error] {\n width: 1.5rem;\n height: 1.5rem;\n color: var(--cfi-ui-error-strong);\n }\n .cfi-ui-pagestate__title {\n margin: 0;\n font-weight: var(--cfi-ui-weight-medium);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-pagestate__muted {\n margin: 0;\n max-width: 34rem;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-pagestate__title + .cfi-ui-pagestate__muted {\n margin-top: var(--cfi-ui-space-3xs);\n }\n .cfi-ui-pagestate__action {\n margin-top: var(--cfi-ui-space-xs);\n }\n .cfi-ui-pagestate__retry {\n height: var(--cfi-ui-control-height-sm);\n padding-inline: var(--cfi-ui-space-sm);\n background: var(--cfi-ui-surface);\n color: var(--cfi-ui-content);\n border: 1px solid var(--cfi-ui-border-strong);\n border-radius: var(--cfi-ui-radius-md);\n font-family: inherit;\n font-size: var(--cfi-ui-text-sm);\n line-height: normal;\n cursor: pointer;\n }\n .cfi-ui-pagestate__retry:hover {\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-pagestate__retry:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n :where(.cfi-ui-boot) {\n display: grid;\n place-items: center;\n min-height: 100dvh;\n background: var(--cfi-ui-surface);\n }\n}\n@keyframes cfi-ui-pagestate-spin {\n to {\n transform: rotate(360deg);\n }\n}\n");
2010
2033
 
2011
2034
  // src/PageState.tsx
2012
2035
  import { jsx as jsx25, jsxs as jsxs25 } from "react/jsx-runtime";
@@ -2070,17 +2093,160 @@ function ComingSoon({ title, detail, className }) {
2070
2093
  }
2071
2094
  );
2072
2095
  }
2096
+ function BootScreen({
2097
+ state,
2098
+ error,
2099
+ label,
2100
+ title = "Could not start the app",
2101
+ onRetry = () => window.location.reload(),
2102
+ retryLabel
2103
+ }) {
2104
+ return /* @__PURE__ */ jsx25("div", { className: "cfi-ui cfi-ui-boot", children: state === "loading" ? /* @__PURE__ */ jsx25(LoadingState, { label }) : /* @__PURE__ */ jsx25(ErrorState, { error, title, onRetry, retryLabel }) });
2105
+ }
2106
+
2107
+ // src/StatTile.css
2108
+ styleInject("@layer components {\n :where(.cfi-ui-stat) {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-3xs);\n min-width: 0;\n padding: 0.75rem var(--cfi-ui-space-md);\n background: var(--cfi-ui-surface);\n border: 1px solid var(--cfi-ui-border);\n border-radius: var(--cfi-ui-radius-lg);\n font-family: var(--cfi-ui-font);\n color: var(--cfi-ui-content);\n text-align: start;\n }\n .cfi-ui-stat--action {\n cursor: pointer;\n font: inherit;\n transition: background-color var(--cfi-ui-transition-fast);\n }\n .cfi-ui-stat--action:hover {\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-stat--action:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n .cfi-ui-stat__top {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--cfi-ui-space-xs);\n }\n .cfi-ui-stat__label {\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-stat__aside {\n flex: none;\n }\n .cfi-ui-stat__value {\n font-size: var(--cfi-ui-text-xl);\n font-weight: var(--cfi-ui-weight-semibold);\n line-height: var(--cfi-ui-leading-tight);\n font-variant-numeric: tabular-nums;\n }\n .cfi-ui-stat__value[data-tone=success] {\n color: var(--cfi-ui-success-text);\n }\n .cfi-ui-stat__value[data-tone=warning] {\n color: var(--cfi-ui-warning-text);\n }\n .cfi-ui-stat__value[data-tone=error] {\n color: var(--cfi-ui-error-text);\n }\n .cfi-ui-stat__value[data-quiet] {\n font-weight: var(--cfi-ui-weight-normal);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-stat__hint {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-stat__footer {\n margin-top: var(--cfi-ui-space-3xs);\n font-size: var(--cfi-ui-text-sm);\n }\n :where(.cfi-ui-stat-grid) {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--cfi-ui-stat-min, 11rem), 1fr));\n gap: var(--cfi-ui-space-sm);\n }\n}\n");
2109
+
2110
+ // src/StatTile.tsx
2111
+ import { Fragment as Fragment6, jsx as jsx26, jsxs as jsxs26 } from "react/jsx-runtime";
2112
+ function StatTile({
2113
+ label,
2114
+ value,
2115
+ hint,
2116
+ tone = "neutral",
2117
+ quiet = false,
2118
+ aside,
2119
+ footer,
2120
+ onClick,
2121
+ className,
2122
+ ...rest
2123
+ }) {
2124
+ const body = /* @__PURE__ */ jsxs26(Fragment6, { children: [
2125
+ /* @__PURE__ */ jsxs26("div", { className: "cfi-ui-stat__top", children: [
2126
+ /* @__PURE__ */ jsx26("span", { className: "cfi-ui-stat__label", children: label }),
2127
+ aside && /* @__PURE__ */ jsx26("span", { className: "cfi-ui-stat__aside", children: aside })
2128
+ ] }),
2129
+ /* @__PURE__ */ jsx26("span", { className: "cfi-ui-stat__value", "data-tone": tone, "data-quiet": quiet || void 0, children: typeof value === "number" ? value.toLocaleString("en-US") : value }),
2130
+ hint && /* @__PURE__ */ jsx26("span", { className: "cfi-ui-stat__hint", children: hint }),
2131
+ footer && /* @__PURE__ */ jsx26("div", { className: "cfi-ui-stat__footer", children: footer })
2132
+ ] });
2133
+ if (onClick) {
2134
+ return /* @__PURE__ */ jsx26(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ onClick,
2139
+ className: cn("cfi-ui", "cfi-ui-stat", "cfi-ui-stat--action", className),
2140
+ ...rest,
2141
+ children: body
2142
+ }
2143
+ );
2144
+ }
2145
+ return /* @__PURE__ */ jsx26("div", { className: cn("cfi-ui", "cfi-ui-stat", className), ...rest, children: body });
2146
+ }
2147
+ function StatGrid({ minTileWidth = "11rem", className, style, ...rest }) {
2148
+ return /* @__PURE__ */ jsx26(
2149
+ "div",
2150
+ {
2151
+ className: cn("cfi-ui", "cfi-ui-stat-grid", className),
2152
+ style: { ["--cfi-ui-stat-min"]: minTileWidth, ...style },
2153
+ ...rest
2154
+ }
2155
+ );
2156
+ }
2157
+
2158
+ // src/ExternalLink.tsx
2159
+ import { useRef as useRef3 } from "react";
2160
+ import { ArrowUpRight } from "lucide-react";
2161
+
2162
+ // src/ExternalLink.css
2163
+ styleInject("@layer components {\n :where(.cfi-ui-extlink) {\n display: inline-flex;\n align-items: center;\n gap: 0.125rem;\n color: var(--cfi-ui-accent);\n text-decoration: none;\n }\n .cfi-ui-extlink:hover {\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .cfi-ui-extlink:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n border-radius: var(--cfi-ui-radius-sm);\n }\n .cfi-ui-extlink__icon {\n flex: none;\n width: 0.75rem;\n height: 0.75rem;\n opacity: 0.7;\n }\n}\n");
2164
+
2165
+ // src/ExternalLink.tsx
2166
+ import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs27 } from "react/jsx-runtime";
2167
+ function ExternalLink({ href, title, className, children }) {
2168
+ if (!href) return /* @__PURE__ */ jsx27(Fragment7, { children });
2169
+ return /* @__PURE__ */ jsxs27(
2170
+ "a",
2171
+ {
2172
+ href,
2173
+ target: "_blank",
2174
+ rel: "noopener noreferrer",
2175
+ title,
2176
+ className: cn("cfi-ui", "cfi-ui-extlink", className),
2177
+ children: [
2178
+ children,
2179
+ /* @__PURE__ */ jsx27(ArrowUpRight, { className: "cfi-ui-extlink__icon", "aria-hidden": "true" })
2180
+ ]
2181
+ }
2182
+ );
2183
+ }
2184
+ function FileButton({ accept, onFile, children, ...button }) {
2185
+ const input = useRef3(null);
2186
+ return /* @__PURE__ */ jsxs27(Fragment7, { children: [
2187
+ /* @__PURE__ */ jsx27(
2188
+ "input",
2189
+ {
2190
+ ref: input,
2191
+ type: "file",
2192
+ accept,
2193
+ hidden: true,
2194
+ onChange: (e) => {
2195
+ const file = e.target.files?.[0];
2196
+ e.target.value = "";
2197
+ if (file) onFile(file);
2198
+ }
2199
+ }
2200
+ ),
2201
+ /* @__PURE__ */ jsx27(Button, { ...button, onClick: () => input.current?.click(), children })
2202
+ ] });
2203
+ }
2204
+
2205
+ // src/hooks.ts
2206
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState5 } from "react";
2207
+ function useDebouncedValue(value, delayMs = 300) {
2208
+ const [debounced, setDebounced] = useState5(value);
2209
+ useEffect3(() => {
2210
+ const timer = setTimeout(() => setDebounced(value), delayMs);
2211
+ return () => clearTimeout(timer);
2212
+ }, [value, delayMs]);
2213
+ return debounced;
2214
+ }
2215
+ function usePersistentState(key, initial) {
2216
+ const [value, setValue] = useState5(() => {
2217
+ try {
2218
+ const raw = window.localStorage.getItem(key);
2219
+ return raw === null ? initial : JSON.parse(raw);
2220
+ } catch {
2221
+ return initial;
2222
+ }
2223
+ });
2224
+ const set = useCallback2(
2225
+ (next) => {
2226
+ setValue((prev) => {
2227
+ const resolved = typeof next === "function" ? next(prev) : next;
2228
+ try {
2229
+ window.localStorage.setItem(key, JSON.stringify(resolved));
2230
+ } catch {
2231
+ }
2232
+ return resolved;
2233
+ });
2234
+ },
2235
+ [key]
2236
+ );
2237
+ return [value, set];
2238
+ }
2073
2239
 
2074
2240
  // src/useSortState.ts
2075
- import { useCallback as useCallback2, useMemo as useMemo5, useState as useState5 } from "react";
2241
+ import { useCallback as useCallback3, useMemo as useMemo5, useState as useState6 } from "react";
2076
2242
  function useSortState(initialKey, initialDirection = "asc") {
2077
- const [sort, setSort] = useState5({ key: initialKey, direction: initialDirection });
2078
- const toggle = useCallback2((key) => {
2243
+ const [sort, setSort] = useState6({ key: initialKey, direction: initialDirection });
2244
+ const toggle = useCallback3((key) => {
2079
2245
  setSort(
2080
2246
  (current) => current.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" }
2081
2247
  );
2082
2248
  }, []);
2083
- const sortProps = useCallback2(
2249
+ const sortProps = useCallback3(
2084
2250
  (key) => ({
2085
2251
  sortable: true,
2086
2252
  sortDirection: sort.key === key ? sort.direction : null,
@@ -2088,7 +2254,7 @@ function useSortState(initialKey, initialDirection = "asc") {
2088
2254
  }),
2089
2255
  [sort, toggle]
2090
2256
  );
2091
- const sortBy = useCallback2(
2257
+ const sortBy = useCallback3(
2092
2258
  (rows, accessor) => {
2093
2259
  const dir = sort.direction === "asc" ? 1 : -1;
2094
2260
  return [...rows].sort((a, b) => {
@@ -2111,21 +2277,21 @@ function useSortState(initialKey, initialDirection = "asc") {
2111
2277
  }
2112
2278
 
2113
2279
  // src/SearchInput.tsx
2114
- import { forwardRef as forwardRef18, useEffect as useEffect3, useRef as useRef3, useState as useState6 } from "react";
2280
+ import { forwardRef as forwardRef18, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
2115
2281
  import { Search as Search2, X as X6 } from "lucide-react";
2116
2282
 
2117
2283
  // src/SearchInput.css
2118
2284
  styleInject("@layer components {\n .cfi-ui-search__clear {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n background: none;\n border: none;\n color: var(--cfi-ui-content-muted);\n cursor: pointer;\n border-radius: var(--cfi-ui-radius-sm);\n }\n .cfi-ui-search__clear > svg {\n width: 0.875rem;\n height: 0.875rem;\n }\n .cfi-ui-search__clear:hover {\n color: var(--cfi-ui-content);\n }\n .cfi-ui-search__clear:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: 1px;\n }\n}\n");
2119
2285
 
2120
2286
  // src/SearchInput.tsx
2121
- import { jsx as jsx26 } from "react/jsx-runtime";
2287
+ import { jsx as jsx28 } from "react/jsx-runtime";
2122
2288
  var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay = 250, placeholder = "Search\u2026", clearLabel = "Clear search", ...rest }, ref) {
2123
- const [local, setLocal] = useState6(value);
2124
- const timer = useRef3(void 0);
2125
- const latest = useRef3(onChange);
2289
+ const [local, setLocal] = useState7(value);
2290
+ const timer = useRef4(void 0);
2291
+ const latest = useRef4(onChange);
2126
2292
  latest.current = onChange;
2127
- useEffect3(() => setLocal(value), [value]);
2128
- useEffect3(() => () => clearTimeout(timer.current), []);
2293
+ useEffect4(() => setLocal(value), [value]);
2294
+ useEffect4(() => () => clearTimeout(timer.current), []);
2129
2295
  const push = (next) => {
2130
2296
  setLocal(next);
2131
2297
  clearTimeout(timer.current);
@@ -2137,7 +2303,7 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2137
2303
  setLocal("");
2138
2304
  latest.current("");
2139
2305
  };
2140
- return /* @__PURE__ */ jsx26(
2306
+ return /* @__PURE__ */ jsx28(
2141
2307
  Input,
2142
2308
  {
2143
2309
  ...rest,
@@ -2146,8 +2312,8 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2146
2312
  value: local,
2147
2313
  placeholder,
2148
2314
  onChange: (e) => push(e.target.value),
2149
- prefix: /* @__PURE__ */ jsx26(Search2, { "aria-hidden": "true" }),
2150
- suffix: local ? /* @__PURE__ */ jsx26("button", { type: "button", className: "cfi-ui-search__clear", onClick: clear, "aria-label": clearLabel, children: /* @__PURE__ */ jsx26(X6, { "aria-hidden": "true" }) }) : void 0
2315
+ prefix: /* @__PURE__ */ jsx28(Search2, { "aria-hidden": "true" }),
2316
+ suffix: local ? /* @__PURE__ */ jsx28("button", { type: "button", className: "cfi-ui-search__clear", onClick: clear, "aria-label": clearLabel, children: /* @__PURE__ */ jsx28(X6, { "aria-hidden": "true" }) }) : void 0
2151
2317
  }
2152
2318
  );
2153
2319
  });
@@ -2159,7 +2325,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
2159
2325
  styleInject("@layer components {\n :where(.cfi-ui-pagination) {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--cfi-ui-space-md);\n padding-block: var(--cfi-ui-space-sm);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-pagination__controls {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n margin-left: auto;\n }\n .cfi-ui-pagination__page {\n font-variant-numeric: tabular-nums;\n color: var(--cfi-ui-content);\n }\n .cfi-ui-pagination__range {\n font-variant-numeric: tabular-nums;\n }\n .cfi-ui-pagination__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--cfi-ui-control-height-sm);\n height: var(--cfi-ui-control-height-sm);\n padding: 0;\n background: var(--cfi-ui-surface);\n color: var(--cfi-ui-content);\n border: 1px solid var(--cfi-ui-border-strong);\n border-radius: var(--cfi-ui-radius-md);\n cursor: pointer;\n }\n .cfi-ui-pagination__btn > svg {\n width: 1rem;\n height: 1rem;\n }\n .cfi-ui-pagination__btn:hover:not(:disabled) {\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-pagination__btn:disabled {\n color: var(--cfi-ui-content-disabled);\n cursor: default;\n }\n .cfi-ui-pagination__btn:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n}\n");
2160
2326
 
2161
2327
  // src/Pagination.tsx
2162
- import { jsx as jsx27, jsxs as jsxs26 } from "react/jsx-runtime";
2328
+ import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
2163
2329
  function Pagination({
2164
2330
  page,
2165
2331
  pageSize,
@@ -2175,12 +2341,12 @@ function Pagination({
2175
2341
  const from = (current - 1) * pageSize + 1;
2176
2342
  const to = Math.min(current * pageSize, totalItems);
2177
2343
  const range = labels?.range?.(from, to, totalItems) ?? `${from}\u2013${to} of ${totalItems.toLocaleString()}`;
2178
- return /* @__PURE__ */ jsxs26("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
2344
+ return /* @__PURE__ */ jsxs28("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
2179
2345
  showRange && // aria-live so a screen-reader user hears where they landed; paging
2180
2346
  // otherwise moves focus nowhere and announces nothing.
2181
- /* @__PURE__ */ jsx27("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2182
- /* @__PURE__ */ jsxs26("div", { className: "cfi-ui-pagination__controls", children: [
2183
- /* @__PURE__ */ jsx27(
2347
+ /* @__PURE__ */ jsx29("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2348
+ /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-pagination__controls", children: [
2349
+ /* @__PURE__ */ jsx29(
2184
2350
  "button",
2185
2351
  {
2186
2352
  type: "button",
@@ -2188,15 +2354,15 @@ function Pagination({
2188
2354
  onClick: () => onPageChange(current - 1),
2189
2355
  disabled: current <= 1,
2190
2356
  "aria-label": labels?.previous ?? "Previous page",
2191
- children: /* @__PURE__ */ jsx27(ChevronLeft, { "aria-hidden": "true" })
2357
+ children: /* @__PURE__ */ jsx29(ChevronLeft, { "aria-hidden": "true" })
2192
2358
  }
2193
2359
  ),
2194
- /* @__PURE__ */ jsxs26("span", { className: "cfi-ui-pagination__page", children: [
2360
+ /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-pagination__page", children: [
2195
2361
  current,
2196
2362
  " / ",
2197
2363
  pageCount
2198
2364
  ] }),
2199
- /* @__PURE__ */ jsx27(
2365
+ /* @__PURE__ */ jsx29(
2200
2366
  "button",
2201
2367
  {
2202
2368
  type: "button",
@@ -2204,7 +2370,7 @@ function Pagination({
2204
2370
  onClick: () => onPageChange(current + 1),
2205
2371
  disabled: current >= pageCount,
2206
2372
  "aria-label": labels?.next ?? "Next page",
2207
- children: /* @__PURE__ */ jsx27(ChevronRight, { "aria-hidden": "true" })
2373
+ children: /* @__PURE__ */ jsx29(ChevronRight, { "aria-hidden": "true" })
2208
2374
  }
2209
2375
  )
2210
2376
  ] })
@@ -2215,12 +2381,12 @@ function Pagination({
2215
2381
  styleInject("@layer components {\n :where(.cfi-ui-list-toolbar) {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--cfi-ui-space-md);\n min-width: 0;\n }\n .cfi-ui-list-toolbar__search {\n flex: 1 1 14rem;\n min-width: 0;\n max-width: 22rem;\n }\n .cfi-ui-list-toolbar__filters {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n min-width: 0;\n flex: 0 1 auto;\n }\n .cfi-ui-list-toolbar__actions {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n margin-inline-start: auto;\n }\n @media (max-width: 40rem) {\n .cfi-ui-list-toolbar__search,\n .cfi-ui-list-toolbar__filters,\n .cfi-ui-list-toolbar__actions {\n flex: 1 1 100%;\n max-width: none;\n margin-inline-start: 0;\n }\n }\n}\n");
2216
2382
 
2217
2383
  // src/ListToolbar.tsx
2218
- import { jsx as jsx28, jsxs as jsxs27 } from "react/jsx-runtime";
2384
+ import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2219
2385
  function ListToolbar({ search, filters, actions, className }) {
2220
- return /* @__PURE__ */ jsxs27("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2221
- search && /* @__PURE__ */ jsx28("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2222
- filters && /* @__PURE__ */ jsx28("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2223
- actions && /* @__PURE__ */ jsx28("div", { className: "cfi-ui-list-toolbar__actions", children: actions })
2386
+ return /* @__PURE__ */ jsxs29("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2387
+ search && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2388
+ filters && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2389
+ actions && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__actions", children: actions })
2224
2390
  ] });
2225
2391
  }
2226
2392
 
@@ -2231,7 +2397,7 @@ import { ArrowLeft, ChevronRight as ChevronRight2 } from "lucide-react";
2231
2397
  styleInject("@layer components {\n :where(.cfi-ui-page-header) {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-xs);\n font-family: var(--cfi-ui-font);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-page-header[data-bordered] {\n padding-bottom: var(--cfi-ui-space-md);\n border-bottom: 1px solid var(--cfi-ui-border);\n }\n .cfi-ui-page-header__crumbs {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--cfi-ui-space-3xs);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-page-header__crumb {\n display: inline-flex;\n align-items: center;\n gap: var(--cfi-ui-space-3xs);\n }\n .cfi-ui-page-header__crumb-sep {\n width: 0.75rem;\n height: 0.75rem;\n flex: none;\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-page-header__crumb a {\n color: inherit;\n text-decoration: none;\n border-radius: var(--cfi-ui-radius-sm);\n transition: color var(--cfi-ui-transition-fast);\n }\n .cfi-ui-page-header__crumb a:hover {\n color: var(--cfi-ui-content);\n }\n .cfi-ui-page-header__crumb a:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n .cfi-ui-page-header__crumb[data-current] {\n color: var(--cfi-ui-content);\n }\n .cfi-ui-page-header__back {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n gap: var(--cfi-ui-space-2xs);\n margin-left: -0.5rem;\n padding: 0.25rem 0.5rem;\n border: 0;\n border-radius: var(--cfi-ui-radius-md);\n background: none;\n font: inherit;\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n cursor: pointer;\n transition: color var(--cfi-ui-transition-fast), background var(--cfi-ui-transition-fast);\n }\n .cfi-ui-page-header__back:hover {\n background: var(--cfi-ui-surface-hover);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-page-header__back:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n .cfi-ui-page-header__back > svg {\n width: 1rem;\n height: 1rem;\n flex: none;\n }\n .cfi-ui-page-header__row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--cfi-ui-space-md);\n }\n .cfi-ui-page-header__titles {\n min-width: 0;\n }\n .cfi-ui-page-header__title,\n h1.cfi-ui-page-header__title,\n h2.cfi-ui-page-header__title,\n h3.cfi-ui-page-header__title {\n margin: 0;\n font-size: var(--cfi-ui-text-xl);\n font-weight: var(--cfi-ui-weight-semibold);\n line-height: var(--cfi-ui-leading-tight);\n letter-spacing: -0.01em;\n color: var(--cfi-ui-content);\n }\n .cfi-ui-page-header__description {\n margin: 0.25rem 0 0;\n font-size: var(--cfi-ui-text-sm);\n line-height: var(--cfi-ui-leading-normal);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-page-header__actions {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n flex: none;\n }\n .cfi-ui-page-header__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n margin-top: var(--cfi-ui-space-3xs);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n }\n @media (max-width: 640px) {\n .cfi-ui-page-header__row {\n flex-direction: column;\n align-items: stretch;\n gap: var(--cfi-ui-space-sm);\n }\n .cfi-ui-page-header__actions {\n flex-wrap: wrap;\n }\n }\n}\n");
2232
2398
 
2233
2399
  // src/PageHeader.tsx
2234
- import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
2400
+ import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2235
2401
  function PageHeader({
2236
2402
  title,
2237
2403
  description,
@@ -2245,32 +2411,32 @@ function PageHeader({
2245
2411
  className
2246
2412
  }) {
2247
2413
  const Heading = `h${level}`;
2248
- return /* @__PURE__ */ jsxs28(
2414
+ return /* @__PURE__ */ jsxs30(
2249
2415
  "div",
2250
2416
  {
2251
2417
  className: cn("cfi-ui", "cfi-ui-page-header", className),
2252
2418
  "data-bordered": bordered ? "" : void 0,
2253
2419
  children: [
2254
- breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx29("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
2420
+ breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx31("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
2255
2421
  const last = i === breadcrumbs.length - 1;
2256
- const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx29("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2257
- return /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2258
- i > 0 && /* @__PURE__ */ jsx29(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2259
- /* @__PURE__ */ jsx29("span", { "aria-current": last ? "page" : void 0, children: label })
2422
+ const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx31("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2423
+ return /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2424
+ i > 0 && /* @__PURE__ */ jsx31(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2425
+ /* @__PURE__ */ jsx31("span", { "aria-current": last ? "page" : void 0, children: label })
2260
2426
  ] }, i);
2261
2427
  }) }),
2262
- onBack && /* @__PURE__ */ jsxs28("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2263
- /* @__PURE__ */ jsx29(ArrowLeft, { "aria-hidden": "true" }),
2428
+ onBack && /* @__PURE__ */ jsxs30("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2429
+ /* @__PURE__ */ jsx31(ArrowLeft, { "aria-hidden": "true" }),
2264
2430
  backLabel
2265
2431
  ] }),
2266
- /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-page-header__row", children: [
2267
- /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-page-header__titles", children: [
2268
- /* @__PURE__ */ jsx29(Heading, { className: "cfi-ui-page-header__title", children: title }),
2269
- description && /* @__PURE__ */ jsx29("p", { className: "cfi-ui-page-header__description", children: description })
2432
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-page-header__row", children: [
2433
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-page-header__titles", children: [
2434
+ /* @__PURE__ */ jsx31(Heading, { className: "cfi-ui-page-header__title", children: title }),
2435
+ description && /* @__PURE__ */ jsx31("p", { className: "cfi-ui-page-header__description", children: description })
2270
2436
  ] }),
2271
- actions && /* @__PURE__ */ jsx29("div", { className: "cfi-ui-page-header__actions", children: actions })
2437
+ actions && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__actions", children: actions })
2272
2438
  ] }),
2273
- children && /* @__PURE__ */ jsx29("div", { className: "cfi-ui-page-header__meta", children })
2439
+ children && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__meta", children })
2274
2440
  ]
2275
2441
  }
2276
2442
  );
@@ -2283,7 +2449,7 @@ import { X as X7 } from "lucide-react";
2283
2449
  styleInject("@layer components {\n :where(.cfi-ui-selection-bar) {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n padding: var(--cfi-ui-space-2xs) var(--cfi-ui-space-sm);\n border: 1px solid var(--cfi-ui-accent);\n border-radius: var(--cfi-ui-radius-md);\n background: var(--cfi-ui-accent-soft);\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content);\n min-height: 2.5rem;\n }\n .cfi-ui-selection-bar__count {\n font-weight: var(--cfi-ui-weight-medium);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n .cfi-ui-selection-bar__actions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--cfi-ui-space-2xs);\n }\n .cfi-ui-selection-bar__clear {\n display: inline-flex;\n align-items: center;\n gap: var(--cfi-ui-space-3xs);\n margin-left: auto;\n padding: 0.25rem 0.5rem;\n border: 0;\n border-radius: var(--cfi-ui-radius-sm);\n background: none;\n font: inherit;\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content-muted);\n cursor: pointer;\n transition: color var(--cfi-ui-transition-fast), background var(--cfi-ui-transition-fast);\n }\n .cfi-ui-selection-bar__clear:hover:not(:disabled) {\n background: var(--cfi-ui-surface-hover);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-selection-bar__clear:focus-visible {\n outline: var(--cfi-ui-focus-width) solid var(--cfi-ui-focus-color);\n outline-offset: var(--cfi-ui-focus-offset);\n }\n .cfi-ui-selection-bar__clear:disabled {\n color: var(--cfi-ui-content-disabled);\n cursor: not-allowed;\n }\n .cfi-ui-selection-bar__clear > svg {\n width: 0.875rem;\n height: 0.875rem;\n flex: none;\n }\n .cfi-ui-selection-bar[data-busy] {\n opacity: 0.6;\n pointer-events: none;\n }\n .cfi-ui-selection-bar[data-variant=floating] {\n position: fixed;\n left: 50%;\n bottom: var(--cfi-ui-space-lg);\n transform: translateX(-50%);\n z-index: var(--cfi-ui-z-dropdown);\n width: max-content;\n max-width: calc(100vw - 2 * var(--cfi-ui-space-lg));\n padding: var(--cfi-ui-space-xs) var(--cfi-ui-space-md);\n border-radius: var(--cfi-ui-radius-lg);\n background: var(--cfi-ui-surface-raised);\n box-shadow: var(--cfi-ui-shadow-lg);\n }\n @media (max-width: 640px) {\n .cfi-ui-selection-bar {\n flex-wrap: wrap;\n }\n .cfi-ui-selection-bar[data-variant=floating] {\n left: var(--cfi-ui-space-sm);\n right: var(--cfi-ui-space-sm);\n width: auto;\n max-width: none;\n transform: none;\n }\n }\n}\n");
2284
2450
 
2285
2451
  // src/SelectionBar.tsx
2286
- import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2452
+ import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2287
2453
  function SelectionBar({
2288
2454
  count,
2289
2455
  children,
@@ -2295,7 +2461,7 @@ function SelectionBar({
2295
2461
  className
2296
2462
  }) {
2297
2463
  if (count === 0) return null;
2298
- return /* @__PURE__ */ jsxs29(
2464
+ return /* @__PURE__ */ jsxs31(
2299
2465
  "div",
2300
2466
  {
2301
2467
  role: "status",
@@ -2304,9 +2470,9 @@ function SelectionBar({
2304
2470
  "data-busy": busy ? "" : void 0,
2305
2471
  className: cn("cfi-ui", "cfi-ui-selection-bar", className),
2306
2472
  children: [
2307
- /* @__PURE__ */ jsx30("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2308
- children && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-selection-bar__actions", children }),
2309
- onClear && /* @__PURE__ */ jsxs29(
2473
+ /* @__PURE__ */ jsx32("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2474
+ children && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-selection-bar__actions", children }),
2475
+ onClear && /* @__PURE__ */ jsxs31(
2310
2476
  "button",
2311
2477
  {
2312
2478
  type: "button",
@@ -2314,7 +2480,7 @@ function SelectionBar({
2314
2480
  onClick: onClear,
2315
2481
  disabled: busy,
2316
2482
  children: [
2317
- /* @__PURE__ */ jsx30(X7, { "aria-hidden": "true" }),
2483
+ /* @__PURE__ */ jsx32(X7, { "aria-hidden": "true" }),
2318
2484
  clearLabel
2319
2485
  ]
2320
2486
  }
@@ -2325,14 +2491,14 @@ function SelectionBar({
2325
2491
  }
2326
2492
 
2327
2493
  // src/usePagedRows.ts
2328
- import { useEffect as useEffect4, useMemo as useMemo6, useState as useState7 } from "react";
2494
+ import { useEffect as useEffect5, useMemo as useMemo6, useState as useState8 } from "react";
2329
2495
  function usePagedRows(all, initialPageSize = 25) {
2330
- const [page, setPage] = useState7(1);
2331
- const [pageSize, setSize] = useState7(initialPageSize);
2496
+ const [page, setPage] = useState8(1);
2497
+ const [pageSize, setSize] = useState8(initialPageSize);
2332
2498
  const totalItems = all.length;
2333
2499
  const pageCount = Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize)));
2334
2500
  const clamped = Math.min(Math.max(1, page), pageCount);
2335
- useEffect4(() => {
2501
+ useEffect5(() => {
2336
2502
  if (clamped !== page) setPage(clamped);
2337
2503
  }, [clamped, page]);
2338
2504
  const rows = useMemo6(
@@ -2349,10 +2515,27 @@ function usePagedRows(all, initialPageSize = 25) {
2349
2515
 
2350
2516
  // src/format.ts
2351
2517
  function formatDate(value) {
2518
+ const day = calendarDay(value);
2519
+ if (day !== null) return day;
2520
+ const d = toDate(value);
2521
+ if (!d) return "\u2014";
2522
+ return ymd(d.getFullYear(), d.getMonth() + 1, d.getDate());
2523
+ }
2524
+ function formatCalendarDate(value) {
2525
+ const day = calendarDay(value);
2526
+ if (day !== null) return day;
2352
2527
  const d = toDate(value);
2353
2528
  if (!d) return "\u2014";
2529
+ return ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
2530
+ }
2531
+ function ymd(year, month, day) {
2354
2532
  const pad = (n) => String(n).padStart(2, "0");
2355
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
2533
+ return `${year}-${pad(month)}-${pad(day)}`;
2534
+ }
2535
+ function calendarDay(value) {
2536
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2537
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
2538
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value ? value : "\u2014";
2356
2539
  }
2357
2540
  function formatDateTime(value) {
2358
2541
  const d = toDate(value);
@@ -2360,8 +2543,9 @@ function formatDateTime(value) {
2360
2543
  const pad = (n) => String(n).padStart(2, "0");
2361
2544
  return `${formatDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
2362
2545
  }
2363
- function formatMoney(value, { currency = "USD", decimals, perUnit = false } = {}) {
2546
+ function formatMoney(value, { currency = "USD", decimals, perUnit = false, compact = false } = {}) {
2364
2547
  if (value == null || Number.isNaN(value)) return "\u2014";
2548
+ if (compact) return compactMoney(value, currency, decimals ?? 1);
2365
2549
  const places = decimals ?? (perUnit ? 4 : 2);
2366
2550
  return new Intl.NumberFormat("en-US", {
2367
2551
  style: "currency",
@@ -2370,6 +2554,19 @@ function formatMoney(value, { currency = "USD", decimals, perUnit = false } = {}
2370
2554
  maximumFractionDigits: places
2371
2555
  }).format(value);
2372
2556
  }
2557
+ function compactMoney(value, currency, decimals) {
2558
+ let symbol = currency;
2559
+ try {
2560
+ symbol = new Intl.NumberFormat("en-US", { style: "currency", currency }).formatToParts(0).find((p) => p.type === "currency")?.value ?? currency;
2561
+ } catch {
2562
+ }
2563
+ const abs = Math.abs(value);
2564
+ const sign = value < 0 ? "-" : "";
2565
+ for (const [size, suffix] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) {
2566
+ if (abs >= size) return `${sign}${symbol}${(abs / size).toFixed(decimals)}${suffix}`;
2567
+ }
2568
+ return `${sign}${symbol}${Math.round(abs)}`;
2569
+ }
2373
2570
  function formatNumber(value, decimals = 0) {
2374
2571
  if (value == null || Number.isNaN(value)) return "\u2014";
2375
2572
  return new Intl.NumberFormat("en-US", {
@@ -2396,6 +2593,7 @@ export {
2396
2593
  Avatar,
2397
2594
  AvatarGroup,
2398
2595
  Badge,
2596
+ BootScreen,
2399
2597
  Button,
2400
2598
  ButtonGroup,
2401
2599
  Card,
@@ -2425,8 +2623,10 @@ export {
2425
2623
  DialogTrigger,
2426
2624
  EmptyState,
2427
2625
  ErrorState,
2626
+ ExternalLink,
2428
2627
  Field,
2429
2628
  Fieldset,
2629
+ FileButton,
2430
2630
  FilterBar,
2431
2631
  IconButton,
2432
2632
  Input,
@@ -2484,6 +2684,8 @@ export {
2484
2684
  Skeleton,
2485
2685
  Slider,
2486
2686
  SplitButton,
2687
+ StatGrid,
2688
+ StatTile,
2487
2689
  Switch,
2488
2690
  TabSet,
2489
2691
  Table,
@@ -2511,13 +2713,16 @@ export {
2511
2713
  TooltipRoot,
2512
2714
  TooltipTrigger,
2513
2715
  cn,
2716
+ formatCalendarDate,
2514
2717
  formatDate,
2515
2718
  formatDateTime,
2516
2719
  formatMoney,
2517
2720
  formatNumber,
2518
2721
  formatPercent,
2722
+ useDebouncedValue,
2519
2723
  useFieldControl,
2520
2724
  usePagedRows,
2725
+ usePersistentState,
2521
2726
  useSortState,
2522
2727
  useToast
2523
2728
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/ui",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Shared UI primitives for the Campfire Suite — Radix behavior, plain CSS styled entirely from the design tokens, no Tailwind required",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",