@tulipfarm/sprout 0.3.0 → 0.4.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 +45 -40
- package/dist/components/accordion.d.ts +2 -2
- package/dist/components/accordion.js +9 -9
- package/dist/components/agent-fanout.d.ts +2 -2
- package/dist/components/agent-fanout.js +65 -87
- package/dist/components/alert-dialog.d.ts +6 -6
- package/dist/components/alert-dialog.js +20 -20
- package/dist/components/alert.d.ts +6 -6
- package/dist/components/alert.js +13 -13
- package/dist/components/aspect-ratio.d.ts +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/attachment.d.ts +17 -17
- package/dist/components/attachment.js +82 -115
- package/dist/components/avatar.d.ts +10 -10
- package/dist/components/avatar.js +14 -14
- package/dist/components/badge.d.ts +4 -4
- package/dist/components/badge.js +18 -18
- package/dist/components/board.d.ts +107 -0
- package/dist/components/board.js +640 -0
- package/dist/components/brand-icon.d.ts +6 -6
- package/dist/components/brand-icon.js +55 -63
- package/dist/components/breadcrumb.d.ts +11 -11
- package/dist/components/breadcrumb.js +16 -18
- package/dist/components/button-group.d.ts +6 -6
- package/dist/components/button-group.js +14 -14
- package/dist/components/button.d.ts +11 -11
- package/dist/components/button.js +46 -56
- package/dist/components/calendar.d.ts +5 -5
- package/dist/components/calendar.js +44 -53
- package/dist/components/canvas.d.ts +4 -4
- package/dist/components/canvas.js +39 -46
- package/dist/components/card.d.ts +6 -6
- package/dist/components/card.js +17 -17
- package/dist/components/carousel.d.ts +7 -7
- package/dist/components/carousel.js +57 -75
- package/dist/components/chart.d.ts +5 -112
- package/dist/components/chart.js +5 -172
- package/dist/components/chat-confirmation.d.ts +13 -11
- package/dist/components/chat-confirmation.js +32 -38
- package/dist/components/chat-input.d.ts +4 -4
- package/dist/components/chat-input.js +74 -81
- package/dist/components/chat-plan.d.ts +2 -2
- package/dist/components/chat-plan.js +15 -20
- package/dist/components/chat-sources.d.ts +8 -8
- package/dist/components/chat-sources.js +18 -27
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/checkbox.js +9 -9
- package/dist/components/choice-list.d.ts +2 -2
- package/dist/components/choice-list.js +16 -18
- package/dist/components/code-block.d.ts +5 -5
- package/dist/components/code-block.js +26 -26
- package/dist/components/collapsible.d.ts +1 -1
- package/dist/components/collapsible.js +7 -11
- package/dist/components/combobox.d.ts +3 -3
- package/dist/components/combobox.js +24 -24
- package/dist/components/command.d.ts +13 -13
- package/dist/components/command.js +61 -73
- package/dist/components/context-menu.d.ts +5 -5
- package/dist/components/context-menu.js +22 -22
- package/dist/components/dialog.d.ts +6 -6
- package/dist/components/dialog.js +23 -28
- package/dist/components/direction.d.ts +1 -1
- package/dist/components/direction.js +1 -1
- package/dist/components/dropdown-menu.d.ts +5 -5
- package/dist/components/dropdown-menu.js +20 -20
- package/dist/components/field.d.ts +11 -11
- package/dist/components/field.js +41 -61
- package/dist/components/file-explorer.d.ts +2 -2
- package/dist/components/file-explorer.js +89 -105
- package/dist/components/file-viewer.d.ts +10 -10
- package/dist/components/file-viewer.js +112 -140
- package/dist/components/form.d.ts +7 -7
- package/dist/components/form.js +24 -28
- package/dist/components/glass-banner.d.ts +3 -3
- package/dist/components/glass-banner.js +21 -24
- package/dist/components/gradient-banner.d.ts +5 -5
- package/dist/components/gradient-banner.js +13 -22
- package/dist/components/hover-card.d.ts +4 -4
- package/dist/components/hover-card.js +6 -6
- package/dist/components/input-group.d.ts +11 -11
- package/dist/components/input-group.js +22 -36
- package/dist/components/input-otp.d.ts +10 -10
- package/dist/components/input-otp.js +13 -15
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +7 -7
- package/dist/components/kbd.d.ts +3 -3
- package/dist/components/kbd.js +5 -5
- package/dist/components/label.d.ts +2 -2
- package/dist/components/label.js +2 -2
- package/dist/components/mention-list.d.ts +3 -3
- package/dist/components/mention-list.js +10 -10
- package/dist/components/menubar.d.ts +6 -6
- package/dist/components/menubar.js +22 -22
- package/dist/components/message-actions.d.ts +2 -2
- package/dist/components/message-actions.js +29 -31
- package/dist/components/message-response.d.ts +3 -3
- package/dist/components/message-response.js +18 -18
- package/dist/components/message-scroller.d.ts +12 -12
- package/dist/components/message-scroller.js +44 -56
- package/dist/components/message.d.ts +11 -11
- package/dist/components/message.js +25 -25
- package/dist/components/navigation-menu.d.ts +2 -2
- package/dist/components/navigation-menu.js +15 -15
- package/dist/components/notice-banner.d.ts +2 -2
- package/dist/components/notice-banner.js +2 -2
- package/dist/components/page-content.d.ts +2 -2
- package/dist/components/page-content.js +2 -2
- package/dist/components/page-header.d.ts +1 -1
- package/dist/components/page-header.js +16 -16
- package/dist/components/pagination.d.ts +9 -9
- package/dist/components/pagination.js +17 -17
- package/dist/components/plan-outline.d.ts +2 -2
- package/dist/components/plan-outline.js +43 -60
- package/dist/components/popover.d.ts +6 -6
- package/dist/components/popover.js +15 -15
- package/dist/components/presence-cursor.d.ts +2 -2
- package/dist/components/presence-cursor.js +3 -3
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/progress.js +8 -8
- package/dist/components/prompt-list.d.ts +3 -3
- package/dist/components/prompt-list.js +6 -6
- package/dist/components/questionnaire.d.ts +19 -19
- package/dist/components/questionnaire.js +183 -242
- package/dist/components/radio-group.d.ts +2 -2
- package/dist/components/radio-group.js +6 -6
- package/dist/components/reasoning-effort.d.ts +3 -3
- package/dist/components/reasoning-effort.js +9 -12
- package/dist/components/record-list.d.ts +66 -0
- package/dist/components/record-list.js +151 -0
- package/dist/components/resizable.d.ts +5 -5
- package/dist/components/resizable.js +76 -102
- package/dist/components/result-card.d.ts +10 -5
- package/dist/components/result-card.js +42 -53
- package/dist/components/result-notice.d.ts +18 -0
- package/dist/components/result-notice.js +29 -0
- package/dist/components/review-card.d.ts +6 -6
- package/dist/components/review-card.js +18 -18
- package/dist/components/rich-text-editor.d.ts +2 -2
- package/dist/components/rich-text-editor.js +56 -72
- package/dist/components/schema-diagram.d.ts +3 -3
- package/dist/components/schema-diagram.js +39 -53
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/scroll-area.js +10 -14
- package/dist/components/search-dialog.d.ts +6 -6
- package/dist/components/search-dialog.js +16 -16
- package/dist/components/search-field.d.ts +1 -1
- package/dist/components/search-field.js +11 -11
- package/dist/components/select.d.ts +3 -3
- package/dist/components/select.js +20 -20
- package/dist/components/selection-slot.d.ts +2 -2
- package/dist/components/selection-slot.js +6 -9
- package/dist/components/separator.d.ts +15 -15
- package/dist/components/separator.js +22 -27
- package/dist/components/settings-list.d.ts +3 -3
- package/dist/components/settings-list.js +5 -5
- package/dist/components/sheet.d.ts +6 -6
- package/dist/components/sheet.js +27 -27
- package/dist/components/sidebar.d.ts +22 -22
- package/dist/components/sidebar.js +57 -58
- package/dist/components/skeleton.d.ts +5 -5
- package/dist/components/skeleton.js +14 -21
- package/dist/components/slider.d.ts +2 -2
- package/dist/components/slider.js +6 -6
- package/dist/components/spinner.d.ts +5 -5
- package/dist/components/spinner.js +22 -26
- package/dist/components/sql-viewer.d.ts +14 -14
- package/dist/components/sql-viewer.js +99 -128
- package/dist/components/state-panel-samples.d.ts +2 -2
- package/dist/components/state-panel-samples.js +209 -245
- package/dist/components/state-panel.d.ts +2 -35
- package/dist/components/state-panel.js +2 -29
- package/dist/components/state-sample.d.ts +7 -7
- package/dist/components/state-sample.js +18 -31
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/switch.js +7 -7
- package/dist/components/table-nuqs.d.ts +2 -2
- package/dist/components/table-nuqs.js +11 -12
- package/dist/components/table.d.ts +4 -4
- package/dist/components/table.js +166 -235
- package/dist/components/tabs.d.ts +2 -2
- package/dist/components/tabs.js +12 -12
- package/dist/components/textarea.d.ts +2 -2
- package/dist/components/textarea.js +6 -8
- package/dist/components/timeline.d.ts +3 -0
- package/dist/components/timeline.js +3 -0
- package/dist/components/toast.d.ts +2 -2
- package/dist/components/toast.js +19 -25
- package/dist/components/toggle-group.d.ts +2 -2
- package/dist/components/toggle-group.js +18 -20
- package/dist/components/toggle.d.ts +2 -2
- package/dist/components/toggle.js +8 -8
- package/dist/components/tool-activity.d.ts +1 -1
- package/dist/components/tool-activity.js +83 -110
- package/dist/components/toolbar-status.d.ts +2 -2
- package/dist/components/toolbar-status.js +7 -7
- package/dist/components/tooltip.d.ts +5 -5
- package/dist/components/tooltip.js +12 -11
- package/dist/components/typed-text.d.ts +2 -2
- package/dist/components/typed-text.js +10 -12
- package/dist/components/view-switcher.d.ts +51 -0
- package/dist/components/view-switcher.js +114 -0
- package/dist/index.d.ts +98 -93
- package/dist/index.js +98 -93
- package/dist/internal/chart-kit.d.ts +142 -0
- package/dist/internal/chart-kit.js +200 -0
- package/dist/internal/charts/bars.d.ts +61 -0
- package/dist/internal/charts/bars.js +86 -0
- package/dist/internal/charts/lines.d.ts +67 -0
- package/dist/internal/charts/lines.js +70 -0
- package/dist/internal/charts/more.d.ts +77 -0
- package/dist/internal/charts/more.js +68 -0
- package/dist/internal/charts/parts.d.ts +54 -0
- package/dist/internal/charts/parts.js +82 -0
- package/dist/internal/charts/timeline-dates.d.ts +38 -0
- package/dist/internal/charts/timeline-dates.js +113 -0
- package/dist/internal/charts/timeline-window.d.ts +31 -0
- package/dist/internal/charts/timeline-window.js +26 -0
- package/dist/internal/charts/timeline.d.ts +72 -0
- package/dist/internal/charts/timeline.js +287 -0
- package/dist/internal/classes.d.ts +1 -1
- package/dist/internal/classes.js +7 -9
- package/dist/internal/code-tokens.d.ts +2 -2
- package/dist/internal/code-tokens.js +12 -12
- package/dist/internal/fieldset.d.ts +1 -1
- package/dist/internal/fieldset.js +6 -6
- package/dist/internal/file-kind.d.ts +1 -1
- package/dist/internal/file-kind.js +43 -51
- package/dist/internal/flow.d.ts +4 -4
- package/dist/internal/flow.js +20 -23
- package/dist/internal/languages.js +999 -999
- package/dist/internal/message-scroller.d.ts +3 -3
- package/dist/internal/message-scroller.js +64 -112
- package/dist/internal/pressable.d.ts +1 -1
- package/dist/internal/pressable.js +7 -7
- package/dist/internal/state-panel.d.ts +35 -0
- package/dist/internal/state-panel.js +22 -0
- package/dist/internal/syntax.d.ts +2 -2
- package/dist/internal/syntax.js +36 -47
- package/dist/styles/accordion.css +3 -5
- package/dist/styles/agent-fanout.css +64 -60
- package/dist/styles/alert-dialog.css +12 -26
- package/dist/styles/alert.css +10 -15
- package/dist/styles/attachment.css +51 -93
- package/dist/styles/avatar.css +17 -24
- package/dist/styles/badge.css +11 -36
- package/dist/styles/board.css +571 -0
- package/dist/styles/brand-icon.css +7 -8
- package/dist/styles/breadcrumb.css +4 -7
- package/dist/styles/button-group.css +8 -14
- package/dist/styles/button.css +22 -27
- package/dist/styles/calendar.css +24 -36
- package/dist/styles/canvas.css +2 -3
- package/dist/styles/card.css +7 -23
- package/dist/styles/carousel.css +11 -12
- package/dist/styles/chart.css +215 -31
- package/dist/styles/chat-input.css +15 -39
- package/dist/styles/chat-plan.css +3 -4
- package/dist/styles/chat-sources.css +3 -6
- package/dist/styles/checkbox.css +11 -25
- package/dist/styles/choice-list.css +17 -31
- package/dist/styles/code-block.css +11 -25
- package/dist/styles/collapsible.css +10 -15
- package/dist/styles/combobox.css +11 -15
- package/dist/styles/command.css +5 -8
- package/dist/styles/context-menu.css +2 -2
- package/dist/styles/dialog.css +5 -16
- package/dist/styles/dropdown-menu.css +2 -2
- package/dist/styles/field.css +15 -21
- package/dist/styles/file-explorer.css +19 -33
- package/dist/styles/file-viewer.css +13 -17
- package/dist/styles/form.css +3 -3
- package/dist/styles/glass-banner.css +12 -47
- package/dist/styles/gradient-banner.css +17 -35
- package/dist/styles/hover-card.css +9 -15
- package/dist/styles/index.css +116 -108
- package/dist/styles/input-group.css +25 -41
- package/dist/styles/input-otp.css +5 -6
- package/dist/styles/input.css +5 -5
- package/dist/styles/kbd.css +1 -1
- package/dist/styles/label.css +9 -30
- package/dist/styles/mention-list.css +6 -8
- package/dist/styles/menubar.css +5 -6
- package/dist/styles/message-actions.css +1 -1
- package/dist/styles/message-response.css +3 -5
- package/dist/styles/message-scroller.css +14 -21
- package/dist/styles/message.css +23 -60
- package/dist/styles/navigation-menu.css +14 -29
- package/dist/styles/notice-banner.css +1 -2
- package/dist/styles/page-header.css +7 -10
- package/dist/styles/pagination.css +1 -4
- package/dist/styles/plan-outline.css +20 -37
- package/dist/styles/popover.css +10 -19
- package/dist/styles/presence-cursor.css +1 -2
- package/dist/styles/progress.css +2 -4
- package/dist/styles/prompt-list.css +6 -17
- package/dist/styles/questionnaire.css +16 -40
- package/dist/styles/radio-group.css +12 -23
- package/dist/styles/reasoning-effort.css +8 -15
- package/dist/styles/record-list.css +243 -0
- package/dist/styles/resizable.css +10 -11
- package/dist/styles/result-card.css +17 -3
- package/dist/styles/result-notice.css +80 -0
- package/dist/styles/review-card.css +2 -3
- package/dist/styles/rich-text-editor.css +1 -2
- package/dist/styles/schema-diagram.css +12 -17
- package/dist/styles/scroll-area.css +8 -10
- package/dist/styles/search-dialog.css +1 -5
- package/dist/styles/search-field.css +1 -2
- package/dist/styles/select.css +7 -7
- package/dist/styles/selection-slot.css +1 -2
- package/dist/styles/separator.css +7 -8
- package/dist/styles/settings-list.css +1 -2
- package/dist/styles/sheet.css +40 -73
- package/dist/styles/sidebar.css +17 -21
- package/dist/styles/skeleton.css +9 -11
- package/dist/styles/slider.css +11 -21
- package/dist/styles/spinner.css +2 -2
- package/dist/styles/sql-viewer.css +29 -29
- package/dist/styles/state-panel.css +8 -9
- package/dist/styles/state-sample.css +20 -33
- package/dist/styles/switch.css +11 -18
- package/dist/styles/table.css +51 -77
- package/dist/styles/tabs.css +12 -16
- package/dist/styles/textarea.css +3 -3
- package/dist/styles/themes/chatgpt.css +11 -23
- package/dist/styles/themes/claude.css +12 -24
- package/dist/styles/themes/cloudflare.css +11 -23
- package/dist/styles/themes/discord.css +11 -23
- package/dist/styles/themes/github.css +11 -23
- package/dist/styles/themes/grok.css +11 -23
- package/dist/styles/themes/linear.css +12 -25
- package/dist/styles/themes/neutral.css +11 -23
- package/dist/styles/themes/notion.css +11 -23
- package/dist/styles/themes/perplexity.css +11 -23
- package/dist/styles/themes/raycast.css +11 -23
- package/dist/styles/themes/spotify.css +11 -23
- package/dist/styles/themes/stripe.css +11 -23
- package/dist/styles/themes/supabase.css +11 -23
- package/dist/styles/themes/tulipfarm.css +9 -10
- package/dist/styles/themes/vercel.css +12 -25
- package/dist/styles/timeline.css +276 -0
- package/dist/styles/toast.css +13 -20
- package/dist/styles/toggle-group.css +17 -23
- package/dist/styles/toggle.css +8 -8
- package/dist/styles/tool-activity.css +7 -13
- package/dist/styles/toolbar-status.css +2 -7
- package/dist/styles/tooltip.css +15 -21
- package/dist/styles/view-switcher.css +279 -0
- package/package.json +21 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, pointerWithin, rectIntersection, useDroppable, useSensor, useSensors, } from '@dnd-kit/core';
|
|
5
|
+
import { SortableContext, arrayMove, defaultAnimateLayoutChanges, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
|
|
6
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
7
|
+
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual';
|
|
8
|
+
import { ChevronDown } from 'reicon-react/icons/ChevronDown';
|
|
9
|
+
import { ChevronRight } from 'reicon-react/icons/ChevronRight';
|
|
10
|
+
import { MoreH } from 'reicon-react/icons/MoreH';
|
|
11
|
+
import { Plus } from 'reicon-react/icons/Plus';
|
|
12
|
+
import { Reorder } from 'reicon-react/icons/Reorder';
|
|
13
|
+
import { cn } from '../internal/classes.js';
|
|
14
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from './alert-dialog.js';
|
|
15
|
+
import { Badge } from './badge.js';
|
|
16
|
+
import { Button } from './button.js';
|
|
17
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './dropdown-menu.js';
|
|
18
|
+
import { Input } from './input.js';
|
|
19
|
+
import { SearchField } from './search-field.js';
|
|
20
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select.js';
|
|
21
|
+
import { Skeleton } from './skeleton.js';
|
|
22
|
+
const CARD = 'card:';
|
|
23
|
+
const COLUMN = 'column:';
|
|
24
|
+
const cellKey = (laneId, columnId) => `${laneId}\u0000${columnId}`;
|
|
25
|
+
const dataOf = (value) => value;
|
|
26
|
+
const dndAccessibility = {
|
|
27
|
+
container: typeof document === 'undefined' ? undefined : document.body,
|
|
28
|
+
};
|
|
29
|
+
const keyboardCodes = {
|
|
30
|
+
start: ['Space'],
|
|
31
|
+
cancel: ['Escape'],
|
|
32
|
+
end: ['Space'],
|
|
33
|
+
};
|
|
34
|
+
// dnd-kit only animates neighbors while the moved card is still in their list,
|
|
35
|
+
// so cards in a cell the dragged card just left would jump without this.
|
|
36
|
+
const animateLayoutChanges = args => defaultAnimateLayoutChanges({ ...args, wasDragging: true });
|
|
37
|
+
const dropAnimation = {
|
|
38
|
+
duration: 160,
|
|
39
|
+
easing: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
40
|
+
};
|
|
41
|
+
const reducedMotion = () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
42
|
+
// Touch goes to TouchSensor, which waits for a press so a swipe still scrolls.
|
|
43
|
+
class NoTouchPointerSensor extends PointerSensor {
|
|
44
|
+
static activators = [
|
|
45
|
+
{
|
|
46
|
+
eventName: 'onPointerDown',
|
|
47
|
+
handler: ({ nativeEvent }) => nativeEvent.pointerType !== 'touch' && nativeEvent.isPrimary && nativeEvent.button === 0,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
const sameList = (a, b) => a !== undefined && a.length === b.length && a.every((x, i) => x === b[i]);
|
|
52
|
+
const collide = args => {
|
|
53
|
+
const type = dataOf(args.active.data.current)?.type;
|
|
54
|
+
const droppableContainers = args.droppableContainers.filter(container => type === 'column'
|
|
55
|
+
? dataOf(container.data.current)?.type === 'column'
|
|
56
|
+
: dataOf(container.data.current)?.type !== 'column');
|
|
57
|
+
const scoped = { ...args, droppableContainers };
|
|
58
|
+
if (type === 'column')
|
|
59
|
+
return closestCenter(scoped);
|
|
60
|
+
const pointer = pointerWithin(scoped);
|
|
61
|
+
const hits = pointer.length ? pointer : rectIntersection(scoped);
|
|
62
|
+
const card = hits.find(hit => dataOf(droppableContainers.find(c => c.id === hit.id)?.data.current)?.type === 'card');
|
|
63
|
+
return card ? [card] : hits.slice(0, 1);
|
|
64
|
+
};
|
|
65
|
+
/** Moves `ids` into `to`, before `beforeId`, or to the end of that cell. */
|
|
66
|
+
function place(list, ids, to, beforeId, laneOf, withLanes) {
|
|
67
|
+
const moving = new Set(ids);
|
|
68
|
+
const moved = ids.flatMap(id => {
|
|
69
|
+
const item = list.find(i => i.id === id);
|
|
70
|
+
if (!item)
|
|
71
|
+
return [];
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
...item,
|
|
75
|
+
columnId: to.columnId,
|
|
76
|
+
...(withLanes ? { laneId: to.laneId } : {}),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
const rest = list.filter(item => !moving.has(item.id));
|
|
81
|
+
let at = beforeId === null ? -1 : rest.findIndex(i => i.id === beforeId);
|
|
82
|
+
if (at === -1) {
|
|
83
|
+
at = rest.length;
|
|
84
|
+
for (let i = rest.length - 1; i >= 0; i--) {
|
|
85
|
+
const item = rest[i];
|
|
86
|
+
if (item.columnId === to.columnId && laneOf(item) === to.laneId) {
|
|
87
|
+
at = i + 1;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
rest.splice(at, 0, ...moved);
|
|
93
|
+
return rest;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A Kanban board. Columns run across, optional lanes run down, and cards sit
|
|
97
|
+
* in the cell where they meet. Cards drag to any cell, columns drag by their
|
|
98
|
+
* header, and titles rename in place. Lanes and the cards in each cell are
|
|
99
|
+
* virtualized, so thousands of cards stay fast.
|
|
100
|
+
*/
|
|
101
|
+
export function Board({ columns, items, lanes, onItemsChange, onColumnsChange, onItemAdd, onColumnAdd, onColumnRetry, onColumnShowAll, columnText, onItemOpen, renderBody, renderTitle, renderCard, itemActions, searchable, searchText, laneBy, label = 'Board', className, }) {
|
|
102
|
+
const dndId = useId();
|
|
103
|
+
const withLanes = lanes !== undefined;
|
|
104
|
+
const laneList = useMemo(() => lanes ?? [{ id: '', title: label }], [lanes, label]);
|
|
105
|
+
const laneOf = (item) => (withLanes ? (item.laneId ?? '') : '');
|
|
106
|
+
const [draft, setDraft] = useState(null);
|
|
107
|
+
const draftRef = useRef(null);
|
|
108
|
+
const setDraftBoth = (next) => {
|
|
109
|
+
draftRef.current = next;
|
|
110
|
+
setDraft(next);
|
|
111
|
+
};
|
|
112
|
+
const shown = draft ?? items;
|
|
113
|
+
const [query, setQuery] = useState('');
|
|
114
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
115
|
+
const [anchor, setAnchor] = useState(null);
|
|
116
|
+
const [collapsedColumns, setCollapsedColumns] = useState(() => new Set());
|
|
117
|
+
const [collapsedLanes, setCollapsedLanes] = useState(() => new Set());
|
|
118
|
+
const [active, setActive] = useState(null);
|
|
119
|
+
const [extras, setExtras] = useState([]);
|
|
120
|
+
const [laneLock, setLaneLock] = useState(null);
|
|
121
|
+
const movedRef = useRef(false);
|
|
122
|
+
const needle = query.trim().toLowerCase();
|
|
123
|
+
const matches = (item) => !needle ||
|
|
124
|
+
item.title.toLowerCase().includes(needle) ||
|
|
125
|
+
(searchText?.(item).toLowerCase().includes(needle) ?? false);
|
|
126
|
+
// Unchanged cells keep their array, so memoized cells skip re-rendering mid-drag.
|
|
127
|
+
const cellsRef = useRef(new Map());
|
|
128
|
+
const cells = useMemo(() => {
|
|
129
|
+
const map = new Map();
|
|
130
|
+
for (const item of shown) {
|
|
131
|
+
if (!matches(item))
|
|
132
|
+
continue;
|
|
133
|
+
const key = cellKey(laneOf(item), item.columnId);
|
|
134
|
+
const list = map.get(key);
|
|
135
|
+
if (list)
|
|
136
|
+
list.push(item);
|
|
137
|
+
else
|
|
138
|
+
map.set(key, [item]);
|
|
139
|
+
}
|
|
140
|
+
for (const [key, list] of map) {
|
|
141
|
+
const before = cellsRef.current.get(key);
|
|
142
|
+
if (sameList(before, list))
|
|
143
|
+
map.set(key, before);
|
|
144
|
+
}
|
|
145
|
+
cellsRef.current = map;
|
|
146
|
+
return map;
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [shown, needle, searchText, withLanes]);
|
|
149
|
+
const columnTotals = useMemo(() => {
|
|
150
|
+
const totals = new Map();
|
|
151
|
+
for (const item of shown)
|
|
152
|
+
totals.set(item.columnId, (totals.get(item.columnId) ?? 0) + 1);
|
|
153
|
+
return totals;
|
|
154
|
+
}, [shown]);
|
|
155
|
+
const laneTotals = (laneId) => columns.reduce((sum, column) => sum + (cells.get(cellKey(laneId, column.id))?.length ?? 0), 0);
|
|
156
|
+
const visibleCount = [...cells.values()].reduce((s, l) => s + l.length, 0);
|
|
157
|
+
const findItem = (id) => (draftRef.current ?? items).find(item => item.id === id);
|
|
158
|
+
const cellOf = (item) => ({
|
|
159
|
+
laneId: laneOf(item),
|
|
160
|
+
columnId: item.columnId,
|
|
161
|
+
});
|
|
162
|
+
const cellName = (cell) => {
|
|
163
|
+
const column = columns.find(c => c.id === cell.columnId)?.title ?? '';
|
|
164
|
+
const lane = laneList.find(l => l.id === cell.laneId)?.title;
|
|
165
|
+
return withLanes && lane ? `${column}, ${lane}` : column;
|
|
166
|
+
};
|
|
167
|
+
const sensors = useSensors(useSensor(NoTouchPointerSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, {
|
|
168
|
+
activationConstraint: { delay: 200, tolerance: 6 },
|
|
169
|
+
}), useSensor(KeyboardSensor, {
|
|
170
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
171
|
+
keyboardCodes,
|
|
172
|
+
}));
|
|
173
|
+
const onDragStart = ({ active: a }) => {
|
|
174
|
+
const data = dataOf(a.data.current);
|
|
175
|
+
if (!data)
|
|
176
|
+
return;
|
|
177
|
+
setActive(data);
|
|
178
|
+
if (data.type !== 'card')
|
|
179
|
+
return;
|
|
180
|
+
// Lanes may grow but not shrink mid-drag, so the lane under the pointer
|
|
181
|
+
// does not move away when a card leaves it.
|
|
182
|
+
setLaneLock(new Map(laneVirtualizer.getVirtualItems().map(row => [laneList[row.index]?.id ?? '', row.size])));
|
|
183
|
+
const others = selected.has(data.id)
|
|
184
|
+
? shown.filter(item => selected.has(item.id) && item.id !== data.id).map(item => item.id)
|
|
185
|
+
: [];
|
|
186
|
+
if (!selected.has(data.id))
|
|
187
|
+
setSelected(new Set());
|
|
188
|
+
setExtras(others);
|
|
189
|
+
const hidden = new Set(others);
|
|
190
|
+
setDraftBoth(items.filter(item => !hidden.has(item.id)));
|
|
191
|
+
};
|
|
192
|
+
const onDragOver = ({ active: a, over }) => {
|
|
193
|
+
const data = dataOf(a.data.current);
|
|
194
|
+
const target = dataOf(over?.data.current);
|
|
195
|
+
if (data?.type !== 'card' || !over || !target || movedRef.current)
|
|
196
|
+
return;
|
|
197
|
+
const list = draftRef.current;
|
|
198
|
+
const item = findItem(data.id);
|
|
199
|
+
if (!list || !item)
|
|
200
|
+
return;
|
|
201
|
+
let to;
|
|
202
|
+
let beforeId = null;
|
|
203
|
+
if (target.type === 'card') {
|
|
204
|
+
if (target.id === data.id)
|
|
205
|
+
return;
|
|
206
|
+
const overItem = findItem(target.id);
|
|
207
|
+
if (!overItem)
|
|
208
|
+
return;
|
|
209
|
+
to = cellOf(overItem);
|
|
210
|
+
const siblings = cells.get(cellKey(to.laneId, to.columnId)) ?? [];
|
|
211
|
+
const translated = a.rect.current.translated;
|
|
212
|
+
const below = translated !== null && translated.top > over.rect.top + over.rect.height / 2;
|
|
213
|
+
const index = siblings.findIndex(s => s.id === target.id);
|
|
214
|
+
beforeId = below ? (siblings[index + 1]?.id ?? null) : target.id;
|
|
215
|
+
}
|
|
216
|
+
else if (target.type === 'cell') {
|
|
217
|
+
to = target.cell;
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
return;
|
|
221
|
+
const from = cellOf(item);
|
|
222
|
+
if (from.laneId === to.laneId && from.columnId === to.columnId)
|
|
223
|
+
return;
|
|
224
|
+
movedRef.current = true;
|
|
225
|
+
requestAnimationFrame(() => {
|
|
226
|
+
movedRef.current = false;
|
|
227
|
+
});
|
|
228
|
+
setDraftBoth(place(list, [data.id], to, beforeId, laneOf, withLanes));
|
|
229
|
+
};
|
|
230
|
+
const finish = () => {
|
|
231
|
+
setDraftBoth(null);
|
|
232
|
+
setLaneLock(null);
|
|
233
|
+
setActive(null);
|
|
234
|
+
setExtras([]);
|
|
235
|
+
};
|
|
236
|
+
const onDragEnd = ({ active: a, over }) => {
|
|
237
|
+
const data = dataOf(a.data.current);
|
|
238
|
+
const target = dataOf(over?.data.current);
|
|
239
|
+
if (data?.type === 'column') {
|
|
240
|
+
if (target?.type === 'column' && target.id !== data.id && onColumnsChange) {
|
|
241
|
+
const ids = columns.map(c => c.id);
|
|
242
|
+
onColumnsChange(arrayMove(columns, ids.indexOf(data.id), ids.indexOf(target.id)));
|
|
243
|
+
}
|
|
244
|
+
return finish();
|
|
245
|
+
}
|
|
246
|
+
let list = draftRef.current;
|
|
247
|
+
const item = data?.type === 'card' ? findItem(data.id) : undefined;
|
|
248
|
+
if (!list || !item || data?.type !== 'card')
|
|
249
|
+
return finish();
|
|
250
|
+
const cell = cellOf(item);
|
|
251
|
+
const siblings = cells.get(cellKey(cell.laneId, cell.columnId)) ?? [];
|
|
252
|
+
if (target?.type === 'card' && target.id !== data.id) {
|
|
253
|
+
const ids = siblings.map(s => s.id);
|
|
254
|
+
const from = ids.indexOf(data.id);
|
|
255
|
+
const to = ids.indexOf(target.id);
|
|
256
|
+
if (from !== -1 && to !== -1) {
|
|
257
|
+
const order = arrayMove(ids, from, to);
|
|
258
|
+
list = place(list, [data.id], cell, order[to + 1] ?? null, laneOf, withLanes);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (extras.length) {
|
|
262
|
+
list = [...list, ...items.filter(i => extras.includes(i.id))];
|
|
263
|
+
const after = list.indexOf(list.find(i => i.id === data.id)) + 1;
|
|
264
|
+
list = place(list, extras, cell, list[after]?.id ?? null, laneOf, withLanes);
|
|
265
|
+
const next = list[list.findIndex(i => i.id === extras.at(-1)) + 1];
|
|
266
|
+
if (next && cellKey(laneOf(next), next.columnId) !== cellKey(cell.laneId, cell.columnId))
|
|
267
|
+
list = place(list, extras, cell, null, laneOf, withLanes);
|
|
268
|
+
}
|
|
269
|
+
const changed = list.some((i, index) => i !== items[index]) || list.length !== items.length;
|
|
270
|
+
finish();
|
|
271
|
+
if (changed)
|
|
272
|
+
onItemsChange?.(list);
|
|
273
|
+
};
|
|
274
|
+
const announcements = {
|
|
275
|
+
onDragStart: ({ active: a }) => {
|
|
276
|
+
const data = dataOf(a.data.current);
|
|
277
|
+
if (data?.type === 'column')
|
|
278
|
+
return `Picked up column ${columns.find(c => c.id === data.id)?.title}.`;
|
|
279
|
+
const item = data?.type === 'card' ? findItem(data.id) : undefined;
|
|
280
|
+
return item ? `Picked up card ${item.title}.` : undefined;
|
|
281
|
+
},
|
|
282
|
+
onDragOver: ({ active: a, over }) => {
|
|
283
|
+
const data = dataOf(a.data.current);
|
|
284
|
+
if (!over)
|
|
285
|
+
return undefined;
|
|
286
|
+
if (data?.type === 'column') {
|
|
287
|
+
const target = dataOf(over.data.current);
|
|
288
|
+
return target?.type === 'column' ? `Over column ${columns.find(c => c.id === target.id)?.title}.` : undefined;
|
|
289
|
+
}
|
|
290
|
+
const item = data?.type === 'card' ? findItem(data.id) : undefined;
|
|
291
|
+
return item ? `Card ${item.title} is in ${cellName(cellOf(item))}.` : undefined;
|
|
292
|
+
},
|
|
293
|
+
onDragEnd: ({ active: a }) => {
|
|
294
|
+
const data = dataOf(a.data.current);
|
|
295
|
+
if (data?.type === 'column')
|
|
296
|
+
return 'Column dropped.';
|
|
297
|
+
const item = data?.type === 'card' ? findItem(data.id) : undefined;
|
|
298
|
+
return item ? `Card ${item.title} dropped in ${cellName(cellOf(item))}.` : undefined;
|
|
299
|
+
},
|
|
300
|
+
onDragCancel: () => 'Move cancelled. The card went back.',
|
|
301
|
+
};
|
|
302
|
+
const select = (item, event) => {
|
|
303
|
+
const next = new Set(selected);
|
|
304
|
+
if (event.shiftKey && anchor) {
|
|
305
|
+
const flat = laneList.flatMap(lane => columns.flatMap(c => cells.get(cellKey(lane.id, c.id)) ?? []));
|
|
306
|
+
const from = flat.findIndex(i => i.id === anchor);
|
|
307
|
+
const to = flat.findIndex(i => i.id === item.id);
|
|
308
|
+
if (from !== -1 && to !== -1)
|
|
309
|
+
for (const i of flat.slice(Math.min(from, to), Math.max(from, to) + 1))
|
|
310
|
+
next.add(i.id);
|
|
311
|
+
}
|
|
312
|
+
else if (next.has(item.id))
|
|
313
|
+
next.delete(item.id);
|
|
314
|
+
else
|
|
315
|
+
next.add(item.id);
|
|
316
|
+
setSelected(next);
|
|
317
|
+
setAnchor(item.id);
|
|
318
|
+
};
|
|
319
|
+
const rename = (item, title) => onItemsChange?.(items.map(i => (i.id === item.id ? { ...i, title } : i)));
|
|
320
|
+
const renameColumn = (column, title) => onColumnsChange?.(columns.map(c => (c.id === column.id ? { ...c, title } : c)));
|
|
321
|
+
const deleteColumn = (column) => {
|
|
322
|
+
onColumnsChange?.(columns.filter(c => c.id !== column.id));
|
|
323
|
+
if (items.some(i => i.columnId === column.id))
|
|
324
|
+
onItemsChange?.(items.filter(i => i.columnId !== column.id));
|
|
325
|
+
};
|
|
326
|
+
const latest = useRef({ select, rename });
|
|
327
|
+
useLayoutEffect(() => {
|
|
328
|
+
latest.current = { select, rename };
|
|
329
|
+
});
|
|
330
|
+
const onSelect = useCallback((item, event) => latest.current.select(item, event), []);
|
|
331
|
+
const onRename = useCallback((item, title) => latest.current.rename(item, title), []);
|
|
332
|
+
const toggle = (set, id) => {
|
|
333
|
+
const next = new Set(set);
|
|
334
|
+
if (next.has(id))
|
|
335
|
+
next.delete(id);
|
|
336
|
+
else
|
|
337
|
+
next.add(id);
|
|
338
|
+
return next;
|
|
339
|
+
};
|
|
340
|
+
const viewportRef = useRef(null);
|
|
341
|
+
const bodyRef = useRef(null);
|
|
342
|
+
const [bodyTop, setBodyTop] = useState(0);
|
|
343
|
+
useLayoutEffect(() => {
|
|
344
|
+
setBodyTop(bodyRef.current?.offsetTop ?? 0);
|
|
345
|
+
});
|
|
346
|
+
// Writes data attributes directly so scrolling never re-renders the board.
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const viewport = viewportRef.current;
|
|
349
|
+
const frame = viewport?.parentElement;
|
|
350
|
+
if (!viewport || !frame)
|
|
351
|
+
return;
|
|
352
|
+
const update = () => {
|
|
353
|
+
const { scrollLeft, scrollWidth, clientWidth } = viewport;
|
|
354
|
+
frame.toggleAttribute('data-more-start', scrollLeft > 1);
|
|
355
|
+
frame.toggleAttribute('data-more-end', scrollLeft + clientWidth < scrollWidth - 1);
|
|
356
|
+
};
|
|
357
|
+
update();
|
|
358
|
+
viewport.addEventListener('scroll', update, { passive: true });
|
|
359
|
+
const observer = new ResizeObserver(update);
|
|
360
|
+
observer.observe(viewport);
|
|
361
|
+
if (viewport.firstElementChild)
|
|
362
|
+
observer.observe(viewport.firstElementChild);
|
|
363
|
+
return () => {
|
|
364
|
+
viewport.removeEventListener('scroll', update);
|
|
365
|
+
observer.disconnect();
|
|
366
|
+
};
|
|
367
|
+
}, []);
|
|
368
|
+
const activeLane = active?.type === 'card'
|
|
369
|
+
? laneList.findIndex(l => {
|
|
370
|
+
const item = findItem(active.id);
|
|
371
|
+
return item ? l.id === laneOf(item) : false;
|
|
372
|
+
})
|
|
373
|
+
: -1;
|
|
374
|
+
const laneVirtualizer = useVirtualizer({
|
|
375
|
+
count: withLanes ? laneList.length : 0,
|
|
376
|
+
getScrollElement: () => viewportRef.current,
|
|
377
|
+
estimateSize: () => 140,
|
|
378
|
+
overscan: 2,
|
|
379
|
+
scrollMargin: bodyTop,
|
|
380
|
+
initialRect: { width: 0, height: 720 },
|
|
381
|
+
getItemKey: index => laneList[index]?.id ?? index,
|
|
382
|
+
rangeExtractor: (range) => {
|
|
383
|
+
const indexes = defaultRangeExtractor(range);
|
|
384
|
+
return activeLane !== -1 && !indexes.includes(activeLane)
|
|
385
|
+
? [...indexes, activeLane].sort((a, b) => a - b)
|
|
386
|
+
: indexes;
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
// Lanes sit in normal flow after one spacer, so a lane that grows pushes the
|
|
390
|
+
// ones below it in the same frame. The dragged card's lane, kept rendered
|
|
391
|
+
// outside the range, is placed on its own.
|
|
392
|
+
const laneRows = laneVirtualizer.getVirtualItems().map((row, i, rows) => ({
|
|
393
|
+
...row,
|
|
394
|
+
pinned: rows.length > 1 &&
|
|
395
|
+
row.index === activeLane &&
|
|
396
|
+
rows[i - 1]?.index !== row.index - 1 &&
|
|
397
|
+
rows[i + 1]?.index !== row.index + 1,
|
|
398
|
+
}));
|
|
399
|
+
const firstFlow = laneRows.find(row => !row.pinned);
|
|
400
|
+
const onBoardKeyDown = (event) => {
|
|
401
|
+
if (event.key === 'Escape' && selected.size && !active)
|
|
402
|
+
setSelected(new Set());
|
|
403
|
+
};
|
|
404
|
+
const activeItem = active?.type === 'card' ? findItem(active.id) : undefined;
|
|
405
|
+
const activeColumn = active?.type === 'column' ? columns.find(c => c.id === active.id) : undefined;
|
|
406
|
+
const columnTemplate = columns
|
|
407
|
+
.map(c => (collapsedColumns.has(c.id) ? 'var(--sprout-board-collapsed-width)' : 'var(--sprout-board-column-width)'))
|
|
408
|
+
.join(' ');
|
|
409
|
+
const gridStyle = {
|
|
410
|
+
gridTemplateColumns: `${columnTemplate}${onColumnAdd ? ' var(--sprout-board-column-width)' : ''}`,
|
|
411
|
+
};
|
|
412
|
+
const loadedIn = (column) => columnTotals.get(column.id) ?? 0;
|
|
413
|
+
const showAllCount = (column) => onColumnShowAll &&
|
|
414
|
+
!column.status &&
|
|
415
|
+
!collapsedColumns.has(column.id) &&
|
|
416
|
+
column.total !== undefined &&
|
|
417
|
+
column.total > loadedIn(column)
|
|
418
|
+
? column.total
|
|
419
|
+
: undefined;
|
|
420
|
+
// With lanes, a column's message and "Show all" show once, under its header
|
|
421
|
+
// and at the board foot, instead of in every lane.
|
|
422
|
+
const laneNotices = withLanes && columns.some(c => c.status === 'error' || c.status === 'unavailable');
|
|
423
|
+
const laneFoot = withLanes && columns.some(c => showAllCount(c) !== undefined);
|
|
424
|
+
const renderRow = (lane) => (_jsx("div", { className: "sprout-board-row", style: gridStyle, children: columns.map(column => (_jsx(BoardCell, { laneId: lane.id, column: column, name: cellName({ laneId: lane.id, columnId: column.id }), items: cells.get(cellKey(lane.id, column.id)) ?? [], collapsed: collapsedColumns.has(column.id), fill: !withLanes, empty: !column.status && !column.total && loadedIn(column) === 0 ? (columnText?.empty ?? 'No cards') : undefined, showAll: withLanes ? undefined : showAllCount(column), onShowAll: !withLanes && showAllCount(column) !== undefined ? onColumnShowAll : undefined, showAllText: columnText?.showAll, onRetry: !withLanes && column.status === 'error' ? onColumnRetry : undefined, activeId: active?.type === 'card' ? active.id : null, movable: !!onItemsChange && !needle, selected: selected, onSelect: onItemsChange ? onSelect : undefined, onRename: onItemsChange ? onRename : undefined, onItemAdd: onItemAdd, onItemOpen: onItemOpen, look: look }, column.id))) }));
|
|
425
|
+
const look = useMemo(() => ({ renderBody, renderTitle, renderCard, itemActions }), [renderBody, renderTitle, renderCard, itemActions]);
|
|
426
|
+
const toolbar = searchable || laneBy || selected.size > 0;
|
|
427
|
+
return (_jsxs("div", { "data-slot": "board", className: cn('sprout-board', className), onKeyDown: onBoardKeyDown, children: [toolbar && (_jsxs("div", { className: "sprout-board-toolbar", "data-slot": "board-toolbar", children: [searchable && (_jsx(SearchField, { label: "Search cards", value: query, onChange: setQuery, className: "sprout-board-search" })), laneBy && (_jsxs("span", { className: "sprout-board-lane-by", children: [_jsx("span", { children: laneBy.label ?? 'Lanes by' }), _jsxs(Select, { value: laneBy.value, onValueChange: value => {
|
|
428
|
+
if (typeof value === 'string')
|
|
429
|
+
laneBy.onChange(value);
|
|
430
|
+
}, children: [_jsx(SelectTrigger, { size: "sm", "aria-label": laneBy.label ?? 'Lanes by', children: _jsx(SelectValue, { children: laneBy.options.find(o => o.value === laneBy.value)?.label }) }), _jsx(SelectContent, { children: laneBy.options.map(option => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] })), _jsx("span", { className: "sprout-board-status", role: "status", children: selected.size > 0 ? (_jsxs(_Fragment, { children: [selected.size, " selected", _jsx(Button, { variant: "ghost", size: "xs", onClick: () => setSelected(new Set()), children: "Clear" })] })) : needle ? (visibleCount ? (`${visibleCount.toLocaleString('en-US')} of ${items.length.toLocaleString('en-US')} cards`) : (`No cards match “${query.trim()}”`)) : (`${items.length.toLocaleString('en-US')} cards`) })] })), _jsxs(DndContext, { id: dndId, sensors: sensors, accessibility: { ...dndAccessibility, announcements }, collisionDetection: collide, onDragStart: onDragStart, onDragOver: onDragOver, onDragEnd: onDragEnd, onDragCancel: finish, children: [_jsx("div", { className: "sprout-board-frame", children: _jsx("div", { ref: viewportRef, role: "region", "aria-label": label, className: "sprout-board-viewport", "data-lanes": withLanes || undefined, children: _jsxs("div", { className: "sprout-board-canvas", children: [_jsxs("div", { className: "sprout-board-header", style: gridStyle, children: [_jsx(SortableContext, { items: columns.map(c => COLUMN + c.id), strategy: horizontalListSortingStrategy, children: columns.map(column => (_jsx(BoardColumnHeader, { column: column, loaded: loadedIn(column), collapsed: collapsedColumns.has(column.id), onCollapse: () => setCollapsedColumns(s => toggle(s, column.id)), onRename: onColumnsChange ? renameColumn : undefined, onDelete: onColumnsChange ? deleteColumn : undefined }, column.id))) }), onColumnAdd && _jsx(AddColumn, { onAdd: onColumnAdd }), laneNotices &&
|
|
431
|
+
columns.map(column => (_jsx("div", { className: "sprout-board-column-notice", children: !collapsedColumns.has(column.id) && _jsx(ColumnNotice, { column: column, onRetry: onColumnRetry }) }, `notice:${column.id}`)))] }), withLanes ? (_jsx("div", { ref: bodyRef, className: "sprout-board-lanes", style: { height: laneVirtualizer.getTotalSize() }, children: laneRows.map(row => {
|
|
432
|
+
const lane = laneList[row.index];
|
|
433
|
+
if (!lane)
|
|
434
|
+
return null;
|
|
435
|
+
const collapsed = collapsedLanes.has(lane.id);
|
|
436
|
+
const count = laneTotals(lane.id);
|
|
437
|
+
return (_jsxs("section", { ref: laneVirtualizer.measureElement, "data-index": row.index, "aria-label": `${lane.title}, ${count} cards`, className: "sprout-board-lane", "data-collapsed": collapsed || undefined, "data-pinned": row.pinned || undefined, style: {
|
|
438
|
+
...(row.pinned
|
|
439
|
+
? { top: row.start - bodyTop }
|
|
440
|
+
: row === firstFlow
|
|
441
|
+
? { marginTop: row.start - bodyTop }
|
|
442
|
+
: {}),
|
|
443
|
+
minHeight: laneLock?.get(lane.id),
|
|
444
|
+
}, children: [_jsx("div", { className: "sprout-board-lane-bar", children: _jsxs("button", { type: "button", className: "sprout-board-lane-toggle", "aria-expanded": !collapsed, onClick: () => setCollapsedLanes(s => toggle(s, lane.id)), children: [collapsed ? _jsx(ChevronRight, { "aria-hidden": "true" }) : _jsx(ChevronDown, { "aria-hidden": "true" }), _jsx("span", { className: "sprout-board-lane-title", children: lane.title }), _jsx("span", { className: "sprout-board-count", children: count })] }) }), !collapsed && renderRow(lane)] }, row.key));
|
|
445
|
+
}) })) : (_jsx("div", { ref: bodyRef, className: "sprout-board-single", children: laneList[0] && renderRow(laneList[0]) })), laneFoot && onColumnShowAll && (_jsx("div", { className: "sprout-board-foot", style: gridStyle, children: columns.map(column => {
|
|
446
|
+
const count = showAllCount(column);
|
|
447
|
+
return (_jsx("div", { children: count !== undefined && (_jsx(ShowAll, { column: column, count: count, onShowAll: onColumnShowAll, text: columnText?.showAll })) }, column.id));
|
|
448
|
+
}) }))] }) }) }), _jsx(DragOverlay, { dropAnimation: reducedMotion() ? null : dropAnimation, children: activeItem ? (_jsxs("div", { className: "sprout-board-overlay", children: [_jsx(CardFace, { item: activeItem, look: look }), extras.length > 0 && (_jsxs(Badge, { variant: "default", className: "sprout-board-overlay-count", children: [extras.length + 1, " cards"] }))] })) : activeColumn ? (_jsx("div", { className: "sprout-board-column-header sprout-board-overlay", children: _jsx("span", { className: "sprout-board-column-title", "data-tone": activeColumn.tone ? '' : undefined, children: _jsx(ColumnTitle, { column: activeColumn }) }) })) : null })] })] }));
|
|
449
|
+
}
|
|
450
|
+
const cardTitle = (item, look) => (_jsx("span", { className: "sprout-board-card-title", children: look.renderTitle ? look.renderTitle(item) : item.title }));
|
|
451
|
+
function cardInside(item, look, parts, bodyId) {
|
|
452
|
+
if (look.renderCard)
|
|
453
|
+
return look.renderCard(item, parts);
|
|
454
|
+
const body = look.renderBody?.(item);
|
|
455
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sprout-board-card-head", children: [parts.title, parts.menu] }), body != null && (_jsx("div", { id: bodyId, className: "sprout-board-card-body", children: body }))] }));
|
|
456
|
+
}
|
|
457
|
+
function CardFace({ item, look }) {
|
|
458
|
+
return (_jsx("div", { className: "sprout-board-card", "data-overlay": "", "data-custom": look.renderCard ? '' : undefined, children: cardInside(item, look, { title: cardTitle(item, look), menu: null }) }));
|
|
459
|
+
}
|
|
460
|
+
const BoardCell = memo(function BoardCell({ laneId, column, name, items, collapsed, fill, empty, showAll, onShowAll, showAllText, onRetry, activeId, movable, selected, onSelect, onRename, onItemAdd, onItemOpen, look, }) {
|
|
461
|
+
const columnId = column.id;
|
|
462
|
+
const status = column.status;
|
|
463
|
+
const cell = useMemo(() => ({ laneId, columnId }), [laneId, columnId]);
|
|
464
|
+
const drop = useDroppable({
|
|
465
|
+
id: `cell:${cellKey(cell.laneId, cell.columnId)}`,
|
|
466
|
+
data: { type: 'cell', cell },
|
|
467
|
+
disabled: status !== undefined,
|
|
468
|
+
});
|
|
469
|
+
const scrollRef = useRef(null);
|
|
470
|
+
const activeIndex = activeId ? items.findIndex(i => i.id === activeId) : -1;
|
|
471
|
+
const virtualizer = useVirtualizer({
|
|
472
|
+
count: collapsed ? 0 : items.length,
|
|
473
|
+
getScrollElement: () => scrollRef.current,
|
|
474
|
+
estimateSize: () => 60,
|
|
475
|
+
gap: 4,
|
|
476
|
+
overscan: 6,
|
|
477
|
+
initialRect: { width: 0, height: 480 },
|
|
478
|
+
getItemKey: index => items[index]?.id ?? index,
|
|
479
|
+
rangeExtractor: (range) => {
|
|
480
|
+
const indexes = defaultRangeExtractor(range);
|
|
481
|
+
return activeIndex !== -1 && !indexes.includes(activeIndex)
|
|
482
|
+
? [...indexes, activeIndex].sort((a, b) => a - b)
|
|
483
|
+
: indexes;
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const [adding, setAdding] = useState(false);
|
|
487
|
+
if (collapsed)
|
|
488
|
+
return (_jsx("div", { ref: drop.setNodeRef, className: "sprout-board-cell", "data-collapsed": "", "data-over": drop.isOver || undefined, "aria-label": `${name}, ${items.length} cards, collapsed`, role: "group", children: _jsx("span", { className: "sprout-board-count", children: items.length || '' }) }));
|
|
489
|
+
return (_jsxs("div", { ref: drop.setNodeRef, className: "sprout-board-cell", "data-fill": fill || undefined, "data-over": drop.isOver || undefined, "data-status": status, children: [fill && _jsx(ColumnNotice, { column: column, onRetry: onRetry }), _jsxs("div", { ref: scrollRef, className: "sprout-board-cell-scroll", children: [_jsx(SortableContext, { items: items.map(i => CARD + i.id), strategy: verticalListSortingStrategy, children: _jsx("div", { role: "list", "aria-label": `${name}, ${items.length} cards`, "aria-busy": status === 'loading' || undefined, className: "sprout-board-cards", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map(row => {
|
|
490
|
+
const item = items[row.index];
|
|
491
|
+
if (!item)
|
|
492
|
+
return null;
|
|
493
|
+
return (_jsx("div", { ref: virtualizer.measureElement, "data-index": row.index, role: "listitem", className: "sprout-board-card-slot", style: { top: row.start }, children: _jsx(BoardCard, { item: item, movable: movable, selected: selected.has(item.id), onSelect: onSelect, onRename: onRename, onOpen: onItemOpen, look: look }) }, row.key));
|
|
494
|
+
}) }) }), status === 'loading' && items.length === 0 && (_jsx("div", { className: "sprout-board-skeletons", "aria-hidden": "true", children: Array.from({ length: fill ? 3 : 2 }, (_, index) => (_jsxs("div", { className: "sprout-board-skeleton-card", children: [_jsx(Skeleton, {}), _jsx(Skeleton, {})] }, index))) })), empty !== undefined && items.length === 0 && _jsx("p", { className: "sprout-board-empty", children: empty })] }), showAll !== undefined && onShowAll && (_jsx(ShowAll, { column: column, count: showAll, onShowAll: onShowAll, text: showAllText })), onItemAdd &&
|
|
495
|
+
!status &&
|
|
496
|
+
(adding ? (_jsx(InlineText, { label: `New card in ${name}`, placeholder: "Card title", initial: "", keepOpen: true, onCommit: title => onItemAdd({
|
|
497
|
+
title,
|
|
498
|
+
columnId: cell.columnId,
|
|
499
|
+
laneId: fill ? undefined : cell.laneId,
|
|
500
|
+
}), onClose: () => setAdding(false) })) : (_jsxs(Button, { variant: "ghost", size: "xs", className: "sprout-board-add", onClick: () => setAdding(true), "aria-label": `Add card to ${name}`, children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), "Add card"] })))] }));
|
|
501
|
+
});
|
|
502
|
+
const BoardCard = memo(function BoardCard({ item, movable, selected, onSelect, onRename, onOpen, look, }) {
|
|
503
|
+
const [editing, setEditing] = useState(false);
|
|
504
|
+
const sortable = useSortable({
|
|
505
|
+
id: CARD + item.id,
|
|
506
|
+
data: { type: 'card', id: item.id },
|
|
507
|
+
disabled: !movable || editing,
|
|
508
|
+
animateLayoutChanges,
|
|
509
|
+
});
|
|
510
|
+
const bodyId = useId();
|
|
511
|
+
const actions = look.itemActions?.(item);
|
|
512
|
+
const describe = !look.renderCard && !!look.renderBody;
|
|
513
|
+
const onClick = (event) => {
|
|
514
|
+
if (onSelect && (event.metaKey || event.ctrlKey || event.shiftKey)) {
|
|
515
|
+
event.preventDefault();
|
|
516
|
+
onSelect(item, event);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
onOpen?.(item);
|
|
520
|
+
};
|
|
521
|
+
const onKeyDown = (event) => {
|
|
522
|
+
if (event.target !== event.currentTarget)
|
|
523
|
+
return;
|
|
524
|
+
if (event.key === 'Enter' && onOpen) {
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
onOpen(item);
|
|
527
|
+
}
|
|
528
|
+
else if (event.key === 'F2' && onRename) {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
setEditing(true);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
return (_jsx("div", { ref: sortable.setNodeRef, ...sortable.attributes, ...sortable.listeners, "aria-disabled": undefined, role: "group", "aria-roledescription": movable ? 'draggable card' : 'card', "aria-label": item.title, "aria-describedby": describe ? bodyId : undefined, tabIndex: 0, className: "sprout-board-card", "data-selected": selected || undefined, "data-dragging": sortable.isDragging || undefined, "data-openable": onOpen ? '' : undefined, "data-custom": look.renderCard ? '' : undefined, style: {
|
|
534
|
+
transform: CSS.Translate.toString(sortable.transform),
|
|
535
|
+
transition: sortable.transition,
|
|
536
|
+
}, onClick: editing ? undefined : onClick, onDoubleClick: onRename && !onOpen && !editing ? () => setEditing(true) : undefined, onKeyDown: event => {
|
|
537
|
+
onKeyDown(event);
|
|
538
|
+
// Space in the menu button or title must not pick the card up.
|
|
539
|
+
if (event.target === event.currentTarget && !event.defaultPrevented)
|
|
540
|
+
sortable.listeners?.onKeyDown?.(event);
|
|
541
|
+
}, children: cardInside(item, look, {
|
|
542
|
+
title: editing && onRename ? (_jsx(InlineText, { label: "Card title", initial: item.title, onCommit: title => onRename(item, title), onClose: () => setEditing(false) })) : (cardTitle(item, look)),
|
|
543
|
+
menu: (actions != null || onRename) && !editing ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-xs", className: "sprout-board-card-menu", "aria-label": `Actions for ${item.title}`, onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation() }), children: _jsx(MoreH, { "aria-hidden": "true" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [onRename && _jsx(DropdownMenuItem, { onClick: () => setEditing(true), children: "Rename" }), onRename && actions != null && _jsx(DropdownMenuSeparator, {}), actions] })] })) : null,
|
|
544
|
+
}, bodyId) }));
|
|
545
|
+
});
|
|
546
|
+
const statusWords = {
|
|
547
|
+
loading: 'loading',
|
|
548
|
+
error: 'couldn’t load',
|
|
549
|
+
unavailable: 'unavailable',
|
|
550
|
+
};
|
|
551
|
+
function BoardColumnHeader({ column, loaded, collapsed, onCollapse, onRename, onDelete }) {
|
|
552
|
+
const [editing, setEditing] = useState(false);
|
|
553
|
+
const [confirming, setConfirming] = useState(false);
|
|
554
|
+
const sortable = useSortable({
|
|
555
|
+
id: COLUMN + column.id,
|
|
556
|
+
data: { type: 'column', id: column.id },
|
|
557
|
+
disabled: !onRename || editing,
|
|
558
|
+
});
|
|
559
|
+
const total = column.total ?? loaded;
|
|
560
|
+
// Without a `total`, the loaded count means nothing until the column is ready.
|
|
561
|
+
const counted = column.total !== undefined || !column.status;
|
|
562
|
+
const over = counted && column.limit !== undefined && total > column.limit;
|
|
563
|
+
const count = column.limit !== undefined ? `${formatCount(total)} / ${formatCount(column.limit)}` : formatCount(total);
|
|
564
|
+
const label = [
|
|
565
|
+
column.title,
|
|
566
|
+
counted && `${formatCount(total)} cards`,
|
|
567
|
+
counted && total > loaded && !column.status && `${loaded} shown`,
|
|
568
|
+
column.status && statusWords[column.status],
|
|
569
|
+
over && 'over limit',
|
|
570
|
+
]
|
|
571
|
+
.filter(Boolean)
|
|
572
|
+
.join(', ');
|
|
573
|
+
const toggleButton = (_jsx("button", { type: "button", className: "sprout-board-column-toggle", "aria-expanded": !collapsed, "aria-label": `${collapsed ? 'Expand' : 'Collapse'} ${column.title}`, onClick: onCollapse, children: collapsed ? _jsx(ChevronRight, { "aria-hidden": "true" }) : _jsx(ChevronDown, { "aria-hidden": "true" }) }));
|
|
574
|
+
return (_jsxs("div", { ref: sortable.setNodeRef, ...sortable.listeners, role: "group", "aria-label": label, "aria-busy": column.status === 'loading' || undefined, className: "sprout-board-column-header", "data-collapsed": collapsed || undefined, "data-over-limit": over || undefined, "data-dragging": sortable.isDragging || undefined, style: {
|
|
575
|
+
transform: CSS.Translate.toString(sortable.transform),
|
|
576
|
+
transition: sortable.transition,
|
|
577
|
+
}, children: [collapsed && toggleButton, editing && onRename ? (_jsx(InlineText, { label: "Column title", initial: column.title, onCommit: title => onRename(column, title), onClose: () => setEditing(false) })) : onRename && !collapsed ? (_jsx("button", { type: "button", className: "sprout-board-column-title", "data-editable": "", onClick: () => setEditing(true), "aria-label": `Rename ${column.title}`, "data-tone": column.tone ? '' : undefined, children: _jsx(ColumnTitle, { column: column }) })) : (_jsx("span", { className: "sprout-board-column-title", "data-tone": column.tone && !collapsed ? '' : undefined, children: collapsed ? column.title : _jsx(ColumnTitle, { column: column }) })), !editing && counted && (_jsx("span", { className: "sprout-board-count", "aria-hidden": "true", children: count })), _jsx("span", { role: "status", className: "sprout-board-sr-only", children: column.status === 'loading' ? `Loading cards in ${column.title}…` : '' }), over && !collapsed && !editing && (_jsx(Badge, { variant: "warning", className: "sprout-board-limit", children: "Over limit" })), !collapsed && !editing && (_jsxs("span", { className: "sprout-board-column-tools", children: [toggleButton, onRename && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", ref: sortable.setActivatorNodeRef, ...sortable.attributes, className: "sprout-board-grip", "aria-label": `Move ${column.title}`, children: _jsx(Reorder, { "aria-hidden": "true" }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": `${column.title} column actions`, onPointerDown: event => event.stopPropagation() }), children: _jsx(MoreH, { "aria-hidden": "true" }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => setEditing(true), children: "Rename" }), _jsx(DropdownMenuItem, { onClick: onCollapse, children: "Collapse" }), onDelete && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { variant: "destructive", onClick: () => setConfirming(true), children: "Delete column\u2026" })] }))] })] })] }))] })), onDelete && (_jsx(AlertDialog, { open: confirming, onOpenChange: setConfirming, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { children: ["Delete \u201C", column.title, "\u201D?"] }), _jsx(AlertDialogDescription, { children: total
|
|
578
|
+
? `Its ${total} ${total === 1 ? 'card is' : 'cards are'} deleted too. This can’t be undone.`
|
|
579
|
+
: 'The column is empty. This can’t be undone.' })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Keep column" }), _jsx(AlertDialogAction, { variant: "destructive", onClick: () => {
|
|
580
|
+
setConfirming(false);
|
|
581
|
+
onDelete(column);
|
|
582
|
+
}, children: "Delete column" })] })] }) }))] }));
|
|
583
|
+
}
|
|
584
|
+
const formatCount = (count) => count.toLocaleString('en-US');
|
|
585
|
+
function ColumnTitle({ column }) {
|
|
586
|
+
if (!column.tone)
|
|
587
|
+
return column.title;
|
|
588
|
+
return (_jsx(Badge, { variant: column.tone, className: "sprout-board-column-badge", children: _jsx("span", { children: column.title }) }));
|
|
589
|
+
}
|
|
590
|
+
/** The error or unavailable message of a column. Nothing for other states. */
|
|
591
|
+
function ColumnNotice({ column, onRetry, }) {
|
|
592
|
+
if (column.status === 'error')
|
|
593
|
+
return (_jsxs("div", { className: "sprout-board-notice", "data-kind": "error", children: [_jsx("p", { role: "alert", children: column.error ?? 'Couldn’t load this column.' }), onRetry && (_jsx(Button, { variant: "outline", size: "xs", "aria-label": `Try again to load ${column.title}`, onClick: () => onRetry(column), children: "Try again" }))] }));
|
|
594
|
+
if (column.status === 'unavailable')
|
|
595
|
+
return (_jsx("p", { className: "sprout-board-notice", "data-kind": "unavailable", children: column.error ?? 'Cards are unavailable.' }));
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
function ShowAll({ column, count, onShowAll, text, }) {
|
|
599
|
+
return (_jsx(Button, { variant: "ghost", size: "xs", className: "sprout-board-show-all", "aria-label": text ? undefined : `Show all ${formatCount(count)} in ${column.title}`, onClick: () => onShowAll(column), children: text ? (_jsxs(_Fragment, { children: [text(count, column), _jsxs("span", { className: "sprout-board-sr-only", children: [", ", column.title] })] })) : (`Show all ${formatCount(count)}`) }));
|
|
600
|
+
}
|
|
601
|
+
function AddColumn({ onAdd }) {
|
|
602
|
+
const [adding, setAdding] = useState(false);
|
|
603
|
+
return (_jsx("div", { className: "sprout-board-add-column", children: adding ? (_jsx(InlineText, { label: "New column", placeholder: "Column title", initial: "", onCommit: onAdd, onClose: () => setAdding(false) })) : (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setAdding(true), children: [_jsx(Plus, { "aria-hidden": "true", "data-icon": "inline-start" }), "Add column"] })) }));
|
|
604
|
+
}
|
|
605
|
+
/** Enter saves, Escape cancels, blur saves. With `keepOpen` it clears after each save. */
|
|
606
|
+
function InlineText({ label, placeholder, initial, keepOpen, onCommit, onClose, }) {
|
|
607
|
+
const [value, setValue] = useState(initial);
|
|
608
|
+
const ref = useRef(null);
|
|
609
|
+
const done = useRef(false);
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
ref.current?.focus();
|
|
612
|
+
ref.current?.select();
|
|
613
|
+
}, []);
|
|
614
|
+
const commit = (close) => {
|
|
615
|
+
const text = value.trim();
|
|
616
|
+
if (text && text !== initial)
|
|
617
|
+
onCommit(text);
|
|
618
|
+
if (close || !keepOpen) {
|
|
619
|
+
done.current = true;
|
|
620
|
+
onClose();
|
|
621
|
+
}
|
|
622
|
+
else
|
|
623
|
+
setValue('');
|
|
624
|
+
};
|
|
625
|
+
return (_jsx(Input, { ref: ref, "aria-label": label, placeholder: placeholder, value: value, className: "sprout-board-inline-input", onChange: event => setValue(event.currentTarget.value), onPointerDown: event => event.stopPropagation(), onClick: event => event.stopPropagation(), onKeyDown: event => {
|
|
626
|
+
event.stopPropagation();
|
|
627
|
+
if (event.key === 'Enter') {
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
commit(false);
|
|
630
|
+
}
|
|
631
|
+
else if (event.key === 'Escape') {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
done.current = true;
|
|
634
|
+
onClose();
|
|
635
|
+
}
|
|
636
|
+
}, onBlur: () => {
|
|
637
|
+
if (!done.current)
|
|
638
|
+
commit(true);
|
|
639
|
+
} }));
|
|
640
|
+
}
|