@campfire-interactive/ui 0.10.8 → 0.12.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/README.md CHANGED
@@ -1,82 +1,82 @@
1
- # @campfire-interactive/ui
2
-
3
- Shared React primitives for the Campfire Suite. Radix for behavior, plain CSS
4
- over `--cfi-*` tokens for looks. No Tailwind is bundled.
5
-
6
- ## ⚠ Read this before adding it to a `package.json`
7
-
8
- **Partly verified. Not yet approved for adoption.**
9
-
10
- This package is on the registry so it can be reviewed and so a migration can be
11
- tried on a branch — **not** because the API is settled.
12
-
13
- | | |
14
- |---|---|
15
- | **Verified** | The 28 exports `analytics` uses. Every screen that renders them was proven identical to the deployed dev app by computed-style diff — five routes plus the Filters drawer, the KPI dialog and the cost-sensitivity sliders. Zero differences. |
16
- | **Unvetted** | The other ~23 exports. They work in the showcase; nothing has driven them against a real screen, so treat their APIs as unsettled. |
17
- | **Unaudited** | Accessibility of the overlay wrappers. Radix does the hard parts — focus trapping, keyboard navigation, collision positioning — but the wrappers around it are ours and have not had an audit. |
18
-
19
- Verified exports: `Button`, `IconButton`, `Card`, `Badge`, `Table` +
20
- `TableHeader` / `TableHead` / `TableBody` / `TableRow` / `TableCell` /
21
- `TableFooter`, `Tabs` + `TabsList` / `TabsTrigger` / `TabsContent`, `Dialog` +
22
- `DialogContent` / `DialogHeader` / `DialogTitle` / `DialogDescription`, `Sheet` +
23
- `SheetTrigger` / `SheetContent` / `SheetHeader` / `SheetTitle`, `Checkbox`,
24
- `Switch`, `Label`, `Slider`.
25
-
26
- **If you are an AI agent working in a product repo: do not add this dependency.**
27
- Being installable is not permission. The remaining gate is in the repo's
28
- `CLAUDE.md`: a second app migrated, an accessibility pass on the overlays, the
29
- token naming settled, and a human saying so.
30
-
31
- ## Install
32
-
33
- ```bash
34
- npm i @campfire-interactive/ui @campfire-interactive/design-tokens
35
- ```
36
-
37
- Import the tokens once at the app entry point, before your own stylesheets.
38
- Component CSS is injected by the bundle, so there is no second CSS import.
39
-
40
- ```ts
41
- import '@campfire-interactive/design-tokens/tokens.css';
42
- import { Button, Dialog, DialogContent, DialogTitle } from '@campfire-interactive/ui';
43
- ```
44
-
45
- ## Conventions worth knowing
46
-
47
- **Bare names are the Radix-shaped root, not a whole widget.** `Dialog` is the
48
- root you pass `open` / `onOpenChange` to, exactly as the vendored shadcn copies
49
- in the seven Tailwind apps do — so swapping an import cannot silently render
50
- something else. The one-component convenience versions are named separately:
51
- `Modal`, `SideModal`, `ConfirmDialog`, `TabSet`.
52
-
53
- **`TableHeader` is `<thead>` and `TableHead` is `<th>`**, matching shadcn.
54
- `TableHead` sets `aria-sort` from the same prop that draws the sort arrow.
55
-
56
- **Component CSS lives in `@layer components`.** Utilities a caller passes —
57
- `className="bg-gray-50"`, `max-w-3xl` — therefore win, because layer order beats
58
- specificity and unlayered CSS would beat every Tailwind utility. Note that this
59
- makes the package *more* obedient than the vendored copies: classes that shadcn's
60
- own `sm:max-w-lg` silently dropped in production now apply.
61
-
62
- **Theming is `--cfi-*` variables only.** Two colors do two different jobs:
63
- `--cfi-action` is the suite-wide color for anything you press (buttons, links,
64
- focus rings — omsf's `#030213`, the same in every app), and `--cfi-app-primary`
65
- is the app's identity color whose only job is the badge in the shell header.
66
- Do not point one at the other.
67
-
68
- **Dark mode is opt-in.** Stamp `data-theme="dark"` on the root element, or add
69
- `class="cfi-theme-auto"` to follow the OS. Nothing happens by accident.
70
-
71
- ## Looking at the components
72
-
73
- The repo builds a single self-contained HTML file — every component, both
74
- themes, live-editable snippets, no server and no network:
75
-
76
- ```bash
77
- npm run standalone --workspace @campfire-interactive/showcase
78
- ```
79
-
80
- ## Peer dependencies
81
-
82
- `react` and `react-dom` (18 or 19).
1
+ # @campfire-interactive/ui
2
+
3
+ Shared React primitives for the Campfire Suite. Radix for behavior, plain CSS
4
+ over `--cfi-*` tokens for looks. No Tailwind is bundled.
5
+
6
+ ## ⚠ Read this before adding it to a `package.json`
7
+
8
+ **Partly verified. Not yet approved for adoption.**
9
+
10
+ This package is on the registry so it can be reviewed and so a migration can be
11
+ tried on a branch — **not** because the API is settled.
12
+
13
+ | | |
14
+ |---|---|
15
+ | **Verified** | The 28 exports `analytics` uses. Every screen that renders them was proven identical to the deployed dev app by computed-style diff — five routes plus the Filters drawer, the KPI dialog and the cost-sensitivity sliders. Zero differences. |
16
+ | **Unvetted** | The other ~23 exports. They work in the showcase; nothing has driven them against a real screen, so treat their APIs as unsettled. |
17
+ | **Unaudited** | Accessibility of the overlay wrappers. Radix does the hard parts — focus trapping, keyboard navigation, collision positioning — but the wrappers around it are ours and have not had an audit. |
18
+
19
+ Verified exports: `Button`, `IconButton`, `Card`, `Badge`, `Table` +
20
+ `TableHeader` / `TableHead` / `TableBody` / `TableRow` / `TableCell` /
21
+ `TableFooter`, `Tabs` + `TabsList` / `TabsTrigger` / `TabsContent`, `Dialog` +
22
+ `DialogContent` / `DialogHeader` / `DialogTitle` / `DialogDescription`, `Sheet` +
23
+ `SheetTrigger` / `SheetContent` / `SheetHeader` / `SheetTitle`, `Checkbox`,
24
+ `Switch`, `Label`, `Slider`.
25
+
26
+ **If you are an AI agent working in a product repo: do not add this dependency.**
27
+ Being installable is not permission. The remaining gate is in the repo's
28
+ `CLAUDE.md`: a second app migrated, an accessibility pass on the overlays, the
29
+ token naming settled, and a human saying so.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm i @campfire-interactive/ui @campfire-interactive/design-tokens
35
+ ```
36
+
37
+ Import the tokens once at the app entry point, before your own stylesheets.
38
+ Component CSS is injected by the bundle, so there is no second CSS import.
39
+
40
+ ```ts
41
+ import '@campfire-interactive/design-tokens/tokens.css';
42
+ import { Button, Dialog, DialogContent, DialogTitle } from '@campfire-interactive/ui';
43
+ ```
44
+
45
+ ## Conventions worth knowing
46
+
47
+ **Bare names are the Radix-shaped root, not a whole widget.** `Dialog` is the
48
+ root you pass `open` / `onOpenChange` to, exactly as the vendored shadcn copies
49
+ in the seven Tailwind apps do — so swapping an import cannot silently render
50
+ something else. The one-component convenience versions are named separately:
51
+ `Modal`, `SideModal`, `ConfirmDialog`, `TabSet`.
52
+
53
+ **`TableHeader` is `<thead>` and `TableHead` is `<th>`**, matching shadcn.
54
+ `TableHead` sets `aria-sort` from the same prop that draws the sort arrow.
55
+
56
+ **Component CSS lives in `@layer components`.** Utilities a caller passes —
57
+ `className="bg-gray-50"`, `max-w-3xl` — therefore win, because layer order beats
58
+ specificity and unlayered CSS would beat every Tailwind utility. Note that this
59
+ makes the package *more* obedient than the vendored copies: classes that shadcn's
60
+ own `sm:max-w-lg` silently dropped in production now apply.
61
+
62
+ **Theming is `--cfi-*` variables only.** Two colors do two different jobs:
63
+ `--cfi-action` is the suite-wide color for anything you press (buttons, links,
64
+ focus rings — omsf's `#030213`, the same in every app), and `--cfi-app-primary`
65
+ is the app's identity color whose only job is the badge in the shell header.
66
+ Do not point one at the other.
67
+
68
+ **Dark mode is opt-in.** Stamp `data-theme="dark"` on the root element, or add
69
+ `class="cfi-theme-auto"` to follow the OS. Nothing happens by accident.
70
+
71
+ ## Looking at the components
72
+
73
+ The repo builds a single self-contained HTML file — every component, both
74
+ themes, live-editable snippets, no server and no network:
75
+
76
+ ```bash
77
+ npm run standalone --workspace @campfire-interactive/showcase
78
+ ```
79
+
80
+ ## Peer dependencies
81
+
82
+ `react` and `react-dom` (18 or 19).
package/dist/index.d.ts CHANGED
@@ -1615,6 +1615,199 @@ declare function useDebouncedValue<T>(value: T, delayMs?: number): T;
1615
1615
  */
1616
1616
  declare function usePersistentState<T>(key: string, initial: T): [T, (next: T | ((prev: T) => T)) => void];
1617
1617
 
1618
+ type TimelineTone = 'neutral' | 'accent' | 'success' | 'warning' | 'error' | 'info';
1619
+ interface TimelineItem {
1620
+ id: string;
1621
+ /**
1622
+ * A `YYYY-MM-DD` day, an ISO instant, or a Date. Under `calendar` (the
1623
+ * default) an instant is read by its UTC day, which is how a Prisma
1624
+ * `@db.Date` arrives (`2028-01-01T00:00:00.000Z`).
1625
+ */
1626
+ date: Date | string;
1627
+ label: ReactNode;
1628
+ /** The line under the label. Defaults to the date; `null` hides it. */
1629
+ detail?: ReactNode | null;
1630
+ /** The marker's color. `neutral` (default) is the accent while upcoming and muted once past. */
1631
+ tone?: TimelineTone;
1632
+ /** `diamond` for an event of a different kind on the same lane — a re-source date beside the program's own. */
1633
+ shape?: 'dot' | 'diamond';
1634
+ /** The lane it sits on; omitted, the first. */
1635
+ lane?: string;
1636
+ }
1637
+ interface TimelineLane {
1638
+ id: string;
1639
+ /** Names the lane; shown above it and read as its list label. */
1640
+ label: ReactNode;
1641
+ /** `dashed` for a lane of projected or alternative dates. */
1642
+ line?: 'solid' | 'dashed';
1643
+ }
1644
+ interface TimelineProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
1645
+ items: TimelineItem[];
1646
+ /**
1647
+ * Lanes, top to bottom. Omitted, one unlabeled lane holds everything. An
1648
+ * item naming a lane that is not here goes to the first.
1649
+ */
1650
+ lanes?: TimelineLane[];
1651
+ /** The span drawn. Defaults to the items' own span, with today in it when shown. */
1652
+ start?: Date | string;
1653
+ end?: Date | string;
1654
+ /** Where the today marker sits; `null` hides it. Defaults to now. */
1655
+ today?: Date | string | null;
1656
+ todayLabel?: ReactNode;
1657
+ /** Dates are calendar days, read by UTC day (default). `false` reads them as local instants. */
1658
+ calendar?: boolean;
1659
+ /** Lead-ins for the items outside `start`–`end`, listed under the axis instead of dropped. */
1660
+ beforeLabel?: ReactNode;
1661
+ afterLabel?: ReactNode;
1662
+ /** Shown when there are no items. */
1663
+ emptyLabel?: ReactNode;
1664
+ }
1665
+ interface TimelineLayoutItem {
1666
+ item: TimelineItem;
1667
+ time: number;
1668
+ /** 0–100 across the axis. */
1669
+ pos: number;
1670
+ /** Which side of the axis the label sits on, and how far out (0 nearest). */
1671
+ side: 'above' | 'below';
1672
+ level: number;
1673
+ past: boolean;
1674
+ /** Near either end the label aligns to that end instead of centering off the edge. */
1675
+ edge: 'start' | 'end' | null;
1676
+ }
1677
+ interface TimelineLayout {
1678
+ start: number;
1679
+ end: number;
1680
+ /** How many label rows the lane needs above its axis and below it. */
1681
+ lanes: {
1682
+ lane: TimelineLane;
1683
+ items: TimelineLayoutItem[];
1684
+ levels: {
1685
+ above: number;
1686
+ below: number;
1687
+ };
1688
+ }[];
1689
+ before: TimelineLayoutItem[];
1690
+ after: TimelineLayoutItem[];
1691
+ years: {
1692
+ year: number;
1693
+ pos: number;
1694
+ }[];
1695
+ today: number | null;
1696
+ todayPos: number | null;
1697
+ }
1698
+ /**
1699
+ * Where everything goes. Pure, so the rules are tested without a DOM:
1700
+ * markers are spread across 3–97% so none sits on the frame; an item
1701
+ * outside the span is listed rather than clamped onto it, because a marker
1702
+ * pinned to the edge claims a date it does not have.
1703
+ */
1704
+ declare function timelineLayout({ items, lanes, start, end, today, calendar, minGap, }: Pick<TimelineProps, 'items' | 'lanes' | 'start' | 'end' | 'today' | 'calendar'> & {
1705
+ /** How far apart, in percent of the axis, two labels in the same row must be. */
1706
+ minGap?: number;
1707
+ }): TimelineLayout;
1708
+ /**
1709
+ * Dated events on one axis, with today marked: a program's sourcing
1710
+ * milestones, a contract's renewals, a build's gates.
1711
+ *
1712
+ * Each lane is an ordered list, so a screen reader hears the events in date
1713
+ * order with their dates; the axis, ticks and today line are decoration. In
1714
+ * a container narrower than about 34rem it lays out as that list, one event
1715
+ * per row, because labels on an axis that narrow land on each other.
1716
+ */
1717
+ declare function Timeline({ items, lanes, start, end, today, todayLabel, calendar, beforeLabel, afterLabel, emptyLabel, className, ...rest }: TimelineProps): react.JSX.Element;
1718
+
1719
+ type KanbanTone = 'neutral' | 'accent' | 'success' | 'warning' | 'error' | 'info';
1720
+ interface KanbanColumn {
1721
+ id: string;
1722
+ label: ReactNode;
1723
+ /** The dot beside the label. */
1724
+ tone?: KanbanTone;
1725
+ /**
1726
+ * The count beside the label. Defaults to the cards in the column; pass one
1727
+ * when the column holds fewer than it counts (done cards hidden by a filter).
1728
+ */
1729
+ count?: number;
1730
+ /**
1731
+ * A line under the cards, e.g. "4 done hidden". A function gets the lane's
1732
+ * id (`'__default'` without lanes), for a figure that differs lane to lane.
1733
+ */
1734
+ note?: ReactNode | ((lane: string) => ReactNode);
1735
+ }
1736
+ interface KanbanLane {
1737
+ id: string;
1738
+ /** Names the swimlane; shown above its row of cells. */
1739
+ label: ReactNode;
1740
+ /** A line after the label, e.g. "3 open". */
1741
+ detail?: ReactNode;
1742
+ /** Any CSS color for the lane's left edge, e.g. a program's color. */
1743
+ color?: string;
1744
+ }
1745
+ /** What the board needs of a card. The rest of the caller's object is passed back untouched. */
1746
+ interface KanbanItem {
1747
+ id: string;
1748
+ /** The column it sits in. An id not in `columns` puts it in the first. */
1749
+ column: string;
1750
+ /** The swimlane it sits in; omitted, or not in `lanes`, the first. */
1751
+ lane?: string;
1752
+ }
1753
+ interface KanbanBoardProps<T extends KanbanItem> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
1754
+ columns: KanbanColumn[];
1755
+ /** In the order each cell lists them; the board does not sort. */
1756
+ items: T[];
1757
+ /** Swimlanes, top to bottom. Omitted, one unlabeled lane holds everything. */
1758
+ lanes?: KanbanLane[];
1759
+ /** The card's content. The board draws the card around it, so keep buttons and links out of it. */
1760
+ renderCard: (item: T) => ReactNode;
1761
+ /** Plain text for the card, read in announcements ("Moved {item} to {column}"). */
1762
+ itemLabel: (item: T) => string;
1763
+ /** Plain text for a column in announcements. Defaults to its label when that is a string, else its id. */
1764
+ columnLabel?: (column: KanbanColumn) => string;
1765
+ /**
1766
+ * A card was dropped on another column in its own lane, or moved with
1767
+ * Alt+Left / Alt+Right. Omitted, the board is read-only: nothing drags.
1768
+ * The board does not move the card itself; the caller updates `items`.
1769
+ */
1770
+ onMove?: (item: T, column: string) => void;
1771
+ /** Click or Enter / Space on a card. */
1772
+ onOpen?: (item: T) => void;
1773
+ /** Shown in an empty cell. */
1774
+ emptyLabel?: ReactNode;
1775
+ /** The announcement after a move. */
1776
+ movedLabel?: (item: string, column: string) => string;
1777
+ /** How narrow a column may get before the board scrolls sideways. */
1778
+ minColumnWidth?: string;
1779
+ }
1780
+ interface KanbanLayout<T extends KanbanItem> {
1781
+ lanes: {
1782
+ lane: KanbanLane;
1783
+ cells: {
1784
+ column: KanbanColumn;
1785
+ items: T[];
1786
+ }[];
1787
+ }[];
1788
+ /** Cards per column across every lane, before any `count` override. */
1789
+ counts: Record<string, number>;
1790
+ }
1791
+ /**
1792
+ * Puts each card in its lane and column. Pure, so the rules are testable: a
1793
+ * card naming a column or lane the board does not have goes to the first,
1794
+ * rather than disappearing from a board that claims to show everything.
1795
+ */
1796
+ declare function kanbanLayout<T extends KanbanItem>(columns: KanbanColumn[], items: T[], lanes?: KanbanLane[]): KanbanLayout<T>;
1797
+ /** The column `delta` steps from `from`, or null past either end. */
1798
+ declare function kanbanStep(columns: KanbanColumn[], from: string, delta: number): string | null;
1799
+ /**
1800
+ * Cards in columns, optionally in swimlanes: tasks by status, deals by stage.
1801
+ *
1802
+ * Cards drag between columns within their own lane (a lane is what the card
1803
+ * belongs to, which a drop on the board is not the place to change). Every
1804
+ * drag has a keyboard twin: Alt+Left / Alt+Right moves the focused card one
1805
+ * column, focus stays on it, and the move is announced. The board never moves
1806
+ * a card on its own; `onMove` tells the caller, and the caller's `items` say
1807
+ * where it is.
1808
+ */
1809
+ declare function KanbanBoard<T extends KanbanItem>({ columns, items, lanes, renderCard, itemLabel, columnLabel, onMove, onOpen, emptyLabel, movedLabel, minColumnWidth, className, style, ...rest }: KanbanBoardProps<T>): react.JSX.Element;
1810
+
1618
1811
  interface SortState<K extends string> {
1619
1812
  key: K;
1620
1813
  direction: SortDirection;
@@ -1984,4 +2177,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
1984
2177
  */
1985
2178
  declare function formatPercent(value: number | null | undefined, decimals?: number): string;
1986
2179
 
1987
- 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, RequiredMark, 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 };
2180
+ 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, KanbanBoard, type KanbanBoardProps, type KanbanColumn, type KanbanItem, type KanbanLane, type KanbanLayout, type KanbanTone, 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, RequiredMark, 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, Timeline, type TimelineItem, type TimelineLane, type TimelineLayout, type TimelineLayoutItem, type TimelineProps, type TimelineTone, 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, kanbanLayout, kanbanStep, timelineLayout, useDebouncedValue, useFieldControl, usePagedRows, usePersistentState, useSortState, useToast };
package/dist/index.js CHANGED
@@ -2243,10 +2243,440 @@ function usePersistentState(key, initial) {
2243
2243
  return [value, set];
2244
2244
  }
