@campfire-interactive/ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1808,6 +1808,120 @@ declare function kanbanStep(columns: KanbanColumn[], from: string, delta: number
1808
1808
  */
1809
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
1810
 
1811
+ type CalendarView = 'month' | 'week';
1812
+ /** What the grid needs of an item. The rest of the caller's object is passed back untouched. */
1813
+ interface CalendarItem {
1814
+ id: string;
1815
+ /**
1816
+ * A `YYYY-MM-DD` day, an ISO instant, or a Date, read by its UTC day (a
1817
+ * Prisma `@db.Date` arrives as `2028-01-01T00:00:00.000Z`). An item without
1818
+ * a date that parses is left off the grid and counted in `undatedLabel`.
1819
+ */
1820
+ date: Date | string | null | undefined;
1821
+ /** Any CSS color for the chip, e.g. a program's color. Defaults to the accent. White text sits on it. */
1822
+ color?: string;
1823
+ /** Drawn dashed in its color and never moved: a milestone beside the tasks. */
1824
+ readOnly?: boolean;
1825
+ /** Ringed in the error color: overdue. */
1826
+ flagged?: boolean;
1827
+ /** Struck through and faded. */
1828
+ done?: boolean;
1829
+ }
1830
+ interface CalendarGridLabels {
1831
+ today: string;
1832
+ previous: (view: CalendarView) => string;
1833
+ next: (view: CalendarView) => string;
1834
+ month: string;
1835
+ week: string;
1836
+ view: string;
1837
+ /** The add button on a day. */
1838
+ add: (day: string) => string;
1839
+ more: (count: number) => string;
1840
+ less: string;
1841
+ moved: (item: string, day: string) => string;
1842
+ }
1843
+ interface CalendarGridProps<T extends CalendarItem> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
1844
+ /** In the order each day lists them; the grid does not sort. */
1845
+ items: T[];
1846
+ /** The chip's content. The grid draws the chip around it, so keep buttons and links out of it. */
1847
+ renderItem: (item: T) => ReactNode;
1848
+ /** Plain text for the item, read as its name and in announcements. */
1849
+ itemLabel: (item: T) => string;
1850
+ /** Controlled view; leave out and the grid keeps its own, starting at `defaultView`. */
1851
+ view?: CalendarView;
1852
+ defaultView?: CalendarView;
1853
+ onViewChange?: (view: CalendarView) => void;
1854
+ /**
1855
+ * Any day in the month or week shown, `YYYY-MM-DD`. Controlled the same
1856
+ * way as `view`; uncontrolled it starts at today.
1857
+ */
1858
+ anchor?: string;
1859
+ defaultAnchor?: string;
1860
+ onAnchorChange?: (anchor: string) => void;
1861
+ /** Which day is today. Defaults to the viewer's local day. */
1862
+ today?: Date | string;
1863
+ /** 1 (Monday, the default) or 0 (Sunday). */
1864
+ weekStartsOn?: 0 | 1;
1865
+ /**
1866
+ * An item was dropped on another day, or moved with Alt+Arrow keys (Left /
1867
+ * Right a day, Up / Down a week). Omitted, nothing drags. The grid does not
1868
+ * move the item itself; the caller updates `items`.
1869
+ */
1870
+ onMove?: (item: T, day: string) => void;
1871
+ /** Click or Enter / Space on an item. */
1872
+ onOpen?: (item: T) => void;
1873
+ /** The add button on a day, or a click on a day's empty space. Omitted, there is no add. */
1874
+ onAddDay?: (day: string) => void;
1875
+ /** How many items a month cell lists before "+N more". Week cells list every item. */
1876
+ maxPerDay?: number;
1877
+ /** Shown under the grid: what the colors mean. */
1878
+ legend?: ReactNode;
1879
+ /** A line in the footer when some items have no date, e.g. "3 tasks have no due date". */
1880
+ undatedLabel?: (count: number) => ReactNode;
1881
+ /** More controls at the end of the header, beside the view switch. */
1882
+ toolbar?: ReactNode;
1883
+ /** Month and weekday names and the date read out for a day. */
1884
+ locale?: string;
1885
+ labels?: Partial<CalendarGridLabels>;
1886
+ }
1887
+ /** An item's day as `YYYY-MM-DD` (by UTC day), or null when it has none. */
1888
+ declare function calendarDayKey(value: Date | string | null | undefined): string | null;
1889
+ /** `day` moved `n` days. */
1890
+ declare function calendarAddDays(day: string, n: number): string;
1891
+ /**
1892
+ * The days a view shows: six weeks from the week holding the 1st for a month
1893
+ * (always 42, so the grid does not jump in height between months), the
1894
+ * anchor's week for a week. `inRange` is false for the days of the months
1895
+ * either side.
1896
+ */
1897
+ declare function calendarDays(view: CalendarView, anchor: string, weekStartsOn?: 0 | 1): {
1898
+ day: string;
1899
+ inRange: boolean;
1900
+ }[];
1901
+ /** The anchor `delta` months or weeks on. A month lands on its 1st, so a 31st never skips a short month. */
1902
+ declare function calendarShift(view: CalendarView, anchor: string, delta: number): string;
1903
+ /** Items by day, in the order given, and how many had no day. */
1904
+ declare function calendarBuckets<T extends CalendarItem>(items: T[]): {
1905
+ byDay: Map<string, T[]>;
1906
+ undated: number;
1907
+ };
1908
+ /**
1909
+ * A month or week of days with items on them: tasks by due date, beside
1910
+ * read-only milestones.
1911
+ *
1912
+ * Days are calendar days, read by UTC day as `Timeline` reads them, so a due
1913
+ * date is the same square in Detroit and in Silao. Every drag has a keyboard
1914
+ * twin: Alt+Left / Alt+Right moves the focused item a day, Alt+Up / Alt+Down
1915
+ * a week, focus stays on it (the grid follows it into the next month), and
1916
+ * the move is announced. The grid never moves an item on its own; `onMove`
1917
+ * tells the caller, and the caller's `items` say where it is.
1918
+ */
1919
+ declare function CalendarGrid<T extends CalendarItem>({ items, renderItem, itemLabel, view: viewProp, defaultView, onViewChange, anchor: anchorProp, defaultAnchor, onAnchorChange, today: todayProp, weekStartsOn, onMove, onOpen, onAddDay, maxPerDay, legend, undatedLabel, toolbar, locale, labels: labelsProp, className, ...rest }: CalendarGridProps<T>): react.JSX.Element;
1920
+ /** A swatch for a legend under the grid, drawn the way the grid draws an item. */
1921
+ declare function CalendarLegendItem({ color, readOnly, flagged, children, }: Pick<CalendarItem, 'color' | 'readOnly' | 'flagged'> & {
1922
+ children: ReactNode;
1923
+ }): react.JSX.Element;
1924
+
1811
1925
  interface SortState<K extends string> {
1812
1926
  key: K;
1813
1927
  direction: SortDirection;
@@ -2177,4 +2291,4 @@ declare function formatNumber(value: number | null | undefined, decimals?: numbe
2177
2291
  */
2178
2292
  declare function formatPercent(value: number | null | undefined, decimals?: number): string;
2179
2293
 
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 };
2294
+ 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, CalendarGrid, type CalendarGridLabels, type CalendarGridProps, type CalendarItem, CalendarLegendItem, type CalendarView, 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, calendarAddDays, calendarBuckets, calendarDayKey, calendarDays, calendarShift, cn, formatCalendarDate, formatDate, formatDateTime, formatMoney, formatNumber, formatPercent, kanbanLayout, kanbanStep, timelineLayout, useDebouncedValue, useFieldControl, usePagedRows, usePersistentState, useSortState, useToast };
package/dist/index.js CHANGED
@@ -777,13 +777,13 @@ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
777
777
  var Slider = forwardRef10(
778
778
  function Slider2({ showValue = false, formatValue, className, ...rest }, ref) {
779
779
  const values = rest.value ?? rest.defaultValue ?? [rest.min ?? 0];
780
- const format = formatValue ?? ((v) => String(v));
780
+ const format2 = formatValue ?? ((v) => String(v));
781
781
  return /* @__PURE__ */ jsxs12("div", { className: cn("cfi-ui", "cfi-ui-slider-wrap", className), children: [
782
782
  /* @__PURE__ */ jsxs12(RadixSlider.Root, { ref, className: "cfi-ui-slider", ...rest, children: [
783
783
  /* @__PURE__ */ jsx12(RadixSlider.Track, { className: "cfi-ui-slider__track", children: /* @__PURE__ */ jsx12(RadixSlider.Range, { className: "cfi-ui-slider__range" }) }),
784
784
  values.map((_, i) => /* @__PURE__ */ jsx12(RadixSlider.Thumb, { className: "cfi-ui-slider__thumb" }, i))
785
785
  ] }),
786
- showValue && /* @__PURE__ */ jsx12("output", { className: "cfi-ui-slider__value", children: values.map(format).join(" \u2013 ") })
786
+ showValue && /* @__PURE__ */ jsx12("output", { className: "cfi-ui-slider__value", children: values.map(format2).join(" \u2013 ") })
787
787
  ] });
788
788
  }
789
789
  );
