@kolkrabbi/kol-component 0.166.0 → 0.167.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.166.0",
3
+ "version": "0.167.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",
@@ -47,7 +47,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
47
47
  * confirm only fires once it resolves
48
48
  * @param {string} href renders an <a> instead of a <button>
49
49
  * @param {number} hold ms to hold the confirm state (default 2000)
50
- * @param {string} size sm | md | lg — the pinned square (28/32/36)
50
+ * @param {string} size xs | sm | md | lg — the pinned square (22/26/32/40)
51
51
  * and its SOLO glyph (16/20/24) move together,
52
52
  * resolved from hooks/glyphLadders.js. This was
53
53
  * a raw px number and defaulted to 16 in a 32px
@@ -28,7 +28,7 @@ import { toneClass } from '../utilities/tone.js'
28
28
  * no per-theme props.
29
29
  *
30
30
  * `size` moves the square and the glyph together on the solo-glyph ladder
31
- * (16/20/24 against the pinned squares 28/32/36) — that pairing is the DEFAULT,
31
+ * (16/20/24 against the pinned squares 22/26/32/40) — that pairing is the DEFAULT,
32
32
  * and it is what every call site should take. `iconSize` unbinds the glyph for
33
33
  * the cases the ladder cannot serve, exactly as it does on `Button` and `Input`:
34
34
  * a `radius="full"` frame reads heavier than the square it was tuned against (a
@@ -37,7 +37,7 @@
37
37
  * 'tonal' (filled tiles, but the clicked cell marks itself by
38
38
  * TONE — surface-tertiary fill, no ring; 2026-08-12).
39
39
  * Every variant renders at the SAME pinned button-ladder
40
- * height (26/32/40) — icon or text, the box never moves.
40
+ * height (22/26/32/40) — icon or text, the box never moves.
41
41
  * size — mirrors Button exactly: 'sm' (26px, mono-12, 4/12 pad) |
42
42
  * 'md' (default, 32px, mono-14, 6/16 pad) | 'lg' (40px,
43
43
  * mono-16, 8/20 pad). Same cell padding + mono type as the
@@ -7,12 +7,12 @@
7
7
  *
8
8
  * The ladders split on ONE question: is there a label beside the glyph?
9
9
  *
10
- * SOLO an icon alone in a pinned square (28/32/36) — takes the room
10
+ * SOLO an icon alone in a pinned square (22/26/32/40) — takes the room
11
11
  * ADJACENT an icon inside a rung's line box, beside a label
12
12
  *
13
13
  * Stated in the DS before this file existed — ThemeToggle.jsx:40 ("solo
14
14
  * 16/20/24, the pinned-square pairing") and IconFrame.jsx ("the solo-glyph
15
- * ladder against the pinned squares 28/32/36"). This file is where they stop
15
+ * ladder against the pinned squares"). This file is where they stop
16
16
  * being transcribed.
17
17
  *
18
18
  * Why it exists: the same two ladders were re-typed in four places
@@ -23,7 +23,7 @@
23
23
  * carrying a label) and nothing connected the two.
24
24
  */
25
25
 
26
- /** Icon alone in a pinned square. Pairs with squares 28 · 32 · 36. */
26
+ /** Icon alone in a pinned square. Pairs with squares 22 · 26 · 32 · 40. */
27
27
  export const SOLO = { xs: 12, sm: 16, md: 20, lg: 24 }
28
28
 
29
29
  /** Icon beside a label, inside the rung's line box. */
@@ -41,10 +41,33 @@ import gsap from 'gsap'
41
41
  * axis-neutral: it is a published token the rail's CSS reads, and the pair
42
42
  * `-x`/`-y` names two real axes rather than one concept twice.
43
43
  *
44
+ * TUNING IS PER-EDGE (BrowsePageRulingsAndSeams, 2026-09-02 — user: *"its a bit
45
+ * different, its not like the sidenav"*). The rail's dwell-and-throw is its own
46
+ * ruling, not a house constant every edge inherits: `ColumnBrowser`'s handles
47
+ * were ruled on their own feel in kol-r2b2 — a longer chase and a much smaller
48
+ * retarget threshold, so the pill tracks the pointer closely instead of landing
49
+ * and holding. `GRAB` stays the default, so the rail does not move.
50
+ *
44
51
  * @param {React.RefObject<HTMLElement>} ref the grab handle
45
- * @param {{axis?: 'x'|'y'}} [options] which way the pill travels (default 'y')
52
+ * EVERY tuning key the constants carry is an option, so a whole set spreads in
53
+ * (`useGrabEdge(ref, { axis, ...GRAB_COLUMN })`) with nothing silently dropped
54
+ * — a key the hook did not read would be a seam wired to nothing.
55
+ *
56
+ * @param {{axis?: 'x'|'y', near?: number, sleep?: number, travel?: object, stick?: number, range?: number}} [options]
57
+ * `axis` — which way the pill travels (default 'y').
58
+ * `near` / `sleep` — wake and sleep distance from the line, the hysteresis pair.
59
+ * `travel` — the gsap chase (default `GRAB.travel`).
60
+ * `stick` — px the pointer must move before the pill re-targets (default `GRAB.stick`).
61
+ * `range` — the travel band as a fraction of the edge (default `GRAB.range`).
46
62
  */
47
- export default function useGrabEdge(ref, { axis = 'y' } = {}) {
63
+ export default function useGrabEdge(ref, {
64
+ axis = 'y',
65
+ near: nearAt = GRAB.near,
66
+ sleep = GRAB.sleep,
67
+ travel = GRAB.travel,
68
+ stick = GRAB.stick,
69
+ range = GRAB.range,
70
+ } = {}) {
48
71
  useEffect(() => {
49
72
  let raf = 0
50
73
  const onMove = ({ clientX, clientY }) => {
@@ -58,25 +81,25 @@ export default function useGrabEdge(ref, { axis = 'y' } = {}) {
58
81
  /* the near test is ACROSS the line, the travel ALONG it */
59
82
  const across = vertical ? clientX - (r.left + r.width / 2) : clientY - (r.top + r.height / 2)
60
83
  const dist = Math.abs(across)
61
- const near = dist <= GRAB.near || (h.classList.contains('is-near') && dist <= GRAB.sleep)
84
+ const near = dist <= nearAt || (h.classList.contains('is-near') && dist <= sleep)
62
85
  h.classList.toggle('is-near', near)
63
86
  if (!near) return
64
- /* the travel band: bipolar from the middle, GRAB.range of the span */
87
+ /* the travel band: bipolar from the middle, `range` of the span */
65
88
  const span = vertical ? r.height : r.width
66
89
  const start = vertical ? r.top : r.left
67
90
  const point = vertical ? clientY : clientX
68
- const edge = (span * (1 - GRAB.range)) / 2
91
+ const edge = (span * (1 - range)) / 2
69
92
  const along = Math.min(Math.max(point - start, edge), span - edge)
70
- if (h.dataset.grabSeeded && Math.abs(along - Number(h.dataset.grabTarget)) < GRAB.stick) return
93
+ if (h.dataset.grabSeeded && Math.abs(along - Number(h.dataset.grabTarget)) < stick) return
71
94
  h.dataset.grabTarget = String(along)
72
95
  const vars = { [vertical ? '--kol-rail-grab-y' : '--kol-rail-grab-x']: `${along}px` }
73
96
  /* the CSS fallback is 50%, a percentage — nothing to tween from, so the
74
97
  * first sighting sets and every move after tweens */
75
- if (h.dataset.grabSeeded) gsap.to(h, { ...vars, ...GRAB.travel, overwrite: 'auto' })
98
+ if (h.dataset.grabSeeded) gsap.to(h, { ...vars, ...travel, overwrite: 'auto' })
76
99
  else { gsap.set(h, vars); h.dataset.grabSeeded = '1' }
77
100
  })
78
101
  }
79
102
  window.addEventListener('pointermove', onMove)
80
103
  return () => { window.removeEventListener('pointermove', onMove); cancelAnimationFrame(raf) }
81
- }, [ref, axis])
104
+ }, [ref, axis, nearAt, sleep, travel, stick, range])
82
105
  }
