@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 +1 -1
- package/src/atoms/ActionButton.jsx +1 -1
- package/src/atoms/IconFrame.jsx +1 -1
- package/src/atoms/SegmentedToggle.jsx +1 -1
- package/src/hooks/glyphLadders.js +3 -3
- package/src/hooks/useGrabEdge.js +31 -8
- package/src/molecules/SearchInput.jsx +2 -2
- package/src/molecules/ShellDrawer.jsx +6 -7
- package/src/organisms/ColumnBrowser.jsx +8 -2
- package/src/utilities/motion.js +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "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 (
|
|
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
|
package/src/atoms/IconFrame.jsx
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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
|
|
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. */
|
package/src/hooks/useGrabEdge.js
CHANGED
|
@@ -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
|
-
*
|
|
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, {
|
|
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 <=
|
|
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,
|
|
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 -
|
|
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)) <
|
|
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, ...
|
|
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,
|
|
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:
|
|
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
|
-
{/*
|
|
202
|
-
*
|
|
203
|
-
* `iconSize`
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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}
|
package/src/utilities/motion.js
CHANGED
|
@@ -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
|
+
}
|