@astryxdesign/cli 0.1.2 → 0.1.3
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/CHANGELOG.md +64 -1
- package/README.md +49 -55
- package/docs/layout.doc.dense.mjs +90 -0
- package/docs/layout.doc.mjs +160 -0
- package/docs/principles.doc.dense.mjs +2 -2
- package/docs/principles.doc.mjs +4 -0
- package/docs/principles.doc.zh.mjs +2 -2
- package/docs/theme.doc.mjs +2 -2
- package/package.json +13 -1
- package/src/api/blog.mjs +192 -0
- package/src/api/blog.test.mjs +144 -0
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +14 -7
- package/src/api/doctor.mjs +10 -25
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +11 -10
- package/src/api/layout.test.mjs +4 -1
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +242 -84
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
- package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/agent-docs.mjs +4 -1
- package/src/commands/blog.mjs +60 -0
- package/src/commands/build-theme.import-path.test.mjs +2 -12
- package/src/commands/build-theme.mjs +117 -0
- package/src/commands/build-theme.prose.test.mjs +5 -15
- package/src/commands/build-theme.watch.test.mjs +149 -0
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/ensure-core-built.mjs +120 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/swizzle.mjs +224 -235
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -170
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +7 -20
- package/src/config.test.mjs +89 -13
- package/src/index.mjs +8 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +41 -55
- package/src/lib/error-codes.mjs +15 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +92 -115
- package/src/lib/integrations.test.mjs +88 -107
- package/src/lib/manifest.mjs +5 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/site.mjs +20 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +56 -85
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -237
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
- package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
- package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
- package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
- package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
- package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
- package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
- package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
- package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
- package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
- package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
- package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
- package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
- package/templates/pages/kanban-board/page.tsx +729 -0
- package/templates/pages/kanban-board/template.doc.mjs +12 -0
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +241 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -464
- package/src/commands/gap-report.test.mjs +0 -168
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -113
- package/src/lib/config.test.mjs +0 -91
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type PointerEvent as ReactPointerEvent,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import * as stylex from '@stylexjs/stylex';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
Layout,
|
|
17
|
+
LayoutHeader,
|
|
18
|
+
LayoutContent,
|
|
19
|
+
HStack,
|
|
20
|
+
VStack,
|
|
21
|
+
} from '@astryxdesign/core/Layout';
|
|
22
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
23
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
24
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
25
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
26
|
+
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
27
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
28
|
+
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
29
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
30
|
+
import {MoreMenu} from '@astryxdesign/core/MoreMenu';
|
|
31
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
32
|
+
import {Tooltip} from '@astryxdesign/core/Tooltip';
|
|
33
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
34
|
+
import {Toolbar} from '@astryxdesign/core/Toolbar';
|
|
35
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
PlusIcon,
|
|
39
|
+
MagnifyingGlassIcon,
|
|
40
|
+
ArrowsUpDownIcon,
|
|
41
|
+
FunnelIcon,
|
|
42
|
+
ArrowPathIcon,
|
|
43
|
+
CheckCircleIcon,
|
|
44
|
+
InboxIcon,
|
|
45
|
+
InformationCircleIcon,
|
|
46
|
+
ClipboardDocumentCheckIcon,
|
|
47
|
+
} from '@heroicons/react/24/outline';
|
|
48
|
+
|
|
49
|
+
// ============= TYPES =============
|
|
50
|
+
|
|
51
|
+
type ColumnId = 'todo' | 'in-progress' | 'in-review' | 'done';
|
|
52
|
+
type Priority = 'high' | 'medium' | 'low';
|
|
53
|
+
|
|
54
|
+
interface WorkItem {
|
|
55
|
+
id: string;
|
|
56
|
+
column: ColumnId;
|
|
57
|
+
ref: string;
|
|
58
|
+
priority: Priority;
|
|
59
|
+
title: string;
|
|
60
|
+
description: string;
|
|
61
|
+
lastEdited: string;
|
|
62
|
+
dueDate: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ColumnMeta {
|
|
66
|
+
id: ColumnId;
|
|
67
|
+
title: string;
|
|
68
|
+
variant: 'neutral' | 'accent' | 'warning' | 'success';
|
|
69
|
+
tooltip: string;
|
|
70
|
+
emptyTitle: string;
|
|
71
|
+
emptyDescription: string;
|
|
72
|
+
emptyIcon: typeof InboxIcon;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Where a dragged card will land: a column and an insertion index within it
|
|
76
|
+
// (measured against the cards remaining after the dragged card is removed).
|
|
77
|
+
interface DropTarget {
|
|
78
|
+
column: ColumnId;
|
|
79
|
+
index: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Live state of an in-progress pointer drag. Coordinates are in viewport space.
|
|
83
|
+
interface DragState {
|
|
84
|
+
id: string;
|
|
85
|
+
width: number;
|
|
86
|
+
height: number;
|
|
87
|
+
offsetX: number;
|
|
88
|
+
offsetY: number;
|
|
89
|
+
pointerX: number;
|
|
90
|
+
pointerY: number;
|
|
91
|
+
target: DropTarget | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============= DATA =============
|
|
95
|
+
|
|
96
|
+
const COLUMNS: ColumnMeta[] = [
|
|
97
|
+
{
|
|
98
|
+
id: 'todo',
|
|
99
|
+
title: 'To-do',
|
|
100
|
+
variant: 'neutral',
|
|
101
|
+
tooltip: 'Items assigned to this sprint, waiting to be picked up.',
|
|
102
|
+
emptyTitle: 'To-do is empty',
|
|
103
|
+
emptyDescription: 'Items pulled into this sprint appear here.',
|
|
104
|
+
emptyIcon: InboxIcon,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'in-progress',
|
|
108
|
+
title: 'In progress',
|
|
109
|
+
variant: 'accent',
|
|
110
|
+
tooltip: 'Items currently in progress.',
|
|
111
|
+
emptyTitle: 'Nothing in progress',
|
|
112
|
+
emptyDescription: 'Items being worked on appear here.',
|
|
113
|
+
emptyIcon: ArrowPathIcon,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'in-review',
|
|
117
|
+
title: 'In review',
|
|
118
|
+
variant: 'warning',
|
|
119
|
+
tooltip: 'Items waiting for your review.',
|
|
120
|
+
emptyTitle: 'Nothing in review',
|
|
121
|
+
emptyDescription: 'Items awaiting your review appear here.',
|
|
122
|
+
emptyIcon: ClipboardDocumentCheckIcon,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'done',
|
|
126
|
+
title: 'Done',
|
|
127
|
+
variant: 'success',
|
|
128
|
+
tooltip: 'Items that have been handled.',
|
|
129
|
+
emptyTitle: 'Nothing done yet',
|
|
130
|
+
emptyDescription: 'Completed items appear here.',
|
|
131
|
+
emptyIcon: CheckCircleIcon,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const PRIORITY_META: Record<
|
|
136
|
+
Priority,
|
|
137
|
+
{label: string; variant: 'error' | 'warning' | 'teal'}
|
|
138
|
+
> = {
|
|
139
|
+
high: {label: 'High', variant: 'error'},
|
|
140
|
+
medium: {label: 'Medium', variant: 'warning'},
|
|
141
|
+
low: {label: 'Low', variant: 'teal'},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const INITIAL_ITEMS: WorkItem[] = [
|
|
145
|
+
{
|
|
146
|
+
id: 't1',
|
|
147
|
+
column: 'todo',
|
|
148
|
+
ref: 'Task 4821',
|
|
149
|
+
priority: 'low',
|
|
150
|
+
title: 'Draft project kickoff brief',
|
|
151
|
+
description:
|
|
152
|
+
'Write a short brief outlining goals, scope, and success criteria for the upcoming project.',
|
|
153
|
+
lastEdited: '2h ago',
|
|
154
|
+
dueDate: 'Jul 8',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 't2',
|
|
158
|
+
column: 'todo',
|
|
159
|
+
ref: 'Task 4842',
|
|
160
|
+
priority: 'low',
|
|
161
|
+
title: 'Collect feedback from stakeholders',
|
|
162
|
+
description:
|
|
163
|
+
'Gather input from key stakeholders and summarize the main themes for the next review.',
|
|
164
|
+
lastEdited: '1d ago',
|
|
165
|
+
dueDate: 'Jul 11',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'p1',
|
|
169
|
+
column: 'in-progress',
|
|
170
|
+
ref: 'Task 4825',
|
|
171
|
+
priority: 'high',
|
|
172
|
+
title: 'Design the landing page layout',
|
|
173
|
+
description:
|
|
174
|
+
'Create a first-pass layout for the landing page and share it for early feedback.',
|
|
175
|
+
lastEdited: '18m ago',
|
|
176
|
+
dueDate: 'Jul 3',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'p2',
|
|
180
|
+
column: 'in-progress',
|
|
181
|
+
ref: 'Task 4833',
|
|
182
|
+
priority: 'medium',
|
|
183
|
+
title: 'Set up the project workspace',
|
|
184
|
+
description:
|
|
185
|
+
'Configure the shared workspace and invite the team so everyone has access.',
|
|
186
|
+
lastEdited: '5m ago',
|
|
187
|
+
dueDate: 'Jul 4',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'r1',
|
|
191
|
+
column: 'done',
|
|
192
|
+
ref: 'Task 4788',
|
|
193
|
+
priority: 'low',
|
|
194
|
+
title: 'Write the weekly status update',
|
|
195
|
+
description:
|
|
196
|
+
'Summarize progress, blockers, and next steps in a short update for the team.',
|
|
197
|
+
lastEdited: 'Yesterday',
|
|
198
|
+
dueDate: 'Jul 1',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'r2',
|
|
202
|
+
column: 'done',
|
|
203
|
+
ref: 'Task 4789',
|
|
204
|
+
priority: 'high',
|
|
205
|
+
title: 'Prepare the demo walkthrough',
|
|
206
|
+
description:
|
|
207
|
+
'Put together a short walkthrough covering the main features for the demo.',
|
|
208
|
+
lastEdited: '3d ago',
|
|
209
|
+
dueDate: 'Jun 30',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'r3',
|
|
213
|
+
column: 'done',
|
|
214
|
+
ref: 'Task 4790',
|
|
215
|
+
priority: 'medium',
|
|
216
|
+
title: 'Review and merge open changes',
|
|
217
|
+
description:
|
|
218
|
+
'Go through the pending changes, leave comments, and merge the ones that are ready.',
|
|
219
|
+
lastEdited: '4d ago',
|
|
220
|
+
dueDate: 'Jun 28',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
// Pointer travel (px) before a press is promoted to a drag, so taps and clicks
|
|
225
|
+
// on card controls still register normally.
|
|
226
|
+
const DRAG_THRESHOLD = 5;
|
|
227
|
+
|
|
228
|
+
// Shared width for every board column, so they stay visually aligned.
|
|
229
|
+
const COLUMN_WIDTH = 300;
|
|
230
|
+
|
|
231
|
+
// ============= STYLES =============
|
|
232
|
+
|
|
233
|
+
const styles = stylex.create({
|
|
234
|
+
boardColumns: {
|
|
235
|
+
overflowX: 'auto',
|
|
236
|
+
overflowY: 'hidden',
|
|
237
|
+
height: '100%',
|
|
238
|
+
padding: 'var(--spacing-4)',
|
|
239
|
+
},
|
|
240
|
+
columnShell: {
|
|
241
|
+
flexShrink: 0,
|
|
242
|
+
flexBasis: COLUMN_WIDTH,
|
|
243
|
+
height: '100%',
|
|
244
|
+
},
|
|
245
|
+
card: {
|
|
246
|
+
cursor: 'grab',
|
|
247
|
+
userSelect: 'none',
|
|
248
|
+
touchAction: 'none',
|
|
249
|
+
transition: 'box-shadow 120ms ease',
|
|
250
|
+
':hover': {
|
|
251
|
+
boxShadow: 'var(--shadow-med)',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
// The dragged card is lifted out of flow and follows the pointer. It ignores
|
|
255
|
+
// pointer events so hit-testing reads the columns underneath it.
|
|
256
|
+
floating: {
|
|
257
|
+
position: 'fixed',
|
|
258
|
+
insetBlockStart: 0,
|
|
259
|
+
insetInlineStart: 0,
|
|
260
|
+
pointerEvents: 'none',
|
|
261
|
+
cursor: 'grabbing',
|
|
262
|
+
boxShadow: 'var(--shadow-high)',
|
|
263
|
+
zIndex: 1000,
|
|
264
|
+
},
|
|
265
|
+
floatingAt: (x: number, y: number, width: number) => ({
|
|
266
|
+
width,
|
|
267
|
+
transform: `translate(${x}px, ${y}px)`,
|
|
268
|
+
}),
|
|
269
|
+
// Placeholder marking the landing slot; matches the dragged card's height.
|
|
270
|
+
ghost: (height: number) => ({
|
|
271
|
+
height,
|
|
272
|
+
borderRadius: 'var(--radius-container)',
|
|
273
|
+
backgroundColor: 'var(--color-background-muted)',
|
|
274
|
+
}),
|
|
275
|
+
toolbarDivider: {
|
|
276
|
+
height: 'auto',
|
|
277
|
+
marginBlock: 'var(--spacing-1)',
|
|
278
|
+
alignSelf: 'stretch',
|
|
279
|
+
},
|
|
280
|
+
columnEmptyState: {
|
|
281
|
+
paddingBlock: 'var(--spacing-10)',
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ============= CARD BODY =============
|
|
286
|
+
|
|
287
|
+
// Shared card contents, rendered both in the column list and inside the
|
|
288
|
+
// floating drag clone so the two stay pixel-identical.
|
|
289
|
+
function BoardCardBody({
|
|
290
|
+
item,
|
|
291
|
+
onMove,
|
|
292
|
+
}: {
|
|
293
|
+
item: WorkItem;
|
|
294
|
+
onMove: (id: string, to: ColumnId) => void;
|
|
295
|
+
}) {
|
|
296
|
+
const priority = PRIORITY_META[item.priority];
|
|
297
|
+
const moveTargets = COLUMNS.filter(c => c.id !== item.column).map(c => ({
|
|
298
|
+
label: `Move to ${c.title}`,
|
|
299
|
+
onClick: () => onMove(item.id, c.id),
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<VStack gap={2}>
|
|
304
|
+
<HStack hAlign="between" vAlign="start">
|
|
305
|
+
<HStack gap={1} vAlign="center" wrap="wrap">
|
|
306
|
+
<Badge label={item.ref} variant="neutral" />
|
|
307
|
+
<Badge label={priority.label} variant={priority.variant} />
|
|
308
|
+
</HStack>
|
|
309
|
+
<MoreMenu
|
|
310
|
+
label="Work item actions"
|
|
311
|
+
size="sm"
|
|
312
|
+
items={[
|
|
313
|
+
{label: 'Open', onClick: () => {}},
|
|
314
|
+
{label: 'Assign to me', onClick: () => {}},
|
|
315
|
+
{type: 'divider'},
|
|
316
|
+
...moveTargets,
|
|
317
|
+
]}
|
|
318
|
+
/>
|
|
319
|
+
</HStack>
|
|
320
|
+
|
|
321
|
+
<VStack gap={1}>
|
|
322
|
+
<Heading level={4}>{item.title}</Heading>
|
|
323
|
+
<Text type="supporting" color="secondary" maxLines={2}>
|
|
324
|
+
{item.description}
|
|
325
|
+
</Text>
|
|
326
|
+
</VStack>
|
|
327
|
+
|
|
328
|
+
<Text type="supporting" color="secondary">
|
|
329
|
+
Edited {item.lastEdited} · Due {item.dueDate}
|
|
330
|
+
</Text>
|
|
331
|
+
</VStack>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============= BOARD CARD =============
|
|
336
|
+
|
|
337
|
+
function BoardCard({
|
|
338
|
+
item,
|
|
339
|
+
cardRef,
|
|
340
|
+
onPointerDown,
|
|
341
|
+
onMove,
|
|
342
|
+
}: {
|
|
343
|
+
item: WorkItem;
|
|
344
|
+
cardRef: (el: HTMLDivElement | null) => void;
|
|
345
|
+
onPointerDown: (e: ReactPointerEvent, id: string) => void;
|
|
346
|
+
onMove: (id: string, to: ColumnId) => void;
|
|
347
|
+
}) {
|
|
348
|
+
return (
|
|
349
|
+
<Card
|
|
350
|
+
ref={cardRef}
|
|
351
|
+
padding={3}
|
|
352
|
+
xstyle={styles.card}
|
|
353
|
+
onPointerDown={e => onPointerDown(e, item.id)}>
|
|
354
|
+
<BoardCardBody item={item} onMove={onMove} />
|
|
355
|
+
</Card>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============= BOARD COLUMN =============
|
|
360
|
+
|
|
361
|
+
function BoardColumn({
|
|
362
|
+
meta,
|
|
363
|
+
count,
|
|
364
|
+
contentRef,
|
|
365
|
+
children,
|
|
366
|
+
}: {
|
|
367
|
+
meta: ColumnMeta;
|
|
368
|
+
count: number;
|
|
369
|
+
contentRef: (el: HTMLDivElement | null) => void;
|
|
370
|
+
children: ReactNode;
|
|
371
|
+
}) {
|
|
372
|
+
return (
|
|
373
|
+
<Card variant="muted" padding={0} xstyle={styles.columnShell}>
|
|
374
|
+
<Layout
|
|
375
|
+
height="fill"
|
|
376
|
+
header={
|
|
377
|
+
<LayoutHeader hasDivider padding={3}>
|
|
378
|
+
<HStack hAlign="between" vAlign="center">
|
|
379
|
+
<HStack gap={2} vAlign="center">
|
|
380
|
+
<StatusDot
|
|
381
|
+
variant={meta.variant}
|
|
382
|
+
label={`${meta.title} status`}
|
|
383
|
+
/>
|
|
384
|
+
<Heading level={4}>{meta.title}</Heading>
|
|
385
|
+
<Tooltip content={meta.tooltip}>
|
|
386
|
+
<Icon
|
|
387
|
+
icon={InformationCircleIcon}
|
|
388
|
+
size="sm"
|
|
389
|
+
color="secondary"
|
|
390
|
+
/>
|
|
391
|
+
</Tooltip>
|
|
392
|
+
</HStack>
|
|
393
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
394
|
+
{count}
|
|
395
|
+
</Text>
|
|
396
|
+
</HStack>
|
|
397
|
+
</LayoutHeader>
|
|
398
|
+
}
|
|
399
|
+
content={
|
|
400
|
+
<LayoutContent ref={contentRef} padding={2}>
|
|
401
|
+
{children ?? (
|
|
402
|
+
<EmptyState
|
|
403
|
+
isCompact
|
|
404
|
+
xstyle={styles.columnEmptyState}
|
|
405
|
+
icon={
|
|
406
|
+
<Icon icon={meta.emptyIcon} size="lg" color="secondary" />
|
|
407
|
+
}
|
|
408
|
+
title={meta.emptyTitle}
|
|
409
|
+
description={meta.emptyDescription}
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
</LayoutContent>
|
|
413
|
+
}
|
|
414
|
+
/>
|
|
415
|
+
</Card>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ============= MAIN =============
|
|
420
|
+
|
|
421
|
+
export default function KanbanBoardTemplate() {
|
|
422
|
+
const [items, setItems] = useState<WorkItem[]>(INITIAL_ITEMS);
|
|
423
|
+
const [sprint, setSprint] = useState('003');
|
|
424
|
+
const [drag, setDrag] = useState<DragState | null>(null);
|
|
425
|
+
|
|
426
|
+
// Live element registries for pointer hit-testing (kept out of render state).
|
|
427
|
+
const columnEls = useRef(new Map<ColumnId, HTMLElement>());
|
|
428
|
+
const cardEls = useRef(new Map<string, HTMLElement>());
|
|
429
|
+
const columnRefCbs = useRef(
|
|
430
|
+
new Map<ColumnId, (el: HTMLDivElement | null) => void>(),
|
|
431
|
+
);
|
|
432
|
+
const cardRefCbs = useRef(
|
|
433
|
+
new Map<string, (el: HTMLDivElement | null) => void>(),
|
|
434
|
+
);
|
|
435
|
+
const teardownRef = useRef<(() => void) | null>(null);
|
|
436
|
+
|
|
437
|
+
// Stable ref callbacks so registering an element never churns across renders.
|
|
438
|
+
const getColumnRef = (id: ColumnId) => {
|
|
439
|
+
let cb = columnRefCbs.current.get(id);
|
|
440
|
+
if (!cb) {
|
|
441
|
+
cb = el => {
|
|
442
|
+
if (el) {columnEls.current.set(id, el);}
|
|
443
|
+
else {columnEls.current.delete(id);}
|
|
444
|
+
};
|
|
445
|
+
columnRefCbs.current.set(id, cb);
|
|
446
|
+
}
|
|
447
|
+
return cb;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const getCardRef = (id: string) => {
|
|
451
|
+
let cb = cardRefCbs.current.get(id);
|
|
452
|
+
if (!cb) {
|
|
453
|
+
cb = el => {
|
|
454
|
+
if (el) {cardEls.current.set(id, el);}
|
|
455
|
+
else {cardEls.current.delete(id);}
|
|
456
|
+
};
|
|
457
|
+
cardRefCbs.current.set(id, cb);
|
|
458
|
+
}
|
|
459
|
+
return cb;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const itemsByColumn = useMemo(() => {
|
|
463
|
+
const map: Record<ColumnId, WorkItem[]> = {
|
|
464
|
+
todo: [],
|
|
465
|
+
'in-progress': [],
|
|
466
|
+
'in-review': [],
|
|
467
|
+
done: [],
|
|
468
|
+
};
|
|
469
|
+
for (const item of items) {
|
|
470
|
+
map[item.column].push(item);
|
|
471
|
+
}
|
|
472
|
+
return map;
|
|
473
|
+
}, [items]);
|
|
474
|
+
|
|
475
|
+
const moveItem = (id: string, to: ColumnId) => {
|
|
476
|
+
setItems(prev =>
|
|
477
|
+
prev.map(item => (item.id === id ? {...item, column: to} : item)),
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Resolve the pointer position to a column + insertion index, ignoring the
|
|
482
|
+
// card being dragged so the math is against the cards that stay in place.
|
|
483
|
+
const computeTarget = (
|
|
484
|
+
px: number,
|
|
485
|
+
py: number,
|
|
486
|
+
draggedId: string,
|
|
487
|
+
): DropTarget | null => {
|
|
488
|
+
for (const [colId, el] of Array.from(columnEls.current.entries())) {
|
|
489
|
+
const r = el.getBoundingClientRect();
|
|
490
|
+
if (px < r.left || px > r.right || py < r.top || py > r.bottom) {continue;}
|
|
491
|
+
|
|
492
|
+
const ids = itemsByColumn[colId]
|
|
493
|
+
.filter(it => it.id !== draggedId)
|
|
494
|
+
.map(it => it.id);
|
|
495
|
+
|
|
496
|
+
let index = ids.length;
|
|
497
|
+
for (let i = 0; i < ids.length; i++) {
|
|
498
|
+
const cardEl = cardEls.current.get(ids[i]);
|
|
499
|
+
if (!cardEl) {continue;}
|
|
500
|
+
const cr = cardEl.getBoundingClientRect();
|
|
501
|
+
if (py < cr.top + cr.height / 2) {
|
|
502
|
+
index = i;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {column: colId, index};
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Rebuild the flat item list so the dragged card lands at the resolved slot
|
|
512
|
+
// while every other card keeps its relative order.
|
|
513
|
+
const commitDrag = (id: string, target: DropTarget) => {
|
|
514
|
+
setItems(prev => {
|
|
515
|
+
const moved = prev.find(it => it.id === id);
|
|
516
|
+
if (!moved) {return prev;}
|
|
517
|
+
|
|
518
|
+
const rest = prev.filter(it => it.id !== id);
|
|
519
|
+
const updated: WorkItem = {...moved, column: target.column};
|
|
520
|
+
const colItems = rest.filter(it => it.column === target.column);
|
|
521
|
+
const anchor = colItems[target.index];
|
|
522
|
+
|
|
523
|
+
if (!anchor) {return [...rest, updated];}
|
|
524
|
+
const at = rest.indexOf(anchor);
|
|
525
|
+
return [...rest.slice(0, at), updated, ...rest.slice(at)];
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const onCardPointerDown = (e: ReactPointerEvent, id: string) => {
|
|
530
|
+
if (e.button !== 0) {return;}
|
|
531
|
+
// Let the card's own controls (the actions menu) handle the press.
|
|
532
|
+
if (
|
|
533
|
+
(e.target as HTMLElement).closest(
|
|
534
|
+
'button, [role="menuitem"], [role="menu"]',
|
|
535
|
+
)
|
|
536
|
+
) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const el = cardEls.current.get(id);
|
|
541
|
+
if (!el) {return;}
|
|
542
|
+
|
|
543
|
+
const rect = el.getBoundingClientRect();
|
|
544
|
+
const startX = e.clientX;
|
|
545
|
+
const startY = e.clientY;
|
|
546
|
+
const offsetX = startX - rect.left;
|
|
547
|
+
const offsetY = startY - rect.top;
|
|
548
|
+
const {width, height} = rect;
|
|
549
|
+
|
|
550
|
+
let started = false;
|
|
551
|
+
let target: DropTarget | null = null;
|
|
552
|
+
|
|
553
|
+
const onMove = (ev: PointerEvent) => {
|
|
554
|
+
if (
|
|
555
|
+
!started &&
|
|
556
|
+
Math.abs(ev.clientX - startX) + Math.abs(ev.clientY - startY) <
|
|
557
|
+
DRAG_THRESHOLD
|
|
558
|
+
) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
started = true;
|
|
562
|
+
target = computeTarget(ev.clientX, ev.clientY, id);
|
|
563
|
+
setDrag({
|
|
564
|
+
id,
|
|
565
|
+
width,
|
|
566
|
+
height,
|
|
567
|
+
offsetX,
|
|
568
|
+
offsetY,
|
|
569
|
+
pointerX: ev.clientX,
|
|
570
|
+
pointerY: ev.clientY,
|
|
571
|
+
target,
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const onUp = () => {
|
|
576
|
+
teardownRef.current?.();
|
|
577
|
+
if (started && target) {commitDrag(id, target);}
|
|
578
|
+
setDrag(null);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const teardown = () => {
|
|
582
|
+
window.removeEventListener('pointermove', onMove);
|
|
583
|
+
window.removeEventListener('pointerup', onUp);
|
|
584
|
+
teardownRef.current = null;
|
|
585
|
+
};
|
|
586
|
+
teardownRef.current = teardown;
|
|
587
|
+
|
|
588
|
+
window.addEventListener('pointermove', onMove);
|
|
589
|
+
window.addEventListener('pointerup', onUp);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const draggedItem = drag ? items.find(it => it.id === drag.id) : undefined;
|
|
593
|
+
const isDragging = drag !== null;
|
|
594
|
+
|
|
595
|
+
// Suppress selection while dragging and detach listeners on unmount.
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
if (!isDragging) {return;}
|
|
598
|
+
const previous = document.body.style.userSelect;
|
|
599
|
+
document.body.style.userSelect = 'none';
|
|
600
|
+
return () => {
|
|
601
|
+
document.body.style.userSelect = previous;
|
|
602
|
+
};
|
|
603
|
+
}, [isDragging]);
|
|
604
|
+
|
|
605
|
+
useEffect(() => () => teardownRef.current?.(), []);
|
|
606
|
+
|
|
607
|
+
// Card nodes for a column, or null when the column should show its empty
|
|
608
|
+
// state. A dashed ghost box marks the landing slot during a drag.
|
|
609
|
+
const renderColumnCards = (colId: ColumnId): ReactNode => {
|
|
610
|
+
const colItems = itemsByColumn[colId];
|
|
611
|
+
const visible = drag ? colItems.filter(it => it.id !== drag.id) : colItems;
|
|
612
|
+
const ghostTarget =
|
|
613
|
+
drag && drag.target && drag.target.column === colId ? drag : null;
|
|
614
|
+
|
|
615
|
+
if (visible.length === 0 && !ghostTarget) {return null;}
|
|
616
|
+
|
|
617
|
+
const nodes: ReactNode[] = visible.map(it => (
|
|
618
|
+
<BoardCard
|
|
619
|
+
key={it.id}
|
|
620
|
+
item={it}
|
|
621
|
+
cardRef={getCardRef(it.id)}
|
|
622
|
+
onPointerDown={onCardPointerDown}
|
|
623
|
+
onMove={moveItem}
|
|
624
|
+
/>
|
|
625
|
+
));
|
|
626
|
+
|
|
627
|
+
if (ghostTarget && ghostTarget.target) {
|
|
628
|
+
const index = Math.min(ghostTarget.target.index, nodes.length);
|
|
629
|
+
nodes.splice(
|
|
630
|
+
index,
|
|
631
|
+
0,
|
|
632
|
+
<VStack key="drag-ghost" xstyle={styles.ghost(ghostTarget.height)} />,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return <VStack gap={2}>{nodes}</VStack>;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<Section height="100dvh">
|
|
641
|
+
<Layout
|
|
642
|
+
height="fill"
|
|
643
|
+
header={
|
|
644
|
+
<LayoutHeader hasDivider padding={4}>
|
|
645
|
+
<Toolbar
|
|
646
|
+
label="Board actions"
|
|
647
|
+
gap={2}
|
|
648
|
+
startContent={
|
|
649
|
+
<>
|
|
650
|
+
<Heading level={3}>Sprint Board</Heading>
|
|
651
|
+
<Badge label={items.length} variant="neutral" />
|
|
652
|
+
</>
|
|
653
|
+
}
|
|
654
|
+
endContent={
|
|
655
|
+
<HStack gap={2}>
|
|
656
|
+
<Selector
|
|
657
|
+
label="Sprint"
|
|
658
|
+
width={200}
|
|
659
|
+
isLabelHidden
|
|
660
|
+
value={sprint}
|
|
661
|
+
onChange={setSprint}
|
|
662
|
+
options={[
|
|
663
|
+
{value: '003', label: 'Sprint 003'},
|
|
664
|
+
{value: '002', label: 'Sprint 002'},
|
|
665
|
+
{value: '001', label: 'Sprint 001'},
|
|
666
|
+
]}
|
|
667
|
+
/>
|
|
668
|
+
<Divider
|
|
669
|
+
variant="strong"
|
|
670
|
+
orientation="vertical"
|
|
671
|
+
xstyle={styles.toolbarDivider}
|
|
672
|
+
/>
|
|
673
|
+
<HStack gap={1} vAlign="center">
|
|
674
|
+
<IconButton
|
|
675
|
+
icon={<Icon icon={ArrowsUpDownIcon} size="sm" />}
|
|
676
|
+
label="Sort"
|
|
677
|
+
/>
|
|
678
|
+
<IconButton
|
|
679
|
+
icon={<Icon icon={FunnelIcon} size="sm" />}
|
|
680
|
+
label="Filter"
|
|
681
|
+
/>
|
|
682
|
+
<IconButton
|
|
683
|
+
icon={<Icon icon={MagnifyingGlassIcon} size="sm" />}
|
|
684
|
+
label="Search"
|
|
685
|
+
/>
|
|
686
|
+
</HStack>
|
|
687
|
+
<Button
|
|
688
|
+
label="Add task"
|
|
689
|
+
variant="primary"
|
|
690
|
+
icon={<Icon icon={PlusIcon} size="sm" />}
|
|
691
|
+
/>
|
|
692
|
+
</HStack>
|
|
693
|
+
}
|
|
694
|
+
/>
|
|
695
|
+
</LayoutHeader>
|
|
696
|
+
}
|
|
697
|
+
content={
|
|
698
|
+
<LayoutContent padding={0}>
|
|
699
|
+
<HStack gap={4} xstyle={styles.boardColumns}>
|
|
700
|
+
{COLUMNS.map(meta => (
|
|
701
|
+
<BoardColumn
|
|
702
|
+
key={meta.id}
|
|
703
|
+
meta={meta}
|
|
704
|
+
count={itemsByColumn[meta.id].length}
|
|
705
|
+
contentRef={getColumnRef(meta.id)}>
|
|
706
|
+
{renderColumnCards(meta.id)}
|
|
707
|
+
</BoardColumn>
|
|
708
|
+
))}
|
|
709
|
+
</HStack>
|
|
710
|
+
</LayoutContent>
|
|
711
|
+
}
|
|
712
|
+
/>
|
|
713
|
+
{drag && draggedItem ? (
|
|
714
|
+
<Card
|
|
715
|
+
padding={3}
|
|
716
|
+
xstyle={[
|
|
717
|
+
styles.floating,
|
|
718
|
+
styles.floatingAt(
|
|
719
|
+
drag.pointerX - drag.offsetX,
|
|
720
|
+
drag.pointerY - drag.offsetY,
|
|
721
|
+
drag.width,
|
|
722
|
+
),
|
|
723
|
+
]}>
|
|
724
|
+
<BoardCardBody item={draggedItem} onMove={() => {}} />
|
|
725
|
+
</Card>
|
|
726
|
+
) : null}
|
|
727
|
+
</Section>
|
|
728
|
+
);
|
|
729
|
+
}
|