@kolkrabbi/kol-component 0.244.0 → 0.246.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/package.json +3 -3
- package/src/atoms/SegmentedToggle.jsx +14 -4
- package/src/hooks/usePointerSort.js +51 -0
- package/src/index.js +4 -0
- package/src/molecules/SectionText.jsx +5 -2
- package/src/molecules/StepList.jsx +109 -0
- package/src/organisms/ContentFilters.jsx +33 -13
- package/src/organisms/RecordManager.jsx +5 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.246.0",
|
|
4
4
|
"description": "The core component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"embla-carousel-react": "^8.6.0",
|
|
27
27
|
"pdfjs-dist": "^6.3.289",
|
|
28
28
|
"react-syntax-highlighter": "^16.1.1",
|
|
29
|
-
"@kolkrabbi/kol-
|
|
30
|
-
"@kolkrabbi/kol-
|
|
29
|
+
"@kolkrabbi/kol-markdown": "^0.1.3",
|
|
30
|
+
"@kolkrabbi/kol-search": "^0.3.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
* strip (canvas alignment, transform cluster); onChange is the
|
|
30
30
|
* action dispatch.
|
|
31
31
|
* onChange — handler (newValue) => void
|
|
32
|
-
* options — [{ value, label, ariaLabel?, tooltip? }] — a cell whose label is not a string
|
|
33
|
-
* (a glyph) shows its `ariaLabel` as a KOL Tooltip; `tooltip` overrides it
|
|
32
|
+
* options — [{ value, label, ariaLabel?, tooltip?, disabled? }] — a cell whose label is not a string
|
|
33
|
+
* (a glyph) shows its `ariaLabel` as a KOL Tooltip; `tooltip` overrides it.
|
|
34
|
+
* `disabled` (kol-fxr 2026-10-09): the cell draws the family's disabled ink
|
|
35
|
+
* (`aria-disabled`, not the attribute — the tooltip still gives the reason),
|
|
36
|
+
* refuses the press, and ←/→ step over it. It may still be the current `value`
|
|
37
|
+
* (the ground changed under it): selected + disabled, no throw.
|
|
34
38
|
* variant — 'default' (shipped chrome: shared outer stroke + dividers,
|
|
35
39
|
* filled active cell) | 'filled' (the state law's tiles: every
|
|
36
40
|
* cell a surface-secondary tile with 1px transparent gaps, NO
|
|
@@ -66,7 +70,12 @@ export default function SegmentedToggle({ value, onChange, options = [], variant
|
|
|
66
70
|
const dir = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1 }[e.key]
|
|
67
71
|
if (!dir || !options.length) return
|
|
68
72
|
e.preventDefault()
|
|
69
|
-
|
|
73
|
+
let next = focusIdx
|
|
74
|
+
for (let step = 0; step < options.length; step++) {
|
|
75
|
+
next = (next + dir + options.length) % options.length
|
|
76
|
+
if (!options[next].disabled) break
|
|
77
|
+
}
|
|
78
|
+
if (options[next].disabled) return
|
|
70
79
|
onChange?.(options[next].value)
|
|
71
80
|
e.currentTarget.children[next]?.focus()
|
|
72
81
|
}
|
|
@@ -94,8 +103,9 @@ export default function SegmentedToggle({ value, onChange, options = [], variant
|
|
|
94
103
|
role={stateless ? undefined : 'radio'}
|
|
95
104
|
aria-checked={stateless ? undefined : isActive}
|
|
96
105
|
aria-label={opt.ariaLabel}
|
|
106
|
+
aria-disabled={opt.disabled || undefined}
|
|
97
107
|
tabIndex={stateless ? 0 : (i === focusIdx ? 0 : -1)}
|
|
98
|
-
onClick={() => onChange?.(opt.value)}
|
|
108
|
+
onClick={opt.disabled ? undefined : () => onChange?.(opt.value)}
|
|
99
109
|
className={['kol-seg-cell', cellType, isActive && 'is-active'].filter(Boolean).join(' ')}
|
|
100
110
|
>
|
|
101
111
|
{opt.label}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usePointerSort — a vertical reorder by pointer, the ~50 lines `RecordManager` carried inline
|
|
5
|
+
* (2026-08-09, "util before any dependency"), lifted so `StepList` is not a second copy (kol-fxr
|
|
6
|
+
* StepList, 2026-10-09). Pointer events, so a finger works where HTML drag does not.
|
|
7
|
+
*
|
|
8
|
+
* const { drag, startDrag } = usePointerSort(onReorder)
|
|
9
|
+
* <button onPointerDown={(e) => startDrag(e, i, rowEls)} className="touch-none">⠿</button>
|
|
10
|
+
*
|
|
11
|
+
* `startDrag(e, from, rowEls)`: `rowEls` is the list of row elements in order (a NodeList or an
|
|
12
|
+
* array); their rects are measured once at the start. While the pointer is down `drag` is
|
|
13
|
+
* `{ from, over, x, y }` — `over` is the row under the pointer, `x`/`y` the pointer, for a
|
|
14
|
+
* floating label — and null otherwise. On release `onReorder(from, over)` fires when they differ.
|
|
15
|
+
* Without `onReorder` a start is a no-op, so a handle can render conditionally on it.
|
|
16
|
+
*/
|
|
17
|
+
export default function usePointerSort(onReorder) {
|
|
18
|
+
const dragRef = useRef(null)
|
|
19
|
+
const [drag, setDrag] = useState(null)
|
|
20
|
+
|
|
21
|
+
const startDrag = (e, from, rowEls) => {
|
|
22
|
+
if (!onReorder) return
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
if (!rowEls?.length) return
|
|
25
|
+
const rects = [...rowEls].map((el) => el.getBoundingClientRect())
|
|
26
|
+
const move = (ev) => {
|
|
27
|
+
const hit = rects.findIndex((r) => ev.clientY < r.bottom)
|
|
28
|
+
const over = hit === -1 ? rects.length - 1 : hit
|
|
29
|
+
dragRef.current = { from, over, x: ev.clientX, y: ev.clientY }
|
|
30
|
+
setDrag(dragRef.current)
|
|
31
|
+
}
|
|
32
|
+
const up = () => {
|
|
33
|
+
window.removeEventListener('pointermove', move)
|
|
34
|
+
window.removeEventListener('pointerup', up)
|
|
35
|
+
document.body.style.cursor = ''
|
|
36
|
+
document.body.style.userSelect = ''
|
|
37
|
+
const d = dragRef.current
|
|
38
|
+
dragRef.current = null
|
|
39
|
+
setDrag(null)
|
|
40
|
+
if (d && d.over !== d.from) onReorder(d.from, d.over)
|
|
41
|
+
}
|
|
42
|
+
window.addEventListener('pointermove', move)
|
|
43
|
+
window.addEventListener('pointerup', up)
|
|
44
|
+
document.body.style.cursor = 'grabbing'
|
|
45
|
+
document.body.style.userSelect = 'none'
|
|
46
|
+
dragRef.current = { from, over: from, x: e.clientX, y: e.clientY }
|
|
47
|
+
setDrag(dragRef.current)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { drag, startDrag }
|
|
51
|
+
}
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,9 @@ export { default as IconFrame } from './atoms/IconFrame.jsx'
|
|
|
38
38
|
export { default as ExitPreview } from './utilities/ExitPreview.jsx'
|
|
39
39
|
export { default as Figure } from './atoms/Figure.jsx'
|
|
40
40
|
export { default as FullscreenOverlay } from './utilities/FullscreenOverlay.jsx'
|
|
41
|
+
/* the overlay layer stack — which open layer owns Escape; exported so a layer in another package
|
|
42
|
+
* (kol-shell's walkthrough) joins the same stack instead of a second one (2026-10-09) */
|
|
43
|
+
export { pushLayer, popLayer, isTopLayer } from './utilities/layerStack.js'
|
|
41
44
|
/* BackgroundVideo — renamed from HlsVideo and moved to utilities 2026-10-01 (user ruling: no UI,
|
|
42
45
|
* and the name said how it streams, not what it is for). `HlsVideo` is the alias, on the ledger. */
|
|
43
46
|
export { default as BackgroundVideo, default as HlsVideo } from './utilities/BackgroundVideo.jsx'
|
|
@@ -79,6 +82,7 @@ export { default as InspectorRail } from './molecules/InspectorRail.jsx'
|
|
|
79
82
|
export { default as PathNodeOverlay } from './utilities/PathNodeOverlay.jsx'
|
|
80
83
|
export { default as CropOverlay } from './utilities/CropOverlay.jsx'
|
|
81
84
|
export { default as LayerStack, AddLayerButton, BLEND_MODES } from './organisms/LayerStack.jsx'
|
|
85
|
+
export { default as StepList } from './molecules/StepList.jsx'
|
|
82
86
|
export { TYPE_LABELS, BOOL_OP_LABELS, SHAPE_KIND_LABELS, labelForLayer, rowLabelForLayer, findLayerDeep } from './hooks/layerTree.js'
|
|
83
87
|
export { default as TimelineDock, sampleTrack, TIMELINE_EASINGS } from './organisms/TimelineDock.jsx'
|
|
84
88
|
export { default as CurveEditor, CURVE_KINDS, defaultCurveFor } from './organisms/CurveEditor.jsx'
|
|
@@ -107,7 +107,7 @@ export default function SectionText({
|
|
|
107
107
|
? <p className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</p>
|
|
108
108
|
: <div className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</div>)
|
|
109
109
|
const cluster = actions && (
|
|
110
|
-
<div className={cls(`${actionsClass} shrink-0 h-0 self-center`, 'actions')} style={slotStyle.actions}>{actions}</div>
|
|
110
|
+
<div className={cls(`${actionsClass} shrink-0 md:h-0 md:self-center`, 'actions')} style={slotStyle.actions}>{actions}</div>
|
|
111
111
|
)
|
|
112
112
|
return (
|
|
113
113
|
<div className={`kol-section-text flex flex-col ${gap} ${alignCls} ${className}`.replace(/\s+/g, ' ').trim()} style={style}>
|
|
@@ -133,7 +133,10 @@ export default function SectionText({
|
|
|
133
133
|
* have been space between … on the right edge"). The column is `items-start`, so this
|
|
134
134
|
* row shrank to its content and `justify-between` had no room to act: the cluster sat
|
|
135
135
|
* against the subtitle on every PageHeader with actions. */
|
|
136
|
-
|
|
136
|
+
/* BELOW `md` THE CLUSTER DROPS UNDER THE BODY (kol-fxr 2026-10-09, HubSettingsHeaderAt390:
|
|
137
|
+
* at 390 the cluster kept its natural width and left the lede ~110px, three lines). It
|
|
138
|
+
* takes its own row there, with its real height; the zero-height trick is a desk rule. */
|
|
139
|
+
<div className="flex flex-col md:flex-row self-stretch md:items-baseline md:justify-between gap-3 md:gap-6">
|
|
137
140
|
{bodyNode}
|
|
138
141
|
{cluster}
|
|
139
142
|
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import Button from '../atoms/Button.jsx'
|
|
3
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
|
+
import usePointerSort from '../hooks/usePointerSort.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* StepList — A numbered list of slots ordered by hand. the Morph rail's steps in kol-fxr (each row a
|
|
8
|
+
* generator preset, the morph tweening from row 1 to row N), generic enough for any ordered slots —
|
|
9
|
+
* a playlist, a chain of stages (lobby: StepList, kol-fxr 2026-10-09). One row is active, × removes
|
|
10
|
+
* a row, the dashed last row adds one.
|
|
11
|
+
*
|
|
12
|
+
* <StepList items={steps} activeIndex={i} onSelect={setI} onRemove={remove} onMove={move} onAdd={add} />
|
|
13
|
+
*
|
|
14
|
+
* Reorder is `usePointerSort` — the pointer sort `RecordManager` runs — not the HTML drag the brief
|
|
15
|
+
* named from `LayerStack`: HTML drag does nothing under a finger, and fxr runs on a phone. The
|
|
16
|
+
* `arrows` variant is the keyboard path.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ id: string|number, label: import('react').ReactNode }[]} items the rows, in order
|
|
19
|
+
* @param {number|null} activeIndex the highlighted (editing) row
|
|
20
|
+
* @param {'grab'|'arrows'} reorder `grab`: a ⠿ handle and a pointer drag (touch too) · `arrows`: ↑ ↓ quiet buttons, one slot a press
|
|
21
|
+
* @param {boolean} readOnly no handle or arrows, no ×, no add row; the body does not open
|
|
22
|
+
* @param {'xs'|'sm'|'md'|'lg'} size the control rung the × and the arrows ride
|
|
23
|
+
* @param {(index: number) => void} onSelect the body pressed
|
|
24
|
+
* @param {(index: number) => void} onRemove ×
|
|
25
|
+
* @param {(from: number, to: number) => void} onMove after a drop or an arrow press
|
|
26
|
+
* @param {() => void} onAdd the dashed row
|
|
27
|
+
* @param {import('react').ReactNode} addLabel the dashed row's words — the consumer's copy
|
|
28
|
+
* @param {string} className on the list
|
|
29
|
+
*/
|
|
30
|
+
export default function StepList({
|
|
31
|
+
items = [],
|
|
32
|
+
activeIndex = null,
|
|
33
|
+
reorder = 'grab',
|
|
34
|
+
readOnly = false,
|
|
35
|
+
size = 'sm',
|
|
36
|
+
onSelect,
|
|
37
|
+
onRemove,
|
|
38
|
+
onMove,
|
|
39
|
+
onAdd,
|
|
40
|
+
addLabel = 'Add a step',
|
|
41
|
+
className = '',
|
|
42
|
+
}) {
|
|
43
|
+
const listRef = useRef(null)
|
|
44
|
+
const { drag, startDrag } = usePointerSort(readOnly ? null : onMove)
|
|
45
|
+
const grab = !readOnly && reorder === 'grab' && !!onMove
|
|
46
|
+
const arrows = !readOnly && reorder === 'arrows' && !!onMove
|
|
47
|
+
const rows = () => listRef.current?.querySelectorAll('.kol-step-list-item')
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ul ref={listRef} className={`flex flex-col gap-1 ${className}`.trim()}>
|
|
51
|
+
{items.map((it, i) => {
|
|
52
|
+
const active = i === activeIndex
|
|
53
|
+
const dragging = drag && drag.from === i
|
|
54
|
+
const over = drag && drag.over === i && drag.over !== drag.from
|
|
55
|
+
const mark = over ? (drag.over > drag.from ? 'bottom-0' : 'top-0') : null
|
|
56
|
+
return (
|
|
57
|
+
<li
|
|
58
|
+
key={it.id}
|
|
59
|
+
className={`kol-step-list-item relative flex items-center gap-2 rounded px-2 py-1${active ? ' bg-fg-08' : ''}${dragging ? ' opacity-40' : ''}`}
|
|
60
|
+
>
|
|
61
|
+
{/* the drop mark: a 1px accent line above or below the row under the pointer */}
|
|
62
|
+
{mark && <span aria-hidden="true" className={`absolute left-2 right-2 h-px ${mark}`} style={{ background: 'var(--kol-accent-primary)' }} />}
|
|
63
|
+
{grab && (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
aria-label={`Reorder step ${i + 1}`}
|
|
67
|
+
className="cursor-grab shrink-0 touch-none border-0 bg-transparent p-0"
|
|
68
|
+
onPointerDown={(e) => startDrag(e, i, rows())}
|
|
69
|
+
>
|
|
70
|
+
<Icon name="drag-handle" size={12} className="text-oq-48" />
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
{arrows && (
|
|
74
|
+
<span className="flex shrink-0 items-center">
|
|
75
|
+
<Button tone="ghost" quiet iconOnly="chevron-up" size={size} aria-label={`Move step ${i + 1} up`} disabled={i === 0} onClick={() => onMove(i, i - 1)} />
|
|
76
|
+
<Button tone="ghost" quiet iconOnly="chevron-down" size={size} aria-label={`Move step ${i + 1} down`} disabled={i === items.length - 1} onClick={() => onMove(i, i + 1)} />
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className="flex min-w-0 flex-1 items-center gap-2 border-0 bg-transparent p-0 text-left"
|
|
82
|
+
onClick={readOnly ? undefined : () => onSelect?.(i)}
|
|
83
|
+
disabled={readOnly}
|
|
84
|
+
>
|
|
85
|
+
<span className="kol-helper-10 text-meta tabular-nums w-4 shrink-0">{i + 1}</span>
|
|
86
|
+
<span className="kol-mono-12 text-emphasis truncate">{it.label}</span>
|
|
87
|
+
</button>
|
|
88
|
+
{!readOnly && onRemove && (
|
|
89
|
+
<Button tone="ghost" quiet iconOnly="x" size={size} aria-label={`Remove step ${i + 1}`} onClick={() => onRemove(i)} />
|
|
90
|
+
)}
|
|
91
|
+
</li>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
{!readOnly && onAdd && (
|
|
95
|
+
<li>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className="flex w-full items-center gap-2 rounded border border-dashed border-oq-16 px-2 py-1 text-meta hover:text-body hover:border-oq-24"
|
|
99
|
+
onClick={onAdd}
|
|
100
|
+
>
|
|
101
|
+
<span className="kol-helper-10 tabular-nums w-4 shrink-0">{items.length + 1}</span>
|
|
102
|
+
<Icon name="plus" size={12} className="text-oq-48" />
|
|
103
|
+
<span className="kol-mono-12">{addLabel}</span>
|
|
104
|
+
</button>
|
|
105
|
+
</li>
|
|
106
|
+
)}
|
|
107
|
+
</ul>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -523,19 +523,39 @@ const ContentFilters = ({
|
|
|
523
523
|
|
|
524
524
|
{/* the view family's own line below md (`auto`) or always (`below`) —
|
|
525
525
|
* full width, wrapping, so no view is ever off the page */}
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
526
|
+
{/* BELOW `md` THE VIEW STRIP AND THE TRAILING CONTROLS SHARE ONE WRAPPING LINE (kol-fxr
|
|
527
|
+
* 2026-10-09, HubSettingsHeaderAt390: fxr's SETTINGS label and its options/shortcuts toggle
|
|
528
|
+
* were two ragged rows of one rung). Same rung, same line; it still wraps when they do not
|
|
529
|
+
* fit, so nothing is ever off the page. `below` placements keep their own line at every width. */}
|
|
530
|
+
{(() => {
|
|
531
|
+
const view = viewModeOptions && viewPlacement !== 'header'
|
|
532
|
+
const trail = trailingActions && trailingPlacement !== 'header'
|
|
533
|
+
const shared = view && trail && viewPlacement === 'auto' && trailingPlacement === 'auto'
|
|
534
|
+
if (shared) return (
|
|
535
|
+
<div className="md:hidden mb-3 flex flex-wrap items-center gap-6 gap-y-2">
|
|
536
|
+
{/* the desk's order — trailing, then the strip — so a consumer's divider sits between them */}
|
|
537
|
+
{trailingActions}
|
|
538
|
+
{viewStrip('flex flex-wrap gap-y-2')}
|
|
539
|
+
</div>
|
|
540
|
+
)
|
|
541
|
+
return (
|
|
542
|
+
<>
|
|
543
|
+
{view && (
|
|
544
|
+
<div className={`${viewPlacement === 'auto' ? 'md:hidden ' : ''}mb-3`}>
|
|
545
|
+
{viewStrip('flex flex-wrap gap-y-2')}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
{/* the consumer's trailing controls: their OWN line below md (`auto`) or
|
|
549
|
+
* always (`below`) — full width, wrapping. In the below row's right
|
|
550
|
+
* group they measured 223px beside LIST/GRID in a 342px track. */}
|
|
551
|
+
{trail && (
|
|
552
|
+
<div className={`${trailingPlacement === 'auto' ? 'md:hidden ' : ''}mb-3 flex flex-wrap items-center gap-6 gap-y-2`}>
|
|
553
|
+
{trailingActions}
|
|
554
|
+
</div>
|
|
555
|
+
)}
|
|
556
|
+
</>
|
|
557
|
+
)
|
|
558
|
+
})()}
|
|
539
559
|
|
|
540
560
|
{/* BELOW the divider: the filter GROUPS only. Left-aligned columns —
|
|
541
561
|
* label above values — visible only while the filter toggle is open.
|
|
@@ -10,6 +10,7 @@ import FieldRow, { StatusChip } from '../molecules/FieldRow'
|
|
|
10
10
|
import { Tooltip } from '../utilities/Popover'
|
|
11
11
|
import Table from './Table'
|
|
12
12
|
import MediaLibrary from './MediaLibrary'
|
|
13
|
+
import usePointerSort from '../hooks/usePointerSort.js'
|
|
13
14
|
|
|
14
15
|
/* TOOLBAR ICONS ARE BARE GLYPHS (user ruling 2026-08-09, verbatim: "did I
|
|
15
16
|
* ask for a button?? … the only thing that should hover is full opacity and
|
|
@@ -93,8 +94,9 @@ export default function RecordManager({
|
|
|
93
94
|
}) {
|
|
94
95
|
const wrapRef = useRef(null)
|
|
95
96
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
/* { from, over, x, y } during a drag — the sort lives in hooks/usePointerSort (lifted 2026-10-09
|
|
98
|
+
* so StepList is not a second copy); this file keeps only which rows it measures */
|
|
99
|
+
const { drag, startDrag: beginSort } = usePointerSort(onReorder)
|
|
98
100
|
const [active, setActive] = useState(null)
|
|
99
101
|
const [picker, setPicker] = useState(null) // the field being picked for
|
|
100
102
|
const [selected, setSelected] = useState(() => new Set())
|
|
@@ -118,36 +120,7 @@ export default function RecordManager({
|
|
|
118
120
|
onSelectionChange?.([...next])
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
const startDrag = (e, from) => {
|
|
123
|
-
if (!onReorder) return
|
|
124
|
-
e.preventDefault()
|
|
125
|
-
const rowEls = wrapRef.current?.querySelectorAll('.kol-table-row')
|
|
126
|
-
if (!rowEls?.length) return
|
|
127
|
-
const rects = [...rowEls].map((el) => el.getBoundingClientRect())
|
|
128
|
-
const move = (ev) => {
|
|
129
|
-
const hit = rects.findIndex((r) => ev.clientY < r.bottom)
|
|
130
|
-
const over = hit === -1 ? rects.length - 1 : hit
|
|
131
|
-
dragRef.current = { from, over, x: ev.clientX, y: ev.clientY }
|
|
132
|
-
setDrag(dragRef.current)
|
|
133
|
-
}
|
|
134
|
-
const up = () => {
|
|
135
|
-
window.removeEventListener('pointermove', move)
|
|
136
|
-
window.removeEventListener('pointerup', up)
|
|
137
|
-
document.body.style.cursor = ''
|
|
138
|
-
document.body.style.userSelect = ''
|
|
139
|
-
const d = dragRef.current
|
|
140
|
-
dragRef.current = null
|
|
141
|
-
setDrag(null)
|
|
142
|
-
if (d && d.over !== d.from) onReorder(d.from, d.over)
|
|
143
|
-
}
|
|
144
|
-
window.addEventListener('pointermove', move)
|
|
145
|
-
window.addEventListener('pointerup', up)
|
|
146
|
-
document.body.style.cursor = 'grabbing'
|
|
147
|
-
document.body.style.userSelect = 'none'
|
|
148
|
-
dragRef.current = { from, over: from, x: e.clientX, y: e.clientY }
|
|
149
|
-
setDrag(dragRef.current)
|
|
150
|
-
}
|
|
123
|
+
const startDrag = (e, from) => beginSort(e, from, wrapRef.current?.querySelectorAll('.kol-table-row'))
|
|
151
124
|
|
|
152
125
|
/* ── columns: resolve types, prepend handle + checkbox ────────────────── */
|
|
153
126
|
const builtColumns = useMemo(() => {
|