@revyme/runtime 0.0.6 → 0.0.8
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/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/withResponsiveProps.tsx +28 -8
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { forwardRef, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { AnimatePresence, isMotionValue, motion, useMotionValue, useSpring } from "framer-motion";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { getStroke } from "perfect-freehand";
|
|
@@ -85,7 +85,7 @@ var PLACEMENT_KEYS = new Set([
|
|
|
85
85
|
"gridArea"
|
|
86
86
|
]);
|
|
87
87
|
function withResponsiveProps(Component) {
|
|
88
|
-
return function ResponsiveSpark(props) {
|
|
88
|
+
return forwardRef(function ResponsiveSpark(props, fwdRef) {
|
|
89
89
|
const canvasVpWidth = props.__canvasViewportWidth;
|
|
90
90
|
const [windowWidth, setWindowWidth] = useState(typeof window !== "undefined" ? window.innerWidth : 1440);
|
|
91
91
|
useEffect(() => {
|
|
@@ -122,22 +122,25 @@ function withResponsiveProps(Component) {
|
|
|
122
122
|
delete mergedProps["data-responsive"];
|
|
123
123
|
delete mergedProps["__canvasViewportWidth"];
|
|
124
124
|
const style = mergedProps.style;
|
|
125
|
-
let needsWrapper =
|
|
126
|
-
if (style) {
|
|
125
|
+
let needsWrapper = fwdRef != null;
|
|
126
|
+
if (!needsWrapper && style) {
|
|
127
127
|
for (const k of Object.keys(style)) if (MOTION_ONLY_KEYS.has(k) || isMotionValue(style[k])) {
|
|
128
128
|
needsWrapper = true;
|
|
129
129
|
break;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
if (needsWrapper
|
|
132
|
+
if (needsWrapper) {
|
|
133
133
|
const wrapperStyle = {};
|
|
134
134
|
const innerStyle = {};
|
|
135
|
-
for (const [k, v] of Object.entries(style)) if (MOTION_ONLY_KEYS.has(k) || PLACEMENT_KEYS.has(k) || isMotionValue(v)) wrapperStyle[k] = v;
|
|
135
|
+
for (const [k, v] of Object.entries(style ?? {})) if (MOTION_ONLY_KEYS.has(k) || PLACEMENT_KEYS.has(k) || isMotionValue(v)) wrapperStyle[k] = v;
|
|
136
136
|
else innerStyle[k] = v;
|
|
137
137
|
if ("width" in wrapperStyle) innerStyle.width = "100%";
|
|
138
138
|
if ("height" in wrapperStyle) innerStyle.height = "100%";
|
|
139
|
+
innerStyle.position = "relative";
|
|
139
140
|
const { style: _split, ...rest } = mergedProps;
|
|
141
|
+
delete rest.ref;
|
|
140
142
|
return /* @__PURE__ */ jsx(motion.div, {
|
|
143
|
+
ref: fwdRef,
|
|
141
144
|
style: wrapperStyle,
|
|
142
145
|
children: /* @__PURE__ */ jsx(Component, {
|
|
143
146
|
...rest,
|
|
@@ -146,7 +149,7 @@ function withResponsiveProps(Component) {
|
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
151
|
return /* @__PURE__ */ jsx(Component, { ...mergedProps });
|
|
149
|
-
};
|
|
152
|
+
});
|
|
150
153
|
}
|
|
151
154
|
//#endregion
|
|
152
155
|
//#region src/cursor-runtime.tsx
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/withResponsiveProps.tsx","../src/cursor-runtime.tsx","../src/useStaticCanvas.ts","../src/sketch-draw.ts"],"sourcesContent":["'use client';\n\nimport { useState, useEffect, type ComponentType } from 'react';\nimport { motion, isMotionValue } from 'framer-motion';\n\n/**\n * HOC that reads a `data-responsive` JSON prop and merges per-viewport overrides.\n * Usage: `export default withResponsiveProps(MyComponent)`\n *\n * On the canvas, CodeComponentHost injects `__canvasViewportWidth` to simulate viewport size.\n * In production, uses `window.innerWidth`.\n *\n * data-responsive='{\"768\":{\"fontSize\":32},\"375\":{\"fontSize\":24}}'\n * Breakpoints are max-width: if viewport <= 768, the 768 overrides apply.\n *\n * ANIMATED-STYLE SOCKET (Framer parity): the editor expresses instance\n * animation effects (Appear, scroll scrubs, …) as framer MotionValues bound\n * into the instance's `style` prop — `style={{ opacity: <mv>, y: <mv> }}`.\n * Design components consume them natively (their roots are motion.* and\n * spread `...style`), but a CODE component's root is a plain element: a\n * MotionValue arrives as an un-serialisable object (the style is dropped)\n * and motion-only keys like `y` aren't CSS at all — the effect silently\n * dies at the component boundary (live find 2026-07-14: Appear on a\n * Marquee). When animated values or motion-only keys are present, the HOC\n * renders a motion.div WRAPPER that carries them (plus the placement props\n * that describe the instance's slot in its parent), and hands the wrapped\n * component a clean static style. Framer does exactly this — the platform\n * owns an animatable container around every code component, so component\n * authors never deal with animation plumbing. No animated values → no\n * wrapper → byte-identical behaviour to before.\n */\n\n/** Style keys only a motion.* element understands (translated into\n * `transform`) — a plain DOM element ignores them entirely. */\nconst MOTION_ONLY_KEYS = new Set([\n 'x', 'y', 'z',\n 'rotate', 'rotateX', 'rotateY', 'rotateZ',\n 'scale', 'scaleX', 'scaleY',\n 'skew', 'skewX', 'skewY',\n 'originX', 'originY', 'originZ',\n 'transformPerspective',\n]);\n\n/** Placement props describing the instance's slot in ITS PARENT's layout —\n * these must ride on whichever element is outermost (the wrapper, when one\n * exists), mirroring the design-instance wrapper/root split. */\nconst PLACEMENT_KEYS = new Set([\n 'position', 'left', 'top', 'right', 'bottom', 'inset',\n 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',\n 'flex', 'flexGrow', 'flexShrink', 'flexBasis',\n 'order', 'alignSelf', 'justifySelf', 'zIndex',\n 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',\n 'gridColumn', 'gridRow', 'gridArea',\n]);\nexport default function withResponsiveProps<P extends Record<string, any>>(\n Component: ComponentType<P>\n): ComponentType<P & { 'data-responsive'?: string; __canvasViewportWidth?: number }> {\n return function ResponsiveSpark(props: any) {\n const canvasVpWidth = props.__canvasViewportWidth as number | undefined;\n const [windowWidth, setWindowWidth] = useState(\n typeof window !== 'undefined' ? window.innerWidth : 1440\n );\n\n useEffect(() => {\n if (canvasVpWidth !== undefined) return;\n const handler = () => setWindowWidth(window.innerWidth);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, [canvasVpWidth]);\n\n const vpWidth = canvasVpWidth ?? windowWidth;\n const responsiveStr = props['data-responsive'];\n let mergedProps = { ...props };\n\n if (responsiveStr) {\n try {\n const overrides = typeof responsiveStr === 'string'\n ? JSON.parse(responsiveStr) : responsiveStr;\n // _bp contains all viewport breakpoint widths for range computation.\n // Each breakpoint's range is (prev_bp, bp]. Prevents cascade.\n const allBp = Array.isArray(overrides._bp)\n ? overrides._bp : Object.keys(overrides).filter(k => k !== '_bp').map(Number);\n const sortedBp = [...allBp].sort((a, b) => a - b);\n let matchedBp;\n for (let i = 0; i < sortedBp.length; i++) {\n const lower = i > 0 ? sortedBp[i - 1] : 0;\n if (vpWidth > lower && vpWidth <= sortedBp[i]) {\n matchedBp = sortedBp[i];\n break;\n }\n }\n if (matchedBp !== undefined && overrides[matchedBp]) {\n const ov = overrides[matchedBp];\n // A Scroll Variant binds `initialVariant={…Sv}` and OWNS it at runtime (it morphs the\n // variant on scroll). The per-viewport `data-responsive` entry must NOT override that\n // here — otherwise it freezes the variant on replicas and the morph never plays. The\n // per-viewport variant CHOICE still drives the canvas + seeds the Sv's resting; only\n // the runtime merge skips `initialVariant`. (No scroll variant → unchanged behaviour.)\n if (props['data-scroll-variant'] && ov && typeof ov === 'object' && 'initialVariant' in ov) {\n const { initialVariant: _skip, ...rest } = ov;\n mergedProps = { ...mergedProps, ...rest };\n } else {\n mergedProps = { ...mergedProps, ...ov };\n }\n }\n } catch {}\n }\n\n delete mergedProps['data-responsive'];\n delete mergedProps['__canvasViewportWidth'];\n\n // ── Animated-style socket (see header comment) ──\n const style = mergedProps.style as Record<string, any> | undefined;\n let needsWrapper = false;\n if (style) {\n for (const k of Object.keys(style)) {\n if (MOTION_ONLY_KEYS.has(k) || isMotionValue(style[k])) { needsWrapper = true; break; }\n }\n }\n if (needsWrapper && style) {\n const wrapperStyle: Record<string, any> = {};\n const innerStyle: Record<string, any> = {};\n for (const [k, v] of Object.entries(style)) {\n if (MOTION_ONLY_KEYS.has(k) || PLACEMENT_KEYS.has(k) || isMotionValue(v)) wrapperStyle[k] = v;\n else innerStyle[k] = v;\n }\n // The inner component fills the wrapper — but only along axes the\n // wrapper actually sized (code-component instances always carry\n // definite dims; the guard keeps an unsized axis hugging content).\n if ('width' in wrapperStyle) innerStyle.width = '100%';\n if ('height' in wrapperStyle) innerStyle.height = '100%';\n const { style: _split, ...rest } = mergedProps;\n return (\n <motion.div style={wrapperStyle}>\n <Component {...rest} style={innerStyle} />\n </motion.div>\n );\n }\n\n return <Component {...mergedProps} />;\n };\n}\n","'use client';\n\nimport { useEffect, useRef, useSyncExternalStore, type ComponentType } from 'react';\nimport { motion, AnimatePresence, useMotionValue, useSpring } from 'framer-motion';\n\nexport type CursorMode = 'follow' | 'replace';\nexport type CursorSide = 'top' | 'bottom' | 'left' | 'right';\nexport type CursorAlign = 'start' | 'center' | 'end';\n\nexport interface CursorTransition {\n type?: 'spring' | 'tween' | 'instant';\n stiffness?: number;\n damping?: number;\n mass?: number;\n duration?: number;\n ease?: string;\n}\n\nexport interface CursorOpts<P = any> {\n variant?: string;\n mode?: CursorMode;\n /**\n * Which side of the mouse the cursor wrapper anchors to (Follow mode).\n * Replace mode ignores side / align / offset — it auto-centers on the mouse.\n */\n side?: CursorSide;\n /**\n * Alignment along the perpendicular axis to `side`.\n * top/bottom: start = left, center = horizontal center, end = right.\n * left/right: start = top, center = vertical center, end = bottom.\n */\n align?: CursorAlign;\n offsetX?: number;\n offsetY?: number;\n transition?: CursorTransition;\n props?: Partial<P>;\n /**\n * Wrapper width / height applied to the cursor's outer motion.div. Useful\n * for code components (canvases, sparks) that fill their parent — without\n * this they'd render at their intrinsic size, which is often the whole\n * viewport. Plain numbers are interpreted as px; pass a string ('100%',\n * '4rem') to use other CSS units.\n */\n width?: number | string;\n height?: number | string;\n /**\n * When true, fade/scale on enter and exit via AnimatePresence. Default\n * false: appear and disappear instantly. The follow movement is always\n * smoothed by the spring config above — `enterExit` only controls the\n * mount/unmount transition.\n */\n enterExit?: boolean;\n}\n\ninterface ActiveCursor {\n key: number;\n Component: ComponentType<any>;\n opts: CursorOpts;\n}\n\n// ─── Global store (vanilla, no React) ───────────────────────────────────────\nlet _active: ActiveCursor | null = null;\nconst _listeners = new Set<() => void>();\n\n// STABLE key per cursor COMPONENT (not per enter event). The old\n// `key: ++_nextKey` per mouseenter forced React to fully REMOUNT the cursor\n// component on every hover transition. Cursor components are typically design\n// components (LayoutGroup + layout motion nodes + variant background images),\n// so scrolling with the pointer over a stack of cursor-hosting elements fired\n// an enter/leave storm → remount storm → framer-motion projection re-registers\n// + image repaints + forced reflows piled onto the main thread — the page\n// froze for seconds (live find 2026-07-07). With a per-Component key, moving\n// between hosts that share a cursor UPDATES the mounted component in place —\n// the per-instance `variant` lands through `initialVariant`, which the design\n// component's internal sync effect animates. A DIFFERENT component still\n// remounts (key changes).\nconst _componentKeys = new WeakMap<ComponentType<any>, number>();\nlet _nextComponentKey = 0;\nfunction _keyFor(c: ComponentType<any>): number {\n let k = _componentKeys.get(c);\n if (k === undefined) {\n k = ++_nextComponentKey;\n _componentKeys.set(c, k);\n }\n return k;\n}\n\n// Pending deactivate from a mouseleave. Scrolling re-hit-tests the pointer, so\n// leave/enter alternate rapidly while the page moves under the mouse; clearing\n// the cursor synchronously on every leave caused an unmount per row boundary.\n// A short grace window absorbs the churn: a follow-up enter cancels the clear\n// (and, same component, is a pure prop update). A REAL exit clears once, ~90ms\n// later — imperceptible.\nlet _pendingClear: ReturnType<typeof setTimeout> | null = null;\n\nfunction _setActive(next: ActiveCursor | null) {\n _active = next;\n _listeners.forEach((l) => l());\n}\n\nfunction _subscribe(l: () => void) {\n _listeners.add(l);\n return () => { _listeners.delete(l); };\n}\n\nfunction _getActive() {\n return _active;\n}\n\n/**\n * Spread the return value into an element to give it a component cursor.\n * Returns onMouseEnter/onMouseLeave handlers that push/pop the global store.\n *\n * <button {...withCursor(Pointer, { mode: 'follow', transition: { type: 'spring', stiffness: 300 } })}>\n */\nexport function withCursor<P>(Component: ComponentType<P>, opts: CursorOpts<P> = {}) {\n return {\n onMouseEnter: () => {\n if (_pendingClear !== null) {\n clearTimeout(_pendingClear);\n _pendingClear = null;\n }\n _setActive({ key: _keyFor(Component as ComponentType<any>), Component: Component as ComponentType<any>, opts });\n },\n onMouseLeave: () => {\n if (_pendingClear !== null) clearTimeout(_pendingClear);\n _pendingClear = setTimeout(() => {\n _pendingClear = null;\n _setActive(null);\n }, 90);\n },\n };\n}\n\n// ─── Portal (mount once in LayoutClient) ────────────────────────────────────\n\nfunction _springConfig(t?: CursorTransition) {\n if (!t || t.type === 'instant') return { stiffness: 1000, damping: 50, mass: 0.1 };\n if (t.type === 'tween' && t.duration) {\n // Map a tween duration to roughly-equivalent spring values.\n const stiffness = Math.max(50, 400 / Math.max(0.1, t.duration));\n return { stiffness, damping: 30, mass: 1 };\n }\n return {\n stiffness: t.stiffness ?? 300,\n damping: t.damping ?? 30,\n mass: t.mass ?? 1,\n };\n}\n\nexport function CursorPortal() {\n const cursor = useSyncExternalStore(_subscribe, _getActive, _getActive);\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n const sx = useSpring(x, _springConfig(cursor?.opts.transition));\n const sy = useSpring(y, _springConfig(cursor?.opts.transition));\n\n // One persistent listener; offsets read through a ref so an active-cursor\n // change never re-registers it (the old `[cursor]` dep re-added the listener\n // on every hover transition).\n const offsetRef = useRef({ x: 0, y: 0 });\n offsetRef.current = { x: cursor?.opts.offsetX ?? 0, y: cursor?.opts.offsetY ?? 0 };\n useEffect(() => {\n const onMove = (e: MouseEvent) => {\n x.set(e.clientX + offsetRef.current.x);\n y.set(e.clientY + offsetRef.current.y);\n };\n window.addEventListener('mousemove', onMove, { passive: true });\n return () => window.removeEventListener('mousemove', onMove);\n }, [x, y]);\n\n // Keyed on the MODE, not the whole active object: writing\n // `document.body.style.cursor` invalidates style for the entire document,\n // and the old `[cursor]` dep re-ran the write on every hover transition —\n // one full-page style recalc per row boundary while scrolling.\n const mode = cursor?.opts.mode;\n useEffect(() => {\n if (mode === 'replace') {\n const prev = document.body.style.cursor;\n document.body.style.cursor = 'none';\n return () => { document.body.style.cursor = prev; };\n }\n }, [mode]);\n\n // Wrapper width/height — numbers become px, strings pass through. Falls\n // back to undefined so intrinsic sizing kicks in if the user hasn't set it.\n const wrapW = typeof cursor?.opts.width === 'number' ? cursor.opts.width + 'px' : cursor?.opts.width;\n const wrapH = typeof cursor?.opts.height === 'number' ? cursor.opts.height + 'px' : cursor?.opts.height;\n\n // The OUTER motion.div carries the spring x/y (mouse position). The INNER\n // div applies a percentage transform for side+align (or auto-center in\n // Replace mode). Splitting them avoids fighting with framer-motion's own\n // transform handling on the x/y motion values.\n const outerStyle = {\n position: 'fixed' as const,\n top: 0,\n left: 0,\n x: sx,\n y: sy,\n pointerEvents: 'none' as const,\n zIndex: 9999,\n };\n const innerTransform = _innerTransform(cursor?.opts);\n const innerStyle = {\n width: wrapW,\n height: wrapH,\n transform: innerTransform,\n };\n\n // Default: instant in/out (no AnimatePresence wrapping). Wrap only when\n // the active cursor opts in via `enterExit: true` — keeps mount/unmount\n // snappy by default and avoids the brief fade-out from the previous cursor\n // when hovering between adjacent elements.\n // `opts.variant` → the design component's `initialVariant` prop. Without\n // this the variant picked in the editor (master call or per-instance\n // `<prop>Opts` override) was stored but NEVER applied — every hover showed\n // the cursor component's default variant (live find 2026-07-06). A fresh\n // `key` per hover means the component mounts with the right variant; its\n // internal `useEffect(() => setVariant(initialVariant), [initialVariant])`\n // covers any same-mount opts change.\n const variantProps = cursor?.opts.variant ? { initialVariant: cursor.opts.variant } : {};\n\n if (!cursor?.opts.enterExit) {\n return cursor ? (\n <motion.div key={cursor.key} style={outerStyle}>\n <div style={innerStyle}>\n <cursor.Component {...(cursor.opts.props ?? {})} {...variantProps} />\n </div>\n </motion.div>\n ) : null;\n }\n\n return (\n <AnimatePresence>\n {cursor && (\n <motion.div\n key={cursor.key}\n style={outerStyle}\n initial={{ opacity: 0, scale: 0.8 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.8 }}\n >\n <div style={innerStyle}>\n <cursor.Component {...(cursor.opts.props ?? {})} {...variantProps} />\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n}\n\n/**\n * Build the inner-wrapper transform from side + align + mode. Pure CSS\n * percentage translates so it works regardless of whether width/height are\n * set explicitly. Replace mode auto-centers; Follow mode anchors a corner /\n * edge / center based on the chosen side and alignment.\n */\nfunction _innerTransform(opts?: CursorOpts) {\n if (!opts || opts.mode === 'replace') return 'translate(-50%, -50%)';\n const side = opts.side ?? 'bottom';\n const align = opts.align ?? 'center';\n let tx = 0;\n let ty = 0;\n if (side === 'top') ty = -100;\n else if (side === 'left') tx = -100;\n // 'bottom' and 'right' default to 0 on the main axis.\n // Align controls the perpendicular axis.\n if (side === 'top' || side === 'bottom') {\n if (align === 'center') tx = -50;\n else if (align === 'end') tx = -100;\n } else {\n if (align === 'center') ty = -50;\n else if (align === 'end') ty = -100;\n }\n return 'translate(' + tx + '%, ' + ty + '%)';\n}\n","'use client';\n\n/**\n * `useStaticCanvas()` — returns `true` when the component is being rendered\n * inside the Revyme canvas editor, `false` in the live preview, published\n * site, or any other consumer environment.\n *\n * Sparks / code components use this to skip GPU-expensive animation work\n * (continuous rAF loops, big CSS blur layers, WebGL frames) on the editor\n * canvas where the user only needs a representative still — paint once,\n * stop. The full animated version still runs in preview and production.\n *\n * Mechanics: this default implementation always returns `false`. The canvas\n * editor's spark loader (`code-component-runtime.ts` MODULE_MAP) overrides\n * the export at compile time so it returns `true` in the canvas iframe and\n * `false` in the spark editor's preview pane (which sets `previewMode`).\n *\n * Mirrors Framer's `useIsStaticRenderer` pattern.\n */\nexport function useStaticCanvas(): boolean {\n return false;\n}\n","// sketch-draw.ts — Runtime player for Revyme sketch draw animations.\n//\n// Replays a brush-stroke sketch over time by feeding the original\n// pointer samples (persisted on each `<path>` as a `data-points`\n// attribute) back through perfect-freehand's `getStroke` at\n// progressively-increasing slice lengths. The result is the visible\n// equivalent of watching the user draw the sketch.\n//\n// Why a runtime function instead of an inline useEffect block in the\n// generated source: the orchestrator is ~80 LOC of imperative timing\n// + easing + RAF logic. Inlining it in every page that has a sketch\n// animation buries the page's actual logic. Living in\n// `@revyme/runtime` means the generated source is just one line:\n//\n// useEffect(() => playSketchDraw(el, opts), []);\n//\n// which reads the same way as `withResponsiveProps` / `withCursor`\n// already do for other generated patterns.\n\nimport { getStroke } from 'perfect-freehand';\n\nexport type SketchAnimMode = 'sequential' | 'staggered' | 'simultaneous';\nexport type SketchAnimTrigger = 'mount' | 'inView' | 'hover' | 'tap';\n\nexport interface SketchAnimTransition {\n type: 'tween' | 'spring';\n duration?: number;\n ease?: string;\n stiffness?: number;\n damping?: number;\n mass?: number;\n}\n\nexport interface SketchAnimOpts {\n trigger?: SketchAnimTrigger;\n mode?: SketchAnimMode;\n /** Multiplier on per-stroke duration. Per-stroke duration scales\n * with point count so a long stroke takes longer than a flick;\n * this dials the overall pace. */\n durationScale?: number;\n /** 0–1, only meaningful in staggered mode. 0 = fully sequential,\n * 1 = fully simultaneous. */\n stagger?: number;\n transition?: SketchAnimTransition;\n /** Brush size used for the intermediate-frame outline replay. The\n * final-frame `d` is restored from source so the end state is\n * pixel-exact regardless of this value. */\n brushSize?: number;\n}\n\nconst DEFAULT_OPTS: Required<Omit<SketchAnimOpts, 'transition'>> & { transition: SketchAnimTransition } = {\n trigger: 'inView',\n mode: 'sequential',\n durationScale: 1,\n stagger: 0.5,\n transition: { type: 'tween', duration: 1, ease: 'easeOut' },\n brushSize: 8,\n};\n\nfunction applyEase(t: number, transition: SketchAnimTransition): number {\n if (transition.type === 'spring') {\n const damping = transition.damping ?? 10;\n const stiffness = transition.stiffness ?? 100;\n const dampedT = 1 - Math.exp(-damping * t * 0.1);\n const oscillation = Math.cos(t * Math.sqrt(stiffness) * 0.3);\n return Math.min(1, dampedT * (1 - 0.1 * oscillation * (1 - t)));\n }\n switch (transition.ease ?? 'easeOut') {\n case 'linear': return t;\n case 'easeIn': return t * t;\n case 'easeOut': return 1 - (1 - t) * (1 - t);\n case 'easeInOut': return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n case 'circIn': return 1 - Math.sqrt(1 - t * t);\n case 'circOut': return Math.sqrt(1 - Math.pow(t - 1, 2));\n case 'backOut': {\n const c1 = 1.70158;\n const c3 = c1 + 1;\n return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);\n }\n default: return 1 - (1 - t) * (1 - t);\n }\n}\n\nfunction parsePoints(raw: string): number[][] {\n if (!raw) return [];\n return raw.split(/\\s+/).filter(Boolean).map(s => {\n const [x, y, p] = s.split(',');\n return [parseFloat(x) || 0, parseFloat(y) || 0, p != null ? parseFloat(p) : 0.5];\n });\n}\n\nfunction outlineToD(outline: number[][]): string {\n if (outline.length === 0) return '';\n let d = `M ${outline[0][0].toFixed(2)} ${outline[0][1].toFixed(2)}`;\n for (let i = 1; i < outline.length; i++) {\n d += ` L ${outline[i][0].toFixed(2)} ${outline[i][1].toFixed(2)}`;\n }\n return d + ' Z';\n}\n\n/**\n * Play a sketch draw animation on the given wrapper SVG. Pass the\n * options the generator emitted in source.\n *\n * Returns a cleanup function — wire as your useEffect's return value\n * so re-mounts cancel an in-flight animation cleanly:\n *\n * useEffect(() => playSketchDraw(svgEl, opts), []);\n *\n * If `wrapperEl` is null or the wrapper has no path children with\n * `data-points`, this is a no-op and returns a noop cleanup.\n */\nexport function playSketchDraw(\n wrapperEl: SVGSVGElement | null,\n userOpts: SketchAnimOpts = {},\n): () => void {\n const noop = () => {};\n if (!wrapperEl) return noop;\n const opts = { ...DEFAULT_OPTS, ...userOpts, transition: { ...DEFAULT_OPTS.transition, ...userOpts.transition } };\n\n const paths = Array.from(wrapperEl.querySelectorAll('path[data-points]')) as SVGPathElement[];\n if (paths.length === 0) return noop;\n\n // Snapshot the final d so the last frame is pixel-exact regardless\n // of the replay-with-default-brush approximation we use during\n // intermediate frames.\n const finalDs = paths.map(p => p.getAttribute('d') || '');\n const pointsList = paths.map(p => parsePoints(p.getAttribute('data-points') || ''));\n\n // Hide everything up front so the first frame doesn't flash.\n paths.forEach(p => p.setAttribute('d', ''));\n\n // Per-stroke duration — point count drives length so a long stroke\n // takes longer than a flick.\n const baseDur = (opts.transition.duration ?? 1) * 1000 * opts.durationScale;\n const maxPoints = pointsList.reduce((m, p) => Math.max(m, p.length), 1);\n const perStrokeDur = pointsList.map(p => baseDur * (p.length / maxPoints));\n const startMs: number[] = [];\n let cursor = 0;\n for (let i = 0; i < paths.length; i++) {\n if (opts.mode === 'simultaneous') {\n startMs.push(0);\n } else if (opts.mode === 'staggered') {\n const overlap = Math.max(0, Math.min(1, opts.stagger));\n const start = i === 0 ? 0 : startMs[i - 1] + perStrokeDur[i - 1] * (1 - overlap);\n startMs.push(start);\n } else {\n // sequential\n startMs.push(cursor);\n cursor += perStrokeDur[i];\n }\n }\n\n let cancelled = false;\n let rafId = 0;\n let started = false;\n let cleanupTrigger: (() => void) | null = null;\n let startTs = 0;\n\n const tick = (now: number) => {\n if (cancelled) return;\n const elapsed = now - startTs;\n let allDone = true;\n for (let i = 0; i < paths.length; i++) {\n const local = elapsed - startMs[i];\n if (local < 0) { allDone = false; continue; }\n const t = Math.min(1, local / Math.max(1, perStrokeDur[i]));\n if (t < 1) allDone = false;\n let d: string;\n if (t >= 1) {\n d = finalDs[i];\n } else {\n const eased = applyEase(t, opts.transition);\n const sliceCount = Math.max(2, Math.floor(pointsList[i].length * eased));\n const subset = pointsList[i].slice(0, sliceCount);\n if (subset.length < 2) {\n d = '';\n } else {\n const outline = getStroke(subset, {\n size: opts.brushSize, thinning: 0.5, smoothing: 0.5, streamline: 0.5,\n });\n d = outlineToD(outline);\n }\n }\n paths[i].setAttribute('d', d);\n }\n if (!allDone) rafId = requestAnimationFrame(tick);\n };\n\n const start = () => {\n if (started) return;\n started = true;\n startTs = performance.now();\n rafId = requestAnimationFrame(tick);\n };\n\n if (opts.trigger === 'inView') {\n const obs = new IntersectionObserver((entries) => {\n if (entries.some(e => e.isIntersecting)) {\n start();\n obs.disconnect();\n }\n }, { threshold: 0.2 });\n obs.observe(wrapperEl);\n cleanupTrigger = () => obs.disconnect();\n } else if (opts.trigger === 'hover') {\n const onEnter = () => start();\n wrapperEl.addEventListener('mouseenter', onEnter);\n cleanupTrigger = () => wrapperEl.removeEventListener('mouseenter', onEnter);\n } else if (opts.trigger === 'tap') {\n const onTap = () => start();\n wrapperEl.addEventListener('click', onTap);\n cleanupTrigger = () => wrapperEl.removeEventListener('click', onTap);\n } else {\n // mount\n start();\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n cleanupTrigger?.();\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/withResponsiveProps.tsx","../src/cursor-runtime.tsx","../src/useStaticCanvas.ts","../src/sketch-draw.ts"],"sourcesContent":["'use client';\n\nimport { useState, useEffect, forwardRef, type ComponentType } from 'react';\nimport { motion, isMotionValue } from 'framer-motion';\n\n/**\n * HOC that reads a `data-responsive` JSON prop and merges per-viewport overrides.\n * Usage: `export default withResponsiveProps(MyComponent)`\n *\n * On the canvas, CodeComponentHost injects `__canvasViewportWidth` to simulate viewport size.\n * In production, uses `window.innerWidth`.\n *\n * data-responsive='{\"768\":{\"fontSize\":32},\"375\":{\"fontSize\":24}}'\n * Breakpoints are max-width: if viewport <= 768, the 768 overrides apply.\n *\n * ANIMATED-STYLE SOCKET (Framer parity): the editor expresses instance\n * animation effects (Appear, scroll scrubs, …) as framer MotionValues bound\n * into the instance's `style` prop — `style={{ opacity: <mv>, y: <mv> }}`.\n * Design components consume them natively (their roots are motion.* and\n * spread `...style`), but a CODE component's root is a plain element: a\n * MotionValue arrives as an un-serialisable object (the style is dropped)\n * and motion-only keys like `y` aren't CSS at all — the effect silently\n * dies at the component boundary (live find 2026-07-14: Appear on a\n * Marquee). When animated values or motion-only keys are present, the HOC\n * renders a motion.div WRAPPER that carries them (plus the placement props\n * that describe the instance's slot in its parent), and hands the wrapped\n * component a clean static style. Framer does exactly this — the platform\n * owns an animatable container around every code component, so component\n * authors never deal with animation plumbing. No animated values → no\n * wrapper → byte-identical behaviour to before.\n */\n\n/** Style keys only a motion.* element understands (translated into\n * `transform`) — a plain DOM element ignores them entirely. */\nconst MOTION_ONLY_KEYS = new Set([\n 'x', 'y', 'z',\n 'rotate', 'rotateX', 'rotateY', 'rotateZ',\n 'scale', 'scaleX', 'scaleY',\n 'skew', 'skewX', 'skewY',\n 'originX', 'originY', 'originZ',\n 'transformPerspective',\n]);\n\n/** Placement props describing the instance's slot in ITS PARENT's layout —\n * these must ride on whichever element is outermost (the wrapper, when one\n * exists), mirroring the design-instance wrapper/root split. */\nconst PLACEMENT_KEYS = new Set([\n 'position', 'left', 'top', 'right', 'bottom', 'inset',\n 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',\n 'flex', 'flexGrow', 'flexShrink', 'flexBasis',\n 'order', 'alignSelf', 'justifySelf', 'zIndex',\n 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',\n 'gridColumn', 'gridRow', 'gridArea',\n]);\nexport default function withResponsiveProps<P extends Record<string, any>>(\n Component: ComponentType<P>\n): ComponentType<P & { 'data-responsive'?: string; __canvasViewportWidth?: number }> {\n // forwardRef: scroll effects target the instance with `ref={…}` for\n // `useScroll({ target })` — a plain function component would silently drop\n // it (framer then throws \"Target ref is defined but not hydrated\"). The ref\n // pins to the animated wrapper (a real DOM box = the component's exact\n // footprint), so scroll measurement works with the component untouched.\n return forwardRef(function ResponsiveSpark(props: any, fwdRef: any) {\n const canvasVpWidth = props.__canvasViewportWidth as number | undefined;\n const [windowWidth, setWindowWidth] = useState(\n typeof window !== 'undefined' ? window.innerWidth : 1440\n );\n\n useEffect(() => {\n if (canvasVpWidth !== undefined) return;\n const handler = () => setWindowWidth(window.innerWidth);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, [canvasVpWidth]);\n\n const vpWidth = canvasVpWidth ?? windowWidth;\n const responsiveStr = props['data-responsive'];\n let mergedProps = { ...props };\n\n if (responsiveStr) {\n try {\n const overrides = typeof responsiveStr === 'string'\n ? JSON.parse(responsiveStr) : responsiveStr;\n // _bp contains all viewport breakpoint widths for range computation.\n // Each breakpoint's range is (prev_bp, bp]. Prevents cascade.\n const allBp = Array.isArray(overrides._bp)\n ? overrides._bp : Object.keys(overrides).filter(k => k !== '_bp').map(Number);\n const sortedBp = [...allBp].sort((a, b) => a - b);\n let matchedBp;\n for (let i = 0; i < sortedBp.length; i++) {\n const lower = i > 0 ? sortedBp[i - 1] : 0;\n if (vpWidth > lower && vpWidth <= sortedBp[i]) {\n matchedBp = sortedBp[i];\n break;\n }\n }\n if (matchedBp !== undefined && overrides[matchedBp]) {\n const ov = overrides[matchedBp];\n // A Scroll Variant binds `initialVariant={…Sv}` and OWNS it at runtime (it morphs the\n // variant on scroll). The per-viewport `data-responsive` entry must NOT override that\n // here — otherwise it freezes the variant on replicas and the morph never plays. The\n // per-viewport variant CHOICE still drives the canvas + seeds the Sv's resting; only\n // the runtime merge skips `initialVariant`. (No scroll variant → unchanged behaviour.)\n if (props['data-scroll-variant'] && ov && typeof ov === 'object' && 'initialVariant' in ov) {\n const { initialVariant: _skip, ...rest } = ov;\n mergedProps = { ...mergedProps, ...rest };\n } else {\n mergedProps = { ...mergedProps, ...ov };\n }\n }\n } catch {}\n }\n\n delete mergedProps['data-responsive'];\n delete mergedProps['__canvasViewportWidth'];\n\n // ── Animated-style socket (see header comment) ──\n // A forwarded ref ALSO forces the wrapper: it's the scroll-effect target,\n // which must resolve to a real DOM box even when the style carries no\n // animated values yet.\n const style = mergedProps.style as Record<string, any> | undefined;\n let needsWrapper = fwdRef != null;\n if (!needsWrapper && style) {\n for (const k of Object.keys(style)) {\n if (MOTION_ONLY_KEYS.has(k) || isMotionValue(style[k])) { needsWrapper = true; break; }\n }\n }\n if (needsWrapper) {\n const wrapperStyle: Record<string, any> = {};\n const innerStyle: Record<string, any> = {};\n for (const [k, v] of Object.entries(style ?? {})) {\n if (MOTION_ONLY_KEYS.has(k) || PLACEMENT_KEYS.has(k) || isMotionValue(v)) wrapperStyle[k] = v;\n else innerStyle[k] = v;\n }\n // The inner component fills the wrapper — but only along axes the\n // wrapper actually sized (code-component instances always carry\n // definite dims; the guard keeps an unsized axis hugging content).\n if ('width' in wrapperStyle) innerStyle.width = '100%';\n if ('height' in wrapperStyle) innerStyle.height = '100%';\n // The wrapper OWNS the instance's slot (all placement keys moved onto\n // it) — the inner root must re-base INTO the wrapper. Design-component\n // masters bake `position: 'absolute'` on their root (canvas master\n // tiling) and rely on the instance style overriding it via the trailing\n // `...style` spread; with the split the position key never reaches them,\n // so the root absolute-positioned inside a zero-size wrapper and\n // vanished (live find 2026-07-28: Sign Up button instance inside an\n // AnimatePresence popLayout header — popLayout's ref forces the\n // wrapper). Position is a PLACEMENT key, so innerStyle can never carry\n // its own — always re-base.\n innerStyle.position = 'relative';\n const { style: _split, ...rest } = mergedProps;\n delete rest.ref; // React 19 passes `ref` as a prop — never forward it inward\n return (\n <motion.div ref={fwdRef} style={wrapperStyle}>\n <Component {...rest} style={innerStyle} />\n </motion.div>\n );\n }\n\n return <Component {...mergedProps} />;\n }) as any;\n}\n","'use client';\n\nimport { useEffect, useRef, useSyncExternalStore, type ComponentType } from 'react';\nimport { motion, AnimatePresence, useMotionValue, useSpring } from 'framer-motion';\n\nexport type CursorMode = 'follow' | 'replace';\nexport type CursorSide = 'top' | 'bottom' | 'left' | 'right';\nexport type CursorAlign = 'start' | 'center' | 'end';\n\nexport interface CursorTransition {\n type?: 'spring' | 'tween' | 'instant';\n stiffness?: number;\n damping?: number;\n mass?: number;\n duration?: number;\n ease?: string;\n}\n\nexport interface CursorOpts<P = any> {\n variant?: string;\n mode?: CursorMode;\n /**\n * Which side of the mouse the cursor wrapper anchors to (Follow mode).\n * Replace mode ignores side / align / offset — it auto-centers on the mouse.\n */\n side?: CursorSide;\n /**\n * Alignment along the perpendicular axis to `side`.\n * top/bottom: start = left, center = horizontal center, end = right.\n * left/right: start = top, center = vertical center, end = bottom.\n */\n align?: CursorAlign;\n offsetX?: number;\n offsetY?: number;\n transition?: CursorTransition;\n props?: Partial<P>;\n /**\n * Wrapper width / height applied to the cursor's outer motion.div. Useful\n * for code components (canvases, sparks) that fill their parent — without\n * this they'd render at their intrinsic size, which is often the whole\n * viewport. Plain numbers are interpreted as px; pass a string ('100%',\n * '4rem') to use other CSS units.\n */\n width?: number | string;\n height?: number | string;\n /**\n * When true, fade/scale on enter and exit via AnimatePresence. Default\n * false: appear and disappear instantly. The follow movement is always\n * smoothed by the spring config above — `enterExit` only controls the\n * mount/unmount transition.\n */\n enterExit?: boolean;\n}\n\ninterface ActiveCursor {\n key: number;\n Component: ComponentType<any>;\n opts: CursorOpts;\n}\n\n// ─── Global store (vanilla, no React) ───────────────────────────────────────\nlet _active: ActiveCursor | null = null;\nconst _listeners = new Set<() => void>();\n\n// STABLE key per cursor COMPONENT (not per enter event). The old\n// `key: ++_nextKey` per mouseenter forced React to fully REMOUNT the cursor\n// component on every hover transition. Cursor components are typically design\n// components (LayoutGroup + layout motion nodes + variant background images),\n// so scrolling with the pointer over a stack of cursor-hosting elements fired\n// an enter/leave storm → remount storm → framer-motion projection re-registers\n// + image repaints + forced reflows piled onto the main thread — the page\n// froze for seconds (live find 2026-07-07). With a per-Component key, moving\n// between hosts that share a cursor UPDATES the mounted component in place —\n// the per-instance `variant` lands through `initialVariant`, which the design\n// component's internal sync effect animates. A DIFFERENT component still\n// remounts (key changes).\nconst _componentKeys = new WeakMap<ComponentType<any>, number>();\nlet _nextComponentKey = 0;\nfunction _keyFor(c: ComponentType<any>): number {\n let k = _componentKeys.get(c);\n if (k === undefined) {\n k = ++_nextComponentKey;\n _componentKeys.set(c, k);\n }\n return k;\n}\n\n// Pending deactivate from a mouseleave. Scrolling re-hit-tests the pointer, so\n// leave/enter alternate rapidly while the page moves under the mouse; clearing\n// the cursor synchronously on every leave caused an unmount per row boundary.\n// A short grace window absorbs the churn: a follow-up enter cancels the clear\n// (and, same component, is a pure prop update). A REAL exit clears once, ~90ms\n// later — imperceptible.\nlet _pendingClear: ReturnType<typeof setTimeout> | null = null;\n\nfunction _setActive(next: ActiveCursor | null) {\n _active = next;\n _listeners.forEach((l) => l());\n}\n\nfunction _subscribe(l: () => void) {\n _listeners.add(l);\n return () => { _listeners.delete(l); };\n}\n\nfunction _getActive() {\n return _active;\n}\n\n/**\n * Spread the return value into an element to give it a component cursor.\n * Returns onMouseEnter/onMouseLeave handlers that push/pop the global store.\n *\n * <button {...withCursor(Pointer, { mode: 'follow', transition: { type: 'spring', stiffness: 300 } })}>\n */\nexport function withCursor<P>(Component: ComponentType<P>, opts: CursorOpts<P> = {}) {\n return {\n onMouseEnter: () => {\n if (_pendingClear !== null) {\n clearTimeout(_pendingClear);\n _pendingClear = null;\n }\n _setActive({ key: _keyFor(Component as ComponentType<any>), Component: Component as ComponentType<any>, opts });\n },\n onMouseLeave: () => {\n if (_pendingClear !== null) clearTimeout(_pendingClear);\n _pendingClear = setTimeout(() => {\n _pendingClear = null;\n _setActive(null);\n }, 90);\n },\n };\n}\n\n// ─── Portal (mount once in LayoutClient) ────────────────────────────────────\n\nfunction _springConfig(t?: CursorTransition) {\n if (!t || t.type === 'instant') return { stiffness: 1000, damping: 50, mass: 0.1 };\n if (t.type === 'tween' && t.duration) {\n // Map a tween duration to roughly-equivalent spring values.\n const stiffness = Math.max(50, 400 / Math.max(0.1, t.duration));\n return { stiffness, damping: 30, mass: 1 };\n }\n return {\n stiffness: t.stiffness ?? 300,\n damping: t.damping ?? 30,\n mass: t.mass ?? 1,\n };\n}\n\nexport function CursorPortal() {\n const cursor = useSyncExternalStore(_subscribe, _getActive, _getActive);\n\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n const sx = useSpring(x, _springConfig(cursor?.opts.transition));\n const sy = useSpring(y, _springConfig(cursor?.opts.transition));\n\n // One persistent listener; offsets read through a ref so an active-cursor\n // change never re-registers it (the old `[cursor]` dep re-added the listener\n // on every hover transition).\n const offsetRef = useRef({ x: 0, y: 0 });\n offsetRef.current = { x: cursor?.opts.offsetX ?? 0, y: cursor?.opts.offsetY ?? 0 };\n useEffect(() => {\n const onMove = (e: MouseEvent) => {\n x.set(e.clientX + offsetRef.current.x);\n y.set(e.clientY + offsetRef.current.y);\n };\n window.addEventListener('mousemove', onMove, { passive: true });\n return () => window.removeEventListener('mousemove', onMove);\n }, [x, y]);\n\n // Keyed on the MODE, not the whole active object: writing\n // `document.body.style.cursor` invalidates style for the entire document,\n // and the old `[cursor]` dep re-ran the write on every hover transition —\n // one full-page style recalc per row boundary while scrolling.\n const mode = cursor?.opts.mode;\n useEffect(() => {\n if (mode === 'replace') {\n const prev = document.body.style.cursor;\n document.body.style.cursor = 'none';\n return () => { document.body.style.cursor = prev; };\n }\n }, [mode]);\n\n // Wrapper width/height — numbers become px, strings pass through. Falls\n // back to undefined so intrinsic sizing kicks in if the user hasn't set it.\n const wrapW = typeof cursor?.opts.width === 'number' ? cursor.opts.width + 'px' : cursor?.opts.width;\n const wrapH = typeof cursor?.opts.height === 'number' ? cursor.opts.height + 'px' : cursor?.opts.height;\n\n // The OUTER motion.div carries the spring x/y (mouse position). The INNER\n // div applies a percentage transform for side+align (or auto-center in\n // Replace mode). Splitting them avoids fighting with framer-motion's own\n // transform handling on the x/y motion values.\n const outerStyle = {\n position: 'fixed' as const,\n top: 0,\n left: 0,\n x: sx,\n y: sy,\n pointerEvents: 'none' as const,\n zIndex: 9999,\n };\n const innerTransform = _innerTransform(cursor?.opts);\n const innerStyle = {\n width: wrapW,\n height: wrapH,\n transform: innerTransform,\n };\n\n // Default: instant in/out (no AnimatePresence wrapping). Wrap only when\n // the active cursor opts in via `enterExit: true` — keeps mount/unmount\n // snappy by default and avoids the brief fade-out from the previous cursor\n // when hovering between adjacent elements.\n // `opts.variant` → the design component's `initialVariant` prop. Without\n // this the variant picked in the editor (master call or per-instance\n // `<prop>Opts` override) was stored but NEVER applied — every hover showed\n // the cursor component's default variant (live find 2026-07-06). A fresh\n // `key` per hover means the component mounts with the right variant; its\n // internal `useEffect(() => setVariant(initialVariant), [initialVariant])`\n // covers any same-mount opts change.\n const variantProps = cursor?.opts.variant ? { initialVariant: cursor.opts.variant } : {};\n\n if (!cursor?.opts.enterExit) {\n return cursor ? (\n <motion.div key={cursor.key} style={outerStyle}>\n <div style={innerStyle}>\n <cursor.Component {...(cursor.opts.props ?? {})} {...variantProps} />\n </div>\n </motion.div>\n ) : null;\n }\n\n return (\n <AnimatePresence>\n {cursor && (\n <motion.div\n key={cursor.key}\n style={outerStyle}\n initial={{ opacity: 0, scale: 0.8 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.8 }}\n >\n <div style={innerStyle}>\n <cursor.Component {...(cursor.opts.props ?? {})} {...variantProps} />\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n}\n\n/**\n * Build the inner-wrapper transform from side + align + mode. Pure CSS\n * percentage translates so it works regardless of whether width/height are\n * set explicitly. Replace mode auto-centers; Follow mode anchors a corner /\n * edge / center based on the chosen side and alignment.\n */\nfunction _innerTransform(opts?: CursorOpts) {\n if (!opts || opts.mode === 'replace') return 'translate(-50%, -50%)';\n const side = opts.side ?? 'bottom';\n const align = opts.align ?? 'center';\n let tx = 0;\n let ty = 0;\n if (side === 'top') ty = -100;\n else if (side === 'left') tx = -100;\n // 'bottom' and 'right' default to 0 on the main axis.\n // Align controls the perpendicular axis.\n if (side === 'top' || side === 'bottom') {\n if (align === 'center') tx = -50;\n else if (align === 'end') tx = -100;\n } else {\n if (align === 'center') ty = -50;\n else if (align === 'end') ty = -100;\n }\n return 'translate(' + tx + '%, ' + ty + '%)';\n}\n","'use client';\n\n/**\n * `useStaticCanvas()` — returns `true` when the component is being rendered\n * inside the Revyme canvas editor, `false` in the live preview, published\n * site, or any other consumer environment.\n *\n * Sparks / code components use this to skip GPU-expensive animation work\n * (continuous rAF loops, big CSS blur layers, WebGL frames) on the editor\n * canvas where the user only needs a representative still — paint once,\n * stop. The full animated version still runs in preview and production.\n *\n * Mechanics: this default implementation always returns `false`. The canvas\n * editor's spark loader (`code-component-runtime.ts` MODULE_MAP) overrides\n * the export at compile time so it returns `true` in the canvas iframe and\n * `false` in the spark editor's preview pane (which sets `previewMode`).\n *\n * Mirrors Framer's `useIsStaticRenderer` pattern.\n */\nexport function useStaticCanvas(): boolean {\n return false;\n}\n","// sketch-draw.ts — Runtime player for Revyme sketch draw animations.\n//\n// Replays a brush-stroke sketch over time by feeding the original\n// pointer samples (persisted on each `<path>` as a `data-points`\n// attribute) back through perfect-freehand's `getStroke` at\n// progressively-increasing slice lengths. The result is the visible\n// equivalent of watching the user draw the sketch.\n//\n// Why a runtime function instead of an inline useEffect block in the\n// generated source: the orchestrator is ~80 LOC of imperative timing\n// + easing + RAF logic. Inlining it in every page that has a sketch\n// animation buries the page's actual logic. Living in\n// `@revyme/runtime` means the generated source is just one line:\n//\n// useEffect(() => playSketchDraw(el, opts), []);\n//\n// which reads the same way as `withResponsiveProps` / `withCursor`\n// already do for other generated patterns.\n\nimport { getStroke } from 'perfect-freehand';\n\nexport type SketchAnimMode = 'sequential' | 'staggered' | 'simultaneous';\nexport type SketchAnimTrigger = 'mount' | 'inView' | 'hover' | 'tap';\n\nexport interface SketchAnimTransition {\n type: 'tween' | 'spring';\n duration?: number;\n ease?: string;\n stiffness?: number;\n damping?: number;\n mass?: number;\n}\n\nexport interface SketchAnimOpts {\n trigger?: SketchAnimTrigger;\n mode?: SketchAnimMode;\n /** Multiplier on per-stroke duration. Per-stroke duration scales\n * with point count so a long stroke takes longer than a flick;\n * this dials the overall pace. */\n durationScale?: number;\n /** 0–1, only meaningful in staggered mode. 0 = fully sequential,\n * 1 = fully simultaneous. */\n stagger?: number;\n transition?: SketchAnimTransition;\n /** Brush size used for the intermediate-frame outline replay. The\n * final-frame `d` is restored from source so the end state is\n * pixel-exact regardless of this value. */\n brushSize?: number;\n}\n\nconst DEFAULT_OPTS: Required<Omit<SketchAnimOpts, 'transition'>> & { transition: SketchAnimTransition } = {\n trigger: 'inView',\n mode: 'sequential',\n durationScale: 1,\n stagger: 0.5,\n transition: { type: 'tween', duration: 1, ease: 'easeOut' },\n brushSize: 8,\n};\n\nfunction applyEase(t: number, transition: SketchAnimTransition): number {\n if (transition.type === 'spring') {\n const damping = transition.damping ?? 10;\n const stiffness = transition.stiffness ?? 100;\n const dampedT = 1 - Math.exp(-damping * t * 0.1);\n const oscillation = Math.cos(t * Math.sqrt(stiffness) * 0.3);\n return Math.min(1, dampedT * (1 - 0.1 * oscillation * (1 - t)));\n }\n switch (transition.ease ?? 'easeOut') {\n case 'linear': return t;\n case 'easeIn': return t * t;\n case 'easeOut': return 1 - (1 - t) * (1 - t);\n case 'easeInOut': return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n case 'circIn': return 1 - Math.sqrt(1 - t * t);\n case 'circOut': return Math.sqrt(1 - Math.pow(t - 1, 2));\n case 'backOut': {\n const c1 = 1.70158;\n const c3 = c1 + 1;\n return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);\n }\n default: return 1 - (1 - t) * (1 - t);\n }\n}\n\nfunction parsePoints(raw: string): number[][] {\n if (!raw) return [];\n return raw.split(/\\s+/).filter(Boolean).map(s => {\n const [x, y, p] = s.split(',');\n return [parseFloat(x) || 0, parseFloat(y) || 0, p != null ? parseFloat(p) : 0.5];\n });\n}\n\nfunction outlineToD(outline: number[][]): string {\n if (outline.length === 0) return '';\n let d = `M ${outline[0][0].toFixed(2)} ${outline[0][1].toFixed(2)}`;\n for (let i = 1; i < outline.length; i++) {\n d += ` L ${outline[i][0].toFixed(2)} ${outline[i][1].toFixed(2)}`;\n }\n return d + ' Z';\n}\n\n/**\n * Play a sketch draw animation on the given wrapper SVG. Pass the\n * options the generator emitted in source.\n *\n * Returns a cleanup function — wire as your useEffect's return value\n * so re-mounts cancel an in-flight animation cleanly:\n *\n * useEffect(() => playSketchDraw(svgEl, opts), []);\n *\n * If `wrapperEl` is null or the wrapper has no path children with\n * `data-points`, this is a no-op and returns a noop cleanup.\n */\nexport function playSketchDraw(\n wrapperEl: SVGSVGElement | null,\n userOpts: SketchAnimOpts = {},\n): () => void {\n const noop = () => {};\n if (!wrapperEl) return noop;\n const opts = { ...DEFAULT_OPTS, ...userOpts, transition: { ...DEFAULT_OPTS.transition, ...userOpts.transition } };\n\n const paths = Array.from(wrapperEl.querySelectorAll('path[data-points]')) as SVGPathElement[];\n if (paths.length === 0) return noop;\n\n // Snapshot the final d so the last frame is pixel-exact regardless\n // of the replay-with-default-brush approximation we use during\n // intermediate frames.\n const finalDs = paths.map(p => p.getAttribute('d') || '');\n const pointsList = paths.map(p => parsePoints(p.getAttribute('data-points') || ''));\n\n // Hide everything up front so the first frame doesn't flash.\n paths.forEach(p => p.setAttribute('d', ''));\n\n // Per-stroke duration — point count drives length so a long stroke\n // takes longer than a flick.\n const baseDur = (opts.transition.duration ?? 1) * 1000 * opts.durationScale;\n const maxPoints = pointsList.reduce((m, p) => Math.max(m, p.length), 1);\n const perStrokeDur = pointsList.map(p => baseDur * (p.length / maxPoints));\n const startMs: number[] = [];\n let cursor = 0;\n for (let i = 0; i < paths.length; i++) {\n if (opts.mode === 'simultaneous') {\n startMs.push(0);\n } else if (opts.mode === 'staggered') {\n const overlap = Math.max(0, Math.min(1, opts.stagger));\n const start = i === 0 ? 0 : startMs[i - 1] + perStrokeDur[i - 1] * (1 - overlap);\n startMs.push(start);\n } else {\n // sequential\n startMs.push(cursor);\n cursor += perStrokeDur[i];\n }\n }\n\n let cancelled = false;\n let rafId = 0;\n let started = false;\n let cleanupTrigger: (() => void) | null = null;\n let startTs = 0;\n\n const tick = (now: number) => {\n if (cancelled) return;\n const elapsed = now - startTs;\n let allDone = true;\n for (let i = 0; i < paths.length; i++) {\n const local = elapsed - startMs[i];\n if (local < 0) { allDone = false; continue; }\n const t = Math.min(1, local / Math.max(1, perStrokeDur[i]));\n if (t < 1) allDone = false;\n let d: string;\n if (t >= 1) {\n d = finalDs[i];\n } else {\n const eased = applyEase(t, opts.transition);\n const sliceCount = Math.max(2, Math.floor(pointsList[i].length * eased));\n const subset = pointsList[i].slice(0, sliceCount);\n if (subset.length < 2) {\n d = '';\n } else {\n const outline = getStroke(subset, {\n size: opts.brushSize, thinning: 0.5, smoothing: 0.5, streamline: 0.5,\n });\n d = outlineToD(outline);\n }\n }\n paths[i].setAttribute('d', d);\n }\n if (!allDone) rafId = requestAnimationFrame(tick);\n };\n\n const start = () => {\n if (started) return;\n started = true;\n startTs = performance.now();\n rafId = requestAnimationFrame(tick);\n };\n\n if (opts.trigger === 'inView') {\n const obs = new IntersectionObserver((entries) => {\n if (entries.some(e => e.isIntersecting)) {\n start();\n obs.disconnect();\n }\n }, { threshold: 0.2 });\n obs.observe(wrapperEl);\n cleanupTrigger = () => obs.disconnect();\n } else if (opts.trigger === 'hover') {\n const onEnter = () => start();\n wrapperEl.addEventListener('mouseenter', onEnter);\n cleanupTrigger = () => wrapperEl.removeEventListener('mouseenter', onEnter);\n } else if (opts.trigger === 'tap') {\n const onTap = () => start();\n wrapperEl.addEventListener('click', onTap);\n cleanupTrigger = () => wrapperEl.removeEventListener('click', onTap);\n } else {\n // mount\n start();\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n cleanupTrigger?.();\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, type ComponentType } from 'react';
|
|
3
|
+
import { useState, useEffect, forwardRef, type ComponentType } from 'react';
|
|
4
4
|
import { motion, isMotionValue } from 'framer-motion';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -55,7 +55,12 @@ const PLACEMENT_KEYS = new Set([
|
|
|
55
55
|
export default function withResponsiveProps<P extends Record<string, any>>(
|
|
56
56
|
Component: ComponentType<P>
|
|
57
57
|
): ComponentType<P & { 'data-responsive'?: string; __canvasViewportWidth?: number }> {
|
|
58
|
-
|
|
58
|
+
// forwardRef: scroll effects target the instance with `ref={…}` for
|
|
59
|
+
// `useScroll({ target })` — a plain function component would silently drop
|
|
60
|
+
// it (framer then throws "Target ref is defined but not hydrated"). The ref
|
|
61
|
+
// pins to the animated wrapper (a real DOM box = the component's exact
|
|
62
|
+
// footprint), so scroll measurement works with the component untouched.
|
|
63
|
+
return forwardRef(function ResponsiveSpark(props: any, fwdRef: any) {
|
|
59
64
|
const canvasVpWidth = props.__canvasViewportWidth as number | undefined;
|
|
60
65
|
const [windowWidth, setWindowWidth] = useState(
|
|
61
66
|
typeof window !== 'undefined' ? window.innerWidth : 1440
|
|
@@ -110,17 +115,20 @@ export default function withResponsiveProps<P extends Record<string, any>>(
|
|
|
110
115
|
delete mergedProps['__canvasViewportWidth'];
|
|
111
116
|
|
|
112
117
|
// ── Animated-style socket (see header comment) ──
|
|
118
|
+
// A forwarded ref ALSO forces the wrapper: it's the scroll-effect target,
|
|
119
|
+
// which must resolve to a real DOM box even when the style carries no
|
|
120
|
+
// animated values yet.
|
|
113
121
|
const style = mergedProps.style as Record<string, any> | undefined;
|
|
114
|
-
let needsWrapper =
|
|
115
|
-
if (style) {
|
|
122
|
+
let needsWrapper = fwdRef != null;
|
|
123
|
+
if (!needsWrapper && style) {
|
|
116
124
|
for (const k of Object.keys(style)) {
|
|
117
125
|
if (MOTION_ONLY_KEYS.has(k) || isMotionValue(style[k])) { needsWrapper = true; break; }
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
|
-
if (needsWrapper
|
|
128
|
+
if (needsWrapper) {
|
|
121
129
|
const wrapperStyle: Record<string, any> = {};
|
|
122
130
|
const innerStyle: Record<string, any> = {};
|
|
123
|
-
for (const [k, v] of Object.entries(style)) {
|
|
131
|
+
for (const [k, v] of Object.entries(style ?? {})) {
|
|
124
132
|
if (MOTION_ONLY_KEYS.has(k) || PLACEMENT_KEYS.has(k) || isMotionValue(v)) wrapperStyle[k] = v;
|
|
125
133
|
else innerStyle[k] = v;
|
|
126
134
|
}
|
|
@@ -129,14 +137,26 @@ export default function withResponsiveProps<P extends Record<string, any>>(
|
|
|
129
137
|
// definite dims; the guard keeps an unsized axis hugging content).
|
|
130
138
|
if ('width' in wrapperStyle) innerStyle.width = '100%';
|
|
131
139
|
if ('height' in wrapperStyle) innerStyle.height = '100%';
|
|
140
|
+
// The wrapper OWNS the instance's slot (all placement keys moved onto
|
|
141
|
+
// it) — the inner root must re-base INTO the wrapper. Design-component
|
|
142
|
+
// masters bake `position: 'absolute'` on their root (canvas master
|
|
143
|
+
// tiling) and rely on the instance style overriding it via the trailing
|
|
144
|
+
// `...style` spread; with the split the position key never reaches them,
|
|
145
|
+
// so the root absolute-positioned inside a zero-size wrapper and
|
|
146
|
+
// vanished (live find 2026-07-28: Sign Up button instance inside an
|
|
147
|
+
// AnimatePresence popLayout header — popLayout's ref forces the
|
|
148
|
+
// wrapper). Position is a PLACEMENT key, so innerStyle can never carry
|
|
149
|
+
// its own — always re-base.
|
|
150
|
+
innerStyle.position = 'relative';
|
|
132
151
|
const { style: _split, ...rest } = mergedProps;
|
|
152
|
+
delete rest.ref; // React 19 passes `ref` as a prop — never forward it inward
|
|
133
153
|
return (
|
|
134
|
-
<motion.div style={wrapperStyle}>
|
|
154
|
+
<motion.div ref={fwdRef} style={wrapperStyle}>
|
|
135
155
|
<Component {...rest} style={innerStyle} />
|
|
136
156
|
</motion.div>
|
|
137
157
|
);
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
return <Component {...mergedProps} />;
|
|
141
|
-
};
|
|
161
|
+
}) as any;
|
|
142
162
|
}
|