@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.121.1",
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',
@@ -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 = 350,
27
- damping = 35,
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 = { stiffness: 250, damping: 25, mass: 0.6 }
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="w-full kol-helper-12 px-3 h-8 shrink-0 inline-flex items-center gap-2 text-body hover:text-emphasis disabled:opacity-40 disabled:cursor-not-allowed text-left"
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
+ }