@flamingo-stack/openframe-frontend-core 0.0.556 → 0.0.557-1828.4873.1

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.
Files changed (163) hide show
  1. package/dist/{chunk-5756Q6UN.js → chunk-25YLMIWU.js} +2 -2
  2. package/dist/{chunk-FLSEJSTN.js → chunk-26HAJBSQ.js} +2 -2
  3. package/dist/{chunk-JDUPAHJR.cjs → chunk-33FOBAWS.cjs} +11 -11
  4. package/dist/{chunk-JDUPAHJR.cjs.map → chunk-33FOBAWS.cjs.map} +1 -1
  5. package/dist/{chunk-CRGIXNUE.cjs → chunk-4EGNYOZA.cjs} +20 -20
  6. package/dist/{chunk-CRGIXNUE.cjs.map → chunk-4EGNYOZA.cjs.map} +1 -1
  7. package/dist/{chunk-SXMJLNOF.cjs → chunk-4WREPWRL.cjs} +3 -3
  8. package/dist/{chunk-SXMJLNOF.cjs.map → chunk-4WREPWRL.cjs.map} +1 -1
  9. package/dist/{chunk-LZD3VSPS.cjs → chunk-5HNZTPAB.cjs} +9 -9
  10. package/dist/{chunk-LZD3VSPS.cjs.map → chunk-5HNZTPAB.cjs.map} +1 -1
  11. package/dist/{chunk-HYABNMBN.cjs → chunk-5JF6H7OJ.cjs} +62 -62
  12. package/dist/{chunk-HYABNMBN.cjs.map → chunk-5JF6H7OJ.cjs.map} +1 -1
  13. package/dist/{chunk-BNMKHG2L.cjs → chunk-64HHDGRN.cjs} +4 -4
  14. package/dist/{chunk-BNMKHG2L.cjs.map → chunk-64HHDGRN.cjs.map} +1 -1
  15. package/dist/{chunk-Z3R3LOC5.cjs → chunk-6YXEZCVG.cjs} +5 -5
  16. package/dist/{chunk-Z3R3LOC5.cjs.map → chunk-6YXEZCVG.cjs.map} +1 -1
  17. package/dist/{chunk-BZKUID4R.cjs → chunk-6ZOVSD4M.cjs} +14 -14
  18. package/dist/{chunk-BZKUID4R.cjs.map → chunk-6ZOVSD4M.cjs.map} +1 -1
  19. package/dist/{chunk-Q5SZ2QTZ.cjs → chunk-A6TAVIBX.cjs} +39 -39
  20. package/dist/{chunk-Q5SZ2QTZ.cjs.map → chunk-A6TAVIBX.cjs.map} +1 -1
  21. package/dist/{chunk-ICG7JRYT.cjs → chunk-BE6TRUKY.cjs} +31 -31
  22. package/dist/{chunk-ICG7JRYT.cjs.map → chunk-BE6TRUKY.cjs.map} +1 -1
  23. package/dist/{chunk-JRWOHH7K.js → chunk-DRNSIK3V.js} +2 -2
  24. package/dist/{chunk-VF4DCOPD.js → chunk-FXVMRZIQ.js} +4 -4
  25. package/dist/{chunk-XGBVHQP7.js → chunk-G3NBTL4I.js} +3244 -2555
  26. package/dist/chunk-G3NBTL4I.js.map +1 -0
  27. package/dist/{chunk-JXJ4FTKN.js → chunk-G7ROZAY5.js} +2 -2
  28. package/dist/{chunk-S4LL6QI6.cjs → chunk-IPO24G7U.cjs} +1630 -941
  29. package/dist/chunk-IPO24G7U.cjs.map +1 -0
  30. package/dist/{chunk-5M4PS3XX.js → chunk-LJWTXODF.js} +2 -2
  31. package/dist/{chunk-CGUSNV6R.js → chunk-NNGQ6ONX.js} +6 -6
  32. package/dist/{chunk-HOEOGFQM.cjs → chunk-QCBBX6DO.cjs} +86 -86
  33. package/dist/{chunk-HOEOGFQM.cjs.map → chunk-QCBBX6DO.cjs.map} +1 -1
  34. package/dist/{chunk-VXNMOGRP.js → chunk-RPCY2FAC.js} +4 -4
  35. package/dist/{chunk-WX5UCLL7.js → chunk-RRWWAH37.js} +2 -2
  36. package/dist/{chunk-5JWLZJK5.js → chunk-SBKX2VSG.js} +2 -2
  37. package/dist/{chunk-5EES5HD4.js → chunk-UTEQHK3H.js} +5 -5
  38. package/dist/{chunk-7Q7GFIFB.cjs → chunk-V5EYGEX7.cjs} +7 -7
  39. package/dist/{chunk-7Q7GFIFB.cjs.map → chunk-V5EYGEX7.cjs.map} +1 -1
  40. package/dist/{chunk-YM2WHYYK.js → chunk-VJL46NHW.js} +2 -2
  41. package/dist/{chunk-FZT7BYNF.js → chunk-Y6TXTBIQ.js} +2 -2
  42. package/dist/{chunk-C74PZU6M.cjs → chunk-YHRE4JBF.cjs} +26 -26
  43. package/dist/{chunk-C74PZU6M.cjs.map → chunk-YHRE4JBF.cjs.map} +1 -1
  44. package/dist/components/case-studies/index.cjs +8 -8
  45. package/dist/components/case-studies/index.js +2 -2
  46. package/dist/components/chat/index.cjs +2 -2
  47. package/dist/components/chat/index.js +1 -1
  48. package/dist/components/contact/index.cjs +3 -3
  49. package/dist/components/contact/index.js +2 -2
  50. package/dist/components/docs/index.cjs +5 -5
  51. package/dist/components/docs/index.js +4 -4
  52. package/dist/components/embeds/index.cjs +3 -3
  53. package/dist/components/embeds/index.js +2 -2
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/board/aim.d.ts +57 -0
  57. package/dist/components/features/board/aim.d.ts.map +1 -0
  58. package/dist/components/features/board/board-column-header.d.ts.map +1 -1
  59. package/dist/components/features/board/board-column.d.ts +8 -4
  60. package/dist/components/features/board/board-column.d.ts.map +1 -1
  61. package/dist/components/features/board/board.d.ts +15 -4
  62. package/dist/components/features/board/board.d.ts.map +1 -1
  63. package/dist/components/features/board/drop-aim.d.ts +34 -0
  64. package/dist/components/features/board/drop-aim.d.ts.map +1 -0
  65. package/dist/components/features/board/lane-geometry.d.ts +22 -0
  66. package/dist/components/features/board/lane-geometry.d.ts.map +1 -0
  67. package/dist/components/features/board/pending-move.d.ts +77 -0
  68. package/dist/components/features/board/pending-move.d.ts.map +1 -0
  69. package/dist/components/features/board/resolve-drop.d.ts +19 -0
  70. package/dist/components/features/board/resolve-drop.d.ts.map +1 -0
  71. package/dist/components/features/board/ticket-card.d.ts +25 -9
  72. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  73. package/dist/components/features/board/use-board-collapse.d.ts.map +1 -1
  74. package/dist/components/features/board/use-lane-scroll-anchor.d.ts +27 -0
  75. package/dist/components/features/board/use-lane-scroll-anchor.d.ts.map +1 -0
  76. package/dist/components/features/index.cjs +8 -2
  77. package/dist/components/features/index.cjs.map +1 -1
  78. package/dist/components/features/index.d.ts +1 -0
  79. package/dist/components/features/index.d.ts.map +1 -1
  80. package/dist/components/features/index.js +7 -1
  81. package/dist/components/features/sortable-list/index.d.ts +4 -0
  82. package/dist/components/features/sortable-list/index.d.ts.map +1 -0
  83. package/dist/components/features/sortable-list/slot-geometry.d.ts +57 -0
  84. package/dist/components/features/sortable-list/slot-geometry.d.ts.map +1 -0
  85. package/dist/components/features/sortable-list/sortable-list.d.ts +50 -0
  86. package/dist/components/features/sortable-list/sortable-list.d.ts.map +1 -0
  87. package/dist/components/features/sortable-list/use-sortable-item.d.ts +29 -0
  88. package/dist/components/features/sortable-list/use-sortable-item.d.ts.map +1 -0
  89. package/dist/components/help-center-pages/index.cjs +22 -22
  90. package/dist/components/help-center-pages/index.js +13 -13
  91. package/dist/components/index.cjs +137 -131
  92. package/dist/components/index.cjs.map +1 -1
  93. package/dist/components/index.js +17 -11
  94. package/dist/components/index.js.map +1 -1
  95. package/dist/components/meeting-scheduler/index.cjs +28 -28
  96. package/dist/components/meeting-scheduler/index.js +3 -3
  97. package/dist/components/navigation/index.cjs +2 -2
  98. package/dist/components/navigation/index.js +1 -1
  99. package/dist/components/onboarding-guides/index.cjs +5 -5
  100. package/dist/components/onboarding-guides/index.js +4 -4
  101. package/dist/components/related-content/index.cjs +3 -3
  102. package/dist/components/related-content/index.js +2 -2
  103. package/dist/components/tickets/index.cjs +6 -6
  104. package/dist/components/tickets/index.js +5 -5
  105. package/dist/components/ui/index.cjs +2 -2
  106. package/dist/components/ui/index.js +1 -1
  107. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  108. package/dist/index.cjs +8 -2
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.js +7 -1
  111. package/dist/utils/auto-scroll-ancestors.d.ts +43 -0
  112. package/dist/utils/auto-scroll-ancestors.d.ts.map +1 -0
  113. package/dist/utils/drag-effect.d.ts +21 -0
  114. package/dist/utils/drag-effect.d.ts.map +1 -0
  115. package/dist/utils/scroll-parent.d.ts +11 -0
  116. package/dist/utils/scroll-parent.d.ts.map +1 -0
  117. package/package.json +6 -3
  118. package/src/__tests__/board-aim.test.ts +152 -0
  119. package/src/__tests__/board-pending-move.test.ts +113 -0
  120. package/src/__tests__/board-resolve-drop.test.ts +68 -0
  121. package/src/__tests__/sortable-slot-geometry.test.ts +105 -0
  122. package/src/components/features/board/.board-column.md +17 -4
  123. package/src/components/features/board/.board.md +11 -7
  124. package/src/components/features/board/.ticket-card.md +27 -5
  125. package/src/components/features/board/aim.ts +131 -0
  126. package/src/components/features/board/board-column-header.tsx +2 -9
  127. package/src/components/features/board/board-column.tsx +137 -66
  128. package/src/components/features/board/board.tsx +501 -258
  129. package/src/components/features/board/drop-aim.ts +50 -0
  130. package/src/components/features/board/lane-geometry.ts +44 -0
  131. package/src/components/features/board/pending-move.ts +116 -0
  132. package/src/components/features/board/resolve-drop.ts +28 -0
  133. package/src/components/features/board/ticket-card.tsx +256 -47
  134. package/src/components/features/board/use-board-collapse.ts +16 -14
  135. package/src/components/features/board/use-lane-scroll-anchor.ts +79 -0
  136. package/src/components/features/index.ts +1 -0
  137. package/src/components/features/sortable-list/.sortable-list.md +82 -0
  138. package/src/components/features/sortable-list/index.ts +3 -0
  139. package/src/components/features/sortable-list/slot-geometry.ts +100 -0
  140. package/src/components/features/sortable-list/sortable-list.tsx +270 -0
  141. package/src/components/features/sortable-list/use-sortable-item.ts +92 -0
  142. package/src/components/ui/square-avatar.tsx +5 -0
  143. package/src/stories/.TicketCard.stories.md +6 -12
  144. package/src/stories/Board.stories.tsx +16 -7
  145. package/src/stories/TicketCard.stories.tsx +38 -76
  146. package/src/utils/auto-scroll-ancestors.ts +90 -0
  147. package/src/utils/drag-effect.ts +26 -0
  148. package/src/utils/scroll-parent.ts +23 -0
  149. package/dist/chunk-S4LL6QI6.cjs.map +0 -1
  150. package/dist/chunk-XGBVHQP7.js.map +0 -1
  151. /package/dist/{chunk-5756Q6UN.js.map → chunk-25YLMIWU.js.map} +0 -0
  152. /package/dist/{chunk-FLSEJSTN.js.map → chunk-26HAJBSQ.js.map} +0 -0
  153. /package/dist/{chunk-JRWOHH7K.js.map → chunk-DRNSIK3V.js.map} +0 -0
  154. /package/dist/{chunk-VF4DCOPD.js.map → chunk-FXVMRZIQ.js.map} +0 -0
  155. /package/dist/{chunk-JXJ4FTKN.js.map → chunk-G7ROZAY5.js.map} +0 -0
  156. /package/dist/{chunk-5M4PS3XX.js.map → chunk-LJWTXODF.js.map} +0 -0
  157. /package/dist/{chunk-CGUSNV6R.js.map → chunk-NNGQ6ONX.js.map} +0 -0
  158. /package/dist/{chunk-VXNMOGRP.js.map → chunk-RPCY2FAC.js.map} +0 -0
  159. /package/dist/{chunk-WX5UCLL7.js.map → chunk-RRWWAH37.js.map} +0 -0
  160. /package/dist/{chunk-5JWLZJK5.js.map → chunk-SBKX2VSG.js.map} +0 -0
  161. /package/dist/{chunk-5EES5HD4.js.map → chunk-UTEQHK3H.js.map} +0 -0
  162. /package/dist/{chunk-YM2WHYYK.js.map → chunk-VJL46NHW.js.map} +0 -0
  163. /package/dist/{chunk-FZT7BYNF.js.map → chunk-Y6TXTBIQ.js.map} +0 -0
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
4
+ import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
5
+ import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'
6
+ import { type KeyboardEvent, useCallback, useEffect, useState } from 'react'
7
+ import { holdMoveDragEffect } from '../../../utils/drag-effect'
8
+ import { SORTABLE_ITEM_ATTRIBUTE, useSortableListContext } from './sortable-list'
9
+
10
+ export interface SortableItem {
11
+ /** Goes on the item's root element — the thing that moves. */
12
+ itemRef: (node: HTMLElement | null) => void
13
+ /**
14
+ * Goes on the grip. Only the grip starts a drag, so text selection and normal
15
+ * clicking inside the item keep working, and it carries the keyboard
16
+ * alternative — so spread it rather than picking it apart.
17
+ */
18
+ dragHandleProps: {
19
+ // Callback refs, not `Ref<HTMLElement>`: a ref OBJECT of the wider type
20
+ // cannot be attached to a `<button>` or a `<div>`, a callback taking the
21
+ // wider type can.
22
+ ref: (node: HTMLElement | null) => void
23
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void
24
+ }
25
+ isDragging: boolean
26
+ }
27
+
28
+ /**
29
+ * Registers one item of a {@link SortableList}.
30
+ *
31
+ * The item's own element is the draggable, and the list finds it by the marker
32
+ * this hook sets — so nothing about a row's identity has to survive a round trip
33
+ * through the DOM, and there is no per-item id to mismatch during hydration.
34
+ *
35
+ * Pragmatic drag and drop deliberately ships no keyboard dragging, so the grip
36
+ * carries its own: Arrow Up / Arrow Down move the item one place. One press, one
37
+ * move — no lift-and-place mode to get stuck in, and it works identically for
38
+ * screen-reader users, who hear the result from the list's live region.
39
+ */
40
+ export function useSortableItem(): SortableItem {
41
+ const { dragType, disabled, reorder, readItems } = useSortableListContext()
42
+ // State rather than refs: registration has to wait until the consumer has
43
+ // actually rendered its grip, and only a re-render tells us it did.
44
+ const [item, setItem] = useState<HTMLElement | null>(null)
45
+ const [handle, setHandle] = useState<HTMLElement | null>(null)
46
+ const [isDragging, setIsDragging] = useState(false)
47
+
48
+ const itemRef = useCallback((node: HTMLElement | null) => {
49
+ // Set here rather than rendered as a prop: it is this module's private
50
+ // bookkeeping, and a consumer forgetting to spread it would break the list
51
+ // silently.
52
+ node?.setAttribute(SORTABLE_ITEM_ATTRIBUTE, '')
53
+ setItem(node)
54
+ }, [])
55
+
56
+ useEffect(() => {
57
+ if (!item || !handle || disabled) return
58
+ return combine(
59
+ // The drag has to start as a "move", or its first frame is drawn with the
60
+ // copy cursor — see `drag-effect.ts`.
61
+ holdMoveDragEffect(item),
62
+ draggable({
63
+ element: item,
64
+ dragHandle: handle,
65
+ getInitialData: () => ({ type: dragType }),
66
+ // The list moves the real item instead — see `SortableList`.
67
+ onGenerateDragPreview: ({ nativeSetDragImage }) => disableNativeDragPreview({ nativeSetDragImage }),
68
+ onDragStart: () => setIsDragging(true),
69
+ onDrop: () => setIsDragging(false),
70
+ }),
71
+ )
72
+ }, [item, handle, disabled, dragType])
73
+
74
+ const onKeyDown = useCallback(
75
+ (event: KeyboardEvent<HTMLElement>) => {
76
+ const step = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0
77
+ if (!step || disabled) return
78
+ const items = readItems()
79
+ const from = items.indexOf(event.currentTarget.closest(`[${SORTABLE_ITEM_ATTRIBUTE}]`) as HTMLElement)
80
+ const to = from + step
81
+ if (from < 0 || to < 0 || to >= items.length) return
82
+ event.preventDefault()
83
+ reorder(from, to)
84
+ // The grip is re-rendered at its new position; keep the focus on it so a
85
+ // run of presses keeps moving the same item.
86
+ requestAnimationFrame(() => handle?.focus())
87
+ },
88
+ [disabled, readItems, reorder, handle],
89
+ )
90
+
91
+ return { itemRef, dragHandleProps: { ref: setHandle, onKeyDown }, isDragging }
92
+ }
@@ -80,6 +80,11 @@ const SquareAvatar = React.memo(React.forwardRef<HTMLDivElement, SquareAvatarPro
80
80
  {resolvedSrc && (
81
81
  <Image
82
82
  className="absolute -inset-px h-[calc(100%+2px)] w-[calc(100%+2px)] max-w-none object-cover"
83
+ // Images are draggable by default, and a draggable child WINS over a
84
+ // draggable ancestor: an avatar inside a drag-and-drop card lets the
85
+ // browser start its own "drag this picture" instead of the card's
86
+ // drag, so grabbing a card by its assignee did nothing.
87
+ draggable={false}
83
88
  src={resolvedSrc}
84
89
  alt={alt || ''}
85
90
  width={sizePx ?? sizePxBySize[size]}
@@ -12,19 +12,17 @@ Storybook stories for the `TicketCard` board component, covering drag-and-drop c
12
12
  | `WithNewMessage` | `Story` | Colored "New Message" tag and tinted card border via `columnColor` |
13
13
  | `PendingClientApproval` | `Story` | Collapsed client approval row with approve/reject handlers |
14
14
  | `PendingTechApproval` | `Story` | Yellow shield admin approval header variant |
15
- | `Approved` | `Story` | Green checkmark resolved state |
15
+ | `EscalatedByUser` | `Story` | Escalation flag raised by the requester |
16
+ | `ActivityIndicators` | `Story` | The four activity states (AI working, user typing, waiting, stale) |
16
17
  | `AsLink` | `Story` | Full-card anchor overlay via `href` |
17
- | `DragDisabled` | `Story` | Pinned card — no grab cursor, sortable listeners off |
18
- | `DragOverlay` | `Story` | Drag-overlay presentation (tilted + elevated shadow) |
18
+ | `DragDisabled` | `Story` | Pinned card — no grab cursor, registers no drag |
19
19
  | `Priorities` | `Story` | All four priority colors (`low`, `medium`, `high`, `urgent`) side by side |
20
- | `Harness` | Helper | Wraps `TicketCard` in the minimal `DndContext` + `SortableContext` required by `useSortable` |
20
+ | `Harness` | Helper | Puts `TicketCard` on the column surface it normally sits on, so its border reads correctly |
21
21
 
22
22
  ## Usage Example
23
23
 
24
24
  ```typescript
25
- // Render the card in a DnD harness (as stories do)
26
- import { DndContext } from '@dnd-kit/core'
27
- import { SortableContext } from '@dnd-kit/sortable'
25
+ // The card registers its own drag and drop — no provider to wrap it in
28
26
  import { TicketCard } from '../components/features/board'
29
27
 
30
28
  const ticket = {
@@ -40,8 +38,6 @@ const ticket = {
40
38
 
41
39
  export function Example() {
42
40
  return (
43
- <DndContext>
44
- <SortableContext items={[ticket.id]}>
45
41
  <TicketCard
46
42
  ticket={ticket}
47
43
  columnId="TECH_REQUIRED"
@@ -49,12 +45,10 @@ export function Example() {
49
45
  onApprove={(ticketId, requestId) => console.log('approve', ticketId, requestId)}
50
46
  onReject={(ticketId, requestId) => console.log('reject', ticketId, requestId)}
51
47
  />
52
- </SortableContext>
53
- </DndContext>
54
48
  )
55
49
  }
56
50
  ```
57
51
 
58
- > **Note:** `TicketCard` internally calls `useSortable`, which requires both `DndContext` and `SortableContext` ancestors with the card's own `id` registered. In the app the Board provides these; in isolation (tests, stories) you must supply the `Harness` wrapper shown above.
52
+ > **Note:** `TicketCard` registers its drag and drop behaviour against its own DOM node and needs no provider around it, so it renders standalone in tests and stories. Only a `<Board>` gives it somewhere to be dropped.
59
53
 
60
54
  **Source:** [`TicketCard.stories.tsx`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/TicketCard.stories.tsx)
@@ -232,7 +232,10 @@ export const Pagination: Story = {
232
232
  render: function Render() {
233
233
  const TOTAL = 50
234
234
  const PAGE = 10
235
- const allTickets = Array.from({ length: TOTAL }, (_, i) => baseTicket(i, 'ACTIVE'))
235
+ // Built once: rebuilding per render hands every card a new object, which
236
+ // defeats the memoization the board relies on to stay smooth under a drag.
237
+ const allTickets = React.useMemo(() => Array.from({ length: TOTAL }, (_, i) => baseTicket(i, 'ACTIVE')), [])
238
+ const loadedRef = React.useRef(PAGE)
236
239
 
237
240
  const [columns, setColumns] = React.useState<BoardColumnDef[]>([
238
241
  columnFromTicketStatus('ACTIVE', allTickets.slice(0, PAGE), { total: TOTAL, hasMore: true }),
@@ -243,13 +246,19 @@ export const Pagination: Story = {
243
246
  const handleLoadMore = (columnId: string) => {
244
247
  setColumns(prev => prev.map(c => (c.id === columnId ? { ...c, isLoadingMore: true } : c)))
245
248
  setTimeout(() => {
249
+ // APPEND the next page. Re-slicing the seed from 0 also threw away every
250
+ // drop the person had made, and a drag that auto-scrolls a lane to its
251
+ // bottom loads a page mid-drag — so the card silently snapped back to
252
+ // where it started, moments after landing where it was dropped.
253
+ const from = loadedRef.current
254
+ loadedRef.current = Math.min(from + PAGE, TOTAL)
255
+ const page = allTickets.slice(from, loadedRef.current)
246
256
  setColumns(prev =>
247
- prev.map(c => {
248
- if (c.id !== columnId) return c
249
- const nextTickets = allTickets.slice(0, c.tickets.length + PAGE)
250
- const hasMore = nextTickets.length < TOTAL
251
- return { ...c, tickets: nextTickets, hasMore, isLoadingMore: false }
252
- }),
257
+ prev.map(c =>
258
+ c.id === columnId
259
+ ? { ...c, tickets: [...c.tickets, ...page], hasMore: loadedRef.current < TOTAL, isLoadingMore: false }
260
+ : c,
261
+ ),
253
262
  )
254
263
  }, 600)
255
264
  }
@@ -1,6 +1,4 @@
1
1
  import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
- import { DndContext } from '@dnd-kit/core'
3
- import { SortableContext } from '@dnd-kit/sortable'
4
2
  import * as React from 'react'
5
3
  import {
6
4
  TicketCard,
@@ -11,22 +9,17 @@ import {
11
9
  import type { PendingToolCallData } from '../components/chat/types'
12
10
 
13
11
  // =============================================================================
14
- // Harness — `TicketCard` calls `useSortable`, which reads from a `DndContext` +
15
- // `SortableContext`. In the app the Board provides these; the story supplies a
16
- // minimal pair (with the card's own id registered) so drag bookkeeping resolves
17
- // without a full board. The card renders on `bg-ods-bg`, so we pad it on the
18
- // darker `bg-ods-card` column surface to keep the border readable.
12
+ // Harness — the card registers its own drag behaviour against the DOM and needs
13
+ // no provider around it, so the story is just the card on the column surface it
14
+ // normally sits on (`bg-ods-card` behind the card's own `bg-ods-bg`, which is
15
+ // what keeps its border readable).
19
16
  // =============================================================================
20
17
 
21
18
  function Harness(args: TicketCardProps) {
22
19
  return (
23
- <DndContext>
24
- <SortableContext items={[args.ticket.id]}>
25
- <div className="w-[320px] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
26
- <TicketCard {...args} />
27
- </div>
28
- </SortableContext>
29
- </DndContext>
20
+ <div className="w-[320px] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
21
+ <TicketCard {...args} />
22
+ </div>
30
23
  )
31
24
  }
32
25
 
@@ -178,25 +171,21 @@ export const ActivityIndicators: Story = {
178
171
  { kind: 'stale', label: 'No activity for 2 hours' },
179
172
  ]
180
173
  return (
181
- <DndContext>
182
- <SortableContext items={activities.map((a) => `ticket-activity-${a.kind}`)}>
183
- <div className="flex w-[320px] flex-col gap-[var(--spacing-system-sf)] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
184
- {activities.map((activity) => (
185
- <TicketCard
186
- key={activity.kind}
187
- columnId="ACTIVE"
188
- ticket={{
189
- ...BASE_TICKET,
190
- id: `ticket-activity-${activity.kind}`,
191
- priority: undefined,
192
- tags: undefined,
193
- activity,
194
- }}
195
- />
196
- ))}
197
- </div>
198
- </SortableContext>
199
- </DndContext>
174
+ <div className="flex w-[320px] flex-col gap-[var(--spacing-system-sf)] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
175
+ {activities.map((activity) => (
176
+ <TicketCard
177
+ key={activity.kind}
178
+ columnId="ACTIVE"
179
+ ticket={{
180
+ ...BASE_TICKET,
181
+ id: `ticket-activity-${activity.kind}`,
182
+ priority: undefined,
183
+ tags: undefined,
184
+ activity,
185
+ }}
186
+ />
187
+ ))}
188
+ </div>
200
189
  )
201
190
  },
202
191
  }
@@ -242,21 +231,6 @@ export const PendingTechApproval: Story = {
242
231
  },
243
232
  }
244
233
 
245
- /** `approved` — green check mark in the header right section (resolved look). */
246
- export const Approved: Story = {
247
- args: {
248
- ticket: {
249
- id: 'ticket-approved',
250
- title: 'Slack Installation',
251
- ticketNumber: '#1042',
252
- status: 'RESOLVED',
253
- deviceHostnames: ['PR-OFFICE-02'],
254
- organizationName: 'TechFlow Solutions',
255
- },
256
- approved: true,
257
- },
258
- }
259
-
260
234
  /** `href` set — the whole card is a link (Link anchor overlays the surface). */
261
235
  export const AsLink: Story = {
262
236
  args: {
@@ -273,39 +247,27 @@ export const DragDisabled: Story = {
273
247
  },
274
248
  }
275
249
 
276
- /** `isOverlay` — the drag-overlay presentation (tilted + elevated shadow). */
277
- export const DragOverlay: Story = {
278
- args: {
279
- ticket: { ...BASE_TICKET, id: 'ticket-overlay' },
280
- isOverlay: true,
281
- },
282
- }
283
-
284
250
  /** All four priority flag colors side by side. */
285
251
  export const Priorities: Story = {
286
252
  render: () => {
287
253
  const priorities: BoardTicket['priority'][] = ['low', 'medium', 'high', 'urgent']
288
254
  return (
289
- <DndContext>
290
- <SortableContext items={priorities.map((p) => `ticket-prio-${p}`)}>
291
- <div className="flex w-[320px] flex-col gap-[var(--spacing-system-sf)] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
292
- {priorities.map((p) => (
293
- <TicketCard
294
- key={p}
295
- columnId="ACTIVE"
296
- ticket={{
297
- ...BASE_TICKET,
298
- id: `ticket-prio-${p}`,
299
- title: `${p![0].toUpperCase()}${p!.slice(1)} priority ticket`,
300
- priority: p,
301
- tags: undefined,
302
- assignees: undefined,
303
- }}
304
- />
305
- ))}
306
- </div>
307
- </SortableContext>
308
- </DndContext>
255
+ <div className="flex w-[320px] flex-col gap-[var(--spacing-system-sf)] rounded-lg bg-ods-card p-[var(--spacing-system-sf)]">
256
+ {priorities.map((p) => (
257
+ <TicketCard
258
+ key={p}
259
+ columnId="ACTIVE"
260
+ ticket={{
261
+ ...BASE_TICKET,
262
+ id: `ticket-prio-${p}`,
263
+ title: `${p![0].toUpperCase()}${p!.slice(1)} priority ticket`,
264
+ priority: p,
265
+ tags: undefined,
266
+ assignees: undefined,
267
+ }}
268
+ />
269
+ ))}
270
+ </div>
309
271
  )
310
272
  },
311
273
  }
@@ -0,0 +1,90 @@
1
+ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
2
+ import {
3
+ autoScrollForElements,
4
+ autoScrollWindowForElements,
5
+ } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'
6
+ import { unsafeOverflowAutoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/unsafe-overflow/element'
7
+ import { scrollableAncestorsOf } from './scroll-parent'
8
+
9
+ /**
10
+ * How far past a scroller's edge a drag still counts as "scroll this".
11
+ *
12
+ * Big on purpose: past the edge there is nothing else to hit, so the only
13
+ * question is whether the intent is obvious, and dragging beyond a container is
14
+ * about as obvious as intent gets.
15
+ */
16
+ const OVERFLOW_REACH = 5000
17
+
18
+ const overflow = () => ({
19
+ forTopEdge: { top: OVERFLOW_REACH, left: OVERFLOW_REACH, right: OVERFLOW_REACH },
20
+ forBottomEdge: { bottom: OVERFLOW_REACH, left: OVERFLOW_REACH, right: OVERFLOW_REACH },
21
+ forLeftEdge: { left: OVERFLOW_REACH, top: OVERFLOW_REACH, bottom: OVERFLOW_REACH },
22
+ forRightEdge: { right: OVERFLOW_REACH, top: OVERFLOW_REACH, bottom: OVERFLOW_REACH },
23
+ })
24
+
25
+ /** Straight up and straight down only — no sideways reach at all. */
26
+ const verticalOverflow = () => ({
27
+ forTopEdge: { top: OVERFLOW_REACH },
28
+ forBottomEdge: { bottom: OVERFLOW_REACH },
29
+ })
30
+
31
+ /**
32
+ * Registers drag auto-scroll for one scroller, both **inside** its edges and
33
+ * **past** them.
34
+ *
35
+ * Pragmatic's over-element scrolling only engages in a band just inside the
36
+ * edge, so a drag that has already left the container stops scrolling it — you
37
+ * have to hunt for the strip where it works. dnd-kit kept scrolling however far
38
+ * out you went, and `unsafeOverflowAutoScrollForElements` is Pragmatic's own
39
+ * answer for that; "unsafe" here only means it can reach a container the pointer
40
+ * is not over, which is exactly what is wanted.
41
+ */
42
+ export function autoScrollBothWays(element: HTMLElement): () => void {
43
+ return combine(
44
+ autoScrollForElements({ element }),
45
+ unsafeOverflowAutoScrollForElements({ element, getOverflow: overflow }),
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Auto-scroll for one of several scrollers standing side by side — a board lane.
51
+ *
52
+ * Same as {@link autoScrollBothWays} except that its reach past the edges is
53
+ * vertical only, and that is the whole point. Lanes sit shoulder to shoulder, so
54
+ * a reach that also spreads sideways puts a pointer below the board inside
55
+ * EVERY lane's overflow region at once and scrolls all of them together. Keeping
56
+ * the region to the lane's own width means the pointer is below exactly one
57
+ * lane, and only that lane moves — which is what a board is expected to do.
58
+ */
59
+ export function autoScrollColumn(element: HTMLElement): () => void {
60
+ return combine(
61
+ autoScrollForElements({ element }),
62
+ unsafeOverflowAutoScrollForElements({ element, getOverflow: verticalOverflow }),
63
+ )
64
+ }
65
+
66
+ /**
67
+ * Registers auto-scroll on every scrollable ancestor of `element`, and on the
68
+ * window.
69
+ *
70
+ * dnd-kit walked the ancestor chain for you; Pragmatic makes each scroller opt
71
+ * in by name, which is why dragging towards the edge of the screen stopped
72
+ * scrolling anything after the migration. Registering the window alone would
73
+ * have fixed nothing either: the app's real scroller is the layout's `<main>`,
74
+ * not the document — so the chain has to be walked.
75
+ *
76
+ * Registration is passive; a scroller only moves while a drag is actually near
77
+ * its edge, so it is safe to call this once on mount.
78
+ *
79
+ * Register this ONCE per page region, not once per thing that can be dragged:
80
+ * Pragmatic keeps one entry per registration and each entry scrolls on its own,
81
+ * so registering the same `<main>` from five lanes scrolls it five times as
82
+ * fast.
83
+ */
84
+ export function autoScrollAncestors(element: HTMLElement): () => void {
85
+ const cleanups = scrollableAncestorsOf(element).map(autoScrollBothWays)
86
+ // Cheap and harmless when the document itself does not scroll, and the only
87
+ // thing that works when it does.
88
+ cleanups.push(autoScrollWindowForElements())
89
+ return combine(...cleanups)
90
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Holds a native drag started from `element` to the "move" effect, from its very
3
+ * first frame.
4
+ *
5
+ * The browser picks the drag cursor from `dataTransfer.dropEffect`, and the
6
+ * value it starts each drag with comes from `effectAllowed`. Pragmatic never
7
+ * writes `effectAllowed`, so it stays `uninitialized` — which the browser reads
8
+ * as "all", whose initial `dropEffect` is `copy`. That is the green plus cursor
9
+ * flashing for one frame at the start of every drag, before the first `dragover`
10
+ * has had a chance to say otherwise.
11
+ *
12
+ * This is a raw listener rather than a Pragmatic callback because `effectAllowed`
13
+ * can only be written during the `dragstart` event itself, and none of
14
+ * Pragmatic's callbacks are handed the event.
15
+ *
16
+ * Distinct from `preventUnhandled`, which answers the other half of the same
17
+ * question: this sets what the drag starts as, that keeps it from falling back
18
+ * to "no drop" over everything that is not a drop target.
19
+ */
20
+ export function holdMoveDragEffect(element: HTMLElement): () => void {
21
+ const onDragStart = (event: DragEvent) => {
22
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
23
+ }
24
+ element.addEventListener('dragstart', onDragStart)
25
+ return () => element.removeEventListener('dragstart', onDragStart)
26
+ }
@@ -0,0 +1,23 @@
1
+ /** Anything the browser can scroll. */
2
+ const SCROLLABLE = /(auto|scroll|overlay)/
3
+
4
+ /** Every scrollable ancestor of `element`, innermost first. */
5
+ export function scrollableAncestorsOf(element: HTMLElement): HTMLElement[] {
6
+ const found: HTMLElement[] = []
7
+ for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {
8
+ const style = getComputedStyle(node)
9
+ if (SCROLLABLE.test(style.overflowY) || SCROLLABLE.test(style.overflowX)) found.push(node)
10
+ }
11
+ return found
12
+ }
13
+
14
+ /**
15
+ * The nearest scrollable ancestor, or `null`.
16
+ *
17
+ * Stands in for "the working area this thing lives in": in an app whose layout
18
+ * scrolls a `<main>` rather than the document, that element is exactly the
19
+ * region a user considers to be the page.
20
+ */
21
+ export function nearestScrollableAncestor(element: HTMLElement): HTMLElement | null {
22
+ return scrollableAncestorsOf(element)[0] ?? null
23
+ }