@@ -2673,10 +2673,347 @@ function KanbanBoard({
2673
2673
  );
2674
2674
  }
2675
2675
 
2676
+ // src/CalendarGrid.tsx
2677
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
2678
+ import { ChevronLeft, ChevronRight, Plus as Plus2 } from "lucide-react";
2679
+
2680
+ // src/CalendarGrid.css
2681
+ styleInject("@layer components {\n :where(.cfi-ui-calendar) {\n min-width: 0;\n overflow: hidden;\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: var(--cfi-ui-shadow-xs);\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-calendar__header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--cfi-ui-space-xs);\n padding: var(--cfi-ui-space-sm) var(--cfi-ui-space-md);\n border-bottom: 1px solid var(--cfi-ui-border);\n }\n .cfi-ui-calendar__title {\n margin: 0 0 0 var(--cfi-ui-space-xs);\n font-size: var(--cfi-ui-text-lg);\n font-weight: var(--cfi-ui-weight-semibold);\n line-height: var(--cfi-ui-leading-tight);\n }\n .cfi-ui-calendar__tools {\n display: flex;\n align-items: center;\n gap: var(--cfi-ui-space-sm);\n margin-left: auto;\n }\n .cfi-ui-calendar__scroll {\n overflow-x: auto;\n }\n .cfi-ui-calendar__weekdays,\n .cfi-ui-calendar__grid {\n display: grid;\n grid-template-columns: repeat(7, minmax(7rem, 1fr));\n }\n .cfi-ui-calendar__weekdays {\n border-bottom: 1px solid var(--cfi-ui-border);\n background: var(--cfi-ui-surface-sunken);\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-semibold);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-calendar__weekdays > div {\n padding: var(--cfi-ui-space-2xs) var(--cfi-ui-space-sm);\n }\n .cfi-ui-calendar__day {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-3xs);\n min-width: 0;\n min-height: 7rem;\n padding: var(--cfi-ui-space-2xs);\n border-right: 1px solid var(--cfi-ui-border);\n border-bottom: 1px solid var(--cfi-ui-border);\n transition: background var(--cfi-ui-transition-fast), box-shadow var(--cfi-ui-transition-fast);\n }\n .cfi-ui-calendar__day:nth-child(7n) {\n border-right: 0;\n }\n .cfi-ui-calendar[data-view=month] .cfi-ui-calendar__day:nth-last-child(-n+7),\n .cfi-ui-calendar[data-view=week] .cfi-ui-calendar__day {\n border-bottom: 0;\n }\n .cfi-ui-calendar[data-view=week] .cfi-ui-calendar__day {\n min-height: 20rem;\n }\n .cfi-ui-calendar__day[data-outside] {\n background: var(--cfi-ui-surface-sunken);\n }\n .cfi-ui-calendar__day[data-addable] {\n cursor: pointer;\n }\n .cfi-ui-calendar__day[data-addable]:hover {\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-calendar__day[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-calendar__day-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n .cfi-ui-calendar__date {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.5rem;\n height: 1.5rem;\n border-radius: var(--cfi-ui-radius-full);\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-medium);\n font-variant-numeric: tabular-nums;\n }\n .cfi-ui-calendar__day[data-outside] .cfi-ui-calendar__date {\n color: var(--cfi-ui-content-subtle);\n }\n .cfi-ui-calendar__day[data-today] .cfi-ui-calendar__date {\n background: var(--cfi-ui-accent);\n color: var(--cfi-ui-accent-fg);\n font-weight: var(--cfi-ui-weight-semibold);\n }\n .cfi-ui-calendar__add {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n padding: 0;\n border: 0;\n border-radius: var(--cfi-ui-radius-sm);\n background: none;\n color: var(--cfi-ui-accent-text);\n cursor: pointer;\n opacity: 0;\n }\n .cfi-ui-calendar__add svg {\n width: 0.875rem;\n height: 0.875rem;\n }\n .cfi-ui-calendar__day:hover .cfi-ui-calendar__add,\n .cfi-ui-calendar__add:focus-visible {\n opacity: 1;\n }\n .cfi-ui-calendar__add:hover {\n background: var(--cfi-ui-accent-soft);\n }\n .cfi-ui-calendar__add:focus-visible,\n .cfi-ui-calendar__more: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-calendar__items {\n display: flex;\n flex-direction: column;\n gap: var(--cfi-ui-space-3xs);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n .cfi-ui-calendar__item {\n overflow: hidden;\n padding: 0.125rem var(--cfi-ui-space-2xs);\n border: 1px solid transparent;\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-calendar-item, var(--cfi-ui-accent));\n color: var(--cfi-ui-content-inverse);\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-medium);\n line-height: var(--cfi-ui-leading-snug);\n text-overflow: ellipsis;\n white-space: nowrap;\n box-shadow: var(--cfi-ui-shadow-xs);\n transition: filter var(--cfi-ui-transition-fast), opacity var(--cfi-ui-transition-fast);\n }\n .cfi-ui-calendar__item[role=button] {\n cursor: pointer;\n }\n .cfi-ui-calendar__item[data-movable] {\n cursor: grab;\n }\n .cfi-ui-calendar__item[data-movable]:active {\n cursor: grabbing;\n }\n .cfi-ui-calendar__item[role=button]:hover {\n filter: brightness(1.1);\n }\n .cfi-ui-calendar__item: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-calendar__item[data-read-only],\n .cfi-ui-calendar__swatch[data-read-only] {\n border: 1px dashed var(--cfi-ui-calendar-item, var(--cfi-ui-accent));\n background: var(--cfi-ui-surface);\n color: var(--cfi-ui-calendar-item, var(--cfi-ui-accent-text));\n box-shadow: none;\n }\n .cfi-ui-calendar__item[data-read-only][role=button]:hover {\n filter: none;\n background: var(--cfi-ui-surface-hover);\n }\n .cfi-ui-calendar__item[data-flagged],\n .cfi-ui-calendar__swatch[data-flagged] {\n box-shadow: 0 0 0 1px var(--cfi-ui-surface), 0 0 0 3px var(--cfi-ui-error);\n }\n .cfi-ui-calendar__item[data-done] {\n opacity: 0.6;\n text-decoration: line-through;\n }\n .cfi-ui-calendar__item[data-dragging] {\n opacity: 0.4;\n }\n .cfi-ui-calendar__more {\n align-self: flex-start;\n padding: 0 var(--cfi-ui-space-2xs);\n border: 0;\n border-radius: var(--cfi-ui-radius-sm);\n background: none;\n font: inherit;\n font-size: var(--cfi-ui-text-xs);\n font-weight: var(--cfi-ui-weight-medium);\n color: var(--cfi-ui-content-muted);\n cursor: pointer;\n }\n .cfi-ui-calendar__more:hover {\n background: var(--cfi-ui-surface-hover);\n color: var(--cfi-ui-content);\n }\n .cfi-ui-calendar__footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--cfi-ui-space-2xs) var(--cfi-ui-space-md);\n padding: var(--cfi-ui-space-sm) var(--cfi-ui-space-md);\n border-top: 1px solid var(--cfi-ui-border);\n font-size: var(--cfi-ui-text-xs);\n color: var(--cfi-ui-content-muted);\n }\n .cfi-ui-calendar__undated {\n margin-left: auto;\n }\n .cfi-ui-calendar__legend-item {\n display: inline-flex;\n align-items: center;\n gap: var(--cfi-ui-space-2xs);\n }\n .cfi-ui-calendar__swatch {\n flex: none;\n width: 1.25rem;\n height: 0.75rem;\n border: 1px solid transparent;\n border-radius: var(--cfi-ui-radius-sm);\n background: var(--cfi-ui-calendar-item, var(--cfi-ui-accent));\n }\n .cfi-ui-calendar__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-calendar__day,\n .cfi-ui-calendar__item {\n transition: none;\n }\n }\n}\n");
2682
+
2683
+ // src/CalendarGrid.tsx
2684
+ import { jsx as jsx30, jsxs as jsxs30 } from "react/jsx-runtime";
2685
+ var DEFAULT_LABELS = {
2686
+ today: "Today",
2687
+ previous: (view) => `Previous ${view}`,
2688
+ next: (view) => `Next ${view}`,
2689
+ month: "Month",
2690
+ week: "Week",
2691
+ view: "Calendar view",
2692
+ add: (day) => `Add on ${day}`,
2693
+ more: (count) => `+${count} more`,
2694
+ less: "Show fewer",
2695
+ moved: (item, day) => `Moved ${item} to ${day}`
2696
+ };
2697
+ var DAY2 = 864e5;
2698
+ function ymd2(t) {
2699
+ return new Date(t).toISOString().slice(0, 10);
2700
+ }
2701
+ function calendarDayKey(value) {
2702
+ const t = timelineTime(value, true);
2703
+ return t === null ? null : ymd2(t);
2704
+ }
2705
+ function calendarAddDays(day, n) {
2706
+ return ymd2(timelineTime(day, true) + n * DAY2);
2707
+ }
2708
+ function calendarDays(view, anchor, weekStartsOn = 1) {
2709
+ const a = new Date(timelineTime(anchor, true));
2710
+ const from = view === "month" ? Date.UTC(a.getUTCFullYear(), a.getUTCMonth(), 1) : a.getTime();
2711
+ const back = (new Date(from).getUTCDay() - weekStartsOn + 7) % 7;
2712
+ const start = from - back * DAY2;
2713
+ const month = a.getUTCMonth();
2714
+ return Array.from({ length: view === "month" ? 42 : 7 }, (_, i) => {
2715
+ const t = start + i * DAY2;
2716
+ return { day: ymd2(t), inRange: view === "week" || new Date(t).getUTCMonth() === month };
2717
+ });
2718
+ }
2719
+ function calendarShift(view, anchor, delta) {
2720
+ if (view === "week") return calendarAddDays(anchor, delta * 7);
2721
+ const a = new Date(timelineTime(anchor, true));
2722
+ return ymd2(Date.UTC(a.getUTCFullYear(), a.getUTCMonth() + delta, 1));
2723
+ }
2724
+ function calendarBuckets(items) {
2725
+ const byDay = /* @__PURE__ */ new Map();
2726
+ let undated = 0;
2727
+ for (const item of items) {
2728
+ const day = calendarDayKey(item.date);
2729
+ if (day === null) {
2730
+ undated += 1;
2731
+ continue;
2732
+ }
2733
+ const list = byDay.get(day);
2734
+ if (list) list.push(item);
2735
+ else byDay.set(day, [item]);
2736
+ }
2737
+ return { byDay, undated };
2738
+ }
2739
+ function localToday() {
2740
+ const now = /* @__PURE__ */ new Date();
2741
+ return ymd2(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
2742
+ }
2743
+ function format(day, locale, options) {
2744
+ return new Date(timelineTime(day, true)).toLocaleDateString(locale, { ...options, timeZone: "UTC" });
2745
+ }
2746
+ function CalendarGrid({
2747
+ items,
2748
+ renderItem,
2749
+ itemLabel,
2750
+ view: viewProp,
2751
+ defaultView = "month",
2752
+ onViewChange,
2753
+ anchor: anchorProp,
2754
+ defaultAnchor,
2755
+ onAnchorChange,
2756
+ today: todayProp,
2757
+ weekStartsOn = 1,
2758
+ onMove,
2759
+ onOpen,
2760
+ onAddDay,
2761
+ maxPerDay = 3,
2762
+ legend,
2763
+ undatedLabel,
2764
+ toolbar,
2765
+ locale = "en-US",
2766
+ labels: labelsProp,
2767
+ className,
2768
+ ...rest
2769
+ }) {
2770
+ const labels = { ...DEFAULT_LABELS, ...labelsProp };
2771
+ const today = todayProp !== void 0 && calendarDayKey(todayProp) || localToday();
2772
+ const [ownView, setOwnView] = useState7(defaultView);
2773
+ const [ownAnchor, setOwnAnchor] = useState7(() => defaultAnchor && calendarDayKey(defaultAnchor) || today);
2774
+ const view = viewProp ?? ownView;
2775
+ const anchor = anchorProp && calendarDayKey(anchorProp) || ownAnchor;
2776
+ const setView = (next) => {
2777
+ if (viewProp === void 0) setOwnView(next);
2778
+ onViewChange?.(next);
2779
+ };
2780
+ const setAnchor = (next) => {
2781
+ if (anchorProp === void 0) setOwnAnchor(next);
2782
+ onAnchorChange?.(next);
2783
+ };
2784
+ const [drag, setDrag] = useState7(null);
2785
+ const [over, setOver] = useState7(null);
2786
+ const [expanded, setExpanded] = useState7(/* @__PURE__ */ new Set());
2787
+ const [announcement, setAnnouncement] = useState7("");
2788
+ const rootRef = useRef5(null);
2789
+ const refocus = useRef5(null);
2790
+ useEffect5(() => {
2791
+ const id = refocus.current;
2792
+ if (!id || !rootRef.current) return;
2793
+ const chip = rootRef.current.querySelector(`[data-calendar-item="${CSS.escape(id)}"]`);
2794
+ if (chip) {
2795
+ chip.focus();
2796
+ refocus.current = null;
2797
+ }
2798
+ });
2799
+ const days = calendarDays(view, anchor, weekStartsOn);
2800
+ const { byDay, undated } = calendarBuckets(items);
2801
+ const byId = new Map(items.map((i) => [i.id, i]));
2802
+ const shown = new Set(days.map((d) => d.day));
2803
+ const showsToday = shown.has(today) && (view === "week" || days.find((d) => d.day === today)?.inRange);
2804
+ const longDay = (day) => format(day, locale, { weekday: "long", month: "long", day: "numeric", year: "numeric" });
2805
+ const title = view === "month" ? format(anchor, locale, { month: "long", year: "numeric" }) : `${format(days[0].day, locale, { month: "short", day: "numeric" })} \u2013 ${format(days[6].day, locale, {
2806
+ month: "short",
2807
+ day: "numeric",
2808
+ year: "numeric"
2809
+ })}`;
2810
+ const move = (item, day, viaKeyboard) => {
2811
+ if (!onMove || item.readOnly || calendarDayKey(item.date) === day) return;
2812
+ if (viaKeyboard) {
2813
+ refocus.current = item.id;
2814
+ if (!shown.has(day) || view === "month" && !days.find((d) => d.day === day)?.inRange) setAnchor(day);
2815
+ }
2816
+ onMove(item, day);
2817
+ setAnnouncement(labels.moved(itemLabel(item), longDay(day)));
2818
+ };
2819
+ const endDrag = () => {
2820
+ setDrag(null);
2821
+ setOver(null);
2822
+ };
2823
+ return /* @__PURE__ */ jsxs30("div", { ref: rootRef, className: cn("cfi-ui", "cfi-ui-calendar", className), "data-view": view, ...rest, children: [
2824
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-calendar__header", children: [
2825
+ /* @__PURE__ */ jsx30(Button, { size: "sm", variant: "secondary", onClick: () => setAnchor(today), disabled: showsToday || void 0, children: labels.today }),
2826
+ /* @__PURE__ */ jsx30(
2827
+ IconButton,
2828
+ {
2829
+ size: "sm",
2830
+ variant: "ghost",
2831
+ icon: /* @__PURE__ */ jsx30(ChevronLeft, { "aria-hidden": "true" }),
2832
+ label: labels.previous(view),
2833
+ onClick: () => setAnchor(calendarShift(view, anchor, -1))
2834
+ }
2835
+ ),
2836
+ /* @__PURE__ */ jsx30(
2837
+ IconButton,
2838
+ {
2839
+ size: "sm",
2840
+ variant: "ghost",
2841
+ icon: /* @__PURE__ */ jsx30(ChevronRight, { "aria-hidden": "true" }),
2842
+ label: labels.next(view),
2843
+ onClick: () => setAnchor(calendarShift(view, anchor, 1))
2844
+ }
2845
+ ),
2846
+ /* @__PURE__ */ jsx30("h2", { className: "cfi-ui-calendar__title", "aria-live": "polite", children: title }),
2847
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-calendar__tools", children: [
2848
+ toolbar,
2849
+ /* @__PURE__ */ jsx30(
2850
+ ToggleGroup,
2851
+ {
2852
+ size: "sm",
2853
+ "aria-label": labels.view,
2854
+ value: view,
2855
+ onValueChange: setView,
2856
+ options: [
2857
+ { value: "month", label: labels.month },
2858
+ { value: "week", label: labels.week }
2859
+ ]
2860
+ }
2861
+ )
2862
+ ] })
2863
+ ] }),
2864
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-calendar__scroll", children: [
2865
+ /* @__PURE__ */ jsx30("div", { className: "cfi-ui-calendar__weekdays", "aria-hidden": "true", children: days.slice(0, 7).map(({ day }) => /* @__PURE__ */ jsx30("div", { children: format(day, locale, { weekday: "short" }) }, day)) }),
2866
+ /* @__PURE__ */ jsx30("div", { className: "cfi-ui-calendar__grid", children: days.map(({ day, inRange }) => {
2867
+ const dayItems = byDay.get(day) ?? [];
2868
+ const capped = view === "month" && !expanded.has(day) && dayItems.length > maxPerDay + 1;
2869
+ const visible = capped ? dayItems.slice(0, maxPerDay) : dayItems;
2870
+ const accepts = drag !== null && calendarDayKey(byId.get(drag)?.date) !== day;
2871
+ const name = longDay(day);
2872
+ return /* @__PURE__ */ jsxs30(
2873
+ "div",
2874
+ {
2875
+ className: "cfi-ui-calendar__day",
2876
+ "data-outside": !inRange || void 0,
2877
+ "data-today": day === today || void 0,
2878
+ "data-over": over === day || void 0,
2879
+ "data-addable": onAddDay ? true : void 0,
2880
+ onClick: (e) => {
2881
+ if (onAddDay && (e.target === e.currentTarget || e.target.dataset.calendarEmpty)) onAddDay(day);
2882
+ },
2883
+ onDragOver: (e) => {
2884
+ if (!accepts) return;
2885
+ e.preventDefault();
2886
+ e.dataTransfer.dropEffect = "move";
2887
+ if (over !== day) setOver(day);
2888
+ },
2889
+ onDragLeave: (e) => {
2890
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver((o) => o === day ? null : o);
2891
+ },
2892
+ onDrop: (e) => {
2893
+ e.preventDefault();
2894
+ const item = drag ? byId.get(drag) : void 0;
2895
+ if (item && accepts) move(item, day, false);
2896
+ endDrag();
2897
+ },
2898
+ children: [
2899
+ /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-calendar__day-head", "data-calendar-empty": "1", children: [
2900
+ /* @__PURE__ */ jsx30("span", { className: "cfi-ui-calendar__date", "data-calendar-empty": "1", "aria-current": day === today ? "date" : void 0, children: Number(day.slice(8)) }),
2901
+ onAddDay ? /* @__PURE__ */ jsx30(
2902
+ "button",
2903
+ {
2904
+ type: "button",
2905
+ className: "cfi-ui-calendar__add",
2906
+ "aria-label": labels.add(name),
2907
+ title: labels.add(name),
2908
+ onClick: (e) => {
2909
+ e.stopPropagation();
2910
+ onAddDay(day);
2911
+ },
2912
+ children: /* @__PURE__ */ jsx30(Plus2, { "aria-hidden": "true" })
2913
+ }
2914
+ ) : null
2915
+ ] }),
2916
+ /* @__PURE__ */ jsx30("ul", { className: "cfi-ui-calendar__items", "aria-label": name, "data-calendar-empty": "1", children: visible.map((item) => {
2917
+ const movable = onMove && !item.readOnly;
2918
+ return /* @__PURE__ */ jsx30("li", { children: /* @__PURE__ */ jsx30(
2919
+ "div",
2920
+ {
2921
+ className: "cfi-ui-calendar__item",
2922
+ "data-calendar-item": item.id,
2923
+ "data-read-only": item.readOnly || void 0,
2924
+ "data-flagged": item.flagged || void 0,
2925
+ "data-done": item.done || void 0,
2926
+ "data-dragging": drag === item.id || void 0,
2927
+ "data-movable": movable || void 0,
2928
+ style: item.color ? { "--cfi-ui-calendar-item": item.color } : void 0,
2929
+ title: itemLabel(item),
2930
+ role: onOpen ? "button" : void 0,
2931
+ tabIndex: onOpen || movable ? 0 : void 0,
2932
+ "aria-keyshortcuts": movable ? "Alt+ArrowLeft Alt+ArrowRight Alt+ArrowUp Alt+ArrowDown" : void 0,
2933
+ draggable: movable || void 0,
2934
+ onDragStart: (e) => {
2935
+ e.stopPropagation();
2936
+ e.dataTransfer.effectAllowed = "move";
2937
+ e.dataTransfer.setData("text/plain", item.id);
2938
+ setTimeout(() => setDrag(item.id), 0);
2939
+ },
2940
+ onDragEnd: endDrag,
2941
+ onClick: (e) => {
2942
+ e.stopPropagation();
2943
+ onOpen?.(item);
2944
+ },
2945
+ onKeyDown: (e) => {
2946
+ if (e.target !== e.currentTarget) return;
2947
+ const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
2948
+ if (movable && e.altKey && step !== void 0) {
2949
+ e.preventDefault();
2950
+ move(item, calendarAddDays(day, step), true);
2951
+ return;
2952
+ }
2953
+ if (onOpen && (e.key === "Enter" || e.key === " ")) {
2954
+ e.preventDefault();
2955
+ onOpen(item);
2956
+ }
2957
+ },
2958
+ children: renderItem(item)
2959
+ }
2960
+ ) }, item.id);
2961
+ }) }),
2962
+ capped || expanded.has(day) ? /* @__PURE__ */ jsx30(
2963
+ "button",
2964
+ {
2965
+ type: "button",
2966
+ className: "cfi-ui-calendar__more",
2967
+ onClick: (e) => {
2968
+ e.stopPropagation();
2969
+ setExpanded((all) => {
2970
+ const next = new Set(all);
2971
+ if (next.has(day)) next.delete(day);
2972
+ else next.add(day);
2973
+ return next;
2974
+ });
2975
+ },
2976
+ children: capped ? labels.more(dayItems.length - visible.length) : labels.less
2977
+ }
2978
+ ) : null
2979
+ ]
2980
+ },
2981
+ day
2982
+ );
2983
+ }) })
2984
+ ] }),
2985
+ legend || undated > 0 && undatedLabel ? /* @__PURE__ */ jsxs30("div", { className: "cfi-ui-calendar__footer", children: [
2986
+ legend,
2987
+ undated > 0 && undatedLabel ? /* @__PURE__ */ jsx30("span", { className: "cfi-ui-calendar__undated", children: undatedLabel(undated) }) : null
2988
+ ] }) : null,
2989
+ /* @__PURE__ */ jsx30("div", { className: "cfi-ui-calendar__live", role: "status", "aria-live": "polite", children: announcement })
2990
+ ] });
2991
+ }
2992
+ function CalendarLegendItem({
2993
+ color,
2994
+ readOnly,
2995
+ flagged,
2996
+ children
2997
+ }) {
2998
+ return /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-calendar__legend-item", children: [
2999
+ /* @__PURE__ */ jsx30(
3000
+ "span",
3001
+ {
3002
+ className: "cfi-ui-calendar__swatch",
3003
+ "data-read-only": readOnly || void 0,
3004
+ "data-flagged": flagged || void 0,
3005
+ style: color ? { "--cfi-ui-calendar-item": color } : void 0,
3006
+ "aria-hidden": "true"
3007
+ }
3008
+ ),
3009
+ children
3010
+ ] });
3011
+ }
3012
+
2676
3013
  // src/useSortState.ts
