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