@@ -103,7 +103,7 @@ export default function SearchInput({
103
103
  if (expanding && isOpen) inputRef.current?.focus()
104
104
  }, [expanding, isOpen])
105
105
 
106
- /* the pinned squares, 05-control-chrome.md — sm 28 · md 32 · lg 36 */
106
+ /* the pinned squares, 01-foundations/09-sizes.md — 22 · 26 · 32 · 40 */
107
107
  const square = { sm: 28, md: 32, lg: 36 }[size] ?? 32
108
108
  /* THE OPEN FIELD'S HEIGHT IS ITS OWN KNOB, defaulting to the square.
109
109
  *
@@ -154,7 +154,7 @@ export default function SearchInput({
154
154
  collapsed pill rendered as a filled circle next to a bare glyph and
155
155
  the pair looked like two different kinds of control. */
156
156
  /* THE SQUARE FOLLOWS THE LADDER, and it follows `size` like every other
157
- control: sm 28 · md 32 · lg 36 (hooks/glyphLadders.js). This was a
157
+ control: 22 · 26 · 32 · 40 (01-foundations/09-sizes.md). This was a
158
158
  hardcoded 36 — the LG square — so an expanding search sat beside a
159
159
  `kol-btn-md` filter button at two different sizes. */
160
160
  className={`kol-expand flex items-center rounded-full ${isOpen ? (toneClass(tone) ? 'kol-tone-sunken' : 'bg-fg-04') : ''} ${className}`.trim()}
