@lovett/ui 0.0.10 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +6064 -80
- package/dist/index.js +21561 -9696
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1586 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +121 -0
- package/package.json +8 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +208 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +572 -0
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/combobox.tsx +796 -0
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +186 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +507 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +242 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +412 -151
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +402 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/anchor.ts +427 -0
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +74 -0
- package/src/lib/layer-stack.ts +215 -0
- package/src/lib/refs.ts +31 -0
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +73 -66
- package/src/popover.tsx +407 -0
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/select.tsx +646 -0
- package/src/sortable.tsx +315 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-row.tsx +108 -70
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1586 -2
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +662 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +915 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +218 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +121 -0
- package/src/tooltip.tsx +297 -0
package/src/sortable.tsx
CHANGED
|
@@ -9,15 +9,22 @@
|
|
|
9
9
|
* - <DragHandle> is a Lucide-grip handle pre-wired to the listeners
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { useRef, useState } from 'react'
|
|
12
13
|
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
|
|
13
14
|
import {
|
|
14
15
|
DndContext,
|
|
16
|
+
DragOverlay,
|
|
17
|
+
MeasuringStrategy,
|
|
15
18
|
closestCenter,
|
|
19
|
+
closestCorners,
|
|
20
|
+
useDroppable,
|
|
16
21
|
KeyboardSensor,
|
|
17
22
|
PointerSensor,
|
|
18
23
|
useSensor,
|
|
19
24
|
useSensors,
|
|
20
25
|
type DragEndEvent,
|
|
26
|
+
type DragOverEvent,
|
|
27
|
+
type DragStartEvent,
|
|
21
28
|
} from '@dnd-kit/core'
|
|
22
29
|
import {
|
|
23
30
|
arrayMove,
|
|
@@ -86,13 +93,51 @@ export function SortableItem({
|
|
|
86
93
|
}: {
|
|
87
94
|
id: string
|
|
88
95
|
children: (props: {
|
|
96
|
+
/** attributes + listeners merged — the single-node case, where the whole
|
|
97
|
+
* node IS the handle (a `<DragHandle>`, a settings row's grip). */
|
|
89
98
|
dragHandleProps: HTMLAttributes<HTMLElement>
|
|
99
|
+
/**
|
|
100
|
+
* The ACTIVATORS — `onPointerDown` from the PointerSensor, `onKeyDown` from
|
|
101
|
+
* the KeyboardSensor. Whatever element receives these is what starts a drag.
|
|
102
|
+
*
|
|
103
|
+
* Split from `dragAttributes` so a card can be a drop target and a
|
|
104
|
+
* transform target (`setNodeRef` + `style`) WITHOUT being a drag handle.
|
|
105
|
+
* Spread these onto the card root and a pointer-down anywhere on it starts
|
|
106
|
+
* a drag, which is the bug this split exists to prevent: the card's own
|
|
107
|
+
* buttons, links and text selection all stop working as themselves.
|
|
108
|
+
*/
|
|
109
|
+
dragListeners: HTMLAttributes<HTMLElement>
|
|
110
|
+
/**
|
|
111
|
+
* `role="button"`, `tabIndex`, `aria-roledescription`, `aria-describedby`.
|
|
112
|
+
*
|
|
113
|
+
* These belong on the SAME element as `dragListeners` — they describe the
|
|
114
|
+
* handle to assistive tech, and dnd-kit's instruction text is reached
|
|
115
|
+
* through the `aria-describedby` here. Putting them on a card that holds
|
|
116
|
+
* its own buttons makes it an interactive element wrapping interactive
|
|
117
|
+
* elements, and `role="button"`'s presentational children erase the
|
|
118
|
+
* buttons inside it.
|
|
119
|
+
*/
|
|
120
|
+
dragAttributes: HTMLAttributes<HTMLElement>
|
|
121
|
+
/** The draggable NODE — measured, transformed, and used as the drop rect. */
|
|
90
122
|
setNodeRef: (node: HTMLElement | null) => void
|
|
123
|
+
/**
|
|
124
|
+
* The HANDLE node. dnd-kit restores focus here when a keyboard drag ends;
|
|
125
|
+
* without it focus lands on the card and the next arrow key does nothing.
|
|
126
|
+
*/
|
|
127
|
+
setActivatorNodeRef: (node: HTMLElement | null) => void
|
|
91
128
|
style: CSSProperties
|
|
92
129
|
isDragging: boolean
|
|
93
130
|
}) => ReactNode
|
|
94
131
|
}) {
|
|
95
|
-
const {
|
|
132
|
+
const {
|
|
133
|
+
attributes,
|
|
134
|
+
listeners,
|
|
135
|
+
setNodeRef,
|
|
136
|
+
setActivatorNodeRef,
|
|
137
|
+
transform,
|
|
138
|
+
transition,
|
|
139
|
+
isDragging,
|
|
140
|
+
} = useSortable({
|
|
96
141
|
id,
|
|
97
142
|
})
|
|
98
143
|
|
|
@@ -108,7 +153,14 @@ export function SortableItem({
|
|
|
108
153
|
<>
|
|
109
154
|
{children({
|
|
110
155
|
dragHandleProps: { ...attributes, ...listeners } as HTMLAttributes<HTMLElement>,
|
|
156
|
+
dragListeners: { ...listeners } as HTMLAttributes<HTMLElement>,
|
|
157
|
+
// Attributes ONLY. This was `{ ...attributes, ...listeners }` — byte
|
|
158
|
+
// identical to `dragHandleProps` — so the "split" was not a split, and
|
|
159
|
+
// every consumer that took `dragAttributes` for the aria wiring got the
|
|
160
|
+
// pointer activators with it.
|
|
161
|
+
dragAttributes: { ...attributes } as HTMLAttributes<HTMLElement>,
|
|
111
162
|
setNodeRef,
|
|
163
|
+
setActivatorNodeRef,
|
|
112
164
|
style,
|
|
113
165
|
isDragging,
|
|
114
166
|
})}
|
|
@@ -135,3 +187,265 @@ export function DragHandle({
|
|
|
135
187
|
</button>
|
|
136
188
|
)
|
|
137
189
|
}
|
|
190
|
+
|
|
191
|
+
/* ============================================================================
|
|
192
|
+
* Multi-container drag — ADR-146 slice A.
|
|
193
|
+
*
|
|
194
|
+
* <SortableList> above is single-container: one list, reorder within it. A
|
|
195
|
+
* kanban needs a card to LEAVE its column, which dnd-kit models as a droppable
|
|
196
|
+
* per container plus a DragOverlay that follows the pointer while the source
|
|
197
|
+
* item stays in place.
|
|
198
|
+
*
|
|
199
|
+
* Kept from the single-container wrapper on purpose:
|
|
200
|
+
* - the KeyboardSensor, so a card can cross columns without a pointer
|
|
201
|
+
* (interface-standards §6 — non-negotiable, not polish)
|
|
202
|
+
* - the 4px pointer activation constraint, so a click on a card still reads
|
|
203
|
+
* as a click and not a 1px drag
|
|
204
|
+
*
|
|
205
|
+
* Added:
|
|
206
|
+
* - `closestCorners` rather than `closestCenter`. Center-based collision
|
|
207
|
+
* mis-resolves when the dragged card is taller than the row it is over,
|
|
208
|
+
* which is the normal case on a board with variable-height cards.
|
|
209
|
+
* - `MeasuringStrategy.Always`, because a container's height changes as
|
|
210
|
+
* cards enter and leave it and a cached rect drops the item in the wrong
|
|
211
|
+
* lane.
|
|
212
|
+
* - screen-reader announcements naming the destination container, not its id.
|
|
213
|
+
* ========================================================================== */
|
|
214
|
+
|
|
215
|
+
/** containerId -> the ids it holds, in order. */
|
|
216
|
+
export type SortableContainers = Readonly<Record<string, readonly string[]>>
|
|
217
|
+
|
|
218
|
+
export interface MultiSortableMove {
|
|
219
|
+
itemId: string
|
|
220
|
+
fromContainerId: string
|
|
221
|
+
toContainerId: string
|
|
222
|
+
/** Destination index within `toContainerId`, after the move. */
|
|
223
|
+
toIndex: number
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface MultiSortableListProps {
|
|
227
|
+
containers: SortableContainers
|
|
228
|
+
/** Container ids in board order. Also the set treated as drop targets. */
|
|
229
|
+
containerOrder: readonly string[]
|
|
230
|
+
/**
|
|
231
|
+
* Commit the move. Called synchronously from the drop handler, so a
|
|
232
|
+
* `setState` here batches with the primitive's own reset and the board does
|
|
233
|
+
* not flash the pre-drag arrangement for a frame.
|
|
234
|
+
*/
|
|
235
|
+
onMove: (move: MultiSortableMove, next: SortableContainers) => void
|
|
236
|
+
/**
|
|
237
|
+
* What follows the pointer. Without this the card appears to vanish.
|
|
238
|
+
*
|
|
239
|
+
* Receives the item's container in the LIVE arrangement, which during a
|
|
240
|
+
* cross-column drag is the DESTINATION, not the origin. Without it the
|
|
241
|
+
* overlay renders in the origin's colour while the placeholder underneath
|
|
242
|
+
* already shows the destination's — one card, on screen twice, in two
|
|
243
|
+
* different hues.
|
|
244
|
+
*/
|
|
245
|
+
renderOverlay?: ((itemId: string, containerId: string | null) => ReactNode) | undefined
|
|
246
|
+
/** Human name for an id, for both the overlay's a11y text and announcements. */
|
|
247
|
+
labelForItem?: ((itemId: string) => string) | undefined
|
|
248
|
+
labelForContainer?: ((containerId: string) => string) | undefined
|
|
249
|
+
/**
|
|
250
|
+
* Receives the live arrangement, which differs from `containers` mid-drag,
|
|
251
|
+
* and the container the drag is currently resolved INTO.
|
|
252
|
+
*
|
|
253
|
+
* The second argument exists because `useDroppable().isOver` cannot answer
|
|
254
|
+
* it on a board: `closestCorners` resolves `over` to a sortable ITEM
|
|
255
|
+
* whenever a column holds one, so the column's own droppable never wins and
|
|
256
|
+
* its `isOver` measured `false` on every column during real pointer AND
|
|
257
|
+
* keyboard drags. The container has to be derived from the resolved `over`
|
|
258
|
+
* id — the same `containerOf` the announcements already use.
|
|
259
|
+
*/
|
|
260
|
+
children: (
|
|
261
|
+
arrangement: SortableContainers,
|
|
262
|
+
overContainerId: string | null,
|
|
263
|
+
) => ReactNode
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function moveBetween(
|
|
267
|
+
arrangement: SortableContainers,
|
|
268
|
+
itemId: string,
|
|
269
|
+
from: string,
|
|
270
|
+
to: string,
|
|
271
|
+
index: number,
|
|
272
|
+
): SortableContainers {
|
|
273
|
+
const next: Record<string, readonly string[]> = { ...arrangement }
|
|
274
|
+
const fromItems = (next[from] ?? []).filter((id) => id !== itemId)
|
|
275
|
+
const toItems = from === to ? [...fromItems] : [...(next[to] ?? [])]
|
|
276
|
+
const clamped = Math.max(0, Math.min(index, toItems.length))
|
|
277
|
+
toItems.splice(clamped, 0, itemId)
|
|
278
|
+
next[from] = fromItems
|
|
279
|
+
next[to] = toItems
|
|
280
|
+
return next
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function MultiSortableList({
|
|
284
|
+
containers,
|
|
285
|
+
containerOrder,
|
|
286
|
+
onMove,
|
|
287
|
+
renderOverlay,
|
|
288
|
+
labelForItem,
|
|
289
|
+
labelForContainer,
|
|
290
|
+
children,
|
|
291
|
+
}: MultiSortableListProps) {
|
|
292
|
+
const sensors = useSensors(
|
|
293
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
294
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const [activeId, setActiveId] = useState<string | null>(null)
|
|
298
|
+
const [overContainerId, setOverContainerId] = useState<string | null>(null)
|
|
299
|
+
const [preview, setPreview] = useState<SortableContainers | null>(null)
|
|
300
|
+
// The arrangement the drag started from, so a cancel restores it exactly.
|
|
301
|
+
const originRef = useRef<SortableContainers | null>(null)
|
|
302
|
+
|
|
303
|
+
const arrangement = preview ?? containers
|
|
304
|
+
|
|
305
|
+
const containerOf = (id: string): string | null => {
|
|
306
|
+
if (containerOrder.includes(id)) return id
|
|
307
|
+
for (const containerId of containerOrder) {
|
|
308
|
+
if ((arrangement[containerId] ?? []).includes(id)) return containerId
|
|
309
|
+
}
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const nameItem = (id: string) => labelForItem?.(id) ?? id
|
|
314
|
+
const nameContainer = (id: string) => labelForContainer?.(id) ?? id
|
|
315
|
+
|
|
316
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
317
|
+
const id = String(event.active.id)
|
|
318
|
+
setActiveId(id)
|
|
319
|
+
originRef.current = containers
|
|
320
|
+
setPreview(containers)
|
|
321
|
+
// The card starts over its own column, so the drop target is lit from the
|
|
322
|
+
// first frame rather than only once the pointer crosses a boundary.
|
|
323
|
+
setOverContainerId(containerOf(id))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const handleDragOver = (event: DragOverEvent) => {
|
|
327
|
+
const { active, over } = event
|
|
328
|
+
if (!over) {
|
|
329
|
+
setOverContainerId(null)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const itemId = String(active.id)
|
|
333
|
+
const overId = String(over.id)
|
|
334
|
+
const from = containerOf(itemId)
|
|
335
|
+
const to = containerOf(overId)
|
|
336
|
+
// Set BEFORE the same-container early return: a reorder inside one column
|
|
337
|
+
// still has a drop target, and it is that column.
|
|
338
|
+
setOverContainerId(to)
|
|
339
|
+
if (!from || !to || from === to) return
|
|
340
|
+
|
|
341
|
+
const overItems = arrangement[to] ?? []
|
|
342
|
+
const overIndex = overItems.indexOf(overId)
|
|
343
|
+
const index = overIndex === -1 ? overItems.length : overIndex
|
|
344
|
+
setPreview(moveBetween(arrangement, itemId, from, to, index))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
348
|
+
const { active, over } = event
|
|
349
|
+
const itemId = String(active.id)
|
|
350
|
+
const origin = originRef.current ?? containers
|
|
351
|
+
const from = Object.keys(origin).find((key) =>
|
|
352
|
+
(origin[key] ?? []).includes(itemId),
|
|
353
|
+
)
|
|
354
|
+
setActiveId(null)
|
|
355
|
+
setOverContainerId(null)
|
|
356
|
+
originRef.current = null
|
|
357
|
+
|
|
358
|
+
if (!over || !from) {
|
|
359
|
+
setPreview(null)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const overId = String(over.id)
|
|
364
|
+
const to = containerOf(overId) ?? from
|
|
365
|
+
const overItems = (arrangement[to] ?? []).filter((id) => id !== itemId)
|
|
366
|
+
const overIndex = overItems.indexOf(overId)
|
|
367
|
+
const index = overIndex === -1 ? overItems.length : overIndex
|
|
368
|
+
const next = moveBetween(origin, itemId, from, to, index)
|
|
369
|
+
|
|
370
|
+
setPreview(null)
|
|
371
|
+
if (from === to && (origin[from] ?? []).indexOf(itemId) === index) return
|
|
372
|
+
onMove({ itemId, fromContainerId: from, toContainerId: to, toIndex: index }, next)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const handleDragCancel = () => {
|
|
376
|
+
setActiveId(null)
|
|
377
|
+
setOverContainerId(null)
|
|
378
|
+
originRef.current = null
|
|
379
|
+
setPreview(null)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<DndContext
|
|
384
|
+
sensors={sensors}
|
|
385
|
+
collisionDetection={closestCorners}
|
|
386
|
+
measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
|
|
387
|
+
onDragStart={handleDragStart}
|
|
388
|
+
onDragOver={handleDragOver}
|
|
389
|
+
onDragEnd={handleDragEnd}
|
|
390
|
+
onDragCancel={handleDragCancel}
|
|
391
|
+
accessibility={{
|
|
392
|
+
announcements: {
|
|
393
|
+
onDragStart: ({ active }) => `Picked up ${nameItem(String(active.id))}.`,
|
|
394
|
+
onDragOver: ({ active, over }) =>
|
|
395
|
+
over
|
|
396
|
+
? `${nameItem(String(active.id))} is over ${nameContainer(
|
|
397
|
+
containerOf(String(over.id)) ?? String(over.id),
|
|
398
|
+
)}.`
|
|
399
|
+
: `${nameItem(String(active.id))} is no longer over a column.`,
|
|
400
|
+
onDragEnd: ({ active, over }) =>
|
|
401
|
+
over
|
|
402
|
+
? `Moved ${nameItem(String(active.id))} to ${nameContainer(
|
|
403
|
+
containerOf(String(over.id)) ?? String(over.id),
|
|
404
|
+
)}.`
|
|
405
|
+
: `${nameItem(String(active.id))} was dropped outside a column and returned.`,
|
|
406
|
+
onDragCancel: ({ active }) =>
|
|
407
|
+
`Cancelled. ${nameItem(String(active.id))} returned to where it started.`,
|
|
408
|
+
},
|
|
409
|
+
}}
|
|
410
|
+
>
|
|
411
|
+
{children(arrangement, overContainerId)}
|
|
412
|
+
<DragOverlay>
|
|
413
|
+
{activeId && renderOverlay ? renderOverlay(activeId, containerOf(activeId)) : null}
|
|
414
|
+
</DragOverlay>
|
|
415
|
+
</DndContext>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* One droppable container inside a <MultiSortableList>.
|
|
421
|
+
*
|
|
422
|
+
* Renders through a prop so the consumer owns the container's chrome — the
|
|
423
|
+
* board column's drag-over state steps its BORDER rather than tinting with the
|
|
424
|
+
* accent (design brief §4), and only the consumer knows what its border is.
|
|
425
|
+
*
|
|
426
|
+
* `isOver` here is `useDroppable`'s raw answer and is NOT the board's drag-over
|
|
427
|
+
* signal: with `closestCorners`, `over` resolves to a sortable item whenever
|
|
428
|
+
* the column holds one, so this reads `false` for the column the card is
|
|
429
|
+
* actually over. It is kept because the droppable registration is what makes an
|
|
430
|
+
* EMPTY column a drop target at all. The state a consumer should paint comes
|
|
431
|
+
* from `MultiSortableList`'s `overContainerId`.
|
|
432
|
+
*/
|
|
433
|
+
export function SortableDropZone({
|
|
434
|
+
id,
|
|
435
|
+
items,
|
|
436
|
+
children,
|
|
437
|
+
}: {
|
|
438
|
+
id: string
|
|
439
|
+
items: readonly string[]
|
|
440
|
+
children: (props: {
|
|
441
|
+
setNodeRef: (node: HTMLElement | null) => void
|
|
442
|
+
isOver: boolean
|
|
443
|
+
}) => ReactNode
|
|
444
|
+
}) {
|
|
445
|
+
const { setNodeRef, isOver } = useDroppable({ id })
|
|
446
|
+
return (
|
|
447
|
+
<SortableContext items={[...items]} strategy={verticalListSortingStrategy}>
|
|
448
|
+
{children({ setNodeRef, isOver })}
|
|
449
|
+
</SortableContext>
|
|
450
|
+
)
|
|
451
|
+
}
|