@kolkrabbi/kol-component 0.165.0 → 0.166.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.165.0",
3
+ "version": "0.166.0",
4
4
  "description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,11 +25,26 @@ import gsap from 'gsap'
25
25
  * than that leaves the pill where it is, which is what stops it twitching.
26
26
  *
27
27
  * The element needs the `.kol-rail-grab` class for the drawing (kol-animation.css);
28
- * this hook only supplies `is-near` and `--kol-rail-grab-y`.
28
+ * this hook only supplies `is-near` and the travel variable.
29
+ *
30
+ * BOTH AXES (BrowsePageRulingsAndSeams, kol-r2b2 2026-09-02). A rail has one
31
+ * vertical edge; `ColumnBrowser` has a vertical handle per column AND a
32
+ * horizontal one along its foot, and the user's ruling is that they wear the
33
+ * same gesture. `axis` says which way the pill TRAVELS, and the near test is
34
+ * always the other one — a vertical line is approached across x, a horizontal
35
+ * line across y:
36
+ *
37
+ * axis: 'y' (default) vertical edge · near on x · travels on y · --kol-rail-grab-y
38
+ * axis: 'x' horizontal edge · near on y · travels on x · --kol-rail-grab-x
39
+ *
40
+ * The `-y` name is kept as it shipped rather than renamed to something
41
+ * axis-neutral: it is a published token the rail's CSS reads, and the pair
42
+ * `-x`/`-y` names two real axes rather than one concept twice.
29
43
  *
30
44
  * @param {React.RefObject<HTMLElement>} ref the grab handle
45
+ * @param {{axis?: 'x'|'y'}} [options] which way the pill travels (default 'y')
31
46
  */
