@kolkrabbi/kol-component 0.121.1 → 0.123.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.
|
|
3
|
+
"version": "0.123.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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./utilities/markdownToHtml": "./src/utilities/markdownToHtml.js",
|
|
18
18
|
"./utilities/mediaKinds": "./src/utilities/mediaKinds.js",
|
|
19
19
|
"./utilities/ratios": "./src/utilities/ratios.js",
|
|
20
|
+
"./utilities/motion": "./src/utilities/motion.js",
|
|
20
21
|
"./hooks/*": "./src/hooks/*.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useLayoutEffect } from 'react'
|
|
2
|
+
import { DURATION } from '../utilities/motion.js'
|
|
2
3
|
import gsap from 'gsap'
|
|
3
4
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
5
|
import { glyphSize } from '../hooks/glyphLadders.js'
|
|
@@ -73,7 +74,7 @@ const HOUSE_EASE = [0.4, 0, 0.2, 1]
|
|
|
73
74
|
* not a mode. The release then plays the full bounce out. */
|
|
74
75
|
const PRESS_HOLD = 1600
|
|
75
76
|
|
|
76
|
-
const SWAP_MS = 500
|
|
77
|
+
const SWAP_MS = DURATION.spring /* the house 500 (utilities/motion.js) */
|
|
77
78
|
|
|
78
79
|
const CHROME = {
|
|
79
80
|
copy: 'kol-copy-btn',
|
package/src/hooks/useTilt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef } from 'react'
|
|
2
2
|
import { useMotionValue, useSpring, useTransform } from 'framer-motion'
|
|
3
|
+
import { SPRING } from '../utilities/motion.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Pointer-driven 3D tilt (framer-motion springs) — the ONE tilt hook.
|
|
@@ -12,7 +13,9 @@ import { useMotionValue, useSpring, useTransform } from 'framer-motion'
|
|
|
12
13
|
* (e.g. TiltCard's `grounded` zone-snapping).
|
|
13
14
|
*
|
|
14
15
|
* Defaults are the design: tilt ±4°, spring 350/35, perspective 700,
|
|
15
|
-
* rest position center (0.5/0.5).
|
|
16
|
+
* rest position center (0.5/0.5). The two spring sets are `SPRING.tilt` and
|
|
17
|
+
* `SPRING.lazy` in `utilities/motion.js` — the JS mirror of kol-animation.css,
|
|
18
|
+
* so a tween elsewhere in the estate cannot drift from this feel (2026-08-28).
|
|
16
19
|
*
|
|
17
20
|
* `grounded` (ShelfCardTiltWrapsCard, kol-website 2026-08-27 — lifted out of
|
|
18
21
|
* TiltCardInner so the shelf and the card share ONE feel, no new component):
|
|
@@ -23,8 +26,8 @@ import { useMotionValue, useSpring, useTransform } from 'framer-motion'
|
|
|
23
26
|
export default function useTilt({
|
|
24
27
|
magnitude = 4,
|
|
25
28
|
perspective = 700,
|
|
26
|
-
stiffness =
|
|
27
|
-
damping =
|
|
29
|
+
stiffness = SPRING.tilt.stiffness,
|
|
30
|
+
damping = SPRING.tilt.damping,
|
|
28
31
|
grounded = false,
|
|
29
32
|
} = {}) {
|
|
30
33
|
const ref = useRef(null)
|
|
@@ -38,7 +41,7 @@ export default function useTilt({
|
|
|
38
41
|
* SELECTED when asked for — a free tilt pays two idle springs, nothing more */
|
|
39
42
|
const zones = 3
|
|
40
43
|
const snap = (v) => Math.round(v * zones) / zones
|
|
41
|
-
const lazy =
|
|
44
|
+
const lazy = SPRING.lazy
|
|
42
45
|
const lazyX = useSpring(useTransform(springX, (v) => Math.min(0, snap(-v / magnitude) * 2.5)), lazy)
|
|
43
46
|
const lazyY = useSpring(useTransform(springY, (v) => snap(v / magnitude) * 2.5), lazy)
|
|
44
47
|
const rotateX = grounded ? lazyX : springX
|
|
@@ -148,6 +148,13 @@ const Dropdown = ({
|
|
|
148
148
|
return (
|
|
149
149
|
<MenuDropdownItem
|
|
150
150
|
key={option.value}
|
|
151
|
+
/* NO HOVER STATE ANYWHERE ON A DROPDOWN (user 2026-08-28,
|
|
152
|
+
* "delete any hover state on dropdowns"): the trigger was
|
|
153
|
+
* already pinned in all three variants — primary and outline
|
|
154
|
+
* back to rest in kol-theme, grey carrying no hover rule at
|
|
155
|
+
* all — and this was the last one left, the option row's ink
|
|
156
|
+
* brighten. The check mark is what marks the current value. */
|
|
157
|
+
hover={false}
|
|
151
158
|
onClick={() => handleSelect(option)}
|
|
152
159
|
shortcut={isActive ? <Icon name="check" size={11} /> : undefined}
|
|
153
160
|
>
|
|
@@ -78,6 +78,11 @@ export function MenuItem({
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* MenuDropdownItem — action row inside a MenuItem's dropdown panel.
|
|
81
|
+
*
|
|
82
|
+
* `hover` (default true) — `false` drops the ink brighten, for a panel ruled
|
|
83
|
+
* to carry NO hover state at all (user 2026-08-28, on `Dropdown`: "delete any
|
|
84
|
+
* hover state on dropdowns"). Scoped to the caller: a MenuItem's own menu keeps
|
|
85
|
+
* its hover, because that ruling was about the select, not every panel.
|
|
81
86
|
* Renders as a button so it picks up disabled, focus, and keyboard
|
|
82
87
|
* activation. The parent MenuItem closes automatically on click via a
|
|
83
88
|
* delegated handler that matches the `data-menu-item` attr.
|
|
@@ -89,7 +94,7 @@ export function MenuItem({
|
|
|
89
94
|
* - children — main label, flex-1.
|
|
90
95
|
* - shortcut — trailing content (text shortcut hint, ✓ marker, or icon).
|
|
91
96
|
*/
|
|
92
|
-
export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut, children }) {
|
|
97
|
+
export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut, hover = true, children }) {
|
|
93
98
|
return (
|
|
94
99
|
<button
|
|
95
100
|
type="button"
|
|
@@ -97,7 +102,7 @@ export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut
|
|
|
97
102
|
onClick={onClick}
|
|
98
103
|
disabled={disabled}
|
|
99
104
|
role="menuitem"
|
|
100
|
-
className=
|
|
105
|
+
className={`w-full kol-helper-12 px-3 h-8 shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
|
|
101
106
|
>
|
|
102
107
|
{prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
|
|
103
108
|
{iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion.js — the JS side of the motion sheet (user ruling 2026-08-28:
|
|
3
|
+
* "localise animation to its own css … and use it to localise all the gsap
|
|
4
|
+
* framer shit we are doing").
|
|
5
|
+
*
|
|
6
|
+
* kol-theme's `kol-animation.css` is where the estate's CSS motion lives. This
|
|
7
|
+
* is its mirror for the motion CSS cannot express — a gsap tween, a framer
|
|
8
|
+
* spring — so a hand-typed `duration: 0.5, ease: "power3.out"` in some
|
|
9
|
+
* component is not a second, private vocabulary. Same numbers, one import.
|
|
10
|
+
*
|
|
11
|
+
* NOT here: a domain's own physics — the foundry's pressure damping, the
|
|
12
|
+
* marquee's px/s, a scroll-scrub timeline. Those are content, not chrome, and
|
|
13
|
+
* flattening them into house constants would say they are interchangeable.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* The house curves. `HOUSE` is `--kol-ease-house` as framer takes it (a
|
|
17
|
+
* cubic-bezier array); the gsap column is the nearest named equivalent, so a
|
|
18
|
+
* tween and a transition on the same element do not disagree. */
|
|
19
|
+
export const EASE = {
|
|
20
|
+
house: [0.4, 0, 0.2, 1],
|
|
21
|
+
houseGsap: 'power2.inOut',
|
|
22
|
+
bounce: [0.34, 1.56, 0.64, 1],
|
|
23
|
+
/* symmetric in-out — an ease-out pops the first 20 % and crawls the rest,
|
|
24
|
+
* which reads as a jerk on a long fade (user, on the rail pill 2026-08-28) */
|
|
25
|
+
longFade: [0.45, 0, 0.55, 1],
|
|
26
|
+
outGsap: 'power3.out',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Milliseconds, mirroring `--kol-transition-*` in kol-design-tokens.css.
|
|
30
|
+
* `s()` because gsap counts in seconds and framer in seconds — the CSS side is
|
|
31
|
+
* the source, so the conversion lives here rather than at each call site. */
|
|
32
|
+
export const DURATION = { fast: 150, base: 200, slow: 300, spring: 500, zoom: 600 }
|
|
33
|
+
export const s = (ms) => ms / 1000
|
|
34
|
+
|
|
35
|
+
/* framer springs. `tilt` is the Tilt family's feel; `lazy` is the grounded
|
|
36
|
+
* chase (heavier, slower to settle) — the two the hook already had inline. */
|
|
37
|
+
export const SPRING = {
|
|
38
|
+
tilt: { stiffness: 350, damping: 35 },
|
|
39
|
+
lazy: { stiffness: 250, damping: 25, mass: 0.6 },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* THE RAIL'S GRAB EDGE (RailFlatGrabOpen, kol-mirror 2026-08-28 — kol-r2b2's
|
|
43
|
+
* pill). The chrome is `.kol-rail-grab` in kol-animation.css; these are the
|
|
44
|
+
* numbers JS owns, because they are pointer behaviour, not paint:
|
|
45
|
+
*
|
|
46
|
+
* near / sleep the pill wakes within 20px of the line and sleeps only past
|
|
47
|
+
* 40 — hovering the line itself flapped the class every frame
|
|
48
|
+
* and restarted the fade, so the hysteresis is not optional
|
|
49
|
+
* marks it rests on 20/40/60/80 % of the rail's height and is STICKY:
|
|
50
|
+
* it leaves a mark only within `stick` of the pitch to another
|
|
51
|
+
* (user: "following the mouse a little too much"). The middle
|
|
52
|
+
* of each gap is dead on purpose
|
|
53
|
+
* travel a long chase, so the pill trails the pointer and catches up
|
|
54
|
+
* snap the width tween on release
|
|
55
|
+
* slop under this, a pointerdown/up is a CLICK, not a drag
|
|
56
|
+
*/
|
|
57
|
+
export const GRAB = {
|
|
58
|
+
near: 20,
|
|
59
|
+
sleep: 40,
|
|
60
|
+
marks: [0.2, 0.4, 0.6, 0.8],
|
|
61
|
+
stick: 0.2 * 0.3,
|
|
62
|
+
travel: { duration: 2.8, ease: EASE.outGsap },
|
|
63
|
+
snap: { duration: 0.5, ease: EASE.outGsap },
|
|
64
|
+
slop: 3,
|
|
65
|
+
}
|