@kolkrabbi/kol-component 0.121.0 → 0.122.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.0",
3
+ "version": "0.122.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',
@@ -73,8 +73,12 @@ const ViewToggle = ({
73
73
  )
74
74
  }
75
75
 
76
+ /* THE WELL IS INSET (ViewToggleWellGap, kol-website 2026-08-28): `p-1` put the box 4px past the
77
+ * last chip on every side, so a row's `gap-4` read 20 on this side and 16 on the other, and every
78
+ * consumer subtracted it with a `-ml-1` on the neighbour. `-mx-1` draws the padding inward from the
79
+ * declared box — the chips sit where the box says, the well bleeds 4px into the gap. */
76
80
  const containerClasses = isIconVariant
77
- ? `kol-view-toggle inline-flex items-center gap-1 p-1 bg-surface-secondary rounded ${toneClass(tone)} ${className}`.replace(/\s+/g, ' ').trim()
81
+ ? `kol-view-toggle inline-flex items-center gap-1 p-1 -mx-1 bg-surface-secondary rounded ${toneClass(tone)} ${className}`.replace(/\s+/g, ' ').trim()
78
82
  : `flex gap-2 ${className}`
79
83
 
80
84
  const buttonClasses = (isActive) => {
@@ -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
@@ -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
+ }