@devxcrew/react-ui 0.0.0-stage → 0.1.7
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 +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCorners,
|
|
3
|
+
DndContext,
|
|
4
|
+
DragOverlay,
|
|
5
|
+
KeyboardSensor,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
type DragOverEvent,
|
|
11
|
+
type DragStartEvent,
|
|
12
|
+
} from '@dnd-kit/core'
|
|
13
|
+
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
|
14
|
+
import { useState } from 'react'
|
|
15
|
+
import { cn } from '../../lib/utils'
|
|
16
|
+
import { KanbanCard } from './kanban-card'
|
|
17
|
+
import { KanbanColumn } from './kanban-column'
|
|
18
|
+
import type { KanbanBoardProps, KanbanCardItem } from './kanban-types'
|
|
19
|
+
|
|
20
|
+
export function KanbanBoard({
|
|
21
|
+
cards,
|
|
22
|
+
className,
|
|
23
|
+
columns,
|
|
24
|
+
emptyColumnMessage,
|
|
25
|
+
onAddCard,
|
|
26
|
+
onCardClick,
|
|
27
|
+
onCardMove,
|
|
28
|
+
renderCardFooter,
|
|
29
|
+
showAddCard = true,
|
|
30
|
+
}: KanbanBoardProps) {
|
|
31
|
+
const [activeCard, setActiveCard] = useState<KanbanCardItem | null>(null)
|
|
32
|
+
|
|
33
|
+
const sensors = useSensors(
|
|
34
|
+
useSensor(PointerSensor, {
|
|
35
|
+
activationConstraint: {
|
|
36
|
+
distance: 5,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
useSensor(KeyboardSensor, {
|
|
40
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
function handleDragStart(event: DragStartEvent) {
|
|
45
|
+
const { active } = event
|
|
46
|
+
const foundCard = cards.find((c) => c.id === active.id)
|
|
47
|
+
if (foundCard) {
|
|
48
|
+
setActiveCard(foundCard)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleDragOver(event: DragOverEvent) {
|
|
53
|
+
const { active, over } = event
|
|
54
|
+
if (!over) return
|
|
55
|
+
|
|
56
|
+
const activeId = String(active.id)
|
|
57
|
+
const overId = String(over.id)
|
|
58
|
+
|
|
59
|
+
if (activeId === overId) return
|
|
60
|
+
|
|
61
|
+
const isActiveCard = active.data.current?.type === 'Card'
|
|
62
|
+
const isOverCard = over.data.current?.type === 'Card'
|
|
63
|
+
const isOverColumn = over.data.current?.type === 'Column'
|
|
64
|
+
|
|
65
|
+
if (!isActiveCard) return
|
|
66
|
+
|
|
67
|
+
const draggedCard = cards.find((c) => c.id === activeId)
|
|
68
|
+
if (!draggedCard) return
|
|
69
|
+
|
|
70
|
+
if (isOverCard) {
|
|
71
|
+
const overCard = cards.find((c) => c.id === overId)
|
|
72
|
+
if (overCard && draggedCard.columnId !== overCard.columnId) {
|
|
73
|
+
const columnCards = cards.filter((c) => c.columnId === overCard.columnId)
|
|
74
|
+
const targetIndex = columnCards.findIndex((c) => c.id === overId)
|
|
75
|
+
onCardMove?.(activeId, overCard.columnId, targetIndex >= 0 ? targetIndex : 0)
|
|
76
|
+
}
|
|
77
|
+
} else if (isOverColumn) {
|
|
78
|
+
if (draggedCard.columnId !== overId) {
|
|
79
|
+
const columnCards = cards.filter((c) => c.columnId === overId)
|
|
80
|
+
onCardMove?.(activeId, overId, columnCards.length)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
86
|
+
const { active, over } = event
|
|
87
|
+
setActiveCard(null)
|
|
88
|
+
|
|
89
|
+
if (!over) return
|
|
90
|
+
|
|
91
|
+
const activeId = String(active.id)
|
|
92
|
+
const overId = String(over.id)
|
|
93
|
+
|
|
94
|
+
const draggedCard = cards.find((c) => c.id === activeId)
|
|
95
|
+
if (!draggedCard) return
|
|
96
|
+
|
|
97
|
+
const isOverCard = over.data.current?.type === 'Card'
|
|
98
|
+
const isOverColumn = over.data.current?.type === 'Column'
|
|
99
|
+
|
|
100
|
+
if (isOverCard) {
|
|
101
|
+
const overCard = cards.find((c) => c.id === overId)
|
|
102
|
+
if (overCard) {
|
|
103
|
+
const columnCards = cards.filter((c) => c.columnId === overCard.columnId)
|
|
104
|
+
const newIndex = columnCards.findIndex((c) => c.id === overId)
|
|
105
|
+
onCardMove?.(activeId, overCard.columnId, newIndex >= 0 ? newIndex : 0)
|
|
106
|
+
}
|
|
107
|
+
} else if (isOverColumn) {
|
|
108
|
+
const columnCards = cards.filter((c) => c.columnId === overId)
|
|
109
|
+
onCardMove?.(activeId, overId, columnCards.length)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<DndContext
|
|
115
|
+
collisionDetection={closestCorners}
|
|
116
|
+
sensors={sensors}
|
|
117
|
+
onDragEnd={handleDragEnd}
|
|
118
|
+
onDragOver={handleDragOver}
|
|
119
|
+
onDragStart={handleDragStart}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className={cn('flex h-full w-full gap-4 overflow-x-auto pb-4 scrollbar-thin', className)}
|
|
123
|
+
>
|
|
124
|
+
{columns.map((column) => {
|
|
125
|
+
const columnCards = cards.filter((c) => c.columnId === column.id)
|
|
126
|
+
return (
|
|
127
|
+
<KanbanColumn
|
|
128
|
+
cards={columnCards}
|
|
129
|
+
column={column}
|
|
130
|
+
emptyMessage={emptyColumnMessage}
|
|
131
|
+
key={column.id}
|
|
132
|
+
onAddCard={onAddCard}
|
|
133
|
+
onCardClick={onCardClick}
|
|
134
|
+
renderCardFooter={renderCardFooter}
|
|
135
|
+
showAddCard={showAddCard}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<DragOverlay>
|
|
142
|
+
{activeCard ? <KanbanCard card={activeCard} isDragOverlay /> : null}
|
|
143
|
+
</DragOverlay>
|
|
144
|
+
</DndContext>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useSortable } from '@dnd-kit/sortable'
|
|
2
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
3
|
+
import { Calendar, GripVertical } from 'lucide-react'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { Badge } from '../../components/badge'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
import type { KanbanCardItem, KanbanPriority } from './kanban-types'
|
|
8
|
+
|
|
9
|
+
const priorityStyles: Record<KanbanPriority, { badge: string; border: string; label: string }> = {
|
|
10
|
+
low: {
|
|
11
|
+
badge: 'bg-muted text-muted-foreground border-transparent',
|
|
12
|
+
border: 'border-l-muted-foreground/40',
|
|
13
|
+
label: 'Low',
|
|
14
|
+
},
|
|
15
|
+
medium: {
|
|
16
|
+
badge: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20',
|
|
17
|
+
border: 'border-l-blue-500',
|
|
18
|
+
label: 'Medium',
|
|
19
|
+
},
|
|
20
|
+
high: {
|
|
21
|
+
badge: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20',
|
|
22
|
+
border: 'border-l-amber-500',
|
|
23
|
+
label: 'High',
|
|
24
|
+
},
|
|
25
|
+
urgent: {
|
|
26
|
+
badge: 'bg-destructive/10 text-destructive border-destructive/20',
|
|
27
|
+
border: 'border-l-destructive',
|
|
28
|
+
label: 'Urgent',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface KanbanCardProps {
|
|
33
|
+
card: KanbanCardItem
|
|
34
|
+
isDragOverlay?: boolean
|
|
35
|
+
onClick?: (card: KanbanCardItem) => void
|
|
36
|
+
renderFooter?: (card: KanbanCardItem) => ReactNode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function KanbanCard({
|
|
40
|
+
card,
|
|
41
|
+
isDragOverlay = false,
|
|
42
|
+
onClick,
|
|
43
|
+
renderFooter,
|
|
44
|
+
}: KanbanCardProps) {
|
|
45
|
+
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
|
|
46
|
+
data: { card, type: 'Card' },
|
|
47
|
+
id: card.id,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const style = {
|
|
51
|
+
transform: CSS.Transform.toString(transform),
|
|
52
|
+
transition,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const priority = card.priority ? priorityStyles[card.priority] : null
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
'group relative flex flex-col gap-2 rounded-xl border border-border/80 bg-card p-3.5 text-card-foreground shadow-xs transition-shadow hover:border-foreground/25 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
61
|
+
priority?.border ? cn('border-l-4', priority.border) : '',
|
|
62
|
+
isDragging && 'opacity-40 shadow-inner',
|
|
63
|
+
isDragOverlay && 'rotate-1 shadow-lg ring-2 ring-primary/20 cursor-grabbing',
|
|
64
|
+
)}
|
|
65
|
+
ref={setNodeRef}
|
|
66
|
+
style={style}
|
|
67
|
+
tabIndex={0}
|
|
68
|
+
onClick={() => onClick?.(card)}
|
|
69
|
+
onKeyDown={(e) => {
|
|
70
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
onClick?.(card)
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-start justify-between gap-2">
|
|
77
|
+
<h4 className="text-sm font-medium leading-snug tracking-tight text-foreground select-none">
|
|
78
|
+
{card.title}
|
|
79
|
+
</h4>
|
|
80
|
+
<button
|
|
81
|
+
aria-label={`Drag card: ${card.title}`}
|
|
82
|
+
className="shrink-0 cursor-grab text-muted-foreground/50 opacity-60 transition-opacity hover:text-foreground hover:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 active:cursor-grabbing"
|
|
83
|
+
type="button"
|
|
84
|
+
{...attributes}
|
|
85
|
+
{...listeners}
|
|
86
|
+
onClick={(e) => e.stopPropagation()}
|
|
87
|
+
>
|
|
88
|
+
<GripVertical className="size-4" />
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{card.description && (
|
|
93
|
+
<p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground select-none">
|
|
94
|
+
{card.description}
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{(card.tags?.length || card.priority) && (
|
|
99
|
+
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
|
100
|
+
{priority && (
|
|
101
|
+
<Badge className={cn('text-[10px] font-semibold tracking-tight', priority.badge)}>
|
|
102
|
+
{priority.label}
|
|
103
|
+
</Badge>
|
|
104
|
+
)}
|
|
105
|
+
{card.tags?.map((tag) => (
|
|
106
|
+
<span
|
|
107
|
+
className="inline-flex items-center rounded-md bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"
|
|
108
|
+
key={tag}
|
|
109
|
+
>
|
|
110
|
+
{tag}
|
|
111
|
+
</span>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<div className="flex items-center justify-between gap-2 border-t border-border/40 pt-2 text-xs text-muted-foreground">
|
|
117
|
+
{card.dueDate ? (
|
|
118
|
+
<span className="flex items-center gap-1 text-[11px]">
|
|
119
|
+
<Calendar className="size-3" />
|
|
120
|
+
{card.dueDate}
|
|
121
|
+
</span>
|
|
122
|
+
) : (
|
|
123
|
+
<span />
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{card.assignee && (
|
|
127
|
+
<div className="flex items-center gap-1.5" title={`Assigned to ${card.assignee.name}`}>
|
|
128
|
+
{card.assignee.avatarUrl ? (
|
|
129
|
+
<img
|
|
130
|
+
alt={card.assignee.name}
|
|
131
|
+
className="size-4.5 rounded-full object-cover ring-1 ring-border"
|
|
132
|
+
src={card.assignee.avatarUrl}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<span className="flex size-4.5 items-center justify-center rounded-full bg-primary/10 text-[9px] font-semibold text-primary">
|
|
136
|
+
{card.assignee.name.charAt(0).toUpperCase()}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
<span className="max-w-20 truncate text-[11px] font-medium text-foreground/80">
|
|
140
|
+
{card.assignee.name}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{renderFooter?.(card)}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useDroppable } from '@dnd-kit/core'
|
|
2
|
+
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|
3
|
+
import { Plus } from 'lucide-react'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { Badge } from '../../components/badge'
|
|
6
|
+
import { Button } from '../../components/button'
|
|
7
|
+
import { cn } from '../../lib/utils'
|
|
8
|
+
import { KanbanCard } from './kanban-card'
|
|
9
|
+
import type { KanbanCardItem, KanbanColumnItem } from './kanban-types'
|
|
10
|
+
|
|
11
|
+
export interface KanbanColumnProps {
|
|
12
|
+
cards: readonly KanbanCardItem[]
|
|
13
|
+
column: KanbanColumnItem
|
|
14
|
+
emptyMessage?: string
|
|
15
|
+
onAddCard?: (columnId: string) => void
|
|
16
|
+
onCardClick?: (card: KanbanCardItem) => void
|
|
17
|
+
renderCardFooter?: (card: KanbanCardItem) => ReactNode
|
|
18
|
+
showAddCard?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function KanbanColumn({
|
|
22
|
+
cards,
|
|
23
|
+
column,
|
|
24
|
+
emptyMessage = 'No cards in this column',
|
|
25
|
+
onAddCard,
|
|
26
|
+
onCardClick,
|
|
27
|
+
renderCardFooter,
|
|
28
|
+
showAddCard = true,
|
|
29
|
+
}: KanbanColumnProps) {
|
|
30
|
+
const { isOver, setNodeRef } = useDroppable({
|
|
31
|
+
data: { column, type: 'Column' },
|
|
32
|
+
id: column.id,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const cardIds = cards.map((c) => c.id)
|
|
36
|
+
const isOverLimit = column.limit && cards.length > column.limit
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
'flex w-80 shrink-0 flex-col rounded-2xl border border-border/70 bg-muted/30 p-3 shadow-2xs transition-colors',
|
|
42
|
+
isOver && 'border-primary/50 bg-primary/5 ring-1 ring-primary/20',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<header className="mb-3 flex items-center justify-between px-1.5 pt-1">
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
{column.accentColor && (
|
|
48
|
+
<span
|
|
49
|
+
className="size-2.5 rounded-full"
|
|
50
|
+
style={{ backgroundColor: column.accentColor }}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
<h3 className="text-sm font-semibold tracking-tight text-foreground">{column.title}</h3>
|
|
54
|
+
<Badge
|
|
55
|
+
className={cn(
|
|
56
|
+
'h-5 px-1.5 text-[11px] font-medium',
|
|
57
|
+
isOverLimit ? 'bg-destructive/15 text-destructive' : 'bg-muted text-muted-foreground',
|
|
58
|
+
)}
|
|
59
|
+
variant="outline"
|
|
60
|
+
>
|
|
61
|
+
{cards.length}
|
|
62
|
+
{column.limit ? ` / ${column.limit}` : ''}
|
|
63
|
+
</Badge>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{showAddCard && onAddCard && (
|
|
67
|
+
<Button
|
|
68
|
+
aria-label={`Add card to ${column.title}`}
|
|
69
|
+
className="size-7 rounded-lg text-muted-foreground hover:text-foreground"
|
|
70
|
+
size="icon"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
onClick={() => onAddCard(column.id)}
|
|
73
|
+
>
|
|
74
|
+
<Plus className="size-4" />
|
|
75
|
+
</Button>
|
|
76
|
+
)}
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<div
|
|
80
|
+
className="flex flex-1 flex-col gap-2.5 overflow-y-auto min-h-32 px-0.5 pb-1"
|
|
81
|
+
ref={setNodeRef}
|
|
82
|
+
>
|
|
83
|
+
<SortableContext items={cardIds} strategy={verticalListSortingStrategy}>
|
|
84
|
+
{cards.map((card) => (
|
|
85
|
+
<KanbanCard
|
|
86
|
+
card={card}
|
|
87
|
+
key={card.id}
|
|
88
|
+
onClick={onCardClick}
|
|
89
|
+
renderFooter={renderCardFooter}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</SortableContext>
|
|
93
|
+
|
|
94
|
+
{cards.length === 0 && (
|
|
95
|
+
<div className="flex flex-1 items-center justify-center rounded-xl border border-dashed border-border/60 py-8 text-center text-xs text-muted-foreground/70">
|
|
96
|
+
{emptyMessage}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type KanbanPriority = 'low' | 'medium' | 'high' | 'urgent'
|
|
4
|
+
|
|
5
|
+
export interface KanbanCardItem {
|
|
6
|
+
assignee?: {
|
|
7
|
+
avatarUrl?: string
|
|
8
|
+
name: string
|
|
9
|
+
}
|
|
10
|
+
columnId: string
|
|
11
|
+
description?: string
|
|
12
|
+
dueDate?: string
|
|
13
|
+
id: string
|
|
14
|
+
priority?: KanbanPriority
|
|
15
|
+
tags?: readonly string[]
|
|
16
|
+
title: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KanbanColumnItem {
|
|
20
|
+
accentColor?: string
|
|
21
|
+
cardIds?: readonly string[]
|
|
22
|
+
description?: string
|
|
23
|
+
id: string
|
|
24
|
+
limit?: number
|
|
25
|
+
title: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface KanbanBoardProps {
|
|
29
|
+
cards: readonly KanbanCardItem[]
|
|
30
|
+
className?: string
|
|
31
|
+
columns: readonly KanbanColumnItem[]
|
|
32
|
+
emptyColumnMessage?: string
|
|
33
|
+
onAddCard?: (columnId: string) => void
|
|
34
|
+
onCardClick?: (card: KanbanCardItem) => void
|
|
35
|
+
onCardMove?: (cardId: string, targetColumnId: string, newIndex: number) => void
|
|
36
|
+
renderCardFooter?: (card: KanbanCardItem) => ReactNode
|
|
37
|
+
showAddCard?: boolean
|
|
38
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ComponentProps } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Spinner } from '@devxcrew/react-ui/components/spinner'
|
|
4
|
+
import { cn } from '@devxcrew/react-ui/lib/utils'
|
|
5
|
+
|
|
6
|
+
type GlobalLoaderProps = ComponentProps<'div'> & {
|
|
7
|
+
active: boolean
|
|
8
|
+
delayMs?: number
|
|
9
|
+
label?: string
|
|
10
|
+
minimumDurationMs?: number
|
|
11
|
+
overlay?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FADE_DURATION_MS = 160
|
|
15
|
+
|
|
16
|
+
export function GlobalLoader({
|
|
17
|
+
active,
|
|
18
|
+
className,
|
|
19
|
+
delayMs = 120,
|
|
20
|
+
label = 'Loading',
|
|
21
|
+
minimumDurationMs = 220,
|
|
22
|
+
overlay = false,
|
|
23
|
+
...props
|
|
24
|
+
}: GlobalLoaderProps) {
|
|
25
|
+
const { mounted, visible } = useLoaderPresence(active, delayMs, minimumDurationMs)
|
|
26
|
+
|
|
27
|
+
if (!mounted) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
aria-busy={active}
|
|
34
|
+
aria-hidden={!visible}
|
|
35
|
+
aria-live="polite"
|
|
36
|
+
data-slot="global-loader"
|
|
37
|
+
role="status"
|
|
38
|
+
className={cn(
|
|
39
|
+
'flex min-h-full items-center justify-center gap-3 bg-background px-6 text-sm text-muted-foreground transition-opacity duration-150 ease-out',
|
|
40
|
+
overlay && 'absolute inset-0 z-10',
|
|
41
|
+
visible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<Spinner className="size-5 text-foreground" />
|
|
47
|
+
<span>{label}</span>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function useLoaderPresence(active: boolean, delayMs: number, minimumDurationMs: number) {
|
|
53
|
+
const [mounted, setMounted] = useState(active && delayMs === 0)
|
|
54
|
+
const [visible, setVisible] = useState(active && delayMs === 0)
|
|
55
|
+
const shownAt = useRef<number | undefined>(undefined)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
59
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
60
|
+
let unmountTimer: ReturnType<typeof setTimeout> | undefined
|
|
61
|
+
|
|
62
|
+
if (active && !mounted) {
|
|
63
|
+
showTimer = setTimeout(() => {
|
|
64
|
+
shownAt.current = Date.now()
|
|
65
|
+
setMounted(true)
|
|
66
|
+
setVisible(true)
|
|
67
|
+
}, delayMs)
|
|
68
|
+
} else if (active) {
|
|
69
|
+
shownAt.current ??= Date.now()
|
|
70
|
+
setVisible(true)
|
|
71
|
+
} else if (mounted) {
|
|
72
|
+
const elapsed = shownAt.current ? Date.now() - shownAt.current : minimumDurationMs
|
|
73
|
+
hideTimer = setTimeout(
|
|
74
|
+
() => {
|
|
75
|
+
setVisible(false)
|
|
76
|
+
unmountTimer = setTimeout(() => setMounted(false), FADE_DURATION_MS)
|
|
77
|
+
},
|
|
78
|
+
Math.max(0, minimumDurationMs - elapsed),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
clearTimeout(hideTimer)
|
|
84
|
+
clearTimeout(showTimer)
|
|
85
|
+
clearTimeout(unmountTimer)
|
|
86
|
+
}
|
|
87
|
+
}, [active, delayMs, minimumDurationMs, mounted])
|
|
88
|
+
|
|
89
|
+
return { mounted, visible }
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GlobalLoader } from './global-loader'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Mascot block
|
|
2
|
+
|
|
3
|
+
The Mascot block provides the shared runtime for a cursor-aware, clickable character.
|
|
4
|
+
|
|
5
|
+
## Asset contract
|
|
6
|
+
|
|
7
|
+
Each character supplies two aligned 3x3 sprite atlases:
|
|
8
|
+
|
|
9
|
+
- `directions`: nine head directions in clockwise pointer order with `center` in the middle.
|
|
10
|
+
- `reactions`: nine expressions, including `blink`, `heart`, `sparkle`, and `dizzy`.
|
|
11
|
+
|
|
12
|
+
The app owns these served or imported asset URLs. This keeps the package reusable across apps and
|
|
13
|
+
allows each app to select a character or generate its own atlases without changing the component.
|
|
14
|
+
Set `trackPointer={false}` for dense variant galleries where only click reactions are needed.
|
|
15
|
+
|
|
16
|
+
## Runtime behavior
|
|
17
|
+
|
|
18
|
+
The component selects a direction from the pointer sector, uses a dead zone and hysteresis to avoid
|
|
19
|
+
jitter, and pre-mounts the reaction sheet so the first click does not fetch it late. Clicks cycle
|
|
20
|
+
through payoff reactions, trigger `dizzy` after four quick boops, and respect
|
|
21
|
+
`prefers-reduced-motion` for the squash animation.
|