@kolkrabbi/kol-component 0.244.0 → 0.245.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
CHANGED
|
@@ -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
|
@@ -79,6 +79,7 @@ export { default as InspectorRail } from './molecules/InspectorRail.jsx'
|
|
|
79
79
|
export { default as PathNodeOverlay } from './utilities/PathNodeOverlay.jsx'
|
|
80
80
|
export { default as CropOverlay } from './utilities/CropOverlay.jsx'
|
|
81
81
|
export { default as LayerStack, AddLayerButton, BLEND_MODES } from './organisms/LayerStack.jsx'
|
|
82
|
+
export { default as StepList } from './molecules/StepList.jsx'
|
|
82
83
|
export { TYPE_LABELS, BOOL_OP_LABELS, SHAPE_KIND_LABELS, labelForLayer, rowLabelForLayer, findLayerDeep } from './hooks/layerTree.js'
|
|
83
84
|
export { default as TimelineDock, sampleTrack, TIMELINE_EASINGS } from './organisms/TimelineDock.jsx'
|
|
84
85
|
export { default as CurveEditor, CURVE_KINDS, defaultCurveFor } from './organisms/CurveEditor.jsx'
|
|
@@ -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
|
+
}
|
|
@@ -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(() => {
|