@campfire-interactive/ui 0.9.3 → 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.
- package/dist/index.d.ts +164 -17
- package/dist/index.js +287 -86
- 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
|
|
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
|
|
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
|
|
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
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
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>(
|
|
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,124 @@ 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
|
+
/** 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];
|
|
1455
1589
|
|
|
1456
1590
|
interface SortState<K extends string> {
|
|
1457
1591
|
key: K;
|
|
@@ -1785,6 +1919,13 @@ declare function usePagedRows<T>(all: readonly T[], initialPageSize?: number): P
|
|
|
1785
1919
|
* one that matches a browser setting.
|
|
1786
1920
|
*/
|
|
1787
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;
|
|
1788
1929
|
/** `2026-09-11 14:30`. Same reasoning as `formatDate`, plus a 24-hour clock. */
|
|
1789
1930
|
declare function formatDateTime(value: Date | string | number | null | undefined): string;
|
|
1790
1931
|
interface MoneyOptions {
|
|
@@ -1798,8 +1939,14 @@ interface MoneyOptions {
|
|
|
1798
1939
|
decimals?: number;
|
|
1799
1940
|
/** Set for a per-piece figure to get 4 decimals without naming the number. */
|
|
1800
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;
|
|
1801
1948
|
}
|
|
1802
|
-
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;
|
|
1803
1950
|
/** Thousands-separated, no currency. `null` renders as an em dash, not `0`. */
|
|
1804
1951
|
declare function formatNumber(value: number | null | undefined, decimals?: number): string;
|
|
1805
1952
|
/**
|
|
@@ -1809,4 +1956,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
|
|
|
1809
1956
|
*/
|
|
1810
1957
|
declare function formatPercent(value: number | null | undefined, decimals?: number): string;
|
|
1811
1958
|
|
|
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 };
|
|
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
|
@@ -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
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
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-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
|
+
}
|
|
2077
2239
|
|
|
2078
2240
|
// src/useSortState.ts
|
|
2079
|
-
import { useCallback as
|
|
2241
|
+
import { useCallback as useCallback3, useMemo as useMemo5, useState as useState6 } from "react";
|
|
2080
2242
|
function useSortState(initialKey, initialDirection = "asc") {
|
|
2081
|
-
const [sort, setSort] =
|
|
2082
|
-
const toggle =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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] =
|
|
2128
|
-
const timer =
|
|
2129
|
-
const latest =
|
|
2289
|
+
const [local, setLocal] = useState7(value);
|
|
2290
|
+
const timer = useRef4(void 0);
|
|
2291
|
+
const latest = useRef4(onChange);
|
|
2130
2292
|
latest.current = onChange;
|
|
2131
|
-
|
|
2132
|
-
|
|
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__ */
|
|
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__ */
|
|
2154
|
-
suffix: local ? /* @__PURE__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
2186
|
-
/* @__PURE__ */
|
|
2187
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2357
|
+
children: /* @__PURE__ */ jsx29(ChevronLeft, { "aria-hidden": "true" })
|
|
2196
2358
|
}
|
|
2197
2359
|
),
|
|
2198
|
-
/* @__PURE__ */
|
|
2360
|
+
/* @__PURE__ */ jsxs28("span", { className: "cfi-ui-pagination__page", children: [
|
|
2199
2361
|
current,
|
|
2200
2362
|
" / ",
|
|
2201
2363
|
pageCount
|
|
2202
2364
|
] }),
|
|
2203
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
2384
|
+
import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
|
|
2223
2385
|
function ListToolbar({ search, filters, actions, className }) {
|
|
2224
|
-
return /* @__PURE__ */
|
|
2225
|
-
search && /* @__PURE__ */
|
|
2226
|
-
filters && /* @__PURE__ */
|
|
2227
|
-
actions && /* @__PURE__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
2261
|
-
return /* @__PURE__ */
|
|
2262
|
-
i > 0 && /* @__PURE__ */
|
|
2263
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2267
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2271
|
-
/* @__PURE__ */
|
|
2272
|
-
/* @__PURE__ */
|
|
2273
|
-
description && /* @__PURE__ */
|
|
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__ */
|
|
2437
|
+
actions && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__actions", children: actions })
|
|
2276
2438
|
] }),
|
|
2277
|
-
children && /* @__PURE__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
2312
|
-
children && /* @__PURE__ */
|
|
2313
|
-
onClear && /* @__PURE__ */
|
|
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__ */
|
|
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
|
|
2494
|
+
import { useEffect as useEffect5, useMemo as useMemo6, useState as useState8 } from "react";
|
|
2333
2495
|
function usePagedRows(all, initialPageSize = 25) {
|
|
2334
|
-
const [page, setPage] =
|
|
2335
|
-
const [pageSize, setSize] =
|
|
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
|
-
|
|
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 `${
|
|
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.
|
|
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.0",
|
|
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
|
+
}
|