@campfire-interactive/ui 0.9.3 → 0.10.1

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 +170 -17
  2. package/dist/index.js +287 -86
  3. package/package.json +56 -56
package/dist/index.d.ts CHANGED
@@ -433,7 +433,9 @@ declare function DialogBody({ className, ...rest }: React.HTMLAttributes<HTMLDiv
433
433
  declare function DialogFooter({ className, ...rest }: React.HTMLAttributes<HTMLDivElement>): react.JSX.Element;
434
434
  interface ModalProps {
435
435
  open: boolean;
436
- onOpenChange: (open: boolean) => void;
436
+ onOpenChange?: (open: boolean) => void;
437
+ /** Called when the dialog asks to close (Esc, ×, outside click). */
438
+ onClose?: () => void;
437
439
  title: React.ReactNode;
438
440
  /** Sub-heading under the title. Also becomes the dialog's accessible description. */
439
441
  description?: React.ReactNode;
@@ -463,7 +465,7 @@ interface ModalProps {
463
465
  * its Save button on screen. That single detail is missing from most of the
464
466
  * hand-built copies.
465
467
  */
466
- 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;
467
469
  /**
468
470
  * A `Modal` that slides in from the side.
469
471
  *
@@ -489,7 +491,9 @@ declare const SheetBody: typeof DialogBody;
489
491
  declare const SheetFooter: typeof DialogFooter;
490
492
  interface ConfirmDialogProps {
491
493
  open: boolean;
492
- onOpenChange: (open: boolean) => void;
494
+ onOpenChange?: (open: boolean) => void;
495
+ /** Called on Cancel, Esc, or any other request to close. */
496
+ onClose?: () => void;
493
497
  title: React.ReactNode;
494
498
  /** What is about to happen, and what cannot be undone. */
495
499
  description?: React.ReactNode;
@@ -514,7 +518,7 @@ interface ConfirmDialogProps {
514
518
  * confirm should not count as an answer either way. Esc still cancels, and
515
519
  * focus starts on Cancel, so a held Enter key cannot confirm a deletion.
516
520
  */
517
- 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;
518
522
 
519
523
  declare const PopoverRoot: react.FC<RadixPopover.PopoverProps>;
520
524
  declare const PopoverTrigger: react.ForwardRefExoticComponent<RadixPopover.PopoverTriggerProps & react.RefAttributes<HTMLButtonElement>>;
@@ -1355,28 +1359,40 @@ interface ToggleOption<T extends string = string> {
1355
1359
  /** Accessible name for an icon-only option. Required when there is no label. */
1356
1360
  ariaLabel?: string;
1357
1361
  }
1358
- interface ToggleGroupProps<T extends string = string> {
1362
+ interface ToggleGroupBaseProps<T extends string> {
1359
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';
1360
1370
  value?: T;
1361
1371
  onValueChange?: (value: T) => void;
1362
- size?: 'sm' | 'md';
1363
1372
  /**
1364
1373
  * Allows deselecting the active option, leaving nothing selected. Off by
1365
1374
  * default — a view switcher with no view selected shows nothing.
1366
1375
  */
1367
1376
  allowEmpty?: boolean;
1368
- className?: string;
1369
- 'aria-label': string;
1370
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>;
1371
1385
  /**
1372
- * A segmented single-choice control — a view switcher, a unit toggle, a
1373
- * density picker.
1374
- *
1375
- * Clicking the active option does nothing unless `allowEmpty` is set. Radix
1376
- * clears the value by default, which is how a chart switcher ends up rendering
1377
- * 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`.
1378
1394
  */
1379
- 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;
1380
1396
 
1381
1397
  /**
1382
1398
  * The states every data-backed screen has to render.
@@ -1452,6 +1468,130 @@ interface ComingSoonProps {
1452
1468
  * invented figures is worse than an honest gap.
1453
1469
  */
1454
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
+ /**
1527
+ * Narrowest a tile gets before the row wraps. Default `8rem`: five tiles fit
1528
+ * one row in a ~750px content column (a 1024px window beside the side nav).
1529
+ * 11rem wrapped them 4 + 1 on pm's Portfolio, which reads as a mistake.
1530
+ * Tracks are auto-fit, so a short row stretches across rather than leaving
1531
+ * empty columns beside it.
1532
+ */
1533
+ minTileWidth?: string;
1534
+ }
1535
+ /** A row of `StatTile`s that wraps by width, not by breakpoint. */
1536
+ declare function StatGrid({ minTileWidth, className, style, ...rest }: StatGridProps): react.JSX.Element;
1537
+
1538
+ interface ExternalLinkProps {
1539
+ /**
1540
+ * Null when the id behind the label is missing. Renders the children and
1541
+ * nothing else: the honest rendering of "we have the number but not the
1542
+ * link" is the number, not underlined text that does nothing.
1543
+ */
1544
+ href: string | null | undefined;
1545
+ /** What the reader is about to open: "Open quote Q-2026-164 in Opportunity Management". */
1546
+ title?: string;
1547
+ className?: string;
1548
+ children: ReactNode;
1549
+ }
1550
+ /**
1551
+ * A link to a record in ANOTHER app, drawn the same way every time.
1552
+ *
1553
+ * pm and analytics each had one, and master-data built its URLs a third way.
1554
+ * A cross-app link says three things an in-app link does not: that it leaves
1555
+ * the app (the arrow), where it goes (the title), and when the id is missing,
1556
+ * that it is not a link at all (null `href`).
1557
+ *
1558
+ * New tab, deliberately: the reader is mid-task and the other app is a
1559
+ * reference. Build the href with shell-header's `getAppUrl`, never from an
1560
+ * env var, so it follows the environment the reader is in.
1561
+ */
1562
+ declare function ExternalLink({ href, title, className, children }: ExternalLinkProps): react.JSX.Element;
1563
+ interface FileButtonProps extends Omit<ButtonProps, 'onClick' | 'asChild'> {
1564
+ /** The input's `accept`: `.csv,.xlsx`, `image/*`. */
1565
+ accept?: string;
1566
+ /** Called with the chosen file. The input is reset after, so choosing the same file again fires again. */
1567
+ onFile: (file: File) => void;
1568
+ }
1569
+ /**
1570
+ * A button that opens the file picker.
1571
+ *
1572
+ * Five copies across pm and master-data, each a hidden `<input type="file">`
1573
+ * plus a ref plus a button that clicks it, and most forgot to reset the
1574
+ * input — so re-uploading the same file after a failed import did nothing.
1575
+ */
1576
+ declare function FileButton({ accept, onFile, children, ...button }: FileButtonProps): react.JSX.Element;
1577
+
1578
+ /**
1579
+ * `value`, once it has stopped changing for `delayMs`.
1580
+ *
1581
+ * For a value that drives a request (a search string into a query key).
1582
+ * Do not wrap a `SearchInput`'s value in this: `SearchInput` already
1583
+ * debounces its `onChange`, and master-data's seven list hooks waited twice.
1584
+ */
1585
+ declare function useDebouncedValue<T>(value: T, delayMs?: number): T;
1586
+ /**
1587
+ * `useState` that survives a reload, per viewer and per browser.
1588
+ *
1589
+ * For conveniences only — a collapsed panel, a chosen tab, column widths.
1590
+ * Storage can be absent or throw (a private window, blocked site data), so
1591
+ * every read and write is guarded and the state still works in memory; and a
1592
+ * stored value that no longer parses falls back to `initial`.
1593
+ */
1594
+ declare function usePersistentState<T>(key: string, initial: T): [T, (next: T | ((prev: T) => T)) => void];
1455
1595
 
1456
1596
  interface SortState<K extends string> {
1457
1597
  key: K;
@@ -1785,6 +1925,13 @@ declare function usePagedRows<T>(all: readonly T[], initialPageSize?: number): P
1785
1925
  * one that matches a browser setting.
1786
1926
  */
1787
1927
  declare function formatDate(value: Date | string | number | null | undefined): string;
1928
+ /**
1929
+ * A calendar date — an SOP, a due date, a birthday — that the API sends as a
1930
+ * UTC-midnight timestamp (`2028-01-01T00:00:00.000Z`, which is how a Prisma
1931
+ * `@db.Date` serializes). Reads the UTC day, so it is the same day in Detroit
1932
+ * and in Silao. Use `formatDate` for an instant (created, updated).
1933
+ */
1934
+ declare function formatCalendarDate(value: Date | string | number | null | undefined): string;
1788
1935
  /** `2026-09-11 14:30`. Same reasoning as `formatDate`, plus a 24-hour clock. */
1789
1936
  declare function formatDateTime(value: Date | string | number | null | undefined): string;
1790
1937
  interface MoneyOptions {
@@ -1798,8 +1945,14 @@ interface MoneyOptions {
1798
1945
  decimals?: number;
1799
1946
  /** Set for a per-piece figure to get 4 decimals without naming the number. */
1800
1947
  perUnit?: boolean;
1948
+ /**
1949
+ * Scale to the magnitude for a tile or an axis: `$1.3M`, `$52.0K`, `$640`.
1950
+ * `decimals` (default 1) applies to the scaled figure; under 1,000 rounds
1951
+ * to whole units. Never for a per-unit price, which needs its 4 places.
1952
+ */
1953
+ compact?: boolean;
1801
1954
  }
1802
- declare function formatMoney(value: number | null | undefined, { currency, decimals, perUnit }?: MoneyOptions): string;
1955
+ declare function formatMoney(value: number | null | undefined, { currency, decimals, perUnit, compact }?: MoneyOptions): string;
1803
1956
  /** Thousands-separated, no currency. `null` renders as an em dash, not `0`. */
1804
1957
  declare function formatNumber(value: number | null | undefined, decimals?: number): string;
1805
1958
  /**
@@ -1809,4 +1962,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
1809
1962
  */
1810
1963
  declare function formatPercent(value: number | null | undefined, decimals?: number): string;
1811
1964
 
1812
- 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 };
1965
+ 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
@@ -846,9 +846,16 @@ function DialogBody({ className, ...rest }) {
846
846
  function DialogFooter({ className, ...rest }) {
847
847
  return /* @__PURE__ */ jsx13("div", { className: cn("cfi-ui-dialog__footer", className), ...rest });
848
848
  }
849
+ function closeHandler(onOpenChange, onClose) {
850
+ return (open) => {
851
+ onOpenChange?.(open);
852
+ if (!open) onClose?.();
853
+ };
854
+ }
849
855
  function Modal({
850
856
  open,
851
857
  onOpenChange,
858
+ onClose,
852
859
  title,
853
860
  description,
854
861
  size = "md",
@@ -860,7 +867,7 @@ function Modal({
860
867
  children
861
868
  }) {
862
869
  const block = dismissible ? void 0 : (e) => e.preventDefault();
863
- return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs13(
870
+ return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange: closeHandler(onOpenChange, onClose), children: /* @__PURE__ */ jsxs13(
864
871
  DialogContent,
865
872
  {
866
873
  size,
@@ -903,6 +910,7 @@ var SheetFooter = DialogFooter;
903
910
  function ConfirmDialog({
904
911
  open,
905
912
  onOpenChange,
913
+ onClose,
906
914
  title,
907
915
  description,
908
916
  confirmLabel = "Confirm",
@@ -912,7 +920,7 @@ function ConfirmDialog({
912
920
  onConfirm,
913
921
  children
914
922
  }) {
915
- return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs13(
923
+ return /* @__PURE__ */ jsx13(Dialog, { open, onOpenChange: closeHandler(onOpenChange, onClose), children: /* @__PURE__ */ jsxs13(
916
924
  DialogContent,
917
925
  {
918
926
  size: "sm",
@@ -1966,15 +1974,42 @@ function Accordion({
1966
1974
  /* @__PURE__ */ jsx24(AccordionContent, { children: section.content })
1967
1975
  ] }, section.value)) });
1968
1976
  }
1969
- function ToggleGroup({
1970
- options,
1971
- value,
1972
- onValueChange,
1973
- size = "md",
1974
- allowEmpty = false,
1975
- className,
1976
- ...aria
1977
- }) {
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;
1978
2013
  return /* @__PURE__ */ jsx24(
1979
2014
  RadixToggleGroup.Root,
1980
2015
  {
@@ -1984,24 +2019,8 @@ function ToggleGroup({
1984
2019
  if (!next && !allowEmpty) return;
1985
2020
  onValueChange?.(next);
1986
2021
  },
1987
- "data-size": size,
1988
- className: cn("cfi-ui", "cfi-ui-toggle-group", className),
1989
- ...aria,
1990
- children: options.map((option) => /* @__PURE__ */ jsxs24(
1991
- RadixToggleGroup.Item,
1992
- {
1993
- value: option.value,
1994
- disabled: option.disabled,
1995
- "aria-label": option.ariaLabel,
1996
- title: option.ariaLabel,
1997
- className: "cfi-ui-toggle-group__item",
1998
- children: [
1999
- option.icon && /* @__PURE__ */ jsx24("span", { className: "cfi-ui-toggle-group__icon", children: option.icon }),
2000
- option.label
2001
- ]
2002
- },
2003
- option.value
2004
- ))
2022
+ ...shared,
2023
+ children: items
2005
2024
  }
2006
2025
  );
2007
2026
  }
@@ -2010,7 +2029,7 @@ function ToggleGroup({
2010
2029
  import { AlertCircle as AlertCircle2, Loader2 } from "lucide-react";
2011
2030
 
2012
2031
  // src/PageState.css
2013
- 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");
2014
2033
 
2015
2034
  // src/PageState.tsx
2016
2035
  import { jsx as jsx25, jsxs as jsxs25 } from "react/jsx-runtime";
@@ -2074,17 +2093,160 @@ function ComingSoon({ title, detail, className }) {
2074
2093
  }
2075
2094
  );
2076
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-xl);\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-fit, minmax(var(--cfi-ui-stat-min, 8rem), 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 = "8rem", 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
+ }
2077
2239
 
2078
2240
  // src/useSortState.ts
2079
- 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";
2080
2242
  function useSortState(initialKey, initialDirection = "asc") {
2081
- const [sort, setSort] = useState5({ key: initialKey, direction: initialDirection });
2082
- const toggle = useCallback2((key) => {
2243
+ const [sort, setSort] = useState6({ key: initialKey, direction: initialDirection });
2244
+ const toggle = useCallback3((key) => {
2083
2245
  setSort(
2084
2246
  (current) => current.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" }
2085
2247
  );
2086
2248
  }, []);
2087
- const sortProps = useCallback2(
2249
+ const sortProps = useCallback3(
2088
2250
  (key) => ({
2089
2251
  sortable: true,
2090
2252
  sortDirection: sort.key === key ? sort.direction : null,
@@ -2092,7 +2254,7 @@ function useSortState(initialKey, initialDirection = "asc") {
2092
2254
  }),
2093
2255
  [sort, toggle]
2094
2256
  );
2095
- const sortBy = useCallback2(
2257
+ const sortBy = useCallback3(
2096
2258
  (rows, accessor) => {
2097
2259
  const dir = sort.direction === "asc" ? 1 : -1;
2098
2260
  return [...rows].sort((a, b) => {
@@ -2115,21 +2277,21 @@ function useSortState(initialKey, initialDirection = "asc") {
2115
2277
  }
2116
2278
 
2117
2279
  // src/SearchInput.tsx
2118
- 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";
2119
2281
  import { Search as Search2, X as X6 } from "lucide-react";
2120
2282
 
2121
2283
  // src/SearchInput.css
2122
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");
2123
2285
 
2124
2286
  // src/SearchInput.tsx
2125
- import { jsx as jsx26 } from "react/jsx-runtime";
2287
+ import { jsx as jsx28 } from "react/jsx-runtime";
2126
2288
  var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay = 250, placeholder = "Search\u2026", clearLabel = "Clear search", ...rest }, ref) {
2127
- const [local, setLocal] = useState6(value);
2128
- const timer = useRef3(void 0);
2129
- const latest = useRef3(onChange);
2289
+ const [local, setLocal] = useState7(value);
2290
+ const timer = useRef4(void 0);
2291
+ const latest = useRef4(onChange);
2130
2292
  latest.current = onChange;
2131
- useEffect3(() => setLocal(value), [value]);
2132
- useEffect3(() => () => clearTimeout(timer.current), []);
2293
+ useEffect4(() => setLocal(value), [value]);
2294
+ useEffect4(() => () => clearTimeout(timer.current), []);
2133
2295
  const push = (next) => {
2134
2296
  setLocal(next);
2135
2297
  clearTimeout(timer.current);
@@ -2141,7 +2303,7 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2141
2303
  setLocal("");
2142
2304
  latest.current("");
2143
2305
  };
2144
- return /* @__PURE__ */ jsx26(
2306
+ return /* @__PURE__ */ jsx28(
2145
2307
  Input,
2146
2308
  {
2147
2309
  ...rest,
@@ -2150,8 +2312,8 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2150
2312
  value: local,
2151
2313
  placeholder,
2152
2314
  onChange: (e) => push(e.target.value),
2153
- prefix: /* @__PURE__ */ jsx26(Search2, { "aria-hidden": "true" }),
2154
- 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
2155
2317
  }
2156
2318
  );
2157
2319
  });
@@ -2163,7 +2325,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
2163
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");
2164
2326
 
2165
2327
  // src/Pagination.tsx
2166
- import { jsx as jsx27, jsxs as jsxs26 } from "react/jsx-runtime";
2328
+ import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
2167
2329
  function Pagination({
2168
2330
  page,
2169
2331
  pageSize,
@@ -2179,12 +2341,12 @@ function Pagination({
2179
2341
  const from = (current - 1) * pageSize + 1;
2180
2342
  const to = Math.min(current * pageSize, totalItems);
2181
2343
  const range = labels?.range?.(from, to, totalItems) ?? `${from}\u2013${to} of ${totalItems.toLocaleString()}`;
2182
- 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: [
2183
2345
  showRange && // aria-live so a screen-reader user hears where they landed; paging
2184
2346
  // otherwise moves focus nowhere and announces nothing.
2185
- /* @__PURE__ */ jsx27("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2186
- /* @__PURE__ */ jsxs26("div", { className: "cfi-ui-pagination__controls", children: [
2187
- /* @__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(
2188
2350
  "button",
2189
2351
  {
2190
2352
  type: "button",
@@ -2192,15 +2354,15 @@ function Pagination({
2192
2354
  onClick: () => onPageChange(current - 1),
2193
2355
  disabled: current <= 1,
2194
2356
  "aria-label": labels?.previous ?? "Previous page",
2195
- children: /* @__PURE__ */ jsx27(ChevronLeft, { "aria-hidden": "true" })
2357
+ children: /* @__PURE__ */ jsx29(ChevronLeft, { "aria-hidden": "true" })
2196
2358
  }
2197
2359
  ),
2198
- /* @__PURE__ */ jsxs26("span", { className: "cfi-ui-pagination__page", children: [
2360
+ /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-pagination__page", children: [
2199
2361
  current,
2200
2362
  " / ",
2201
2363
  pageCount
2202
2364
  ] }),
2203
- /* @__PURE__ */ jsx27(
2365
+ /* @__PURE__ */ jsx29(
2204
2366
  "button",
2205
2367
  {
2206
2368
  type: "button",
@@ -2208,7 +2370,7 @@ function Pagination({
2208
2370
  onClick: () => onPageChange(current + 1),
2209
2371
  disabled: current >= pageCount,
2210
2372
  "aria-label": labels?.next ?? "Next page",
2211
- children: /* @__PURE__ */ jsx27(ChevronRight, { "aria-hidden": "true" })
2373
+ children: /* @__PURE__ */ jsx29(ChevronRight, { "aria-hidden": "true" })
2212
2374
  }
2213
2375
  )
2214
2376
  ] })
@@ -2219,12 +2381,12 @@ function Pagination({
2219
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");
2220
2382
 
2221
2383
  // src/ListToolbar.tsx
2222
- import { jsx as jsx28, jsxs as jsxs27 } from "react/jsx-runtime";
2384
+ import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2223
2385
  function ListToolbar({ search, filters, actions, className }) {
2224
- return /* @__PURE__ */ jsxs27("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2225
- search && /* @__PURE__ */ jsx28("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2226
- filters && /* @__PURE__ */ jsx28("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2227
- 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 })
2228
2390
  ] });
2229
2391
  }
2230
2392
 
@@ -2235,7 +2397,7 @@ import { ArrowLeft, ChevronRight as ChevronRight2 } from "lucide-react";
2235
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");
2236
2398
 
2237
2399
  // src/PageHeader.tsx
2238
- import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
2400
+ import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2239
2401
  function PageHeader({
2240
2402
  title,
2241
2403
  description,
@@ -2249,32 +2411,32 @@ function PageHeader({
2249
2411
  className
2250
2412
  }) {
2251
2413
  const Heading = `h${level}`;
2252
- return /* @__PURE__ */ jsxs28(
2414
+ return /* @__PURE__ */ jsxs30(
2253
2415
  "div",
2254
2416
  {
2255
2417
  className: cn("cfi-ui", "cfi-ui-page-header", className),
2256
2418
  "data-bordered": bordered ? "" : void 0,
2257
2419
  children: [
2258
- 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) => {
2259
2421
  const last = i === breadcrumbs.length - 1;
2260
- const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx29("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2261
- return /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2262
- i > 0 && /* @__PURE__ */ jsx29(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2263
- /* @__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 })
2264
2426
  ] }, i);
2265
2427
  }) }),
2266
- onBack && /* @__PURE__ */ jsxs28("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2267
- /* @__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" }),
2268
2430
  backLabel
2269
2431
  ] }),
2270
- /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-page-header__row", children: [
2271
- /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-page-header__titles", children: [
2272
- /* @__PURE__ */ jsx29(Heading, { className: "cfi-ui-page-header__title", children: title }),
2273
- 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 })
2274
2436
  ] }),
2275
- 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 })
2276
2438
  ] }),
2277
- children && /* @__PURE__ */ jsx29("div", { className: "cfi-ui-page-header__meta", children })
2439
+ children && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__meta", children })
2278
2440
  ]
2279
2441
  }
2280
2442
  );
@@ -2287,7 +2449,7 @@ import { X as X7 } from "lucide-react";
2287
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");
2288
2450
 
2289
2451
  // src/SelectionBar.tsx
2290
- import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2452
+ import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2291
2453
  function SelectionBar({
2292
2454
  count,
2293
2455
  children,
@@ -2299,7 +2461,7 @@ function SelectionBar({
2299
2461
  className
2300
2462
  }) {
2301
2463
  if (count === 0) return null;
2302
- return /* @__PURE__ */ jsxs29(
2464
+ return /* @__PURE__ */ jsxs31(
2303
2465
  "div",
2304
2466
  {
2305
2467
  role: "status",
@@ -2308,9 +2470,9 @@ function SelectionBar({
2308
2470
  "data-busy": busy ? "" : void 0,
2309
2471
  className: cn("cfi-ui", "cfi-ui-selection-bar", className),
2310
2472
  children: [
2311
- /* @__PURE__ */ jsx30("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2312
- children && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-selection-bar__actions", children }),
2313
- 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(
2314
2476
  "button",
2315
2477
  {
2316
2478
  type: "button",
@@ -2318,7 +2480,7 @@ function SelectionBar({
2318
2480
  onClick: onClear,
2319
2481
  disabled: busy,
2320
2482
  children: [
2321
- /* @__PURE__ */ jsx30(X7, { "aria-hidden": "true" }),
2483
+ /* @__PURE__ */ jsx32(X7, { "aria-hidden": "true" }),
2322
2484
  clearLabel
2323
2485
  ]
2324
2486
  }
@@ -2329,14 +2491,14 @@ function SelectionBar({
2329
2491
  }
2330
2492
 
2331
2493
  // src/usePagedRows.ts
2332
- 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";
2333
2495
  function usePagedRows(all, initialPageSize = 25) {
2334
- const [page, setPage] = useState7(1);
2335
- const [pageSize, setSize] = useState7(initialPageSize);
2496
+ const [page, setPage] = useState8(1);
2497
+ const [pageSize, setSize] = useState8(initialPageSize);
2336
2498
  const totalItems = all.length;
2337
2499
  const pageCount = Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize)));
2338
2500
  const clamped = Math.min(Math.max(1, page), pageCount);
2339
- useEffect4(() => {
2501
+ useEffect5(() => {
2340
2502
  if (clamped !== page) setPage(clamped);
2341
2503
  }, [clamped, page]);
2342
2504
  const rows = useMemo6(
@@ -2353,10 +2515,27 @@ function usePagedRows(all, initialPageSize = 25) {
2353
2515
 
2354
2516
  // src/format.ts
2355
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;
2356
2527
  const d = toDate(value);
2357
2528
  if (!d) return "\u2014";
2529
+ return ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
2530
+ }
2531
+ function ymd(year, month, day) {
2358
2532
  const pad = (n) => String(n).padStart(2, "0");
2359
- 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";
2360
2539
  }
2361
2540
  function formatDateTime(value) {
2362
2541
  const d = toDate(value);
@@ -2364,8 +2543,9 @@ function formatDateTime(value) {
2364
2543
  const pad = (n) => String(n).padStart(2, "0");
2365
2544
  return `${formatDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
2366
2545
  }
2367
- function formatMoney(value, { currency = "USD", decimals, perUnit = false } = {}) {
2546
+ function formatMoney(value, { currency = "USD", decimals, perUnit = false, compact = false } = {}) {
2368
2547
  if (value == null || Number.isNaN(value)) return "\u2014";
2548
+ if (compact) return compactMoney(value, currency, decimals ?? 1);
2369
2549
  const places = decimals ?? (perUnit ? 4 : 2);
2370
2550
  return new Intl.NumberFormat("en-US", {
2371
2551
  style: "currency",
@@ -2374,6 +2554,19 @@ function formatMoney(value, { currency = "USD", decimals, perUnit = false } = {}
2374
2554
  maximumFractionDigits: places
2375
2555
  }).format(value);
2376
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
+ }
2377
2570
  function formatNumber(value, decimals = 0) {
2378
2571
  if (value == null || Number.isNaN(value)) return "\u2014";
2379
2572
  return new Intl.NumberFormat("en-US", {
@@ -2400,6 +2593,7 @@ export {
2400
2593
  Avatar,
2401
2594
  AvatarGroup,
2402
2595
  Badge,
2596
+ BootScreen,
2403
2597
  Button,
2404
2598
  ButtonGroup,
2405
2599
  Card,
@@ -2429,8 +2623,10 @@ export {
2429
2623
  DialogTrigger,
2430
2624
  EmptyState,
2431
2625
  ErrorState,
2626
+ ExternalLink,
2432
2627
  Field,
2433
2628
  Fieldset,
2629
+ FileButton,
2434
2630
  FilterBar,
2435
2631
  IconButton,
2436
2632
  Input,
@@ -2488,6 +2684,8 @@ export {
2488
2684
  Skeleton,
2489
2685
  Slider,
2490
2686
  SplitButton,
2687
+ StatGrid,
2688
+ StatTile,
2491
2689
  Switch,
2492
2690
  TabSet,
2493
2691
  Table,
@@ -2515,13 +2713,16 @@ export {
2515
2713
  TooltipRoot,
2516
2714
  TooltipTrigger,
2517
2715
  cn,
2716
+ formatCalendarDate,
2518
2717
  formatDate,
2519
2718
  formatDateTime,
2520
2719
  formatMoney,
2521
2720
  formatNumber,
2522
2721
  formatPercent,
2722
+ useDebouncedValue,
2523
2723
  useFieldControl,
2524
2724
  usePagedRows,
2725
+ usePersistentState,
2525
2726
  useSortState,
2526
2727
  useToast
2527
2728
  };
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "@campfire-interactive/ui",
3
- "version": "0.9.3",
4
- "description": "Shared UI primitives for the Campfire Suite — Radix behavior, plain CSS styled entirely from the design tokens, no Tailwind required",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist"
16
- ],
17
- "scripts": {
18
- "build": "tsup",
19
- "prepublishOnly": "npm run build",
20
- "dev": "tsup --watch",
21
- "test": "vitest run",
22
- "typecheck": "tsc --noEmit"
23
- },
24
- "peerDependencies": {
25
- "react": "^18.0.0 || ^19.0.0",
26
- "react-dom": "^18.0.0 || ^19.0.0"
27
- },
28
- "dependencies": {
29
- "@campfire-interactive/design-tokens": "*",
30
- "@radix-ui/react-accordion": "^1.2.12",
31
- "@radix-ui/react-checkbox": "^1.3.3",
32
- "@radix-ui/react-dialog": "^1.1.15",
33
- "@radix-ui/react-dropdown-menu": "^2.1.16",
34
- "@radix-ui/react-popover": "^1.1.15",
35
- "@radix-ui/react-select": "^2.2.6",
36
- "@radix-ui/react-slider": "^1.3.6",
37
- "@radix-ui/react-slot": "^1.2.3",
38
- "@radix-ui/react-switch": "^1.2.6",
39
- "@radix-ui/react-tabs": "^1.1.13",
40
- "@radix-ui/react-toggle-group": "^1.1.11",
41
- "@radix-ui/react-tooltip": "^1.2.8",
42
- "lucide-react": "^0.487.0"
43
- },
44
- "devDependencies": {
45
- "@types/react": "^19.0.0",
46
- "@types/react-dom": "^19.0.0",
47
- "react": "^19.0.0",
48
- "react-dom": "^19.0.0",
49
- "tsup": "^8.0.0",
50
- "typescript": "^5.3.3"
51
- },
52
- "publishConfig": {
53
- "registry": "https://registry.npmjs.org",
54
- "access": "public"
55
- }
56
- }
1
+ {
2
+ "name": "@campfire-interactive/ui",
3
+ "version": "0.10.1",
4
+ "description": "Shared UI primitives for the Campfire Suite — Radix behavior, plain CSS styled entirely from the design tokens, no Tailwind required",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "prepublishOnly": "npm run build",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "peerDependencies": {
25
+ "react": "^18.0.0 || ^19.0.0",
26
+ "react-dom": "^18.0.0 || ^19.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@campfire-interactive/design-tokens": "*",
30
+ "@radix-ui/react-accordion": "^1.2.12",
31
+ "@radix-ui/react-checkbox": "^1.3.3",
32
+ "@radix-ui/react-dialog": "^1.1.15",
33
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
34
+ "@radix-ui/react-popover": "^1.1.15",
35
+ "@radix-ui/react-select": "^2.2.6",
36
+ "@radix-ui/react-slider": "^1.3.6",
37
+ "@radix-ui/react-slot": "^1.2.3",
38
+ "@radix-ui/react-switch": "^1.2.6",
39
+ "@radix-ui/react-tabs": "^1.1.13",
40
+ "@radix-ui/react-toggle-group": "^1.1.11",
41
+ "@radix-ui/react-tooltip": "^1.2.8",
42
+ "lucide-react": "^0.487.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/react": "^19.0.0",
46
+ "@types/react-dom": "^19.0.0",
47
+ "react": "^19.0.0",
48
+ "react-dom": "^19.0.0",
49
+ "tsup": "^8.0.0",
50
+ "typescript": "^5.3.3"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org",
54
+ "access": "public"
55
+ }
56
+ }