@@ -198,18 +198,17 @@ export default function ShellDrawer({
198
198
  </button>
199
199
  )}
200
200
  {header != null && <div className="min-w-0 flex-1">{header}</div>}
201
- {/* The box has an owner (2026-08-01). This hand-wrote the icon-button
202
- * square and its hover wash; `Button variant="nav"` IS that string.
203
- * `iconSize` holds the glyph where it was the ladder's md rung is
204
- * heavier than a drawer close wants, and Button documents iconSize
205
- * for exactly the cases the ladder cannot serve. The SQUARE is what
206
- * needed an owner, and it now has one. */}
201
+ {/* A NORMAL ICON BUTTON (user 2026-09-02: *"it should just be like a
202
+ * normal button with a close icon, its not new?"*). It carried
203
+ * `iconSize={14}` kept in 2026-08-01 to preserve the glyph size the
204
+ * hand-rolled button before it happened to have so the box sat at
205
+ * the md rung's 32px around a glyph six under it, and it read as an
206
+ * oversized empty square. No override: the rung sets both. */}
207
207
  {closeSide !== 'start' && (
208
208
  <Button
209
209
  variant="nav"
210
210
  size="md"
211
211
  iconOnly="x"
212
- iconSize={14}
213
212
  onClick={onClose}
214
213
  aria-label="Close"
215
214
  className="ml-auto shrink-0"
@@ -4,6 +4,7 @@ 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
6
  import useGrabEdge from '../hooks/useGrabEdge.js'
7
+ import { GRAB_COLUMN } from '../utilities/motion.js'
7
8
 
8
9
  /**
9
10
  * ColumnBrowser — Finder-style Miller columns over a flat key space (kol-r2b2's
@@ -211,12 +212,17 @@ function Preview({ o, urlOf, kindOf, kindLabel, formatSize, renderPreview, width
211
212
  * — and the pill on it travels the other way, down the vertical edge. So the
212
213
  * `x` handle takes `axis: 'y'` and the `y` handle `axis: 'x'`: the hook's axis
213
214
  * is the pill's travel, not the drag. Getting this pair backwards is the one
214
- * easy mistake here. */
215
+ * easy mistake here.
216
+ *
217
+ * `GRAB_COLUMN`, not the rail's `GRAB` (user 2026-09-02: *"its not like the
218
+ * sidenav. the handles are different from the sidenav"*) — a 2.8s chase and a
219
+ * 30px retarget, so the pill tracks the pointer instead of landing and holding
220
+ * on the rail's 90px dwell. */
215
221
  function ResizeHandle({ axis, onDrag, onEnd }) {
216
222
  const [dragging, setDragging] = useState(false)
217
223
  const start = useRef(null)
218
224
  const ref = useRef(null)
219
- useGrabEdge(ref, { axis: axis === 'x' ? 'y' : 'x' })
225
+ useGrabEdge(ref, { axis: axis === 'x' ? 'y' : 'x', ...GRAB_COLUMN })
220
226
  return (
221
227
  <div
222
228
  ref={ref}
@@ -86,3 +86,29 @@ export const GRAB = {
86
86
  snap: { duration: 0.5, ease: EASE.outGsap },
87
87
  slop: 3,
88
88
  }
89
+
90
+ /* THE COLUMN BROWSER'S HANDLES ARE NOT THE RAIL'S (BrowsePageRulingsAndSeams,
91
+ * kol-r2b2 2026-09-02 — user: *"its a bit different, its not like the sidenav.
92
+ * the handles are different from the sidenav"*). Same gesture, its own feel,
93
+ * ruled in kol-r2b2 and running there since 2026-08-28:
94
+ *
95
+ * travel 2.8s, not 1.1 — a long, unhurried chase
96
+ * stick 30px, not 90 — a much smaller retarget threshold, so the pill
97
+ * TRACKS the pointer down the edge instead of landing and holding.
98
+ * The dwell that fixed the rail's four-mark grid is not what these
99
+ * want; the deadband exists only so a slow drag does not nudge the
100
+ * target every frame — and it is measured against the last TARGET,
101
+ * not the animated value, because mid-flight the live value is still
102
+ * travelling.
103
+ * range 1 — no bipolar band. The rail chokes its travel so the pill never
104
+ * rides up beside the logomark; there is nothing at these edges to
105
+ * avoid, and the CSS `clamp()` at 2.25rem already stops it flush.
106
+ *
107
+ * `near` / `sleep` are shared — proximity is proximity. The FADE curve differs
108
+ * too and lives with the rule (kol-theme's kol-components-molecules.css). */
109
+ export const GRAB_COLUMN = {
110
+ ...GRAB,
111
+ stick: 30,
112
+ range: 1,
113
+ travel: { duration: 2.8, ease: EASE.outGsap },
114
+ }