@particle-academy/react-fancy 2.7.0 → 2.8.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.cjs +275 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -13
- package/dist/index.d.ts +87 -13
- package/dist/index.js +275 -33
- package/dist/index.js.map +1 -1
- package/docs/AccordionPanel.md +6 -0
- package/docs/Kanban.md +115 -21
- package/package.json +1 -1
package/docs/AccordionPanel.md
CHANGED
|
@@ -118,6 +118,7 @@ const [open, setOpen] = useState<string[]>(["wishlist"]);
|
|
|
118
118
|
| `pinned` | `boolean` | `false` | Never collapses; doesn't need a Trigger |
|
|
119
119
|
| `className` | `string` | - | Class on the section's outer container |
|
|
120
120
|
| `openClassName` / `closedClassName` | `string` | - | Class applied per state |
|
|
121
|
+
| `unstyled` | `boolean` | `false` | Skip the default flex layout (`items-center`, `gap-1`, row/col by orientation). Use for full-bleed panel sections where the trigger should stretch to fill the panel. *Since v2.7.0.* |
|
|
121
122
|
|
|
122
123
|
### AccordionPanel.Trigger
|
|
123
124
|
|
|
@@ -133,3 +134,8 @@ const [open, setOpen] = useState<string[]>(["wishlist"]);
|
|
|
133
134
|
|------|------|---------|-------------|
|
|
134
135
|
| `children` | `ReactNode` | **required** | Open-state content |
|
|
135
136
|
| `className` | `string` | - | Layout class on the content container |
|
|
137
|
+
| `unstyled` | `boolean` | `false` | Skip the default flex layout. Same intent as `Section.unstyled` — for full-bleed panel content (chat panes, canvas surfaces). *Since v2.7.0.* |
|
|
138
|
+
|
|
139
|
+
## Hitbox
|
|
140
|
+
|
|
141
|
+
The default open-state Trigger renders as a thin 1px divider on the section's trailing edge, but the **clickable hitbox is 12px wide** (`w-3` / `h-3`) so users don't have to land pixel-perfect on the line. The visible 1px line darkens on hover and an inset chevron fades in. *Since v2.7.0.*
|
package/docs/Kanban.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Kanban
|
|
2
2
|
|
|
3
|
-
A drag-and-drop kanban board using the HTML5 Drag and Drop API. Cards can be
|
|
3
|
+
A drag-and-drop kanban board using the HTML5 Drag and Drop API. Cards drag between columns and within a column; columns can be reordered with a `<Kanban.ColumnHandle>`. Cards accept arbitrary children, so you have full creative control over what each card looks like.
|
|
4
4
|
|
|
5
5
|
## Import
|
|
6
6
|
|
|
@@ -11,12 +11,16 @@ import { Kanban } from "@particle-academy/react-fancy";
|
|
|
11
11
|
## Basic Usage
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
<Kanban
|
|
15
|
-
|
|
14
|
+
<Kanban
|
|
15
|
+
onCardMove={(cardId, from, to, toIndex) => {
|
|
16
|
+
// toIndex is the destination position within `to`.
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<Kanban.Column id="todo" title="To Do" wipLimit={4}>
|
|
16
20
|
<Kanban.Card id="task-1">Design homepage</Kanban.Card>
|
|
17
21
|
<Kanban.Card id="task-2">Write tests</Kanban.Card>
|
|
18
22
|
</Kanban.Column>
|
|
19
|
-
<Kanban.Column id="in-progress" title="In Progress">
|
|
23
|
+
<Kanban.Column id="in-progress" title="In Progress" wipLimit={2}>
|
|
20
24
|
<Kanban.Card id="task-3">Build API</Kanban.Card>
|
|
21
25
|
</Kanban.Column>
|
|
22
26
|
<Kanban.Column id="done" title="Done">
|
|
@@ -25,30 +29,78 @@ import { Kanban } from "@particle-academy/react-fancy";
|
|
|
25
29
|
</Kanban>
|
|
26
30
|
```
|
|
27
31
|
|
|
32
|
+
The board ships with a drop-position indicator (a thin blue line that appears where the dragged card will land), full within-column reorder, and a header chip that shows `count / wipLimit` and turns red over capacity.
|
|
33
|
+
|
|
34
|
+
## Compound parts
|
|
35
|
+
|
|
36
|
+
| Component | Role |
|
|
37
|
+
|-----------|------|
|
|
38
|
+
| `Kanban` | Root. Owns drag state and computes column drop positions. |
|
|
39
|
+
| `Kanban.Column` | A drop target. Renders a column header (when `title` is set), tracks card drop position. |
|
|
40
|
+
| `Kanban.Card` | A draggable card. Accepts arbitrary children. |
|
|
41
|
+
| `Kanban.ColumnHandle` | Optional. Mount inside a column to make that column draggable for column reorder. |
|
|
42
|
+
|
|
28
43
|
## Props
|
|
29
44
|
|
|
30
45
|
### Kanban (root)
|
|
31
46
|
|
|
32
47
|
| Prop | Type | Default | Description |
|
|
33
48
|
|------|------|---------|-------------|
|
|
34
|
-
| onCardMove | `(cardId
|
|
35
|
-
|
|
|
49
|
+
| `onCardMove` | `(cardId, fromColumn, toColumn, toIndex) => void` | - | Fires on every card drop. `toIndex` is the destination position within `toColumn` (0-based; equal to current count means append). For a same-column reorder, `fromColumn === toColumn`. |
|
|
50
|
+
| `onColumnMove` | `(columnId, toIndex) => void` | - | Fires when a column is dropped at a new position. Only fires if at least one column has a `<Kanban.ColumnHandle>` and a column was actually dragged. |
|
|
51
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
36
52
|
|
|
37
53
|
### Kanban.Column
|
|
38
54
|
|
|
39
55
|
| Prop | Type | Default | Description |
|
|
40
56
|
|------|------|---------|-------------|
|
|
41
|
-
| id | `string` |
|
|
42
|
-
| title | `string` | - | Column header text |
|
|
43
|
-
|
|
|
57
|
+
| `id` | `string` | **required** | Unique column identifier |
|
|
58
|
+
| `title` | `string` | - | Column header text. When set, the column renders a built-in header with a count chip (and `count / wipLimit` if `wipLimit` is set). |
|
|
59
|
+
| `wipLimit` | `number` | - | Soft work-in-progress limit. The header pill turns red when `cardCount > wipLimit`. Drops are not refused — enforce hard via `onCardMove`. *Since v2.8.0.* |
|
|
60
|
+
| `hideWhenEmpty` | `boolean` | `false` | Render nothing when the column has zero card children. The column still re-mounts to receive drops while a card is being dragged elsewhere. *Since v2.8.0.* |
|
|
61
|
+
| `unstyled` | `boolean` | `false` | Skip the default visuals (background, padding, min-height, fixed `w-72`) so you can render your own surface around the children. The drop target, drag-over ring, and column id wiring are kept. *Since v2.7.0.* |
|
|
62
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
44
63
|
|
|
45
64
|
### Kanban.Card
|
|
46
65
|
|
|
47
66
|
| Prop | Type | Default | Description |
|
|
48
67
|
|------|------|---------|-------------|
|
|
49
|
-
| id | `string` |
|
|
50
|
-
| children | `ReactNode` | - | Card content |
|
|
51
|
-
|
|
|
68
|
+
| `id` | `string` | **required** | Unique card identifier |
|
|
69
|
+
| `children` | `ReactNode` | - | Card content |
|
|
70
|
+
| `unstyled` | `boolean` | `false` | Skip the default border / padding / shadow so you can wrap your own `<Card>` (or any surface) inside. Drag handlers and `draggable` stay in place. *Since v2.7.0.* |
|
|
71
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
72
|
+
|
|
73
|
+
### Kanban.ColumnHandle (since v2.8.0)
|
|
74
|
+
|
|
75
|
+
| Prop | Type | Default | Description |
|
|
76
|
+
|------|------|---------|-------------|
|
|
77
|
+
| `children` | `ReactNode` | **required** | Render the column header (or a grip icon) inside the handle. Anywhere inside is the drag origin. |
|
|
78
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
79
|
+
|
|
80
|
+
Mount inside a `Kanban.Column`. Without it, the column is static.
|
|
81
|
+
|
|
82
|
+
## Composing custom card visuals
|
|
83
|
+
|
|
84
|
+
When you want full control over the card body — wrapping a `<Card>` with avatars, badges, dropdown menus — pass `unstyled` so the Kanban wrapper just provides drag plumbing:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<Kanban.Card id={card.id} unstyled>
|
|
88
|
+
<Card variant="elevated" padding="none" className="overflow-hidden">
|
|
89
|
+
{/* your fancy card body */}
|
|
90
|
+
</Card>
|
|
91
|
+
</Kanban.Card>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Same pattern works for columns:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<Kanban.Column id="doing" unstyled wipLimit={3} className="w-80 rounded-xl bg-zinc-50 p-3 ring-1 ring-zinc-200">
|
|
98
|
+
<Kanban.ColumnHandle>
|
|
99
|
+
<MyColumnHeader title="In progress" wipLimit={3} />
|
|
100
|
+
</Kanban.ColumnHandle>
|
|
101
|
+
{cards.map((c) => <Kanban.Card key={c.id} id={c.id} unstyled>...</Kanban.Card>)}
|
|
102
|
+
</Kanban.Column>
|
|
103
|
+
```
|
|
52
104
|
|
|
53
105
|
## Stateful Example
|
|
54
106
|
|
|
@@ -58,22 +110,64 @@ const [columns, setColumns] = useState({
|
|
|
58
110
|
doing: ["task-3"],
|
|
59
111
|
done: [],
|
|
60
112
|
});
|
|
113
|
+
const [order, setOrder] = useState(["todo", "doing", "done"]);
|
|
114
|
+
|
|
115
|
+
function handleCardMove(cardId, from, to, toIndex) {
|
|
116
|
+
setColumns((prev) => {
|
|
117
|
+
const fromList = prev[from].filter((id) => id !== cardId);
|
|
118
|
+
if (from === to) {
|
|
119
|
+
fromList.splice(toIndex, 0, cardId);
|
|
120
|
+
return { ...prev, [from]: fromList };
|
|
121
|
+
}
|
|
122
|
+
const toList = [...prev[to]];
|
|
123
|
+
toList.splice(toIndex, 0, cardId);
|
|
124
|
+
return { ...prev, [from]: fromList, [to]: toList };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
61
127
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
})
|
|
128
|
+
function handleColumnMove(id, toIndex) {
|
|
129
|
+
setOrder((prev) => {
|
|
130
|
+
const next = prev.filter((c) => c !== id);
|
|
131
|
+
next.splice(toIndex, 0, id);
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
68
134
|
}
|
|
69
135
|
|
|
70
|
-
<Kanban onCardMove={
|
|
71
|
-
{
|
|
136
|
+
<Kanban onCardMove={handleCardMove} onColumnMove={handleColumnMove}>
|
|
137
|
+
{order.map((colId) => (
|
|
72
138
|
<Kanban.Column key={colId} id={colId} title={colId}>
|
|
73
|
-
{
|
|
139
|
+
<Kanban.ColumnHandle>{/* header */}</Kanban.ColumnHandle>
|
|
140
|
+
{columns[colId].map((id) => (
|
|
74
141
|
<Kanban.Card key={id} id={id}>{id}</Kanban.Card>
|
|
75
142
|
))}
|
|
76
143
|
</Kanban.Column>
|
|
77
144
|
))}
|
|
78
145
|
</Kanban>
|
|
79
146
|
```
|
|
147
|
+
|
|
148
|
+
## Async move with rollback
|
|
149
|
+
|
|
150
|
+
`onCardMove` can be `async`. Apply optimistically, revert on failure:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
async function handleMove(id, from, to, toIndex) {
|
|
154
|
+
const prev = state;
|
|
155
|
+
setState(s => move(s, id, from, to, toIndex));
|
|
156
|
+
try {
|
|
157
|
+
await api.move(id, to, toIndex);
|
|
158
|
+
} catch {
|
|
159
|
+
setState(prev);
|
|
160
|
+
toast.error("Move failed");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Pending
|
|
166
|
+
|
|
167
|
+
The component still doesn't handle:
|
|
168
|
+
|
|
169
|
+
- **Touch / mobile** — HTML5 DnD is desktop-only.
|
|
170
|
+
- **Full keyboard navigation** — arrow-key / space-to-lift move pattern not yet implemented. Roles (`application`, `group`) are in place to lay groundwork.
|
|
171
|
+
- **Custom drag preview** — uses the browser default ghost.
|
|
172
|
+
- **Multi-select drag.**
|
|
173
|
+
- **Swimlanes / row grouping.**
|