32
- export default function useGrabEdge(ref) {
47
+ export default function useGrabEdge(ref, { axis = 'y' } = {}) {
33
48
  useEffect(() => {
34
49
  let raf = 0
35
50
  const onMove = ({ clientX, clientY }) => {
@@ -39,16 +54,22 @@ export default function useGrabEdge(ref) {
39
54
  const h = ref.current
40
55
  if (!h) return
41
56
  const r = h.getBoundingClientRect()
42
- const dist = Math.abs(clientX - (r.left + r.width / 2))
57
+ const vertical = axis === 'y'
58
+ /* the near test is ACROSS the line, the travel ALONG it */
59
+ const across = vertical ? clientX - (r.left + r.width / 2) : clientY - (r.top + r.height / 2)
60
+ const dist = Math.abs(across)
43
61
  const near = dist <= GRAB.near || (h.classList.contains('is-near') && dist <= GRAB.sleep)
44
62
  h.classList.toggle('is-near', near)
45
63
  if (!near) return
46
- /* the travel band: bipolar from the middle, GRAB.range of the height */
47
- const edge = (r.height * (1 - GRAB.range)) / 2
48
- const along = Math.min(Math.max(clientY - r.top, edge), r.height - edge)
64
+ /* the travel band: bipolar from the middle, GRAB.range of the span */
65
+ const span = vertical ? r.height : r.width
66
+ const start = vertical ? r.top : r.left
67
+ const point = vertical ? clientY : clientX
68
+ const edge = (span * (1 - GRAB.range)) / 2
69
+ const along = Math.min(Math.max(point - start, edge), span - edge)
49
70
  if (h.dataset.grabSeeded && Math.abs(along - Number(h.dataset.grabTarget)) < GRAB.stick) return
50
71
  h.dataset.grabTarget = String(along)
51
- const vars = { '--kol-rail-grab-y': `${along}px` }
72
+ const vars = { [vertical ? '--kol-rail-grab-y' : '--kol-rail-grab-x']: `${along}px` }
52
73
  /* the CSS fallback is 50%, a percentage — nothing to tween from, so the
53
74
  * first sighting sets and every move after tweens */
54
75
  if (h.dataset.grabSeeded) gsap.to(h, { ...vars, ...GRAB.travel, overwrite: 'auto' })
@@ -57,5 +78,5 @@ export default function useGrabEdge(ref) {
57
78
  }
58
79
  window.addEventListener('pointermove', onMove)
59
80
  return () => { window.removeEventListener('pointermove', onMove); cancelAnimationFrame(raf) }
60
- }, [ref])
81
+ }, [ref, axis])
61
82
  }
@@ -3,6 +3,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import KindPreview from '../molecules/KindPreview.jsx'
4
4
  import { formatLength } from '../molecules/AudioPreview.jsx'
5
5
  import { kindOf as dsKindOf, KIND_LABEL as DS_KIND_LABEL } from '../utilities/mediaKinds.js'
6
+ import useGrabEdge from '../hooks/useGrabEdge.js'
6
7
 
7
8
  /**
8
9
  * ColumnBrowser — Finder-style Miller columns over a flat key space (kol-r2b2's
@@ -24,17 +25,21 @@ import { kindOf as dsKindOf, KIND_LABEL as DS_KIND_LABEL } from '../utilities/me
24
25
  * `window` (the overlay holds focus) and space closes — the opening keystroke
25
26
  * bubbles from here and is ignored.
26
27
  *
27
- * Rows wear the DS Table's cell metrics (12px 16px, mono 12, an oq-08 hairline
28
- * between rows, none after the last). Selected and cursor rows draw the HOVER
29
- * fill (`bg-fg-04`) user ruling: "make hover state the selected state".
28
+ * Rows wear the DS Table's cell metrics (12px 16px, mono 12) with NO divider —
29
+ * the row is a rounded pill on a constant 4px inset, so selecting never shifts
30
+ * the layout (BrowsePageRulingsAndSeams, 2026-09-02). ONE fill on screen and it
31
+ * means selected: hover and the bare keyboard cursor paint nothing, the trail is
32
+ * `fg-02` and the deepest column holding a selection is `fg-04`.
30
33
  * Height defaults to 528px = 12 rows × 44px (user ruling); columns scroll. Both
31
34
  * the height and every column's width are DRAGGABLE, Finder-style
32
35
  * (ColumnBrowserResize, kol-r2b2 2026-08-27 — user: "column height drag yes and
33
36
  * individual column width drag" · "that should be a set in ds"): a strip along
34
37
  * the browser's bottom edge (`row-resize`) and one on each column's right edge
35
38
  * (`col-resize`, the border already there is the visual — the strip is the hit
36
- * area, invisible at rest, `fg-08` on hover / while dragging). Pointer events
37
- * with `setPointerCapture`, no library; native CSS `resize:` was rejected.
39
+ * area). Each strip carries the estate's GRAB PILL: hidden at rest, woken by
40
+ * pointer PROXIMITY (`useGrabEdge`, the rail's hook) and travelling along the
41
+ * line to meet the cursor. Pointer events with `setPointerCapture`, no library;
42
+ * native CSS `resize:` was rejected.
38
43
  *
39
44
  * The app-owned bits are SEAMS with working defaults: `urlOf(o)` (the preview
40
45
  * image — no URL, no image), `kindOf(o)` (image / video / audio / file from
@@ -193,12 +198,28 @@ function Preview({ o, urlOf, kindOf, kindLabel, formatSize, renderPreview, width
193
198
  }
194
199
 
195
200
  /* one edge handle: captures the pointer, reports the delta along its axis;
196
- * `is-dragging` keeps the wash on while the pointer is captured */
201
+ * `is-dragging` keeps the pill lit while the pointer is captured.
202
+ *
203
+ * THE PILL FOLLOWS THE POINTER (BrowsePageRulingsAndSeams, kol-r2b2 2026-09-02,
204
+ * user ruling). It used to sit dead centre and wake on :hover — the theme's own
205
+ * comment said pointer-following "was built and rejected", while the SAME
206
+ * gesture had shipped on the rail's grab edge the same day and has run in
207
+ * kol-r2b2 since. `useGrabEdge` is that implementation; this is the second
208
+ * consumer of it, which is why the hook grew an `axis` rather than a fork.
209
+ *
210
+ * The handle's axis is named for what it RESIZES — `x` drags the column's width
211
+ * — and the pill on it travels the other way, down the vertical edge. So the
212
+ * `x` handle takes `axis: 'y'` and the `y` handle `axis: 'x'`: the hook's axis
213
+ * is the pill's travel, not the drag. Getting this pair backwards is the one
214
+ * easy mistake here. */
197
215
  function ResizeHandle({ axis, onDrag, onEnd }) {
198
216
  const [dragging, setDragging] = useState(false)
199
217
  const start = useRef(null)
218
+ const ref = useRef(null)
219
+ useGrabEdge(ref, { axis: axis === 'x' ? 'y' : 'x' })
200
220
  return (
201
221
  <div
222
+ ref={ref}
202
223
  className={`kol-column-browser-resize-${axis} ${dragging ? 'is-dragging' : ''}`.trim()}
203
224
  role="separator"
204
225
  aria-orientation={axis === 'x' ? 'vertical' : 'horizontal'}