2677
- import { useCallback as useCallback3, useMemo as useMemo5, useState as useState7 } from "react";
3014
+ import { useCallback as useCallback3, useMemo as useMemo5, useState as useState8 } from "react";
2678
3015
  function useSortState(initialKey, initialDirection = "asc") {
2679
- const [sort, setSort] = useState7({ key: initialKey, direction: initialDirection });
3016
+ const [sort, setSort] = useState8({ key: initialKey, direction: initialDirection });
2680
3017
  const toggle = useCallback3((key) => {
2681
3018
  setSort(
2682
3019
  (current) => current.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" }
@@ -2713,21 +3050,21 @@ function useSortState(initialKey, initialDirection = "asc") {
2713
3050
  }
2714
3051
 
2715
3052
  // src/SearchInput.tsx
2716
- import { forwardRef as forwardRef18, useEffect as useEffect5, useRef as useRef5, useState as useState8 } from "react";
3053
+ import { forwardRef as forwardRef18, useEffect as useEffect6, useRef as useRef6, useState as useState9 } from "react";
2717
3054
  import { Search as Search2, X as X6 } from "lucide-react";
2718
3055
 
2719
3056
  // src/SearchInput.css
2720
3057
  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");
2721
3058
 
2722
3059
  // src/SearchInput.tsx
2723
- import { jsx as jsx30 } from "react/jsx-runtime";
3060
+ import { jsx as jsx31 } from "react/jsx-runtime";
2724
3061
  var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay = 250, placeholder = "Search\u2026", clearLabel = "Clear search", ...rest }, ref) {
2725
- const [local, setLocal] = useState8(value);
2726
- const timer = useRef5(void 0);
2727
- const latest = useRef5(onChange);
3062
+ const [local, setLocal] = useState9(value);
3063
+ const timer = useRef6(void 0);
3064
+ const latest = useRef6(onChange);
2728
3065
  latest.current = onChange;
2729
- useEffect5(() => setLocal(value), [value]);
2730
- useEffect5(() => () => clearTimeout(timer.current), []);
3066
+ useEffect6(() => setLocal(value), [value]);
3067
+ useEffect6(() => () => clearTimeout(timer.current), []);
2731
3068
  const push = (next) => {
2732
3069
  setLocal(next);
2733
3070
  clearTimeout(timer.current);
@@ -2739,7 +3076,7 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2739
3076
  setLocal("");
2740
3077
  latest.current("");
2741
3078
  };
2742
- return /* @__PURE__ */ jsx30(
3079
+ return /* @__PURE__ */ jsx31(
2743
3080
  Input,
2744
3081
  {
2745
3082
  ...rest,
@@ -2748,20 +3085,20 @@ var SearchInput = forwardRef18(function SearchInput2({ value, onChange, delay =
2748
3085
  value: local,
2749
3086
  placeholder,
2750
3087
  onChange: (e) => push(e.target.value),
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
3088
+ prefix: /* @__PURE__ */ jsx31(Search2, { "aria-hidden": "true" }),
3089
+ suffix: local ? /* @__PURE__ */ jsx31("button", { type: "button", className: "cfi-ui-search__clear", onClick: clear, "aria-label": clearLabel, children: /* @__PURE__ */ jsx31(X6, { "aria-hidden": "true" }) }) : void 0
2753
3090
  }
2754
3091
  );
2755
3092
  });
2756
3093
 
2757
3094
  // src/Pagination.tsx
2758
- import { ChevronLeft, ChevronRight } from "lucide-react";
3095
+ import { ChevronLeft as ChevronLeft2, ChevronRight as ChevronRight2 } from "lucide-react";
2759
3096
 
2760
3097
  // src/Pagination.css
2761
3098
  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");
2762
3099
 
2763
3100
  // src/Pagination.tsx
2764
- import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
3101
+ import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
2765
3102
  function Pagination({
2766
3103
  page,
2767
3104
  pageSize,
@@ -2777,12 +3114,12 @@ function Pagination({
2777
3114
  const from = (current - 1) * pageSize + 1;
2778
3115
  const to = Math.min(current * pageSize, totalItems);
2779
3116
  const range = labels?.range?.(from, to, totalItems) ?? `${from}\u2013${to} of ${totalItems.toLocaleString()}`;
2780
- return /* @__PURE__ */ jsxs30("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
3117
+ return /* @__PURE__ */ jsxs31("nav", { className: cn("cfi-ui", "cfi-ui-pagination", className), "aria-label": "Pagination", children: [
2781
3118
  showRange && // aria-live so a screen-reader user hears where they landed; paging
2782
3119
  // otherwise moves focus nowhere and announces nothing.
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(
3120
+ /* @__PURE__ */ jsx32("span", { className: "cfi-ui-pagination__range", "aria-live": "polite", children: range }),
3121
+ /* @__PURE__ */ jsxs31("div", { className: "cfi-ui-pagination__controls", children: [
3122
+ /* @__PURE__ */ jsx32(
2786
3123
  "button",
2787
3124
  {
2788
3125
  type: "button",
@@ -2790,15 +3127,15 @@ function Pagination({
2790
3127
  onClick: () => onPageChange(current - 1),
2791
3128
  disabled: current <= 1,
2792
3129
  "aria-label": labels?.previous ?? "Previous page",
2793
- children: /* @__PURE__ */ jsx31(ChevronLeft, { "aria-hidden": "true" })
3130
+ children: /* @__PURE__ */ jsx32(ChevronLeft2, { "aria-hidden": "true" })
2794
3131
  }
2795
3132
  ),
2796
- /* @__PURE__ */ jsxs30("span", { className: "cfi-ui-pagination__page", children: [
3133
+ /* @__PURE__ */ jsxs31("span", { className: "cfi-ui-pagination__page", children: [
2797
3134
  current,
2798
3135
  " / ",
2799
3136
  pageCount
2800
3137
  ] }),
2801
- /* @__PURE__ */ jsx31(
3138
+ /* @__PURE__ */ jsx32(
2802
3139
  "button",
2803
3140
  {
2804
3141
  type: "button",
@@ -2806,7 +3143,7 @@ function Pagination({
2806
3143
  onClick: () => onPageChange(current + 1),
2807
3144
  disabled: current >= pageCount,
2808
3145
  "aria-label": labels?.next ?? "Next page",
2809
- children: /* @__PURE__ */ jsx31(ChevronRight, { "aria-hidden": "true" })
3146
+ children: /* @__PURE__ */ jsx32(ChevronRight2, { "aria-hidden": "true" })
2810
3147
  }
2811
3148
  )
2812
3149
  ] })
@@ -2817,23 +3154,23 @@ function Pagination({
2817
3154
  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");
2818
3155
 
2819
3156
  // src/ListToolbar.tsx
2820
- import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
3157
+ import { jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
2821
3158
  function ListToolbar({ search, filters, actions, className }) {
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 })
3159
+ return /* @__PURE__ */ jsxs32("div", { className: cn("cfi-ui", "cfi-ui-list-toolbar", className), children: [
3160
+ search && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-list-toolbar__search", children: search }),
3161
+ filters && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-list-toolbar__filters", children: filters }),
3162
+ actions && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-list-toolbar__actions", children: actions })
2826
3163
  ] });
2827
3164
  }
2828
3165
 
2829
3166
  // src/PageHeader.tsx
2830
- import { ArrowLeft, ChevronRight as ChevronRight2 } from "lucide-react";
3167
+ import { ArrowLeft, ChevronRight as ChevronRight3 } from "lucide-react";
2831
3168
 
2832
3169
  // src/PageHeader.css
2833
3170
  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");
2834
3171
 
2835
3172
  // src/PageHeader.tsx
2836
- import { jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
3173
+ import { jsx as jsx34, jsxs as jsxs33 } from "react/jsx-runtime";
2837
3174
  function PageHeader({
2838
3175
  title,
2839
3176
  description,
@@ -2847,32 +3184,32 @@ function PageHeader({
2847
3184
  className
2848
3185
  }) {
2849
3186
  const Heading = `h${level}`;
2850
- return /* @__PURE__ */ jsxs32(
3187
+ return /* @__PURE__ */ jsxs33(
2851
3188
  "div",
2852
3189
  {
2853
3190
  className: cn("cfi-ui", "cfi-ui-page-header", className),
2854
3191
  "data-bordered": bordered ? "" : void 0,
2855
3192
  children: [
2856
- breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx33("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
3193
+ breadcrumbs && breadcrumbs.length > 0 && /* @__PURE__ */ jsx34("nav", { className: "cfi-ui-page-header__crumbs", "aria-label": "Breadcrumb", children: breadcrumbs.map((crumb, i) => {
2857
3194
  const last = i === breadcrumbs.length - 1;
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 })
3195
+ const label = crumb.render ? crumb.render(crumb.label) : crumb.href ? /* @__PURE__ */ jsx34("a", { href: crumb.href, children: crumb.label }) : crumb.label;
3196
+ return /* @__PURE__ */ jsxs33("span", { className: "cfi-ui-page-header__crumb", "data-current": last ? "" : void 0, children: [
3197
+ i > 0 && /* @__PURE__ */ jsx34(ChevronRight3, { className: "cfi-ui-page-header__crumb-sep", "aria-hidden": "true" }),
3198
+ /* @__PURE__ */ jsx34("span", { "aria-current": last ? "page" : void 0, children: label })
2862
3199
  ] }, i);
2863
3200
  }) }),
2864
- onBack && /* @__PURE__ */ jsxs32("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
2865
- /* @__PURE__ */ jsx33(ArrowLeft, { "aria-hidden": "true" }),
3201
+ onBack && /* @__PURE__ */ jsxs33("button", { type: "button", className: "cfi-ui-page-header__back", onClick: onBack, children: [
3202
+ /* @__PURE__ */ jsx34(ArrowLeft, { "aria-hidden": "true" }),
2866
3203
  backLabel
2867
3204
  ] }),
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 })
3205
+ /* @__PURE__ */ jsxs33("div", { className: "cfi-ui-page-header__row", children: [
3206
+ /* @__PURE__ */ jsxs33("div", { className: "cfi-ui-page-header__titles", children: [
3207
+ /* @__PURE__ */ jsx34(Heading, { className: "cfi-ui-page-header__title", children: title }),
3208
+ description && /* @__PURE__ */ jsx34("p", { className: "cfi-ui-page-header__description", children: description })
2872
3209
  ] }),
2873
- actions && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__actions", children: actions })
3210
+ actions && /* @__PURE__ */ jsx34("div", { className: "cfi-ui-page-header__actions", children: actions })
2874
3211
  ] }),
2875
- children && /* @__PURE__ */ jsx33("div", { className: "cfi-ui-page-header__meta", children })
3212
+ children && /* @__PURE__ */ jsx34("div", { className: "cfi-ui-page-header__meta", children })
2876
3213
  ]
2877
3214
  }
2878
3215
  );
@@ -2885,7 +3222,7 @@ import { X as X7 } from "lucide-react";
2885
3222
  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");
2886
3223
 
2887
3224
  // src/SelectionBar.tsx
2888
- import { jsx as jsx34, jsxs as jsxs33 } from "react/jsx-runtime";
3225
+ import { jsx as jsx35, jsxs as jsxs34 } from "react/jsx-runtime";
2889
3226
  function SelectionBar({
2890
3227
  count,
2891
3228
  children,
@@ -2897,7 +3234,7 @@ function SelectionBar({
2897
3234
  className
2898
3235
  }) {
2899
3236
  if (count === 0) return null;
2900
- return /* @__PURE__ */ jsxs33(
3237
+ return /* @__PURE__ */ jsxs34(
2901
3238
  "div",
2902
3239
  {
2903
3240
  role: "status",
@@ -2906,9 +3243,9 @@ function SelectionBar({
2906
3243
  "data-busy": busy ? "" : void 0,
2907
3244
  className: cn("cfi-ui", "cfi-ui-selection-bar", className),
2908
3245
  children: [
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(
3246
+ /* @__PURE__ */ jsx35("span", { className: "cfi-ui-selection-bar__count", children: label(count) }),
3247
+ children && /* @__PURE__ */ jsx35("div", { className: "cfi-ui-selection-bar__actions", children }),
3248
+ onClear && /* @__PURE__ */ jsxs34(
2912
3249
  "button",
2913
3250
  {
2914
3251
  type: "button",
@@ -2916,7 +3253,7 @@ function SelectionBar({
2916
3253
  onClick: onClear,
2917
3254
  disabled: busy,
2918
3255
  children: [
2919
- /* @__PURE__ */ jsx34(X7, { "aria-hidden": "true" }),
3256
+ /* @__PURE__ */ jsx35(X7, { "aria-hidden": "true" }),
2920
3257
  clearLabel
2921
3258
  ]
2922
3259
  }
@@ -2927,14 +3264,14 @@ function SelectionBar({
2927
3264
  }
2928
3265
 
2929
3266
  // src/usePagedRows.ts
2930
- import { useEffect as useEffect6, useMemo as useMemo6, useState as useState9 } from "react";
3267
+ import { useEffect as useEffect7, useMemo as useMemo6, useState as useState10 } from "react";
2931
3268
  function usePagedRows(all, initialPageSize = 25) {
2932
- const [page, setPage] = useState9(1);
2933
- const [pageSize, setSize] = useState9(initialPageSize);
3269
+ const [page, setPage] = useState10(1);
3270
+ const [pageSize, setSize] = useState10(initialPageSize);
2934
3271
  const totalItems = all.length;
2935
3272
  const pageCount = Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize)));
2936
3273
  const clamped = Math.min(Math.max(1, page), pageCount);
2937
- useEffect6(() => {
3274
+ useEffect7(() => {
2938
3275
  if (clamped !== page) setPage(clamped);
2939
3276
  }, [clamped, page]);
2940
3277
  const rows = useMemo6(
@@ -2961,6 +3298,8 @@ export {
2961
3298
  BootScreen,
2962
3299
  Button,
2963
3300
  ButtonGroup,
3301
+ CalendarGrid,
3302
+ CalendarLegendItem,
2964
3303
  Card,
2965
3304
  CardBody,
2966
3305
  CardDescription,
@@ -3080,6 +3419,11 @@ export {
3080
3419
  TooltipProvider,
3081
3420
  TooltipRoot,
3082
3421
  TooltipTrigger,
3422
+ calendarAddDays,
3423
+ calendarBuckets,
3424
+ calendarDayKey,
3425
+ calendarDays,
3426
+ calendarShift,
3083
3427
  cn,
3084
3428
  formatCalendarDate,
3085
3429
  formatDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campfire-interactive/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",