2245
2245
 
2246
+ // src/format.ts
2247
+ function formatDate(value) {
2248
+ const day = calendarDay(value);
2249
+ if (day !== null) return day;
2250
+ const d = toDate(value);
2251
+ if (!d) return "\u2014";
2252
+ return ymd(d.getFullYear(), d.getMonth() + 1, d.getDate());
2253
+ }
2254
+ function formatCalendarDate(value) {
2255
+ const day = calendarDay(value);
2256
+ if (day !== null) return day;
2257
+ const d = toDate(value);
2258
+ if (!d) return "\u2014";
2259
+ return ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
2260
+ }
2261
+ function ymd(year, month, day) {
2262
+ const pad = (n) => String(n).padStart(2, "0");
2263
+ return `${year}-${pad(month)}-${pad(day)}`;
2264
+ }
2265
+ function calendarDay(value) {
2266
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2267
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
2268
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value ? value : "\u2014";
2269
+ }
2270
+ function formatDateTime(value) {
2271
+ const d = toDate(value);
2272
+ if (!d) return "\u2014";
2273
+ const pad = (n) => String(n).padStart(2, "0");
2274
+ return `${formatDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
2275
+ }
2276
+ function formatMoney(value, { currency = "USD", decimals, perUnit = false, compact = false } = {}) {
2277
+ if (value == null || Number.isNaN(value)) return "\u2014";
2278
+ if (compact) return compactMoney(value, currency, decimals ?? 1);
2279
+ const places = decimals ?? (perUnit ? 4 : 2);
2280
+ return new Intl.NumberFormat("en-US", {
2281
+ style: "currency",
2282
+ currency,
2283
+ minimumFractionDigits: places,
2284
+ maximumFractionDigits: places
2285
+ }).format(value);
2286
+ }
2287
+ function compactMoney(value, currency, decimals) {
2288
+ let symbol = currency;
2289
+ try {
2290
+ symbol = new Intl.NumberFormat("en-US", { style: "currency", currency }).formatToParts(0).find((p) => p.type === "currency")?.value ?? currency;
2291
+ } catch {
2292
+ }
2293
+ const abs = Math.abs(value);
2294
+ const sign = value < 0 ? "-" : "";
2295
+ for (const [size, suffix] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) {
2296
+ if (abs >= size) return `${sign}${symbol}${(abs / size).toFixed(decimals)}${suffix}`;
2297
+ }
2298
+ return `${sign}${symbol}${Math.round(abs)}`;
2299
+ }
2300
+ function formatNumber(value, decimals = 0) {
2301
+ if (value == null || Number.isNaN(value)) return "\u2014";
2302
+ return new Intl.NumberFormat("en-US", {
2303
+ minimumFractionDigits: decimals,
2304
+ maximumFractionDigits: decimals
2305
+ }).format(value);
2306
+ }
2307
+ function formatPercent(value, decimals = 1) {
2308
+ if (value == null || Number.isNaN(value)) return "\u2014";
2309
+ return `${formatNumber(value, decimals)}%`;
2310
+ }
2311
+ function toDate(value) {
2312
+ if (value == null || value === "") return null;
2313
+ const d = value instanceof Date ? value : new Date(value);
2314
+ return Number.isNaN(d.getTime()) ? null : d;
2315
+ }
2316
+
2317
+ // src/Timeline.css
2318
+ styleInject('@layer components {\n :where(.cfi-ui-timeline) {\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-sm);\n min-width: 0;\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-timeline__empty {\n margin: 0;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-timeline__lane-label {\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n letter-spacing: var(--cfi-ui-tracking-caps);\n text-transform: uppercase;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-timeline__track {\n --_row: 2.25rem;\n --_above: calc(var(--cfi-ui-timeline-above, 1) * var(--_row) + 0.75rem);\n --_below: calc(var(--cfi-ui-timeline-below, 1) * var(--_row) + 0.75rem);\n position: relative;\n height: calc(var(--_above) + var(--_below) + 2.5rem);\n margin: 0 var(--cfi-ui-space-xs);\n }\n .cfi-ui-timeline__axis {\n position: absolute;\n inset-inline: 0;\n top: var(--_above);\n height: 0.375rem;\n margin-top: -0.1875rem;\n border-radius: var(--cfi-ui-radius-full);\n background: var(--cfi-ui-surface-muted);\n }\n .cfi-ui-timeline__lane[data-line=dashed] .cfi-ui-timeline__axis {\n height: 0;\n margin-top: 0;\n border-radius: 0;\n background: none;\n border-top: 2px dashed var(--cfi-ui-border-strong);\n }\n .cfi-ui-timeline__year {\n position: absolute;\n top: var(--_above);\n height: calc(var(--_below) + 0.25rem);\n width: 0;\n border-left: 1px solid var(--cfi-ui-border);\n }\n .cfi-ui-timeline__year > span {\n position: absolute;\n top: 100%;\n transform: translateX(-50%);\n padding: 0 var(--cfi-ui-space-3xs);\n background: var(--cfi-ui-surface);\n color: var(--cfi-ui-content-muted);\n font-variant-numeric: tabular-nums;\n }\n .cfi-ui-timeline__today {\n position: absolute;\n top: 0.25rem;\n bottom: 0;\n width: 2px;\n margin-left: -1px;\n background: var(--cfi-ui-content);\n z-index: 1;\n }\n .cfi-ui-timeline__today-label {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n padding: 0 var(--cfi-ui-space-2xs);\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-content);\n color: var(--cfi-ui-content-inverse);\n font-weight: var(--cfi-ui-weight-semibold);\n white-space: nowrap;\n }\n .cfi-ui-timeline__items {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n .cfi-ui-timeline__item {\n position: absolute;\n top: var(--_above);\n width: 0;\n height: 0;\n z-index: 2;\n }\n .cfi-ui-timeline__marker {\n position: absolute;\n left: 0;\n top: 0;\n width: 0.875rem;\n height: 0.875rem;\n transform: translate(-50%, -50%);\n border: 2px solid var(--cfi-ui-surface);\n border-radius: var(--cfi-ui-radius-full);\n background: var(--cfi-ui-accent);\n }\n .cfi-ui-timeline__item[data-shape=diamond] .cfi-ui-timeline__marker {\n width: 0.75rem;\n height: 0.75rem;\n border-radius: 2px;\n transform: translate(-50%, -50%) rotate(45deg);\n }\n .cfi-ui-timeline__item[data-tone=neutral][data-past] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-timeline__item[data-tone=success] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-success);\n }\n .cfi-ui-timeline__item[data-tone=warning] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-warning);\n }\n .cfi-ui-timeline__item[data-tone=error] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-error);\n }\n .cfi-ui-timeline__item[data-tone=info] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-info);\n }\n .cfi-ui-timeline__item[data-tone=accent] .cfi-ui-timeline__marker {\n background: var(--cfi-ui-accent);\n }\n .cfi-ui-timeline__text {\n position: absolute;\n left: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n transform: translateX(-50%);\n padding: 0 var(--cfi-ui-space-3xs);\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-surface);\n white-space: nowrap;\n line-height: var(--cfi-ui-leading-snug);\n text-align: center;\n }\n .cfi-ui-timeline__item[data-side=above] .cfi-ui-timeline__text {\n bottom: 0.625rem;\n }\n .cfi-ui-timeline__item[data-side=below] .cfi-ui-timeline__text {\n top: 0.625rem;\n }\n .cfi-ui-timeline__item[data-side=above][data-level="1"] .cfi-ui-timeline__text {\n bottom: calc(0.625rem + var(--_row));\n }\n .cfi-ui-timeline__item[data-side=below][data-level="1"] .cfi-ui-timeline__text {\n top: calc(0.625rem + var(--_row));\n }\n .cfi-ui-timeline__item[data-side=above][data-level="2"] .cfi-ui-timeline__text {\n bottom: calc(0.625rem + 2 * var(--_row));\n }\n .cfi-ui-timeline__item[data-side=below][data-level="2"] .cfi-ui-timeline__text {\n top: calc(0.625rem + 2 * var(--_row));\n }\n .cfi-ui-timeline__item[data-edge=start] .cfi-ui-timeline__text {\n align-items: flex-start;\n text-align: start;\n transform: translateX(-0.5rem);\n }\n .cfi-ui-timeline__item[data-edge=end] .cfi-ui-timeline__text {\n align-items: flex-end;\n text-align: end;\n transform: translateX(calc(-100% + 0.5rem));\n }\n .cfi-ui-timeline__label {\n font-weight: var(--cfi-ui-weight-semibold);\n }\n .cfi-ui-timeline__detail {\n color: var(--cfi-ui-content-muted);\n font-variant-numeric: tabular-nums;\n }\n .cfi-ui-timeline__outside {\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-timeline__outside > p {\n margin: 0;\n }\n .cfi-ui-timeline__outside-lead {\n font-weight: var(--cfi-ui-weight-medium);\n }\n @container (max-width: 34rem) {\n .cfi-ui-timeline__track {\n height: auto;\n margin: 0;\n }\n .cfi-ui-timeline__axis,\n .cfi-ui-timeline__year,\n .cfi-ui-timeline__today {\n display: none;\n }\n .cfi-ui-timeline__items {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-2xs);\n }\n .cfi-ui-timeline__item {\n position: static;\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n width: auto;\n height: auto;\n }\n .cfi-ui-timeline__marker {\n position: relative;\n flex: none;\n transform: none;\n }\n .cfi-ui-timeline__item[data-shape=diamond] .cfi-ui-timeline__marker {\n transform: rotate(45deg);\n }\n .cfi-ui-timeline__item[data-side] .cfi-ui-timeline__text {\n position: static;\n padding: 0;\n flex-direction: row;\n gap: var(--cfi-ui-space-xs);\n align-items: baseline;\n transform: none;\n white-space: normal;\n text-align: start;\n }\n }\n}\n');
2319
+
2320
+ // src/Timeline.tsx
2321
+ import { Fragment as Fragment8, jsx as jsx28, jsxs as jsxs28 } from "react/jsx-runtime";
2322
+ var DAY = 864e5;
2323
+ function timelineTime(value, calendar = true) {
2324
+ if (value == null || value === "") return null;
2325
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
2326
+ const [y, m, d] = value.split("-").map(Number);
2327
+ return calendar ? Date.UTC(y, m - 1, d) : new Date(y, m - 1, d).getTime();
2328
+ }
2329
+ const date = value instanceof Date ? value : new Date(value);
2330
+ const t = date.getTime();
2331
+ if (Number.isNaN(t)) return null;
2332
+ return calendar ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) : t;
2333
+ }
2334
+ function todayTime(value, calendar) {
2335
+ if (value === null) return null;
2336
+ if (value !== void 0) return timelineTime(value, calendar);
2337
+ const now = /* @__PURE__ */ new Date();
2338
+ return calendar ? Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) : now.getTime();
2339
+ }
2340
+ var DEFAULT_LANE = { id: "__default", label: null };
2341
+ var SLOTS = [
2342
+ ["above", 0],
2343
+ ["below", 0],
2344
+ ["above", 1],
2345
+ ["below", 1],
2346
+ ["above", 2],
2347
+ ["below", 2]
2348
+ ];
2349
+ function placeLabels(items, minGap) {
2350
+ const last = SLOTS.map(() => -Infinity);
2351
+ const levels = { above: 1, below: 1 };
2352
+ for (const x of items) {
2353
+ let slot = SLOTS.findIndex((_, i) => x.pos - last[i] >= minGap);
2354
+ if (slot === -1) slot = last.indexOf(Math.min(...last));
2355
+ last[slot] = x.pos;
2356
+ [x.side, x.level] = SLOTS[slot];
2357
+ levels[x.side] = Math.max(levels[x.side], x.level + 1);
2358
+ }
2359
+ return levels;
2360
+ }
2361
+ function timelineLayout({
2362
+ items,
2363
+ lanes,
2364
+ start,
2365
+ end,
2366
+ today,
2367
+ calendar = true,
2368
+ minGap = 12
2369
+ }) {
2370
+ const laneList = lanes && lanes.length > 0 ? lanes : [DEFAULT_LANE];
2371
+ const dated = items.map((item) => ({ item, time: timelineTime(item.date, calendar) })).filter((x) => x.time !== null).sort((a, b) => a.time - b.time || a.item.id.localeCompare(b.item.id));
2372
+ const now = todayTime(today, calendar);
2373
+ const times = dated.map((d) => d.time);
2374
+ if (now !== null) times.push(now);
2375
+ let lo = timelineTime(start, calendar) ?? (times.length ? Math.min(...times) : now ?? Date.now());
2376
+ let hi = timelineTime(end, calendar) ?? (times.length ? Math.max(...times) : lo);
2377
+ if (hi <= lo) {
2378
+ lo -= 30 * DAY;
2379
+ hi += 30 * DAY;
2380
+ }
2381
+ const pos = (t) => 3 + (t - lo) / (hi - lo) * 94;
2382
+ const laneIndex = new Map(laneList.map((l, i) => [l.id, i]));
2383
+ const byLane = laneList.map((lane) => ({ lane, items: [], levels: { above: 1, below: 1 } }));
2384
+ const before = [];
2385
+ const after = [];
2386
+ for (const { item, time } of dated) {
2387
+ const p = pos(time);
2388
+ const placed = {
2389
+ item,
2390
+ time,
2391
+ pos: p,
2392
+ side: "above",
2393
+ level: 0,
2394
+ past: now !== null && time < now,
2395
+ edge: p < 12 ? "start" : p > 88 ? "end" : null
2396
+ };
2397
+ if (time < lo) before.push(placed);
2398
+ else if (time > hi) after.push(placed);
2399
+ else byLane[laneIndex.get(item.lane ?? "") ?? 0].items.push(placed);
2400
+ }
2401
+ for (const lane of byLane) lane.levels = placeLabels(lane.items, minGap);
2402
+ const firstYear = new Date(lo).getUTCFullYear() + 1;
2403
+ const lastYear = new Date(hi).getUTCFullYear();
2404
+ const step = lastYear - firstYear >= 12 ? 2 : 1;
2405
+ const years = [];
2406
+ for (let y = firstYear; y <= lastYear; y += step) {
2407
+ const t = calendar ? Date.UTC(y, 0, 1) : new Date(y, 0, 1).getTime();
2408
+ if (t > lo && t <= hi) years.push({ year: y, pos: pos(t) });
2409
+ }
2410
+ const todayInSpan = now !== null && now >= lo && now <= hi;
2411
+ return { start: lo, end: hi, lanes: byLane, before, after, years, today: now, todayPos: todayInSpan ? pos(now) : null };
2412
+ }
2413
+ function Timeline({
2414
+ items,
2415
+ lanes,
2416
+ start,
2417
+ end,
2418
+ today,
2419
+ todayLabel = "Today",
2420
+ calendar = true,
2421
+ beforeLabel = "Before this view",
2422
+ afterLabel = "After this view",
2423
+ emptyLabel = "No dates",
2424
+ className,
2425
+ ...rest
2426
+ }) {
2427
+ const layout = timelineLayout({ items, lanes, start, end, today, calendar });
2428
+ const fmt = (value) => calendar ? formatCalendarDate(value) : formatDate(value);
2429
+ const detailOf = (x) => x.item.detail === void 0 ? fmt(x.item.date) : x.item.detail;
2430
+ const empty = layout.lanes.every((l) => l.items.length === 0) && layout.before.length === 0 && layout.after.length === 0;
2431
+ return /* @__PURE__ */ jsx28("div", { className: cn("cfi-ui", "cfi-ui-timeline", className), ...rest, children: empty ? /* @__PURE__ */ jsx28("p", { className: "cfi-ui-timeline__empty", children: emptyLabel }) : /* @__PURE__ */ jsxs28(Fragment8, { children: [
2432
+ layout.lanes.map(({ lane, items: laneItems, levels }, laneIdx) => /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-timeline__lane", "data-line": lane.line ?? "solid", "data-first": laneIdx === 0 || void 0, children: [
2433
+ lane.label ? /* @__PURE__ */ jsx28("div", { className: "cfi-ui-timeline__lane-label", children: lane.label }) : null,
2434
+ /* @__PURE__ */ jsxs28(
2435
+ "div",
2436
+ {
2437
+ className: "cfi-ui-timeline__track",
2438
+ style: { "--cfi-ui-timeline-above": levels.above, "--cfi-ui-timeline-below": levels.below },
2439
+ children: [
2440
+ /* @__PURE__ */ jsx28("div", { className: "cfi-ui-timeline__axis", "aria-hidden": "true" }),
2441
+ laneIdx === 0 ? layout.years.map((y) => /* @__PURE__ */ jsx28("div", { className: "cfi-ui-timeline__year", style: { left: `${y.pos}%` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx28("span", { children: y.year }) }, y.year)) : null,
2442
+ layout.todayPos !== null ? /* @__PURE__ */ jsx28("div", { className: "cfi-ui-timeline__today", style: { left: `${layout.todayPos}%` }, "aria-hidden": "true", children: laneIdx === layout.lanes.length - 1 ? /* @__PURE__ */ jsx28("span", { className: "cfi-ui-timeline__today-label", children: todayLabel }) : null }) : null,
2443
+ /* @__PURE__ */ jsx28("ol", { className: "cfi-ui-timeline__items", "aria-label": typeof lane.label === "string" ? lane.label : void 0, children: laneItems.map((x) => {
2444
+ const detail = detailOf(x);
2445
+ return /* @__PURE__ */ jsxs28(
2446
+ "li",
2447
+ {
2448
+ className: "cfi-ui-timeline__item",
2449
+ style: { left: `${x.pos}%` },
2450
+ "data-side": x.side,
2451
+ "data-level": x.level,
2452
+ "data-edge": x.edge ?? void 0,
2453
+ "data-past": x.past || void 0,
2454
+ "data-tone": x.item.tone ?? "neutral",
2455
+ "data-shape": x.item.shape ?? "dot",
2456
+ children: [
2457
+ /* @__PURE__ */ jsx28("span", { className: "cfi-ui-timeline__marker", "aria-hidden": "true" }),
2458
+ /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-timeline__text", children: [
2459
+ /* @__PURE__ */ jsx28("span", { className: "cfi-ui-timeline__label", children: x.item.label }),
2460
+ detail != null && detail !== "" ? /* @__PURE__ */ jsx28("span", { className: "cfi-ui-timeline__detail", children: detail }) : null
2461
+ ] })
2462
+ ]
2463
+ },
2464
+ x.item.id
2465
+ );
2466
+ }) })
2467
+ ]
2468
+ }
2469
+ )
2470
+ ] }, lane.id)),
2471
+ layout.before.length > 0 || layout.after.length > 0 ? /* @__PURE__ */ jsx28("div", { className: "cfi-ui-timeline__outside", children: [
2472
+ [beforeLabel, layout.before],
2473
+ [afterLabel, layout.after]
2474
+ ].map(
2475
+ ([lead, list], i) => list.length > 0 ? /* @__PURE__ */ jsxs28("p", { children: [
2476
+ /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-timeline__outside-lead", children: [
2477
+ lead,
2478
+ ":"
2479
+ ] }),
2480
+ " ",
2481
+ list.map((x, j) => /* @__PURE__ */ jsxs28("span", { children: [
2482
+ j > 0 ? " \xB7 " : null,
2483
+ x.item.label,
2484
+ " ",
2485
+ detailOf(x)
2486
+ ] }, x.item.id))
2487
+ ] }, i) : null
2488
+ ) }) : null
2489
+ ] }) });
2490
+ }
2491
+
2492
+ // src/KanbanBoard.tsx
2493
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState6 } from "react";
2494
+
2495
+ // src/KanbanBoard.css
2496
+ styleInject("@layer components {\n :where(.cfi-ui-kanban) {\n min-width: 0;\n font-family: var(--cfi-ui-font);\n font-size: var(--cfi-ui-text-sm);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-kanban__scroll {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-sm);\n overflow-x: auto;\n padding-bottom: var(--cfi-ui-space-xs);\n }\n .cfi-ui-kanban__grid {\n display: grid;\n grid-template-columns: repeat(var(--cfi-ui-kanban-columns, 4), minmax(var(--cfi-ui-kanban-min, 13rem), 1fr));\n gap: var(--cfi-ui-space-sm);\n }\n .cfi-ui-kanban__column-head {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n padding: 0 var(--cfi-ui-space-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n }\n .cfi-ui-kanban__dot {\n flex: none;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: var(--cfi-ui-radius-full);\n background: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-kanban__column-head[data-tone=accent] .cfi-ui-kanban__dot {\n background: var(--cfi-ui-accent);\n }\n .cfi-ui-kanban__column-head[data-tone=success] .cfi-ui-kanban__dot {\n background: var(--cfi-ui-success);\n }\n .cfi-ui-kanban__column-head[data-tone=warning] .cfi-ui-kanban__dot {\n background: var(--cfi-ui-warning);\n }\n .cfi-ui-kanban__column-head[data-tone=error] .cfi-ui-kanban__dot {\n background: var(--cfi-ui-error);\n }\n .cfi-ui-kanban__column-head[data-tone=info] .cfi-ui-kanban__dot {\n background: var(--cfi-ui-info);\n }\n .cfi-ui-kanban__column-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .cfi-ui-kanban__count {\n padding: 0 var(--cfi-ui-space-xs);\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-surface-muted);\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-medium);\n font-variant-numeric: tabular-nums;\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-kanban__lane[data-labeled] {\n padding: var(--cfi-ui-space-sm);\n border: 1px solid var(--cfi-ui-border);\n border-radius: var(--cfi-ui-radius-xl);\n background: var(--cfi-ui-surface);\n box-shadow: inset 4px 0 0 var(--cfi-ui-kanban-lane-color, var(--cfi-ui-border-strong));\n }\n .cfi-ui-kanban__lane-head {\n display: flex;\n align-items: baseline;\n flex-wrap: wrap;\n gap: var(--cfi-ui-space-xs) var(--cfi-ui-space-sm);\n margin-bottom: var(--cfi-ui-space-sm);\n padding-left: var(--cfi-ui-space-xs);\n }\n .cfi-ui-kanban__lane-label {\n font-weight: var(--cfi-ui-weight-semibold);\n }\n .cfi-ui-kanban__lane-detail {\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-kanban__cell {\n min-height: 6rem;\n padding: var(--cfi-ui-space-xs);\n border-radius: var(--cfi-ui-radius-lg);\n background: var(--cfi-ui-surface-sunken);\n transition: background var(--cfi-ui-transition-fast), box-shadow var(--cfi-ui-transition-fast);\n }\n .cfi-ui-kanban__cell[data-over] {\n background: var(--cfi-ui-accent-soft);\n box-shadow: inset 0 0 0 2px var(--cfi-ui-accent);\n }\n .cfi-ui-kanban__cards {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n .cfi-ui-kanban__card {\n padding: var(--cfi-ui-space-sm);\n border: 1px solid var(--cfi-ui-border);\n border-radius: var(--cfi-ui-radius-lg);\n background: var(--cfi-ui-surface);\n box-shadow: var(--cfi-ui-shadow-xs);\n transition:\n border-color var(--cfi-ui-transition-fast),\n box-shadow var(--cfi-ui-transition-fast),\n opacity var(--cfi-ui-transition-fast);\n }\n .cfi-ui-kanban__card[role=button] {\n cursor: pointer;\n }\n .cfi-ui-kanban__card[data-movable] {\n cursor: grab;\n }\n .cfi-ui-kanban__card[data-movable]:active {\n cursor: grabbing;\n }\n .cfi-ui-kanban__card[role=button]:hover,\n .cfi-ui-kanban__card[data-movable]:hover {\n border-color: var(--cfi-ui-border-strong);\n box-shadow: var(--cfi-ui-shadow-md);\n }\n .cfi-ui-kanban__card: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-kanban__card[data-dragging] {\n opacity: 0.4;\n }\n .cfi-ui-kanban__note,\n .cfi-ui-kanban__empty {\n margin: 0;\n padding: var(--cfi-ui-space-xs);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-kanban__live {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n }\n @media (prefers-reduced-motion: reduce) {\n .cfi-ui-kanban__cell,\n .cfi-ui-kanban__card {\n transition: none;\n }\n }\n}\n");
2497
+
2498
+ // src/KanbanBoard.tsx
2499
+ import { jsx as jsx29, jsxs as jsxs29 } from "react/jsx-runtime";
2500
+ var DEFAULT_LANE2 = { id: "__default", label: null };
2501
+ function kanbanLayout(columns, items, lanes) {
2502
+ const laneList = lanes && lanes.length > 0 ? lanes : [DEFAULT_LANE2];
2503
+ const columnIds = new Set(columns.map((c) => c.id));
2504
+ const laneIds = new Set(laneList.map((l) => l.id));
2505
+ const counts = Object.fromEntries(columns.map((c) => [c.id, 0]));
2506
+ const cells = /* @__PURE__ */ new Map();
2507
+ for (const item of items) {
2508
+ const column = columnIds.has(item.column) ? item.column : columns[0]?.id;
2509
+ if (column === void 0) continue;
2510
+ const lane = item.lane !== void 0 && laneIds.has(item.lane) ? item.lane : laneList[0].id;
2511
+ const key = `${lane}\0${column}`;
2512
+ const list = cells.get(key);
2513
+ if (list) list.push(item);
2514
+ else cells.set(key, [item]);
2515
+ counts[column] += 1;
2516
+ }
2517
+ return {
2518
+ lanes: laneList.map((lane) => ({
2519
+ lane,
2520
+ cells: columns.map((column) => ({ column, items: cells.get(`${lane.id}\0${column.id}`) ?? [] }))
2521
+ })),
2522
+ counts
2523
+ };
2524
+ }
2525
+ function kanbanStep(columns, from, delta) {
2526
+ const i = columns.findIndex((c) => c.id === from);
2527
+ const next = columns[(i < 0 ? 0 : i) + delta];
2528
+ return next ? next.id : null;
2529
+ }
2530
+ function KanbanBoard({
2531
+ columns,
2532
+ items,
2533
+ lanes,
2534
+ renderCard,
2535
+ itemLabel,
2536
+ columnLabel = (c) => typeof c.label === "string" ? c.label : c.id,
2537
+ onMove,
2538
+ onOpen,
2539
+ emptyLabel = "Nothing here",
2540
+ movedLabel = (item, column) => `Moved ${item} to ${column}`,
2541
+ minColumnWidth = "13rem",
2542
+ className,
2543
+ style,
2544
+ ...rest
2545
+ }) {
2546
+ const layout = kanbanLayout(columns, items, lanes);
2547
+ const [drag, setDrag] = useState6(null);
2548
+ const [over, setOver] = useState6(null);
2549
+ const [announcement, setAnnouncement] = useState6("");
2550
+ const rootRef = useRef4(null);
2551
+ const refocus = useRef4(null);
2552
+ useEffect4(() => {
2553
+ const id = refocus.current;
2554
+ if (!id || !rootRef.current) return;
2555
+ const card = rootRef.current.querySelector(`[data-kanban-card="${CSS.escape(id)}"]`);
2556
+ if (card) {
2557
+ card.focus();
2558
+ refocus.current = null;
2559
+ }
2560
+ });
2561
+ const byId = new Map(items.map((i) => [i.id, i]));
2562
+ const columnById = new Map(columns.map((c) => [c.id, c]));
2563
+ const move = (item, column, viaKeyboard) => {
2564
+ if (!onMove || column === item.column) return;
2565
+ if (viaKeyboard) refocus.current = item.id;
2566
+ onMove(item, column);
2567
+ const target = columnById.get(column);
2568
+ setAnnouncement(movedLabel(itemLabel(item), target ? columnLabel(target) : column));
2569
+ };
2570
+ const endDrag = () => {
2571
+ setDrag(null);
2572
+ setOver(null);
2573
+ };
2574
+ return /* @__PURE__ */ jsxs29(
2575
+ "div",
2576
+ {
2577
+ ref: rootRef,
2578
+ className: cn("cfi-ui", "cfi-ui-kanban", className),
2579
+ style: { "--cfi-ui-kanban-columns": columns.length, "--cfi-ui-kanban-min": minColumnWidth, ...style },
2580
+ ...rest,
2581
+ children: [
2582
+ /* @__PURE__ */ jsxs29("div", { className: "cfi-ui-kanban__scroll", children: [
2583
+ /* @__PURE__ */ jsx29("div", { className: "cfi-ui-kanban__grid cfi-ui-kanban__head", children: columns.map((c) => /* @__PURE__ */ jsxs29("div", { className: "cfi-ui-kanban__column-head", "data-tone": c.tone ?? "neutral", children: [
2584
+ /* @__PURE__ */ jsx29("span", { className: "cfi-ui-kanban__dot", "aria-hidden": "true" }),
2585
+ /* @__PURE__ */ jsx29("span", { className: "cfi-ui-kanban__column-label", children: c.label }),
2586
+ /* @__PURE__ */ jsx29("span", { className: "cfi-ui-kanban__count", children: c.count ?? layout.counts[c.id] })
2587
+ ] }, c.id)) }),
2588
+ layout.lanes.map(({ lane, cells }) => /* @__PURE__ */ jsxs29(
2589
+ "section",
2590
+ {
2591
+ className: "cfi-ui-kanban__lane",
2592
+ "data-labeled": lane.label ? true : void 0,
2593
+ style: lane.color ? { "--cfi-ui-kanban-lane-color": lane.color } : void 0,
2594
+ children: [
2595
+ lane.label ? /* @__PURE__ */ jsxs29("div", { className: "cfi-ui-kanban__lane-head", children: [
2596
+ /* @__PURE__ */ jsx29("span", { className: "cfi-ui-kanban__lane-label", children: lane.label }),
2597
+ lane.detail ? /* @__PURE__ */ jsx29("span", { className: "cfi-ui-kanban__lane-detail", children: lane.detail }) : null
2598
+ ] }) : null,
2599
+ /* @__PURE__ */ jsx29("div", { className: "cfi-ui-kanban__grid", children: cells.map(({ column, items: cellItems }) => {
2600
+ const zone = `${lane.id}\0${column.id}`;
2601
+ const accepts = drag !== null && drag.lane === lane.id && drag.column !== column.id;
2602
+ const note = typeof column.note === "function" ? column.note(lane.id) : column.note;
2603
+ return /* @__PURE__ */ jsxs29(
2604
+ "div",
2605
+ {
2606
+ className: "cfi-ui-kanban__cell",
2607
+ "data-over": over === zone || void 0,
2608
+ onDragOver: (e) => {
2609
+ if (!accepts) return;
2610
+ e.preventDefault();
2611
+ e.dataTransfer.dropEffect = "move";
2612
+ if (over !== zone) setOver(zone);
2613
+ },
2614
+ onDragLeave: (e) => {
2615
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver((o) => o === zone ? null : o);
2616
+ },
2617
+ onDrop: (e) => {
2618
+ e.preventDefault();
2619
+ const item = drag ? byId.get(drag.id) : void 0;
2620
+ if (item && accepts) move(item, column.id, false);
2621
+ endDrag();
2622
+ },
2623
+ children: [
2624
+ /* @__PURE__ */ jsx29("ul", { className: "cfi-ui-kanban__cards", "aria-label": columnLabel(column), children: cellItems.map((item) => /* @__PURE__ */ jsx29("li", { children: /* @__PURE__ */ jsx29(
2625
+ "div",
2626
+ {
2627
+ className: "cfi-ui-kanban__card",
2628
+ "data-kanban-card": item.id,
2629
+ "data-dragging": drag?.id === item.id || void 0,
2630
+ "data-movable": onMove ? true : void 0,
2631
+ role: onOpen ? "button" : void 0,
2632
+ tabIndex: onOpen || onMove ? 0 : void 0,
2633
+ "aria-keyshortcuts": onMove ? "Alt+ArrowLeft Alt+ArrowRight" : void 0,
2634
+ draggable: onMove ? true : void 0,
2635
+ onDragStart: (e) => {
2636
+ e.dataTransfer.effectAllowed = "move";
2637
+ e.dataTransfer.setData("text/plain", item.id);
2638
+ setTimeout(() => setDrag({ id: item.id, lane: lane.id, column: column.id }), 0);
2639
+ },
2640
+ onDragEnd: endDrag,
2641
+ onClick: onOpen ? () => onOpen(item) : void 0,
2642
+ onKeyDown: (e) => {
2643
+ if (e.target !== e.currentTarget) return;
2644
+ if (onMove && e.altKey && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
2645
+ e.preventDefault();
2646
+ const to = kanbanStep(columns, column.id, e.key === "ArrowLeft" ? -1 : 1);
2647
+ if (to) move(item, to, true);
2648
+ return;
2649
+ }
2650
+ if (onOpen && (e.key === "Enter" || e.key === " ")) {
2651
+ e.preventDefault();
2652
+ onOpen(item);
2653
+ }
2654
+ },
2655
+ children: renderCard(item)
2656
+ }
2657
+ ) }, item.id)) }),
2658
+ note ? /* @__PURE__ */ jsx29("p", { className: "cfi-ui-kanban__note", children: note }) : null,
2659
+ cellItems.length === 0 && !note ? /* @__PURE__ */ jsx29("p", { className: "cfi-ui-kanban__empty", children: emptyLabel }) : null
2660
+ ]
2661
+ },
2662
+ column.id
2663
+ );
2664
+ }) })
2665
+ ]
2666
+ },
2667
+ lane.id
2668
+ ))
2669
+ ] }),
2670
+ /* @__PURE__ */ jsx29("div", { className: "cfi-ui-kanban__live", role: "status", "aria-live": "polite", children: announcement })
2671
+ ]
2672
+ }
2673
+ );
2674
+ }
2675
+
2246
2676
  // src/useSortState.ts
2247
- import { useCallback as useCallback3, useMemo as useMemo5, useState as useState6 } from "react";
2677
+ import { useCallback as useCallback3, useMemo as useMemo5, useState as useState7 } from "react";
2248
2678
  function useSortState(initialKey, initialDirection = "asc") {
2249
- const [sort, setSort] = useState6({ key: initialKey, direction: initialDirection });
2679
+ const [sort, setSort] = useState7({ key: initialKey, direction: initialDirection });
2250
2680
  const toggle = useCallback3((key) => {
2251
2681
  setSort(
2252
2682
  (current) => current.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" }
@@ -2283,21 +2713,21 @@ function useSortState(initialKey, initialDirection = "asc") {
2283
2713
  }
2284
2714
 
2285
2715
  // src/SearchInput.tsx
2286
- import { forwardRef as forwardRef18, useEffect as useEffect4, useRef as useRef4, useState as useState7 } from "react";
2716
+ import { forwardRef as forwardRef18, useEffect as useEffect5, useRef as useRef5, useState as useState8 } from "react";
2287
2717
  import { Search as Search2, X as X6 } from "lucide-react";
2288
2718
 
2289
2719
  // src/SearchInput.css
2290
2720
  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");
2291
2721
 
2292
2722
  // src/SearchInput.tsx
2293
- import { jsx as jsx28 } from "react/jsx-runtime";
2723
+ import { jsx as jsx30 } from "react/jsx-runtime";
2294
2724
  var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay = 250, placeholder = "Search\u2026", clearLabel = "Clear search", ...rest }, ref) {
2295
- const [local, setLocal] = useState7(value);
2296
- const timer = useRef4(void 0);
2297
- const latest = useRef4(onChange);
2725
+ const [local, setLocal] = useState8(value);
2726
+ const timer = useRef5(void 0);
2727
+ const latest = useRef5(onChange);
2298
2728
  latest.current = onChange;
2299
- useEffect4(() => setLocal(value), [value]);
2300
- useEffect4(() => () => clearTimeout(timer.current), []);
2729
+ useEffect5(() => setLocal(value), [value]);
2730
+ useEffect5(() => () => clearTimeout(timer.current), []);
2301
2731
  const push = (next) => {
2302
2732
  setLocal(next);
2303
2733
  clearTimeout(timer.current);
@@ -2309,7 +2739,7 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2309
2739
  setLocal("");
2310
2740
  latest.current("");
2311
2741
  };
2312
- return /* @__PURE__ */ jsx28(
2742
+ return /* @__PURE__ */ jsx30(
2313
2743
  Input,
2314
2744
  {
2315
2745
  ...rest,
@@ -2318,8 +2748,8 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2318
2748
  value: local,
2319
2749
  placeholder,
2320
2750
  onChange: (e) => push(e.target.value),
2321
- prefix: /* @__PURE__ */ jsx28(Search2, { "aria-hidden": "true" }),
2322
- 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
2751
+ prefix: /* @__PURE__ */ jsx30(Search2, { "aria-hidden": "true" }),
2752
+ suffix: local ? /* @__PURE__ */ jsx30("button", { type: "button", className: "cfi-ui-search__clear", onClick: clear, "aria-label": clearLabel, children: /* @__PURE__ */ jsx30(X6, { "aria-hidden": "true" }) }) : void 0
2323
2753
  }
2324
2754
  );
2325
2755
  });
@@ -2331,7 +2761,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
2331
2761
  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");
2332
2762
 
2333
2763
  // src/Pagination.tsx
2334
- import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
2764
+ import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2335
2765
  function Pagination({
2336
2766
  page,
2337
2767
  pageSize,
@@ -2347,12 +2777,12 @@ function Pagination({
2347
2777
  const from = (current - 1) * pageSize + 1;
2348
2778
  const to = Math.min(current * pageSize, totalItems);
2349
2779
  const range = labels?.range?.(from, to, totalItems) ?? `${from}\u2013${to} of ${totalItems.toLocaleString()}`;
2350
- return /* @__PURE__ */ jsxs28("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
2780
+ return /* @__PURE__ */ jsxs30("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
2351
2781
  showRange && // aria-live so a screen-reader user hears where they landed; paging
2352
2782
  // otherwise moves focus nowhere and announces nothing.
2353
- /* @__PURE__ */ jsx29("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2354
- /* @__PURE__ */ jsxs28("div", { className: "cfi-ui-pagination__controls", children: [
2355
- /* @__PURE__ */ jsx29(
2783
+ /* @__PURE__ */ jsx31("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2784
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-pagination__controls", children: [
2785
+ /* @__PURE__ */ jsx31(
2356
2786
  "button",
2357
2787
  {
2358
2788
  type: "button",
@@ -2360,15 +2790,15 @@ function Pagination({
2360
2790
  onClick: () => onPageChange(current - 1),
2361
2791
  disabled: current <= 1,
2362
2792
  "aria-label": labels?.previous ?? "Previous page",
2363
- children: /* @__PURE__ */ jsx29(ChevronLeft, { "aria-hidden": "true" })
2793
+ children: /* @__PURE__ */ jsx31(ChevronLeft, { "aria-hidden": "true" })
2364
2794
  }
2365
2795
  ),
2366
- /* @__PURE__ */ jsxs28("span", { className: "cfi-ui-pagination__page", children: [
2796
+ /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-pagination__page", children: [
2367
2797
  current,
2368
2798
  " / ",
2369
2799
  pageCount
2370
2800
  ] }),
2371
- /* @__PURE__ */ jsx29(
2801
+ /* @__PURE__ */ jsx31(
2372
2802
  "button",
2373
2803
  {
2374
2804
  type: "button",
@@ -2376,7 +2806,7 @@ function Pagination({
2376
2806
  onClick: () => onPageChange(current + 1),
2377
2807
  disabled: current >= pageCount,
2378
2808
  "aria-label": labels?.next ?? "Next page",
2379
- children: /* @__PURE__ */ jsx29(ChevronRight, { "aria-hidden": "true" })
2809
+ children: /* @__PURE__ */ jsx31(ChevronRight, { "aria-hidden": "true" })
2380
2810
  }
2381
2811
  )
2382
2812
  ] })
@@ -2387,12 +2817,12 @@ function Pagination({
2387
2817
  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");
2388
2818
 
2389
2819
  // src/ListToolbar.tsx
2390
- import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2820
+ import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2391
2821
  function ListToolbar({ search, filters, actions, className }) {
2392
- return /* @__PURE__ */ jsxs29("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2393
- search && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2394
- filters && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2395
- actions && /* @__PURE__ */ jsx30("div", { className: "cfi-ui-list-toolbar__actions", children: actions })
2822
+ return /* @__PURE__ */ jsxs31("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2823
+ search && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2824
+ filters && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2825
+ actions && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-list-toolbar__actions", children: actions })
2396
2826
  ] });
2397
2827
  }
2398
2828
 
@@ -2403,7 +2833,7 @@ import { ArrowLeft, ChevronRight as ChevronRight2 } from "lucide-react";
2403
2833
  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");
2404
2834
 
2405
2835
  // src/PageHeader.tsx
2406
- import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2836
+ import { jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
2407
2837
  function PageHeader({
2408
2838
  title,
2409
2839
  description,
@@ -2417,32 +2847,32 @@ function PageHeader({
2417
2847
  className
2418
2848
  }) {
2419
2849
  const Heading = `h${level}`;
2420
- return /* @__PURE__ */ jsxs30(
2850
+ return /* @__PURE__ */ jsxs32(
2421
2851
  "div",
2422
2852
  {
2423
2853
  className: cn("cfi-ui", "cfi-ui-page-header", className),
2424
2854
  "data-bordered": bordered ? "" : void 0,
2425
2855
  children: [
2426
- breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx31("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
2856
+ breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx33("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
2427
2857
  const last = i === breadcrumbs.length - 1;
2428
- const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx31("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2429
- return /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2430
- i > 0 && /* @__PURE__ */ jsx31(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2431
- /* @__PURE__ */ jsx31("span", { "aria-current": last ? "page" : void 0, children: label })
2858
+ const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx33("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2859
+ return /* @__PURE__ */ jsxs32("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2860
+ i > 0 && /* @__PURE__ */ jsx33(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2861
+ /* @__PURE__ */ jsx33("span", { "aria-current": last ? "page" : void 0, children: label })
2432
2862
  ] }, i);
2433
2863
  }) }),
2434
- onBack && /* @__PURE__ */ jsxs30("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2435
- /* @__PURE__ */ jsx31(ArrowLeft, { "aria-hidden": "true" }),
2864
+ onBack && /* @__PURE__ */ jsxs32("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2865
+ /* @__PURE__ */ jsx33(ArrowLeft, { "aria-hidden": "true" }),
2436
2866
  backLabel
2437
2867
  ] }),
2438
- /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-page-header__row", children: [
2439
- /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-page-header__titles", children: [
2440
- /* @__PURE__ */ jsx31(Heading, { className: "cfi-ui-page-header__title", children: title }),
2441
- description && /* @__PURE__ */ jsx31("p", { className: "cfi-ui-page-header__description", children: description })
2868
+ /* @__PURE__ */ jsxs32("div", { className: "cfi-ui-page-header__row", children: [
2869
+ /* @__PURE__ */ jsxs32("div", { className: "cfi-ui-page-header__titles", children: [
2870
+ /* @__PURE__ */ jsx33(Heading, { className: "cfi-ui-page-header__title", children: title }),
2871
+ description && /* @__PURE__ */ jsx33("p", { className: "cfi-ui-page-header__description", children: description })
2442
2872
  ] }),
2443
- actions && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__actions", children: actions })
2873
+ actions && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__actions", children: actions })
2444
2874
  ] }),
2445
- children && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-page-header__meta", children })
2875
+ children && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__meta", children })
2446
2876
  ]
2447
2877
  }
2448
2878
  );
@@ -2455,7 +2885,7 @@ import { X as X7 } from "lucide-react";
2455
2885
  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");
2456
2886
 
2457
2887
  // src/SelectionBar.tsx
2458
- import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2888
+ import { jsx as jsx34, jsxs as jsxs33 } from "react/jsx-runtime";
2459
2889
  function SelectionBar({
2460
2890
  count,
2461
2891
  children,
@@ -2467,7 +2897,7 @@ function SelectionBar({
2467
2897
  className
2468
2898
  }) {
2469
2899
  if (count === 0) return null;
2470
- return /* @__PURE__ */ jsxs31(
2900
+ return /* @__PURE__ */ jsxs33(
2471
2901
  "div",
2472
2902
  {
2473
2903
  role: "status",
@@ -2476,9 +2906,9 @@ function SelectionBar({
2476
2906
  "data-busy": busy ? "" : void 0,
2477
2907
  className: cn("cfi-ui", "cfi-ui-selection-bar", className),
2478
2908
  children: [
2479
- /* @__PURE__ */ jsx32("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2480
- children && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-selection-bar__actions", children }),
2481
- onClear && /* @__PURE__ */ jsxs31(
2909
+ /* @__PURE__ */ jsx34("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2910
+ children && /* @__PURE__ */ jsx34("div", { className: "cfi-ui-selection-bar__actions", children }),
2911
+ onClear && /* @__PURE__ */ jsxs33(
2482
2912
  "button",
2483
2913
  {
2484
2914
  type: "button",
@@ -2486,7 +2916,7 @@ function SelectionBar({
2486
2916
  onClick: onClear,
2487
2917
  disabled: busy,
2488
2918
  children: [
2489
- /* @__PURE__ */ jsx32(X7, { "aria-hidden": "true" }),
2919
+ /* @__PURE__ */ jsx34(X7, { "aria-hidden": "true" }),
2490
2920
  clearLabel
2491
2921
  ]
2492
2922
  }
@@ -2497,14 +2927,14 @@ function SelectionBar({
2497
2927
  }
2498
2928
 
2499
2929
  // src/usePagedRows.ts
2500
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState8 } from "react";
2930
+ import { useEffect as useEffect6, useMemo as useMemo6, useState as useState9 } from "react";
2501
2931
  function usePagedRows(all, initialPageSize = 25) {
2502
- const [page, setPage] = useState8(1);
2503
- const [pageSize, setSize] = useState8(initialPageSize);
2932
+ const [page, setPage] = useState9(1);
2933
+ const [pageSize, setSize] = useState9(initialPageSize);
2504
2934
  const totalItems = all.length;
2505
2935
  const pageCount = Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize)));
2506
2936
  const clamped = Math.min(Math.max(1, page), pageCount);
2507
- useEffect5(() => {
2937
+ useEffect6(() => {
2508
2938
  if (clamped !== page) setPage(clamped);
2509
2939
  }, [clamped, page]);
2510
2940
  const rows = useMemo6(
@@ -2518,77 +2948,6 @@ function usePagedRows(all, initialPageSize = 25) {
2518
2948
  };
2519
2949
  return { rows, page: clamped, setPage, pageSize, setPageSize, totalItems };
2520
2950
  }
2521
-
2522
- // src/format.ts
2523
- function formatDate(value) {
2524
- const day = calendarDay(value);
2525
- if (day !== null) return day;
2526
- const d = toDate(value);
2527
- if (!d) return "\u2014";
2528
- return ymd(d.getFullYear(), d.getMonth() + 1, d.getDate());
2529
- }
2530
- function formatCalendarDate(value) {
2531
- const day = calendarDay(value);
2532
- if (day !== null) return day;
2533
- const d = toDate(value);
2534
- if (!d) return "\u2014";
2535
- return ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
2536
- }
2537
- function ymd(year, month, day) {
2538
- const pad = (n) => String(n).padStart(2, "0");
2539
- return `${year}-${pad(month)}-${pad(day)}`;
2540
- }
2541
- function calendarDay(value) {
2542
- if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2543
- const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
2544
- return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value ? value : "\u2014";
2545
- }
2546
- function formatDateTime(value) {
2547
- const d = toDate(value);
2548
- if (!d) return "\u2014";
2549
- const pad = (n) => String(n).padStart(2, "0");
2550
- return `${formatDate(d)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
2551
- }
2552
- function formatMoney(value, { currency = "USD", decimals, perUnit = false, compact = false } = {}) {
2553
- if (value == null || Number.isNaN(value)) return "\u2014";
2554
- if (compact) return compactMoney(value, currency, decimals ?? 1);
2555
- const places = decimals ?? (perUnit ? 4 : 2);
2556
- return new Intl.NumberFormat("en-US", {
2557
- style: "currency",
2558
- currency,
2559
- minimumFractionDigits: places,
2560
- maximumFractionDigits: places
2561
- }).format(value);
2562
- }
2563
- function compactMoney(value, currency, decimals) {
2564
- let symbol = currency;
2565
- try {
2566
- symbol = new Intl.NumberFormat("en-US", { style: "currency", currency }).formatToParts(0).find((p) => p.type === "currency")?.value ?? currency;
2567
- } catch {
2568
- }
2569
- const abs = Math.abs(value);
2570
- const sign = value < 0 ? "-" : "";
2571
- for (const [size, suffix] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) {
2572
- if (abs >= size) return `${sign}${symbol}${(abs / size).toFixed(decimals)}${suffix}`;
2573
- }
2574
- return `${sign}${symbol}${Math.round(abs)}`;
2575
- }
2576
- function formatNumber(value, decimals = 0) {
2577
- if (value == null || Number.isNaN(value)) return "\u2014";
2578
- return new Intl.NumberFormat("en-US", {
2579
- minimumFractionDigits: decimals,
2580
- maximumFractionDigits: decimals
2581
- }).format(value);
2582
- }
2583
- function formatPercent(value, decimals = 1) {
2584
- if (value == null || Number.isNaN(value)) return "\u2014";
2585
- return `${formatNumber(value, decimals)}%`;
2586
- }
2587
- function toDate(value) {
2588
- if (value == null || value === "") return null;
2589
- const d = value instanceof Date ? value : new Date(value);
2590
- return Number.isNaN(d.getTime()) ? null : d;
2591
- }
2592
2951
  export {
2593
2952
  Accordion,
2594
2953
  AccordionContent,
@@ -2636,6 +2995,7 @@ export {
2636
2995
  FilterBar,
2637
2996
  IconButton,
2638
2997
  Input,
2998
+ KanbanBoard,
2639
2999
  Label3 as Label,
2640
3000
  ListToolbar,
2641
3001
  LoadingState,
@@ -2712,6 +3072,7 @@ export {
2712
3072
  TabsRoot,
2713
3073
  TabsTrigger,
2714
3074
  Textarea,
3075
+ Timeline,
2715
3076
  ToastProvider,
2716
3077
  ToggleGroup,
2717
3078
  Tooltip,
@@ -2726,6 +3087,9 @@ export {
2726
3087
  formatMoney,
2727
3088
  formatNumber,
2728
3089
  formatPercent,
3090
+ kanbanLayout,
3091
+ kanbanStep,
3092
+ timelineLayout,
2729
3093
  useDebouncedValue,
2730
3094
  useFieldControl,
2731
3095
  usePagedRows,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/ui",
3
- "version": "0.10.8",
3
+ "version": "0.12.0",
4
4
  "description": "Shared UI primitives for the Campfire Suite — Radix behavior, plain CSS styled entirely from the design tokens, no Tailwind required",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",