{"_id":"@agallaoui/board-dnd","name":"@agallaoui/board-dnd","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@agallaoui/board-dnd","version":"1.0.0","description":"Kanban board drag-and-drop extension for @agallaoui/dnd-core","main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.mjs","require":"./dist/react/index.js"},"./react/components":{"types":"./dist/react/components/index.d.ts","import":"./dist/react/components/index.mjs","require":"./dist/react/components/index.js"},"./angular":{"types":"./dist/angular/index.d.ts","import":"./dist/angular/index.mjs","require":"./dist/angular/index.js"},"./styles":"./dist/styles.css"},"sideEffects":["*.css"],"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"dependencies":{"@agallaoui/dnd-core":"^1.0.0"},"peerDependencies":{"react":">=17.0.0","react-dom":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"devDependencies":{"@types/react":"^18.2.48","@types/react-dom":"^18.2.18","react":"^18.2.0","react-dom":"^18.2.0","tsup":"^8.0.1","typescript":"^5.3.3","vitest":"^4.0.18"},"keywords":["drag-and-drop","dnd","kanban","board","trello","columns","react","angular"],"author":{"name":"agallaoui"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/agallaoui/dnd.git","directory":"packages/board-dnd"},"homepage":"https://github.com/agallaoui/dnd/tree/main/packages/board-dnd","_id":"@agallaoui/board-dnd@1.0.0","gitHead":"bf113d54e5507d192517a839c8d1ea1f758f9d3a","bugs":{"url":"https://github.com/agallaoui/dnd/issues"},"_nodeVersion":"22.18.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-GpWozqKwwAXe9/Sat8Y4uClVoHIbgeJwue9dNVz6G3ATjxnROtTbbj4MGgCuYk/F9hbY64ILoRgpunt1eHRnog==","shasum":"4218f1b4a43e340920a030bef0e262700e664c16","tarball":"https://registry.npmjs.org/@agallaoui/board-dnd/-/board-dnd-1.0.0.tgz","fileCount":35,"unpackedSize":305588,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICbhd/3YImAWSnuerg9sGZzCHd70zxwQ95WX0YMBmuFEAiEAyX8ImQFC95N/FQYZi7Qpo4fZCywsYyhF9d/sDUBj+GM="}]},"_npmUser":{"name":"agallaoui","email":"achref.gallaoui.dev@gmail.com"},"directories":{},"maintainers":[{"name":"agallaoui","email":"achref.gallaoui.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/board-dnd_1.0.0_1770424381382_0.8215086000072966"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-07T00:33:01.315Z","1.0.0":"2026-02-07T00:33:01.540Z","modified":"2026-02-07T00:33:01.700Z"},"maintainers":[{"name":"agallaoui","email":"achref.gallaoui.dev@gmail.com"}],"description":"Kanban board drag-and-drop extension for @agallaoui/dnd-core","homepage":"https://github.com/agallaoui/dnd/tree/main/packages/board-dnd","keywords":["drag-and-drop","dnd","kanban","board","trello","columns","react","angular"],"repository":{"type":"git","url":"git+https://github.com/agallaoui/dnd.git","directory":"packages/board-dnd"},"author":{"name":"agallaoui"},"bugs":{"url":"https://github.com/agallaoui/dnd/issues"},"license":"MIT","readme":"# @agallaoui/board-dnd\n\nKanban-style board drag-and-drop extension for `@agallaoui/dnd-core`.\n\n## Installation\n\n```bash\nnpm install @agallaoui/board-dnd @agallaoui/dnd-core\n```\n\n## Features\n\n- **Column-Based Layout** - Designed for Kanban/Trello-style boards\n- **Drop Indicators** - Visual feedback showing insertion point\n- **Ghost Items** - Source item shows at reduced opacity during drag\n- **Index Tracking** - Precise insertion position within columns\n- **Customizable** - Custom drop indicators, settings persistence, pre-built components\n- **State-on-Drop** - No DOM reordering until drop completes\n\n## Import Paths\n\n| Path | Description |\n|---|---|\n| `@agallaoui/board-dnd` | Board engine (framework-agnostic) |\n| `@agallaoui/board-dnd/react` | React hooks & providers |\n| `@agallaoui/board-dnd/react/components` | Pre-built React components |\n| `@agallaoui/board-dnd/angular` | Angular service & helpers |\n| `@agallaoui/board-dnd/styles` | Optional CSS styles |\n\n## React - Hooks API\n\n```tsx\nimport {\n  BoardProvider,\n  useBoardColumn,\n  useBoardItem,\n  DragOverlay,\n  DropIndicator,\n} from '@agallaoui/board-dnd/react';\n\nfunction Board({ columns, onDrop }) {\n  return (\n    <BoardProvider onDrop={onDrop}>\n      <div className=\"board\">\n        {columns.map(col => <Column key={col.id} {...col} />)}\n      </div>\n      <DragOverlay>{item => <Card {...item.data} />}</DragOverlay>\n    </BoardProvider>\n  );\n}\n\nfunction Column({ id, title, items }) {\n  const { ref, itemsContainerRef, isOver, dropIndicator } = useBoardColumn({\n    id,\n    data: { title },\n  });\n\n  return (\n    <div ref={ref} className={isOver ? 'column over' : 'column'}>\n      <h3>{title}</h3>\n      <div ref={itemsContainerRef}>\n        {items.map((item, i) => (\n          <Fragment key={item.id}>\n            {dropIndicator?.insertIndex === i && <DropIndicator />}\n            <Card item={item} columnId={id} index={i} />\n          </Fragment>\n        ))}\n        {dropIndicator?.insertIndex === items.length && <DropIndicator />}\n      </div>\n    </div>\n  );\n}\n\nfunction Card({ item, columnId, index }) {\n  const { ref, isDragging, style } = useBoardItem({\n    id: item.id,\n    data: item,\n    columnId,\n    index,\n  });\n\n  return <div ref={ref} style={style}>{item.title}</div>;\n}\n```\n\n## React - Pre-built Components\n\nFor quick setup without wiring hooks manually:\n\n```tsx\nimport { Board } from '@agallaoui/board-dnd/react/components';\n\nfunction App() {\n  return (\n    <Board\n      columns={[\n        { id: 'todo', data: { title: 'To Do' }, items: todoItems },\n        { id: 'done', data: { title: 'Done' }, items: doneItems },\n      ]}\n      onDrop={handleDrop}\n      renderItem={(task) => <TaskCard task={task} />}\n      renderColumnHeader={(col) => <h2>{col.data.title}</h2>}\n      renderOverlay={(item) => <TaskCard task={item.data} />}\n    />\n  );\n}\n```\n\nIndividual components are also available:\n\n```tsx\nimport { BoardColumn, BoardItem } from '@agallaoui/board-dnd/react/components';\n```\n\n## Custom Drop Indicator\n\nUse the `render` prop for a fully custom indicator:\n\n```tsx\n<DropIndicator\n  columnId={columnId}\n  insertIndex={insertIndex}\n  render={({ columnId, insertIndex }) => (\n    <div className=\"my-custom-indicator\" />\n  )}\n/>\n```\n\n## Settings Provider\n\nPersist user preferences with `BoardSettingsProvider`:\n\n```tsx\nimport { BoardSettingsProvider, useBoardSettings } from '@agallaoui/board-dnd/react';\n\n<BoardSettingsProvider\n  storageKey=\"my-board-settings\"\n  initialSettings={{ ghostOpacity: 0.3, indicatorColor: '#10b981' }}\n>\n  <BoardProvider onDrop={handleDrop}>\n    {/* board content */}\n  </BoardProvider>\n</BoardSettingsProvider>\n\n// Access settings anywhere\nfunction SettingsPanel() {\n  const { settings, updateSettings, resetSettings } = useBoardSettings();\n  // ...\n}\n```\n\n## Angular\n\n### 1. Create a service\n\n```typescript\nimport { Injectable, OnDestroy } from '@angular/core';\nimport { BehaviorSubject, Subject } from 'rxjs';\nimport { BoardDndServiceBase, BoardDropResult, BoardDragState } from '@agallaoui/board-dnd/angular';\n\n@Injectable({ providedIn: 'root' })\nexport class BoardDndService extends BoardDndServiceBase<Task> implements OnDestroy {\n  readonly drop$ = new Subject<BoardDropResult<Task>>();\n  private readonly _dragState$ = new BehaviorSubject<BoardDragState<Task>>(this.state);\n  readonly dragState$ = this._dragState$.asObservable();\n\n  constructor() {\n    super({\n      callbacks: {\n        onDrop: (result) => this.drop$.next(result),\n      }\n    });\n    this.subscribe((state) => this._dragState$.next(state));\n  }\n\n  ngOnDestroy(): void {\n    this.destroy();\n    this.drop$.complete();\n    this._dragState$.complete();\n  }\n}\n```\n\n### 2. Create directives\n\n```typescript\n@Directive({ selector: '[boardColumn]', standalone: true })\nexport class BoardColumnDirective implements OnInit, OnDestroy {\n  @Input({ required: true }) boardColumnId!: string;\n  @Input() boardColumnData: unknown = {};\n\n  constructor(private el: ElementRef, private boardService: BoardDndService) {}\n\n  ngOnInit() {\n    this.handle = this.boardService.registerColumn(this.el.nativeElement, {\n      id: this.boardColumnId,\n      data: this.boardColumnData,\n      getItemPositions: () => this.getItemPositions(),\n    });\n  }\n  // ...\n}\n\n@Directive({ selector: '[boardItem]', standalone: true })\nexport class BoardItemDirective implements OnInit, OnDestroy {\n  @Input({ required: true }) boardItemId!: string;\n  @Input() boardItemData!: any;\n  @Input() boardItemColumnId!: string;\n  @Input() boardItemIndex!: number;\n  // ...\n}\n```\n\n### 3. Use in templates\n\n```html\n<div boardColumn [boardColumnId]=\"column.id\" [boardColumnData]=\"column\">\n  <div *ngFor=\"let task of column.items; let i = index\"\n    boardItem\n    [boardItemId]=\"task.id\"\n    [boardItemData]=\"task\"\n    [boardItemColumnId]=\"column.id\"\n    [boardItemIndex]=\"i\"\n  >\n    {{ task.title }}\n  </div>\n</div>\n```\n\n### Angular Settings Manager\n\n```typescript\nimport { BoardSettingsManager } from '@agallaoui/board-dnd/angular';\n\n@Injectable({ providedIn: 'root' })\nexport class BoardSettingsService extends BoardSettingsManager {\n  constructor() {\n    super({ storageKey: 'board-settings', initialSettings: { ghostOpacity: 0.3 } });\n  }\n}\n```\n\n### Utility: Same-column drag index\n\n```typescript\nimport { getAdjustedInsertIndex } from '@agallaoui/board-dnd/angular';\n\nconst adjustedIndex = getAdjustedInsertIndex(\n  dropIndicator.insertIndex,\n  items,\n  boardService.sourceColumnId === columnId,\n  boardService.draggedItem?.id\n);\n```\n\n## Vanilla JavaScript\n\n```typescript\nimport { createBoardEngine } from '@agallaoui/board-dnd';\n\nconst engine = createBoardEngine({\n  callbacks: {\n    onDrop: ({ item, fromColumnId, toColumnId, toIndex }) => {\n      moveItem(item.id, fromColumnId, toColumnId, toIndex);\n    }\n  }\n});\n\nengine.registerColumn(element, {\n  id: 'column-1',\n  data: { name: 'Todo' },\n  getItemPositions: () => getPositions()\n});\n\nengine.registerItem(element, {\n  id: 'item-1',\n  data: { title: 'Task' },\n  columnId: 'column-1',\n  index: 0\n});\n```\n\n## Board Settings\n\nAll settings are optional and have sensible defaults:\n\n| Setting | Type | Default | Description |\n|---|---|---|---|\n| `dragThreshold` | `number` | `5` | Pixels before drag starts |\n| `animate` | `boolean` | `true` | Enable animations |\n| `animationDuration` | `number` | `200` | Animation duration (ms) |\n| `itemGap` | `number` | `8` | Gap between items (px) |\n| `ghostOpacity` | `number` | `0.5` | Opacity of dragged item ghost |\n| `indicatorColor` | `string` | `'#3b82f6'` | Drop indicator color |\n| `indicatorHeight` | `number` | `4` | Drop indicator height (px) |\n| `indicatorBorderRadius` | `number` | `2` | Drop indicator border radius |\n| `allowCrossColumnDrag` | `boolean` | `true` | Allow cross-column dragging |\n| `dragCursor` | `string` | `'grabbing'` | Cursor during drag |\n| `showOverlay` | `boolean` | `true` | Show drag overlay |\n| `overlayZIndex` | `number` | `9999` | Overlay z-index |\n\n## CSS Custom Properties\n\n```css\n:root {\n  --board-dnd-indicator-color: #3b82f6;\n  --board-dnd-overlay-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n  --board-dnd-transition-duration: 200ms;\n}\n```\n\n## Bundle Size\n\n| Import | Size (minified + gzip) |\n|---|---|\n| Core | ~1.5kb |\n| + React adapter | ~2.5kb |\n| + React components | ~3.5kb |\n| + Angular adapter | ~2.2kb |\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-13d3fb14a05cf3cb29c060aadcce659b"}