@kolkrabbi/kol-component 0.2.0 → 0.3.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 +11 -3
- package/src/atoms/AnimatedTitle.jsx +108 -0
- package/src/atoms/AssetGrid.jsx +29 -0
- package/src/atoms/Button.jsx +17 -6
- package/src/atoms/CurveOverlay.jsx +180 -0
- package/src/atoms/DocsToc.jsx +48 -0
- package/src/atoms/EmptyState.jsx +22 -0
- package/src/atoms/Figure.jsx +27 -0
- package/src/atoms/HlsVideo.jsx +56 -0
- package/src/atoms/OverlayGlassPanel.jsx +40 -0
- package/src/atoms/PriceDisplay.jsx +34 -0
- package/src/atoms/ProsePreview.jsx +53 -0
- package/src/atoms/RotaryDial.jsx +150 -0
- package/src/atoms/SearchInput.jsx +108 -0
- package/src/atoms/TextPressure.jsx +331 -0
- package/src/atoms/TiltCard.jsx +127 -0
- package/src/atoms/TypeSample.jsx +50 -0
- package/src/atoms/TypeSpecCard.jsx +42 -0
- package/src/hooks/cssVar.js +53 -0
- package/src/hooks/useAxisAnimation.js +91 -0
- package/src/hooks/usePrefersReducedMotion.js +21 -0
- package/src/hooks/useTilt.js +49 -0
- package/src/index.js +66 -1
- package/src/molecules/AlignmentGrid.jsx +53 -0
- package/src/molecules/ArticleCard.jsx +178 -0
- package/src/molecules/CardFeatureItem.jsx +130 -0
- package/src/molecules/ColorInputRow.jsx +179 -0
- package/src/molecules/ColorRamp.jsx +114 -0
- package/src/molecules/FramedMediaBand.jsx +56 -0
- package/src/molecules/ImageBlock.jsx +34 -0
- package/src/molecules/SelectionOverlay.jsx +108 -0
- package/src/molecules/ShapeDropdown.jsx +92 -0
- package/src/molecules/ShellDrawer.jsx +169 -0
- package/src/molecules/ShellSearchOverlay.jsx +177 -0
- package/src/molecules/SpecList.jsx +30 -0
- package/src/molecules/SpectrumControls.jsx +504 -0
- package/src/molecules/SwatchControls.jsx +217 -0
- package/src/molecules/TabsRow.jsx +87 -0
- package/src/molecules/VideoBlock.jsx +86 -0
- package/src/molecules/WorkListItem.jsx +83 -0
- package/src/molecules/foundry/SpecimenSectionHeader.jsx +89 -0
- package/src/organisms/ArticleHeader.jsx +95 -0
- package/src/organisms/AsciiCursor.jsx +526 -0
- package/src/organisms/BentoCard.jsx +187 -0
- package/src/organisms/Canvas.jsx +299 -0
- package/src/organisms/ColorLoader.jsx +155 -0
- package/src/organisms/CtaGlobal.jsx +67 -0
- package/src/organisms/DiagonalMarqueeRiver.jsx +138 -0
- package/src/organisms/EditorShell.jsx +111 -0
- package/src/organisms/ErrorBoundary.jsx +70 -0
- package/src/organisms/FeatureSplit.jsx +82 -0
- package/src/organisms/FeaturedCarousel.jsx +258 -0
- package/src/organisms/FeaturesCardSection.jsx +90 -0
- package/src/organisms/FullBleedHero.jsx +111 -0
- package/src/organisms/GalleryCarousel.jsx +83 -0
- package/src/organisms/LoaderOverlay.jsx +30 -0
- package/src/organisms/MediaViewer.jsx +95 -0
- package/src/organisms/NewsletterBand.jsx +122 -0
- package/src/organisms/ParallaxShelf.jsx +141 -0
- package/src/organisms/PortableTextRenderer.jsx +115 -0
- package/src/organisms/ProductDetailLayout.jsx +189 -0
- package/src/organisms/ScrollDriftGallery.jsx +214 -0
- package/src/organisms/SpectrumGrid.jsx +90 -0
- package/src/organisms/StackHero.jsx +83 -0
- package/src/organisms/WorkCard.jsx +120 -0
- package/src/organisms/WorkViewToggle.jsx +170 -0
- package/src/organisms/foundry/FontPreviewSection.jsx +187 -0
- package/src/organisms/foundry/FoundryCharacterSets.jsx +113 -0
- package/src/organisms/foundry/GlyphMetricsGrid.jsx +335 -0
- package/src/organisms/foundry/TypefaceHero.jsx +107 -0
- package/src/organisms/foundry/TypefaceStyleSection.jsx +163 -0
- package/src/organisms/foundry/VariableFontSection.jsx +158 -0
- package/src/organisms/foundry/glyphData.js +30 -0
- package/src/organisms/foundry/index.js +21 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { motion, useSpring, useTransform } from 'framer-motion'
|
|
3
|
+
import useTilt from '../hooks/useTilt.js'
|
|
4
|
+
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* True on coarse-pointer (touch) devices. Local to TiltCard; re-evaluates on
|
|
8
|
+
* device/orientation change via the media-query change event (the monorepo
|
|
9
|
+
* source froze this in a module-load const — fixed on recreate).
|
|
10
|
+
*/
|
|
11
|
+
function useCoarsePointer() {
|
|
12
|
+
const [coarse, setCoarse] = useState(
|
|
13
|
+
() => typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const mq = window.matchMedia('(pointer: coarse)')
|
|
18
|
+
const onChange = () => setCoarse(mq.matches)
|
|
19
|
+
mq.addEventListener('change', onChange)
|
|
20
|
+
return () => mq.removeEventListener('change', onChange)
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
return coarse
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* TiltCard — self-contained image card with a spring-based 3D tilt that
|
|
28
|
+
* follows the pointer (via the shared useTilt hook). On coarse-pointer
|
|
29
|
+
* devices or when the user prefers reduced motion it renders a plain,
|
|
30
|
+
* tilt-free card — no springs, no listeners.
|
|
31
|
+
*
|
|
32
|
+
* `variant="grounded"` gives a "planted at the bottom" feel: tilt targets
|
|
33
|
+
* are snapped to 3 zones, rescaled, chased by a slower lazy spring, and the
|
|
34
|
+
* card only ever tilts back — never forward — pivoting about its bottom edge
|
|
35
|
+
* (`transform-origin: center bottom`).
|
|
36
|
+
*
|
|
37
|
+
* Purely structural — no tokens, no colors. Corner radius, size, and any
|
|
38
|
+
* surface treatment come from the consumer via `className` (the image
|
|
39
|
+
* wrapper is `rounded-[inherit]`, so the root's radius clips the image);
|
|
40
|
+
* overlays (labels, gradients) render above the image via `children`.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} src image source (rendered `object-cover`)
|
|
43
|
+
* @param {string} alt image alt text
|
|
44
|
+
* @param {string} className classes on the root — supplies size + corner radius
|
|
45
|
+
* @param {string} variant 'default' (free tilt) | 'grounded' (zone-snapped, bottom-pinned)
|
|
46
|
+
* @param {number} magnitude max tilt in degrees (±)
|
|
47
|
+
* @param {number} perspective CSS transform perspective in px
|
|
48
|
+
* @param {ReactNode} children content overlaid above the image
|
|
49
|
+
*/
|
|
50
|
+
export default function TiltCard({
|
|
51
|
+
src,
|
|
52
|
+
alt = '',
|
|
53
|
+
className = '',
|
|
54
|
+
variant = 'default',
|
|
55
|
+
magnitude = 4,
|
|
56
|
+
perspective = 700,
|
|
57
|
+
children,
|
|
58
|
+
}) {
|
|
59
|
+
const coarse = useCoarsePointer()
|
|
60
|
+
const reduced = usePrefersReducedMotion()
|
|
61
|
+
|
|
62
|
+
if (coarse || reduced) {
|
|
63
|
+
return (
|
|
64
|
+
<div className={`relative ${className}`}>
|
|
65
|
+
<div className="absolute inset-0 overflow-hidden rounded-[inherit]">
|
|
66
|
+
<img src={src} alt={alt} className="w-full h-full object-cover" />
|
|
67
|
+
</div>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TiltCardInner
|
|
75
|
+
src={src}
|
|
76
|
+
alt={alt}
|
|
77
|
+
className={className}
|
|
78
|
+
variant={variant}
|
|
79
|
+
magnitude={magnitude}
|
|
80
|
+
perspective={perspective}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</TiltCardInner>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function TiltCardInner({ src, alt, className, variant, magnitude, perspective, children }) {
|
|
88
|
+
const tilt = useTilt({ magnitude, perspective })
|
|
89
|
+
const grounded = variant === 'grounded'
|
|
90
|
+
|
|
91
|
+
/* Zone-based tilt: normalize the spring output back to -1..1, snap it to
|
|
92
|
+
* 3 zones, rescale to ±2.5°, and let a slower lazy spring catch up so the
|
|
93
|
+
* card lags and settles into quantized angles. rotateX is clamped to
|
|
94
|
+
* min(0, …) — grounded cards only ever tilt back. */
|
|
95
|
+
const zones = 3
|
|
96
|
+
const snap = (v) => Math.round(v * zones) / zones
|
|
97
|
+
const lazySpring = { stiffness: 250, damping: 25, mass: 0.6 }
|
|
98
|
+
|
|
99
|
+
const targetX = useTransform(tilt.motionValues.rotateX, (v) =>
|
|
100
|
+
grounded ? Math.min(0, snap(-v / magnitude) * 2.5) : v,
|
|
101
|
+
)
|
|
102
|
+
const targetY = useTransform(tilt.motionValues.rotateY, (v) =>
|
|
103
|
+
grounded ? snap(v / magnitude) * 2.5 : v,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const lazyRotateX = useSpring(targetX, lazySpring)
|
|
107
|
+
const lazyRotateY = useSpring(targetY, lazySpring)
|
|
108
|
+
|
|
109
|
+
const style = grounded
|
|
110
|
+
? { ...tilt.style, rotateX: lazyRotateX, rotateY: lazyRotateY, transformOrigin: 'center bottom' }
|
|
111
|
+
: tilt.style
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<motion.div
|
|
115
|
+
ref={tilt.ref}
|
|
116
|
+
className={`relative ${className}`}
|
|
117
|
+
style={style}
|
|
118
|
+
onMouseMove={tilt.onMouseMove}
|
|
119
|
+
onMouseLeave={tilt.onMouseLeave}
|
|
120
|
+
>
|
|
121
|
+
<div className="absolute inset-0 overflow-hidden rounded-[inherit]">
|
|
122
|
+
<img src={src} alt={alt} className="w-full h-full object-cover" />
|
|
123
|
+
</div>
|
|
124
|
+
{children}
|
|
125
|
+
</motion.div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeSample — a single labeled type-specimen block: an optional mono caption
|
|
3
|
+
* over one paragraph whose typography is driven entirely by props via inline
|
|
4
|
+
* style. The atomic unit of the type-specimen kit — stack several to show a
|
|
5
|
+
* scale, a weight range, or a family; adjacent samples get a hairline
|
|
6
|
+
* separator (`.kol-type-sample + .kol-type-sample`).
|
|
7
|
+
*
|
|
8
|
+
* Presentational — arbitrary px values are the point, so props map to inline
|
|
9
|
+
* style rather than fixed kol-* size stops. Label and children render
|
|
10
|
+
* verbatim as authored.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} family font family name (wrapped as `"family", sans-serif`); defaults to the KOL sans token
|
|
13
|
+
* @param {number} weight font-weight
|
|
14
|
+
* @param {boolean} italic italic sample
|
|
15
|
+
* @param {number} size font-size in px
|
|
16
|
+
* @param {number} lineHeight line-height in px; unitless 1.2 when unset
|
|
17
|
+
* @param {string} label mono caption above the sample (omit to hide)
|
|
18
|
+
* @param {ReactNode} children the specimen text itself
|
|
19
|
+
*/
|
|
20
|
+
export default function TypeSample({
|
|
21
|
+
family,
|
|
22
|
+
weight = 400,
|
|
23
|
+
italic = false,
|
|
24
|
+
size = 32,
|
|
25
|
+
lineHeight,
|
|
26
|
+
label,
|
|
27
|
+
children,
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="kol-type-sample py-6">
|
|
31
|
+
{label && (
|
|
32
|
+
<p className="kol-helper-12 tracking-wider text-meta m-0 mb-3">
|
|
33
|
+
{label}
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
<p
|
|
37
|
+
className="kol-type-sample-body m-0 text-auto"
|
|
38
|
+
style={{
|
|
39
|
+
fontFamily: family ? `"${family}", sans-serif` : 'var(--kol-font-family-sans)',
|
|
40
|
+
fontWeight: weight,
|
|
41
|
+
fontStyle: italic ? 'italic' : 'normal',
|
|
42
|
+
fontSize: `${size}px`,
|
|
43
|
+
lineHeight: lineHeight ? `${lineHeight}px` : '1.2',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeSpecCard — a two-column type-spec row: a left meta panel of key/value
|
|
3
|
+
* pairs (font metrics) beside a live sample slot on the right, with an
|
|
4
|
+
* optional corner label. The "data-sheet" member of the type-specimen kit —
|
|
5
|
+
* pairs the numeric spec of a type style with a rendered example of it
|
|
6
|
+
* (often a TypeSample passed as children, composed at the call site).
|
|
7
|
+
*
|
|
8
|
+
* Single column on mobile; `240px + fluid` two-column at lg. `meta` is
|
|
9
|
+
* arbitrary tuples — no fixed metric schema. Sample paragraphs get 16px
|
|
10
|
+
* rhythm via `.kol-type-spec-sample p` (Preflight zeroes <p> margins).
|
|
11
|
+
*
|
|
12
|
+
* @param {string} label corner caption, absolute top-left (omit to hide)
|
|
13
|
+
* @param {Array<[string, ReactNode]>} meta key/value rows in the left panel
|
|
14
|
+
* @param {ReactNode} children the live type sample on the right
|
|
15
|
+
*/
|
|
16
|
+
export default function TypeSpecCard({ label, meta = [], children }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="kol-type-spec relative py-12 border-t border-fg-08">
|
|
19
|
+
{label && (
|
|
20
|
+
<span className="kol-type-spec-label kol-helper-12 tracking-widest text-meta absolute top-4 left-0">
|
|
21
|
+
{label}
|
|
22
|
+
</span>
|
|
23
|
+
)}
|
|
24
|
+
<div className="kol-type-spec-row grid grid-cols-1 gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-12 items-start pt-6">
|
|
25
|
+
<div className="kol-type-spec-meta flex flex-col">
|
|
26
|
+
{meta.map(([key, value]) => (
|
|
27
|
+
<div
|
|
28
|
+
key={key}
|
|
29
|
+
className="kol-type-spec-meta-row grid grid-cols-[auto_minmax(0,1fr)] gap-4 items-baseline py-2 border-b border-[var(--kol-fg-04)] last:border-b-0"
|
|
30
|
+
>
|
|
31
|
+
<span className="kol-helper-10 text-meta">{key}</span>
|
|
32
|
+
<span className="kol-helper-10 text-strong text-right [overflow-wrap:anywhere]">{value}</span>
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
<div className="kol-type-spec-sample min-w-0">
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS-variable + color utilities for token-doc widgets (ColorRamp,
|
|
3
|
+
* SpectrumGrid, swatch tables). One probe-based resolver instead of the
|
|
4
|
+
* three inline getComputedStyle forks the monorepo sources carried.
|
|
5
|
+
* Plain functions, not hooks — they live here because src/hooks is the
|
|
6
|
+
* taxonomy's only non-component folder.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Read a custom property's raw value off an element (default: :root). */
|
|
10
|
+
export function resolveCssVar(name, el) {
|
|
11
|
+
if (typeof document === 'undefined') return ''
|
|
12
|
+
const target = el || document.documentElement
|
|
13
|
+
const prop = name.startsWith('--') ? name : `--${name}`
|
|
14
|
+
return getComputedStyle(target).getPropertyValue(prop).trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve ANY css color expression (var(), color-mix(), keywords) to the
|
|
19
|
+
* browser's computed `rgb(...)`/`rgba(...)` form via a detached probe —
|
|
20
|
+
* getPropertyValue alone can't evaluate color-mix.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveCssColor(value, el) {
|
|
23
|
+
if (typeof document === 'undefined') return ''
|
|
24
|
+
const probe = document.createElement('span')
|
|
25
|
+
probe.style.display = 'none'
|
|
26
|
+
probe.style.color = value
|
|
27
|
+
;(el || document.body).appendChild(probe)
|
|
28
|
+
const out = getComputedStyle(probe).color
|
|
29
|
+
probe.remove()
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Perceptual is-this-color-light test (BT.709 luma) — drives contrast
|
|
35
|
+
* label flips on swatches. Accepts hex or rgb()/rgba() strings; any other
|
|
36
|
+
* expression goes through resolveCssColor first.
|
|
37
|
+
*/
|
|
38
|
+
export function isLight(color) {
|
|
39
|
+
let r, g, b
|
|
40
|
+
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
|
|
41
|
+
if (hex) {
|
|
42
|
+
let h = hex[1]
|
|
43
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('')
|
|
44
|
+
r = parseInt(h.slice(0, 2), 16)
|
|
45
|
+
g = parseInt(h.slice(2, 4), 16)
|
|
46
|
+
b = parseInt(h.slice(4, 6), 16)
|
|
47
|
+
} else {
|
|
48
|
+
const rgb = (color.startsWith('rgb') ? color : resolveCssColor(color)).match(/[\d.]+/g)
|
|
49
|
+
if (!rgb) return false
|
|
50
|
+
;[r, g, b] = rgb.map(Number)
|
|
51
|
+
}
|
|
52
|
+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.55
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import usePrefersReducedMotion from './usePrefersReducedMotion.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useAxisAnimation — ping-pong rAF over a variable-font axis range.
|
|
6
|
+
*
|
|
7
|
+
* Owns a single numeric value that auto-oscillates between `min` and `max`
|
|
8
|
+
* (bouncing at each end) while `running` is true, then hands the value + a
|
|
9
|
+
* setter back to the caller so a slider can scrub it (scrub = call `setValue`,
|
|
10
|
+
* flip `running` off). This is the reusable core lifted out of the monorepo's
|
|
11
|
+
* VariableFontSection, whose auto-oscillator was inline and unguarded.
|
|
12
|
+
*
|
|
13
|
+
* Timing uses the `performance.now()` interval-accumulator pattern (the same
|
|
14
|
+
* shape as fontviewer's GlyphAnimator) rather than a `Date.now()` / `delta>16`
|
|
15
|
+
* throttle — the accumulator carries the sub-interval remainder so the step is
|
|
16
|
+
* frame-rate independent and doesn't jitter.
|
|
17
|
+
*
|
|
18
|
+
* Reduced-motion gate: when the user asks for reduced motion the loop never
|
|
19
|
+
* mounts and the hook returns its static default (`initial`, else the range
|
|
20
|
+
* midpoint) — the widget still renders and stays scrubbable, it just doesn't
|
|
21
|
+
* auto-play. `reduced` is returned so the caller can reflect it in its
|
|
22
|
+
* play/pause affordance.
|
|
23
|
+
*
|
|
24
|
+
* The value is axis-agnostic: the render boundary decides whether it feeds
|
|
25
|
+
* `fontWeight` (wght) or `font-variation-settings` (wdth/slnt/…).
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} opts
|
|
28
|
+
* @param {number} opts.min - Lower bound of the oscillation + slider min (default 300).
|
|
29
|
+
* @param {number} opts.max - Upper bound of the oscillation + slider max (default 900).
|
|
30
|
+
* @param {number} opts.step - Units advanced per frame (default 2).
|
|
31
|
+
* @param {number} opts.fps - Target frame cadence for the accumulator (default 60).
|
|
32
|
+
* @param {boolean} opts.running - Whether the loop is active (default true). Gated by reduced-motion.
|
|
33
|
+
* @param {number} opts.initial - Starting / reduced-motion static value (default range midpoint).
|
|
34
|
+
* @returns {{ value: number, setValue: (v:number)=>void, reduced: boolean }}
|
|
35
|
+
*/
|
|
36
|
+
export default function useAxisAnimation({
|
|
37
|
+
min = 300,
|
|
38
|
+
max = 900,
|
|
39
|
+
step = 2,
|
|
40
|
+
fps = 60,
|
|
41
|
+
running = true,
|
|
42
|
+
initial,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const reduced = usePrefersReducedMotion()
|
|
45
|
+
const staticDefault = initial ?? Math.round((min + max) / 2)
|
|
46
|
+
|
|
47
|
+
const [value, setValueState] = useState(staticDefault)
|
|
48
|
+
const valueRef = useRef(staticDefault)
|
|
49
|
+
const dirRef = useRef(1)
|
|
50
|
+
const rafRef = useRef(null)
|
|
51
|
+
|
|
52
|
+
// External override (slider scrub). Keeps the ref in sync so a subsequent
|
|
53
|
+
// resume ping-pongs from where the user left it, not from a stale value.
|
|
54
|
+
const setValue = useCallback((v) => {
|
|
55
|
+
valueRef.current = v
|
|
56
|
+
setValueState(v)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (reduced || !running) return
|
|
61
|
+
|
|
62
|
+
const interval = 1000 / fps
|
|
63
|
+
let last = performance.now()
|
|
64
|
+
|
|
65
|
+
const animate = (now) => {
|
|
66
|
+
const elapsed = now - last
|
|
67
|
+
if (elapsed >= interval) {
|
|
68
|
+
let next = valueRef.current + dirRef.current * step
|
|
69
|
+
if (next >= max) {
|
|
70
|
+
next = max
|
|
71
|
+
dirRef.current = -1
|
|
72
|
+
} else if (next <= min) {
|
|
73
|
+
next = min
|
|
74
|
+
dirRef.current = 1
|
|
75
|
+
}
|
|
76
|
+
valueRef.current = next
|
|
77
|
+
setValueState(next)
|
|
78
|
+
// Carry the sub-interval remainder so the cadence stays even.
|
|
79
|
+
last = now - (elapsed % interval)
|
|
80
|
+
}
|
|
81
|
+
rafRef.current = requestAnimationFrame(animate)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rafRef.current = requestAnimationFrame(animate)
|
|
85
|
+
return () => {
|
|
86
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
|
87
|
+
}
|
|
88
|
+
}, [reduced, running, min, max, step, fps])
|
|
89
|
+
|
|
90
|
+
return { value, setValue, reduced }
|
|
91
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when the user asks for reduced motion. The DS-wide motion gate:
|
|
5
|
+
* every animated/effect component checks this and renders its static
|
|
6
|
+
* form when true (the monorepo sources never did — added on recreate).
|
|
7
|
+
*/
|
|
8
|
+
export default function usePrefersReducedMotion() {
|
|
9
|
+
const [reduced, setReduced] = useState(
|
|
10
|
+
() => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
15
|
+
const onChange = () => setReduced(mq.matches)
|
|
16
|
+
mq.addEventListener('change', onChange)
|
|
17
|
+
return () => mq.removeEventListener('change', onChange)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return reduced
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { useMotionValue, useSpring, useTransform } from 'framer-motion'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pointer-driven 3D tilt (framer-motion springs) — the ONE tilt hook.
|
|
6
|
+
* Ported from the monorepo's useBentoTiltMotion; TiltCard, BentoCard and
|
|
7
|
+
* friends all compose this instead of forking their own.
|
|
8
|
+
*
|
|
9
|
+
* Returns `{ ref, style, onMouseMove, onMouseLeave, motionValues }` —
|
|
10
|
+
* spread `ref`/handlers on a `motion.div` and pass `style` to it.
|
|
11
|
+
* `motionValues` exposes the raw springs for derived variants
|
|
12
|
+
* (e.g. TiltCard's `grounded` zone-snapping).
|
|
13
|
+
*
|
|
14
|
+
* Defaults are the design: tilt ±4°, spring 350/35, perspective 700,
|
|
15
|
+
* rest position center (0.5/0.5).
|
|
16
|
+
*/
|
|
17
|
+
export default function useTilt({
|
|
18
|
+
magnitude = 4,
|
|
19
|
+
perspective = 700,
|
|
20
|
+
stiffness = 350,
|
|
21
|
+
damping = 35,
|
|
22
|
+
} = {}) {
|
|
23
|
+
const ref = useRef(null)
|
|
24
|
+
const mouseX = useMotionValue(0.5)
|
|
25
|
+
const mouseY = useMotionValue(0.5)
|
|
26
|
+
|
|
27
|
+
const rotateX = useSpring(useTransform(mouseY, [0, 1], [magnitude, -magnitude]), { stiffness, damping })
|
|
28
|
+
const rotateY = useSpring(useTransform(mouseX, [0, 1], [-magnitude, magnitude]), { stiffness, damping })
|
|
29
|
+
|
|
30
|
+
const onMouseMove = (e) => {
|
|
31
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
32
|
+
if (!rect) return
|
|
33
|
+
mouseX.set((e.clientX - rect.left) / rect.width)
|
|
34
|
+
mouseY.set((e.clientY - rect.top) / rect.height)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const onMouseLeave = () => {
|
|
38
|
+
mouseX.set(0.5)
|
|
39
|
+
mouseY.set(0.5)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ref,
|
|
44
|
+
style: { rotateX, rotateY, transformStyle: 'preserve-3d', transformPerspective: perspective },
|
|
45
|
+
onMouseMove,
|
|
46
|
+
onMouseLeave,
|
|
47
|
+
motionValues: { mouseX, mouseY, rotateX, rotateY },
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* lives in @kol/theme (kol-components-*.css).
|
|
7
7
|
*
|
|
8
8
|
* Placement follows the taxonomy rules in
|
|
9
|
-
* docs/
|
|
9
|
+
* docs/documentation/02-components/02-placement.md:
|
|
10
10
|
* atom — nests no KOL component (kol-loader Icon/Graphic are
|
|
11
11
|
* infrastructure, they don't count)
|
|
12
12
|
* molecule — nests at least one KOL component
|
|
@@ -16,22 +16,34 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
// atoms
|
|
19
|
+
export { default as AnimatedTitle } from './atoms/AnimatedTitle.jsx'
|
|
20
|
+
export { default as AssetGrid } from './atoms/AssetGrid.jsx'
|
|
19
21
|
export { default as AssetPlaceholder } from './atoms/AssetPlaceholder.jsx'
|
|
20
22
|
export { default as Avatar } from './atoms/Avatar.jsx'
|
|
21
23
|
export { default as Badge } from './atoms/Badge.jsx'
|
|
22
24
|
export { default as Button } from './atoms/Button.jsx'
|
|
23
25
|
export { default as CopyButton } from './atoms/CopyButton.jsx'
|
|
26
|
+
export { default as CurveOverlay } from './atoms/CurveOverlay.jsx'
|
|
24
27
|
export { default as Divider } from './atoms/Divider.jsx'
|
|
28
|
+
export { default as DocsToc } from './atoms/DocsToc.jsx'
|
|
25
29
|
export { default as DropdownTagFilter } from './atoms/DropdownTagFilter.jsx'
|
|
30
|
+
export { default as EmptyState } from './atoms/EmptyState.jsx'
|
|
26
31
|
export { default as ExitPreview } from './atoms/ExitPreview.jsx'
|
|
32
|
+
export { default as Figure } from './atoms/Figure.jsx'
|
|
27
33
|
export { default as FullscreenOverlay } from './atoms/FullscreenOverlay.jsx'
|
|
34
|
+
export { default as HlsVideo } from './atoms/HlsVideo.jsx'
|
|
28
35
|
export { default as Input } from './atoms/Input.jsx'
|
|
29
36
|
export { default as Label } from './atoms/Label.jsx'
|
|
30
37
|
export { default as LabeledControl } from './atoms/LabeledControl.jsx'
|
|
38
|
+
export { default as OverlayGlassPanel } from './atoms/OverlayGlassPanel.jsx'
|
|
31
39
|
export { default as Pill } from './atoms/Pill.jsx'
|
|
32
40
|
export { usePopover, PopoverPanel, Tooltip } from './atoms/Popover.jsx'
|
|
41
|
+
export { default as PriceDisplay } from './atoms/PriceDisplay.jsx'
|
|
42
|
+
export { default as ProsePreview } from './atoms/ProsePreview.jsx'
|
|
33
43
|
export { default as QuantityInput } from './atoms/QuantityInput.jsx'
|
|
34
44
|
export { default as QuantityStepper } from './atoms/QuantityStepper.jsx'
|
|
45
|
+
export { default as RotaryDial } from './atoms/RotaryDial.jsx'
|
|
46
|
+
export { default as SearchInput } from './atoms/SearchInput.jsx'
|
|
35
47
|
export { default as Section } from './atoms/Section.jsx'
|
|
36
48
|
export { default as SectionLabel } from './atoms/SectionLabel.jsx'
|
|
37
49
|
export { default as SegmentedToggle } from './atoms/SegmentedToggle.jsx'
|
|
@@ -41,14 +53,29 @@ export { default as Textarea } from './atoms/Textarea.jsx'
|
|
|
41
53
|
export { default as ToggleBracket } from './atoms/ToggleBracket.jsx'
|
|
42
54
|
export { default as ToggleCheckbox } from './atoms/ToggleCheckbox.jsx'
|
|
43
55
|
export { default as ToggleSwitch } from './atoms/ToggleSwitch.jsx'
|
|
56
|
+
export { default as TextPressure } from './atoms/TextPressure.jsx'
|
|
57
|
+
export { default as TiltCard } from './atoms/TiltCard.jsx'
|
|
44
58
|
export { default as TransparentX } from './atoms/TransparentX.jsx'
|
|
59
|
+
export { default as TypeSample } from './atoms/TypeSample.jsx'
|
|
60
|
+
export { default as TypeSpecCard } from './atoms/TypeSpecCard.jsx'
|
|
45
61
|
export { default as ViewToggle } from './atoms/ViewToggle.jsx'
|
|
46
62
|
|
|
47
63
|
// molecules
|
|
48
64
|
export { Accordion, AccordionPanel } from './molecules/Accordion.jsx'
|
|
65
|
+
/* monorepo sets (P6–P10) — molecule members */
|
|
66
|
+
export { default as AlignmentGrid } from './molecules/AlignmentGrid.jsx'
|
|
67
|
+
export { default as ArticleCard } from './molecules/ArticleCard.jsx'
|
|
68
|
+
export { default as ImageBlock } from './molecules/ImageBlock.jsx'
|
|
69
|
+
export { default as SelectionOverlay } from './molecules/SelectionOverlay.jsx'
|
|
70
|
+
export { default as VideoBlock, getEmbedUrl } from './molecules/VideoBlock.jsx'
|
|
71
|
+
export { default as WorkListItem } from './molecules/WorkListItem.jsx'
|
|
72
|
+
export { default as CardFeatureItem } from './molecules/CardFeatureItem.jsx'
|
|
49
73
|
export { default as CodeBlock } from './molecules/CodeBlock.jsx'
|
|
74
|
+
export { default as ColorInputRow } from './molecules/ColorInputRow.jsx'
|
|
75
|
+
export { default as ColorRamp } from './molecules/ColorRamp.jsx'
|
|
50
76
|
export { default as ColorSwatch } from './molecules/ColorSwatch.jsx'
|
|
51
77
|
export { default as Dropdown } from './molecules/Dropdown.jsx'
|
|
78
|
+
export { default as FramedMediaBand } from './molecules/FramedMediaBand.jsx'
|
|
52
79
|
export { default as Image } from './molecules/Image.jsx'
|
|
53
80
|
export { default as MediaCard } from './molecules/MediaCard.jsx'
|
|
54
81
|
export { default as MediaRow } from './molecules/MediaRow.jsx'
|
|
@@ -56,11 +83,45 @@ export { MenuItem, MenuDropdownItem, MenuDropdownDivider, MenuDropdownNest } fro
|
|
|
56
83
|
export { MenuPopover } from './molecules/MenuPopover.jsx'
|
|
57
84
|
export { ModalProvider, useModal } from './molecules/Modal.jsx'
|
|
58
85
|
export { default as PropertyInput } from './molecules/PropertyInput.jsx'
|
|
86
|
+
export { default as ShapeDropdown } from './molecules/ShapeDropdown.jsx'
|
|
87
|
+
export { default as ShellDrawer } from './molecules/ShellDrawer.jsx'
|
|
88
|
+
export { default as ShellSearchOverlay } from './molecules/ShellSearchOverlay.jsx'
|
|
59
89
|
export { default as Slider } from './molecules/Slider.jsx'
|
|
90
|
+
export { default as SpecList } from './molecules/SpecList.jsx'
|
|
91
|
+
export { default as SpectrumControls, HueStrip, SBSquare, WheelTriangle } from './molecules/SpectrumControls.jsx'
|
|
92
|
+
export { default as SwatchControls, SwatchStack, EyedropPick } from './molecules/SwatchControls.jsx'
|
|
93
|
+
export { default as TabsRow } from './molecules/TabsRow.jsx'
|
|
60
94
|
|
|
61
95
|
// organisms
|
|
96
|
+
/* monorepo sets (P6–P10) — organism members. Foundry members are isolated
|
|
97
|
+
under the './foundry' subpath (opentype-heavy) — not re-exported here. */
|
|
98
|
+
export { default as ArticleHeader } from './organisms/ArticleHeader.jsx'
|
|
99
|
+
export { default as Canvas, CanvasFrame, PanViewport, CANVAS_VIRTUAL_W, DEFAULT_ASPECTS, CANVAS_DEFAULTS } from './organisms/Canvas.jsx'
|
|
100
|
+
export { default as DiagonalMarqueeRiver } from './organisms/DiagonalMarqueeRiver.jsx'
|
|
101
|
+
export { default as EditorShell } from './organisms/EditorShell.jsx'
|
|
102
|
+
export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
|
|
103
|
+
export { default as ParallaxShelf } from './organisms/ParallaxShelf.jsx'
|
|
104
|
+
export { default as PortableTextRenderer, slugify } from './organisms/PortableTextRenderer.jsx'
|
|
105
|
+
export { default as ProductDetailLayout } from './organisms/ProductDetailLayout.jsx'
|
|
106
|
+
export { default as ScrollDriftGallery } from './organisms/ScrollDriftGallery.jsx'
|
|
107
|
+
export { default as StackHero } from './organisms/StackHero.jsx'
|
|
108
|
+
export { default as WorkCard } from './organisms/WorkCard.jsx'
|
|
109
|
+
export { default as WorkViewToggle } from './organisms/WorkViewToggle.jsx'
|
|
110
|
+
export { default as AsciiCursor } from './organisms/AsciiCursor.jsx'
|
|
111
|
+
export { default as BentoCard } from './organisms/BentoCard.jsx'
|
|
62
112
|
export { default as Carousel } from './organisms/Carousel.jsx'
|
|
113
|
+
export { default as ColorLoader } from './organisms/ColorLoader.jsx'
|
|
63
114
|
export { default as ContentFilters } from './organisms/ContentFilters.jsx'
|
|
115
|
+
export { default as CtaGlobal } from './organisms/CtaGlobal.jsx'
|
|
116
|
+
export { default as ErrorBoundary } from './organisms/ErrorBoundary.jsx'
|
|
117
|
+
export { default as FeatureSplit } from './organisms/FeatureSplit.jsx'
|
|
118
|
+
export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
|
|
119
|
+
export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
|
|
120
|
+
export { default as FullBleedHero } from './organisms/FullBleedHero.jsx'
|
|
121
|
+
export { default as LoaderOverlay } from './organisms/LoaderOverlay.jsx'
|
|
122
|
+
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
123
|
+
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
124
|
+
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
64
125
|
export { default as Table } from './organisms/Table.jsx'
|
|
65
126
|
|
|
66
127
|
// loaders (re-export — infrastructure, documented on /docs/loaders)
|
|
@@ -70,5 +131,9 @@ export { Icon } from '@kolkrabbi/kol-loader'
|
|
|
70
131
|
export { default as Graphic, GRAPHICS } from './graphics/Graphic.jsx'
|
|
71
132
|
|
|
72
133
|
// hooks
|
|
134
|
+
export { default as usePrefersReducedMotion } from './hooks/usePrefersReducedMotion.js'
|
|
73
135
|
export { default as useReveal } from './hooks/useReveal.js'
|
|
74
136
|
export { default as useScrollSpy } from './hooks/useScrollSpy.js'
|
|
137
|
+
export { default as useTilt } from './hooks/useTilt.js'
|
|
138
|
+
export { default as useAxisAnimation } from './hooks/useAxisAnimation.js'
|
|
139
|
+
export { resolveCssVar, resolveCssColor, isLight } from './hooks/cssVar.js'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Button from '../atoms/Button.jsx'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AlignmentGrid — a six-cell alignment row: horizontal start/center/end then
|
|
5
|
+
* vertical start/center/end, each a quiet icon Button that emits an
|
|
6
|
+
* `(axis, mode)` alignment intent. Driven off a static config array (the
|
|
7
|
+
* default 6, overridable via `items`). Presentation only — the consumer wires
|
|
8
|
+
* `onAlign` to whatever "align these" means in its context (align a box to the
|
|
9
|
+
* canvas bounds, align a multi-selection's common bbox, …).
|
|
10
|
+
*
|
|
11
|
+
* Ported from the brand editor's AlignmentPanel with the store coupling
|
|
12
|
+
* dropped (per lobby spec): `useComposeState().alignSelected` → an `onAlign`
|
|
13
|
+
* prop; the hand-rolled `kol-btn-quiet` buttons → DS `Button` (quiet +
|
|
14
|
+
* iconOnly) so the DS owns the button atom; `EditorIcon` → the DS Icon
|
|
15
|
+
* (through Button).
|
|
16
|
+
*
|
|
17
|
+
* [icon-gap] The source's `align-h-{start,center,end}` / `align-v-{start,
|
|
18
|
+
* center,end}` glyph names are NOT in the loader. Remapped to the closest
|
|
19
|
+
* existing loader glyphs (stroke/layout): `align-horizontal-{left,center,
|
|
20
|
+
* right}` and `align-vertical-{top,center,bottom}`. Same six align marks,
|
|
21
|
+
* different names — no visual gap.
|
|
22
|
+
*
|
|
23
|
+
* @param {(axis:'h'|'v', mode:'start'|'center'|'end') => void} onAlign fired on cell click
|
|
24
|
+
* @param {Array} items [{ axis, mode, icon, title }] cells (default the standard 6)
|
|
25
|
+
*/
|
|
26
|
+
const ALIGN_BUTTONS = [
|
|
27
|
+
{ axis: 'h', mode: 'start', icon: 'align-horizontal-left', title: 'Align left' },
|
|
28
|
+
{ axis: 'h', mode: 'center', icon: 'align-horizontal-center', title: 'Align horizontal center' },
|
|
29
|
+
{ axis: 'h', mode: 'end', icon: 'align-horizontal-right', title: 'Align right' },
|
|
30
|
+
{ axis: 'v', mode: 'start', icon: 'align-vertical-top', title: 'Align top' },
|
|
31
|
+
{ axis: 'v', mode: 'center', icon: 'align-vertical-center', title: 'Align vertical center' },
|
|
32
|
+
{ axis: 'v', mode: 'end', icon: 'align-vertical-bottom', title: 'Align bottom' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export default function AlignmentGrid({ onAlign, items = ALIGN_BUTTONS }) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="grid grid-cols-6 gap-1">
|
|
38
|
+
{items.map((b) => (
|
|
39
|
+
<Button
|
|
40
|
+
key={`${b.axis}-${b.mode}`}
|
|
41
|
+
quiet
|
|
42
|
+
iconOnly={b.icon}
|
|
43
|
+
iconSize={16}
|
|
44
|
+
onClick={() => onAlign?.(b.axis, b.mode)}
|
|
45
|
+
title={b.title}
|
|
46
|
+
aria-label={b.title}
|
|
47
|
+
className="w-full"
|
|
48
|
+
style={{ height: 28, padding: 6 }}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|