@campfire-interactive/ui 0.11.0 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +93 -1
  2. package/dist/index.js +238 -51
  3. package/package.json +56 -56
package/dist/index.d.ts CHANGED
@@ -1716,6 +1716,98 @@ declare function timelineLayout({ items, lanes, start, end, today, calendar, min
1716
1716
  */
1717
1717
  declare function Timeline({ items, lanes, start, end, today, todayLabel, calendar, beforeLabel, afterLabel, emptyLabel, className, ...rest }: TimelineProps): react.JSX.Element;
1718
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
+
1719
1811
  interface SortState<K extends string> {
1720
1812
  key: K;
1721
1813
  direction: SortDirection;
@@ -2085,4 +2177,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
2085
2177
  */
2086
2178
  declare function formatPercent(value: number | null | undefined, decimals?: number): string;
2087
2179
 
2088
- 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, 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, timelineLayout, 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
@@ -2489,10 +2489,194 @@ function Timeline({
2489
2489
  ] }) });
2490
2490
  }
2491
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
+
2492
2676
  // src/useSortState.ts
2493
- 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";
2494
2678
  function useSortState(initialKey, initialDirection = "asc") {
2495
- const [sort, setSort] = useState6({ key: initialKey, direction: initialDirection });
2679
+ const [sort, setSort] = useState7({ key: initialKey, direction: initialDirection });
2496
2680
  const toggle = useCallback3((key) => {
2497
2681
  setSort(
2498
2682
  (current) => current.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" }
@@ -2529,21 +2713,21 @@ function useSortState(initialKey, initialDirection = "asc") {
2529
2713
  }
2530
2714
 
2531
2715
  // src/SearchInput.tsx
2532
- 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";
2533
2717
  import { Search as Search2, X as X6 } from "lucide-react";
2534
2718
 
2535
2719
  // src/SearchInput.css
2536
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");
2537
2721
 
2538
2722
  // src/SearchInput.tsx
2539
- import { jsx as jsx29 } from "react/jsx-runtime";
2723
+ import { jsx as jsx30 } from "react/jsx-runtime";
2540
2724
  var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay = 250, placeholder = "Search\u2026", clearLabel = "Clear search", ...rest }, ref) {
2541
- const [local, setLocal] = useState7(value);
2542
- const timer = useRef4(void 0);
2543
- const latest = useRef4(onChange);
2725
+ const [local, setLocal] = useState8(value);
2726
+ const timer = useRef5(void 0);
2727
+ const latest = useRef5(onChange);
2544
2728
  latest.current = onChange;
2545
- useEffect4(() => setLocal(value), [value]);
2546
- useEffect4(() => () => clearTimeout(timer.current), []);
2729
+ useEffect5(() => setLocal(value), [value]);
2730
+ useEffect5(() => () => clearTimeout(timer.current), []);
2547
2731
  const push = (next) => {
2548
2732
  setLocal(next);
2549
2733
  clearTimeout(timer.current);
@@ -2555,7 +2739,7 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2555
2739
  setLocal("");
2556
2740
  latest.current("");
2557
2741
  };
2558
- return /* @__PURE__ */ jsx29(
2742
+ return /* @__PURE__ */ jsx30(
2559
2743
  Input,
2560
2744
  {
2561
2745
  ...rest,
@@ -2564,8 +2748,8 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2564
2748
  value: local,
2565
2749
  placeholder,
2566
2750
  onChange: (e) => push(e.target.value),
2567
- prefix: /* @__PURE__ */ jsx29(Search2, { "aria-hidden": "true" }),
2568
- suffix: local ? /* @__PURE__ */ jsx29("button", { type: "button", className: "cfi-ui-search__clear", onClick: clear, "aria-label": clearLabel, children: /* @__PURE__ */ jsx29(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
2569
2753
  }
2570
2754
  );
2571
2755
  });
@@ -2577,7 +2761,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
2577
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");
2578
2762
 
2579
2763
  // src/Pagination.tsx
2580
- import { jsx as jsx30, jsxs as jsxs29 } from "react/jsx-runtime";
2764
+ import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2581
2765
  function Pagination({
2582
2766
  page,
2583
2767
  pageSize,
@@ -2593,12 +2777,12 @@ function Pagination({
2593
2777
  const from = (current - 1) * pageSize + 1;
2594
2778
  const to = Math.min(current * pageSize, totalItems);
2595
2779
  const range = labels?.range?.(from, to, totalItems) ?? `${from}\u2013${to} of ${totalItems.toLocaleString()}`;
2596
- return /* @__PURE__ */ jsxs29("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: [
2597
2781
  showRange && // aria-live so a screen-reader user hears where they landed; paging
2598
2782
  // otherwise moves focus nowhere and announces nothing.
2599
- /* @__PURE__ */ jsx30("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
2600
- /* @__PURE__ */ jsxs29("div", { className: "cfi-ui-pagination__controls", children: [
2601
- /* @__PURE__ */ jsx30(
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(
2602
2786
  "button",
2603
2787
  {
2604
2788
  type: "button",
@@ -2606,15 +2790,15 @@ function Pagination({
2606
2790
  onClick: () => onPageChange(current - 1),
2607
2791
  disabled: current <= 1,
2608
2792
  "aria-label": labels?.previous ?? "Previous page",
2609
- children: /* @__PURE__ */ jsx30(ChevronLeft, { "aria-hidden": "true" })
2793
+ children: /* @__PURE__ */ jsx31(ChevronLeft, { "aria-hidden": "true" })
2610
2794
  }
2611
2795
  ),
2612
- /* @__PURE__ */ jsxs29("span", { className: "cfi-ui-pagination__page", children: [
2796
+ /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-pagination__page", children: [
2613
2797
  current,
2614
2798
  " / ",
2615
2799
  pageCount
2616
2800
  ] }),
2617
- /* @__PURE__ */ jsx30(
2801
+ /* @__PURE__ */ jsx31(
2618
2802
  "button",
2619
2803
  {
2620
2804
  type: "button",
@@ -2622,7 +2806,7 @@ function Pagination({
2622
2806
  onClick: () => onPageChange(current + 1),
2623
2807
  disabled: current >= pageCount,
2624
2808
  "aria-label": labels?.next ?? "Next page",
2625
- children: /* @__PURE__ */ jsx30(ChevronRight, { "aria-hidden": "true" })
2809
+ children: /* @__PURE__ */ jsx31(ChevronRight, { "aria-hidden": "true" })
2626
2810
  }
2627
2811
  )
2628
2812
  ] })
@@ -2633,12 +2817,12 @@ function Pagination({
2633
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");
2634
2818
 
2635
2819
  // src/ListToolbar.tsx
2636
- import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
2820
+ import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2637
2821
  function ListToolbar({ search, filters, actions, className }) {
2638
- return /* @__PURE__ */ jsxs30("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
2639
- search && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-list-toolbar__search", children: search }),
2640
- filters && /* @__PURE__ */ jsx31("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
2641
- actions && /* @__PURE__ */ jsx31("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 })
2642
2826
  ] });
2643
2827
  }
2644
2828
 
@@ -2649,7 +2833,7 @@ import { ArrowLeft, ChevronRight as ChevronRight2 } from "lucide-react";
2649
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");
2650
2834
 
2651
2835
  // src/PageHeader.tsx
2652
- import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2836
+ import { jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
2653
2837
  function PageHeader({
2654
2838
  title,
2655
2839
  description,
@@ -2663,32 +2847,32 @@ function PageHeader({
2663
2847
  className
2664
2848
  }) {
2665
2849
  const Heading = `h${level}`;
2666
- return /* @__PURE__ */ jsxs31(
2850
+ return /* @__PURE__ */ jsxs32(
2667
2851
  "div",
2668
2852
  {
2669
2853
  className: cn("cfi-ui", "cfi-ui-page-header", className),
2670
2854
  "data-bordered": bordered ? "" : void 0,
2671
2855
  children: [
2672
- breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx32("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) => {
2673
2857
  const last = i === breadcrumbs.length - 1;
2674
- const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx32("a", { href: crumb.href, children: crumb.label }) : crumb.label;
2675
- return /* @__PURE__ */ jsxs31("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
2676
- i > 0 && /* @__PURE__ */ jsx32(ChevronRight2, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
2677
- /* @__PURE__ */ jsx32("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 })
2678
2862
  ] }, i);
2679
2863
  }) }),
2680
- onBack && /* @__PURE__ */ jsxs31("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2681
- /* @__PURE__ */ jsx32(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" }),
2682
2866
  backLabel
2683
2867
  ] }),
2684
- /* @__PURE__ */ jsxs31("div", { className: "cfi-ui-page-header__row", children: [
2685
- /* @__PURE__ */ jsxs31("div", { className: "cfi-ui-page-header__titles", children: [
2686
- /* @__PURE__ */ jsx32(Heading, { className: "cfi-ui-page-header__title", children: title }),
2687
- description && /* @__PURE__ */ jsx32("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 })
2688
2872
  ] }),
2689
- actions && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-page-header__actions", children: actions })
2873
+ actions && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__actions", children: actions })
2690
2874
  ] }),
2691
- children && /* @__PURE__ */ jsx32("div", { className: "cfi-ui-page-header__meta", children })
2875
+ children && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__meta", children })
2692
2876
  ]
2693
2877
  }
2694
2878
  );
@@ -2701,7 +2885,7 @@ import { X as X7 } from "lucide-react";
2701
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");
2702
2886
 
2703
2887
  // src/SelectionBar.tsx
2704
- import { jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
2888
+ import { jsx as jsx34, jsxs as jsxs33 } from "react/jsx-runtime";
2705
2889
  function SelectionBar({
2706
2890
  count,
2707
2891
  children,
@@ -2713,7 +2897,7 @@ function SelectionBar({
2713
2897
  className
2714
2898
  }) {
2715
2899
  if (count === 0) return null;
2716
- return /* @__PURE__ */ jsxs32(
2900
+ return /* @__PURE__ */ jsxs33(
2717
2901
  "div",
2718
2902
  {
2719
2903
  role: "status",
@@ -2722,9 +2906,9 @@ function SelectionBar({
2722
2906
  "data-busy": busy ? "" : void 0,
2723
2907
  className: cn("cfi-ui", "cfi-ui-selection-bar", className),
2724
2908
  children: [
2725
- /* @__PURE__ */ jsx33("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
2726
- children && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-selection-bar__actions", children }),
2727
- onClear && /* @__PURE__ */ jsxs32(
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(
2728
2912
  "button",
2729
2913
  {
2730
2914
  type: "button",
@@ -2732,7 +2916,7 @@ function SelectionBar({
2732
2916
  onClick: onClear,
2733
2917
  disabled: busy,
2734
2918
  children: [
2735
- /* @__PURE__ */ jsx33(X7, { "aria-hidden": "true" }),
2919
+ /* @__PURE__ */ jsx34(X7, { "aria-hidden": "true" }),
2736
2920
  clearLabel
2737
2921
  ]
2738
2922
  }
@@ -2743,14 +2927,14 @@ function SelectionBar({
2743
2927
  }
2744
2928
 
2745
2929
  // src/usePagedRows.ts
2746
- 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";
2747
2931
  function usePagedRows(all, initialPageSize = 25) {
2748
- const [page, setPage] = useState8(1);
2749
- const [pageSize, setSize] = useState8(initialPageSize);
2932
+ const [page, setPage] = useState9(1);
2933
+ const [pageSize, setSize] = useState9(initialPageSize);
2750
2934
  const totalItems = all.length;
2751
2935
  const pageCount = Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize)));
2752
2936
  const clamped = Math.min(Math.max(1, page), pageCount);
2753
- useEffect5(() => {
2937
+ useEffect6(() => {
2754
2938
  if (clamped !== page) setPage(clamped);
2755
2939
  }, [clamped, page]);
2756
2940
  const rows = useMemo6(
@@ -2811,6 +2995,7 @@ export {
2811
2995
  FilterBar,
2812
2996
  IconButton,
2813
2997
  Input,
2998
+ KanbanBoard,
2814
2999
  Label3 as Label,
2815
3000
  ListToolbar,
2816
3001
  LoadingState,
@@ -2902,6 +3087,8 @@ export {
2902
3087
  formatMoney,
2903
3088
  formatNumber,
2904
3089
  formatPercent,
3090
+ kanbanLayout,
3091
+ kanbanStep,
2905
3092
  timelineLayout,
2906
3093
  useDebouncedValue,
2907
3094
  useFieldControl,
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "@campfire-interactive/ui",
3
- "version": "0.11.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.12.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
+ }