From 4804597f6846a09a4295d293d0610f12a1e7df30 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 14:18:51 -0400 Subject: [PATCH 01/10] Init --- fern/assets/styles.css | 29 +++ fern/docs.yml | 2 + fern/products/home/pages/welcome.mdx | 30 ++- fern/rive-animation.js | 285 +++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 18 deletions(-) create mode 100644 fern/rive-animation.js diff --git a/fern/assets/styles.css b/fern/assets/styles.css index 990236544..8a6f5678d 100644 --- a/fern/assets/styles.css +++ b/fern/assets/styles.css @@ -846,4 +846,33 @@ a[href*="changelog"] svg { } } + + +.sdk-rive { + aspect-ratio: 369/93; + width: 100%; + display: block; + overflow: hidden; + transform: translateY(0px); + transition: transform 1s ease-out !important; + will-change: transform; +} + +.sdk-rive:hover { + transform: translateY(-4px) !important; +} + +.rive-container { + width: 100%; + height: 100%; + display: block; +} + +.rive-container canvas { + width: 100%; + height: 100%; + vertical-align: baseline; + display: inline-block; +} + /*** END -- LANDING PAGE STYLING ***/ diff --git a/fern/docs.yml b/fern/docs.yml index b98a38de0..d19875b64 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -143,6 +143,8 @@ layout: js: - path: ./footer-dist/output.js strategy: beforeInteractive + - path: ./rive-animation.js + strategy: afterInteractive analytics: # posthog: diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx index 1d959a9bc..1e5dc3fe7 100644 --- a/fern/products/home/pages/welcome.mdx +++ b/fern/products/home/pages/welcome.mdx @@ -58,20 +58,12 @@ import { FernFooter } from "../../../components/FernFooter";

- - SDK Generation Preview - SDK Generation Preview - + {/* Rive Animation */} +
+
+ +
+
{/* Language Icons */}
@@ -139,8 +131,9 @@ import { FernFooter } from "../../../components/FernFooter";

- Docs Preview - Docs Preview +
+ +
@@ -190,8 +183,9 @@ import { FernFooter } from "../../../components/FernFooter";

- AI Search Mockup - AI Search Mockup +
+ +
diff --git a/fern/rive-animation.js b/fern/rive-animation.js new file mode 100644 index 000000000..6a5ec5732 --- /dev/null +++ b/fern/rive-animation.js @@ -0,0 +1,285 @@ +// Reusable Rive Animation Integration for Fern Docs +(function() { + let riveRuntimeLoaded = false; + let pendingAnimations = []; + + // Main function to create a Rive animation + function createRiveAnimation(config) { + const { + canvasSelector, // CSS selector for canvas (e.g., '#rive-canvas') + riveUrl, // CDN URL for .riv file + aspectRatio, // e.g., 369/93 or 16/9 + stateMachine = "State Machine 1", + interactiveStates = [], // e.g., ['Ruby', 'Python', 'TypeScript'] + fallbackImages = [], // Array of {src, className, alt} + fit = 'Contain' // Rive fit mode + } = config; + + const canvas = document.querySelector(canvasSelector); + if (!canvas) { + console.warn(`Canvas not found: ${canvasSelector}`); + return; + } + + // Set up container references + const riveContainer = canvas.parentElement; + const sdkRiveContainer = riveContainer.parentElement; + + // Load Rive runtime if not already loaded + if (!riveRuntimeLoaded && typeof rive === 'undefined') { + pendingAnimations.push(() => createRiveAnimation(config)); + + if (!document.querySelector('script[src*="@rive-app/canvas"]')) { + const script = document.createElement('script'); + script.src = 'https://unpkg.com/@rive-app/canvas@2.7.0'; + script.onload = function() { + riveRuntimeLoaded = true; + // Process all pending animations + pendingAnimations.forEach(fn => fn()); + pendingAnimations = []; + }; + document.head.appendChild(script); + } + return; + } + + try { + // Calculate correct dimensions with high-DPI support + const rect = riveContainer.getBoundingClientRect(); + + // Use available width, fallback to a reasonable default if container has no width yet + const width = rect.width > 0 ? rect.width : 800; + const height = width / aspectRatio; // Force correct aspect ratio + + // Set container dimensions to prevent layout shift + riveContainer.style.width = width + 'px'; + riveContainer.style.height = height + 'px'; + + // Also ensure the parent container accommodates the content + if (sdkRiveContainer) { + sdkRiveContainer.style.minHeight = height + 'px'; + } + + // Force minimum 2x resolution for crispness (even on non-retina displays) + const dpr = Math.max(window.devicePixelRatio || 1, 2); + canvas.width = width * dpr; + canvas.height = height * dpr; + + // Set explicit CSS size to prevent stretching + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + canvas.style.display = 'block'; + + // Create Rive instance + const r = new rive.Rive({ + src: riveUrl, + canvas: canvas, + autoplay: true, + stateMachines: stateMachine, + layout: new rive.Layout({ + fit: rive.Fit[fit], + alignment: rive.Alignment.Center + }), + shouldDisableRiveListeners: false, + onLoad: () => { + canvas.style.pointerEvents = 'auto'; + canvas.style.userSelect = 'none'; + + // Add interactive click handling if states are provided + if (interactiveStates.length > 0) { + setupInteractiveClicks(canvas, r, stateMachine, interactiveStates); + } + }, + onLoadError: (err) => { + console.error('Rive load error:', err); + + showFallbackImages(canvas, fallbackImages); + } + }); + + // Store instance for cleanup + canvas._riveInstance = r; + + // Add resize observer for responsive behavior + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + if (entry.target === riveContainer) { + const newWidth = entry.contentRect.width; + if (newWidth > 0) { + const newHeight = newWidth / aspectRatio; + + // Update container size + riveContainer.style.height = newHeight + 'px'; + if (sdkRiveContainer) { + sdkRiveContainer.style.minHeight = newHeight + 'px'; + } + + // Update canvas size + const dpr = Math.max(window.devicePixelRatio || 1, 2); + canvas.width = newWidth * dpr; + canvas.height = newHeight * dpr; + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; + + // Resize Rive animation + if (r && r.resizeDrawingSurfaceToCanvas) { + r.resizeDrawingSurfaceToCanvas(); + } + } + } + } + }); + + resizeObserver.observe(riveContainer); + canvas._resizeObserver = resizeObserver; + + } catch (error) { + console.error('Rive creation error:', error); + + showFallbackImages(canvas, fallbackImages); + } + } + + // Setup interactive click handling + function setupInteractiveClicks(canvas, riveInstance, stateMachine, states) { + canvas.addEventListener('click', () => { + const inputs = riveInstance.stateMachineInputs(stateMachine); + + // Find which hover state is currently active and trigger its click + for (const state of states) { + const hoverInput = inputs.find(i => i.name === `${state} Hovered`); + const clickInput = inputs.find(i => i.name === `${state} Clicked`); + + if (hoverInput && hoverInput.value && clickInput) { + clickInput.fire(); + break; + } + } + }); + } + + // Show fallback images when Rive fails to load + function showFallbackImages(canvas, fallbackImages) { + if (fallbackImages.length === 0) return; + + canvas.style.display = 'none'; + const fallbackContainer = document.createElement('div'); + + fallbackContainer.innerHTML = fallbackImages.map(img => + `${img.alt}` + ).join(''); + + canvas.parentNode.insertBefore(fallbackContainer, canvas); + } + + // Initialize predefined animations + function initializeAnimations() { + // SDK Animation (your current one) + createRiveAnimation({ + canvasSelector: '#sdk-rive-canvas', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_sdk-rive.riv', + aspectRatio: 369/93, + stateMachine: "State Machine 1", + interactiveStates: ['Ruby', 'PHP', 'NET', 'Java', 'GO', 'TypeScript', 'Python'], + fallbackImages: [ + { + src: '/learn/home/images/sdks-preview-light.svg', + className: 'sdks-preview-img dark:hidden', + alt: 'SDK Generation Preview' + }, + { + src: '/learn/home/images/sdks-preview-dark.svg', + className: 'sdks-preview-img hidden dark:block', + alt: 'SDK Generation Preview' + } + ] + }); + + // Docs Animation + createRiveAnimation({ + canvasSelector: '#docs-rive-canvas', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_579220ccfc15555b0fe23088ea2cb9bc_docs_animation.riv', + aspectRatio: 404/262, + stateMachine: "State Machine 1", + interactiveStates: [], + fallbackImages: [ + { + src: '/learn/home/images/docs-preview-light.svg', + className: 'docs-preview-img dark:hidden', + alt: 'Docs Animation Preview' + }, + { + src: '/learn/home/images/docs-preview-dark.svg', + className: 'docs-preview-img hidden dark:block', + alt: 'Docs Animation Preview' + } + ] + }); + + // AI Animation + createRiveAnimation({ + canvasSelector: '#ai-rive-canvas', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_941db85f022a36b2727f67c219fe3416_ai_animation.riv', + aspectRatio: 371/99, + stateMachine: "State Machine 1", + interactiveStates: [], + fallbackImages: [ + { + src: '/learn/home/images/ai-preview-light.svg', + className: 'ai-preview-img dark:hidden', + alt: 'AI Animation Preview' + }, + { + src: '/learn/home/images/ai-preview-dark.svg', + className: 'ai-preview-img hidden dark:block', + alt: 'AI Animation Preview' + } + ] + }); + + // Add your other Rive animations here: + // createRiveAnimation({ + // canvasSelector: '#docs-rive-canvas', + // riveUrl: 'https://your-cdn.com/docs-animation.riv', + // aspectRatio: 16/9, + // stateMachine: "Main State Machine", + // interactiveStates: ['Button1', 'Button2'], + // fallbackImages: [...] + // }); + } + + // Cleanup function to dispose of Rive instances + function cleanupRiveInstances() { + document.querySelectorAll('canvas[id$="rive-canvas"]').forEach(canvas => { + if (canvas._riveInstance) { + canvas._riveInstance.cleanup(); + canvas._riveInstance = null; + } + if (canvas._resizeObserver) { + canvas._resizeObserver.disconnect(); + canvas._resizeObserver = null; + } + }); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAnimations); + } else { + initializeAnimations(); + } + + // Re-initialize on navigation changes (for SPA behavior) + let lastUrl = location.href; + new MutationObserver(() => { + const url = location.href; + if (url !== lastUrl) { + lastUrl = url; + cleanupRiveInstances(); // Clean up before reinitializing + setTimeout(initializeAnimations, 100); + } + }).observe(document, { subtree: true, childList: true }); + + // Expose function globally for manual use + window.createRiveAnimation = createRiveAnimation; +})(); \ No newline at end of file From 4015ad64f29684eb3befb821bef0f1fa7dae6110 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 15:42:12 -0400 Subject: [PATCH 02/10] Update AI animation --- fern/products/home/pages/welcome.mdx | 2 ++ fern/rive-animation.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx index 1e5dc3fe7..bfae5a5b4 100644 --- a/fern/products/home/pages/welcome.mdx +++ b/fern/products/home/pages/welcome.mdx @@ -131,6 +131,7 @@ import { FernFooter } from "../../../components/FernFooter";

+ {/* Rive Animation */}
@@ -183,6 +184,7 @@ import { FernFooter } from "../../../components/FernFooter";

+ {/* Rive Animation */}
diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 6a5ec5732..88a94886f 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -219,8 +219,8 @@ // AI Animation createRiveAnimation({ canvasSelector: '#ai-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_941db85f022a36b2727f67c219fe3416_ai_animation.riv', - aspectRatio: 371/99, + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_bded8d9202f9014c85204d35ebaed64e_ai_animation.riv', + aspectRatio: 400/99, stateMachine: "State Machine 1", interactiveStates: [], fallbackImages: [ From bb78fea0356959811a6e7bc976b730cc59e4b50d Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 16:32:27 -0400 Subject: [PATCH 03/10] Better Rive interactions --- fern/assets/styles.css | 14 +++++- fern/rive-animation.js | 105 +++++++++++++++++++++++++++-------------- 2 files changed, 82 insertions(+), 37 deletions(-) diff --git a/fern/assets/styles.css b/fern/assets/styles.css index 8a6f5678d..5656f21e1 100644 --- a/fern/assets/styles.css +++ b/fern/assets/styles.css @@ -852,20 +852,26 @@ a[href*="changelog"] svg { aspect-ratio: 369/93; width: 100%; display: block; - overflow: hidden; + overflow: visible; /* Changed from hidden - was clipping interactive areas */ transform: translateY(0px); transition: transform 1s ease-out !important; will-change: transform; + /* Ensure click events can reach the canvas */ + pointer-events: auto; } .sdk-rive:hover { transform: translateY(-4px) !important; } +/* Ensure the canvas can receive clicks and is properly layered */ .rive-container { width: 100%; height: 100%; display: block; + position: relative; + /* Ensure container doesn't block events */ + pointer-events: none; } .rive-container canvas { @@ -873,6 +879,12 @@ a[href*="changelog"] svg { height: 100%; vertical-align: baseline; display: inline-block; + /* Allow canvas to receive events while container doesn't */ + pointer-events: auto !important; + position: relative; + z-index: 1; + vertical-align: baseline; + display: inline-block; } /*** END -- LANDING PAGE STYLING ***/ diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 88a94886f..1c68b12e7 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -1,16 +1,15 @@ -// Reusable Rive Animation Integration for Fern Docs +// Reusable Rive Animation Integration - Uses Native Rive Interactions Only (function() { let riveRuntimeLoaded = false; let pendingAnimations = []; - // Main function to create a Rive animation + // Create a Rive animation with automatic cursor detection - all interactions handled by Rive natively function createRiveAnimation(config) { const { canvasSelector, // CSS selector for canvas (e.g., '#rive-canvas') riveUrl, // CDN URL for .riv file aspectRatio, // e.g., 369/93 or 16/9 stateMachine = "State Machine 1", - interactiveStates = [], // e.g., ['Ruby', 'Python', 'TypeScript'] fallbackImages = [], // Array of {src, className, alt} fit = 'Contain' // Rive fit mode } = config; @@ -31,7 +30,7 @@ if (!document.querySelector('script[src*="@rive-app/canvas"]')) { const script = document.createElement('script'); - script.src = 'https://unpkg.com/@rive-app/canvas@2.7.0'; + script.src = 'https://unpkg.com/@rive-app/canvas@latest'; script.onload = function() { riveRuntimeLoaded = true; // Process all pending animations @@ -80,15 +79,14 @@ fit: rive.Fit[fit], alignment: rive.Alignment.Center }), - shouldDisableRiveListeners: false, + shouldDisableRiveListeners: false, // Enable native Rive interactions (hover, click, etc.) onLoad: () => { + // Let Rive handle its own interactions natively canvas.style.pointerEvents = 'auto'; canvas.style.userSelect = 'none'; - // Add interactive click handling if states are provided - if (interactiveStates.length > 0) { - setupInteractiveClicks(canvas, r, stateMachine, interactiveStates); - } + // Set up dynamic cursor changes based on Rive's interactive areas + setupDynamicCursor(canvas, r, stateMachine); }, onLoadError: (err) => { console.error('Rive load error:', err); @@ -140,22 +138,68 @@ } } - // Setup interactive click handling - function setupInteractiveClicks(canvas, riveInstance, stateMachine, states) { - canvas.addEventListener('click', () => { - const inputs = riveInstance.stateMachineInputs(stateMachine); - - // Find which hover state is currently active and trigger its click - for (const state of states) { - const hoverInput = inputs.find(i => i.name === `${state} Hovered`); - const clickInput = inputs.find(i => i.name === `${state} Clicked`); + // Setup dynamic cursor changes based on Rive's interactive areas + function setupDynamicCursor(canvas, riveInstance, stateMachine) { + canvas.addEventListener('mousemove', (event) => { + try { + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // Scale coordinates for high DPI displays + const dpr = window.devicePixelRatio || 1; + const scaledX = x * dpr; + const scaledY = y * dpr; - if (hoverInput && hoverInput.value && clickInput) { - clickInput.fire(); - break; + let shouldShowPointer = false; + + // Try different methods to detect interactive areas (in priority order) + if (riveInstance.hitTest && typeof riveInstance.hitTest === 'function') { + // Best method: Use Rive's built-in hit testing + shouldShowPointer = riveInstance.hitTest(scaledX, scaledY); + } else if (riveInstance.cursor) { + // Good method: Check Rive's cursor property + shouldShowPointer = riveInstance.cursor === 'pointer' || riveInstance.cursor === 'hand'; + } else if (riveInstance.renderer && riveInstance.renderer.cursor) { + // Fallback method: Check renderer's cursor + shouldShowPointer = riveInstance.renderer.cursor === 'pointer'; + } else { + // Last resort: Check hover states in any state machine + try { + const stateMachineNames = riveInstance.stateMachineNames || [stateMachine]; + let foundHoverStates = []; + + for (const smName of stateMachineNames) { + try { + const inputs = riveInstance.stateMachineInputs(smName); + const hoverStates = inputs.filter(input => + (input.name.toLowerCase().includes('hover') || + input.name.toLowerCase().includes('over') || + input.name.toLowerCase().includes('hovered')) && + input.value + ); + foundHoverStates.push(...hoverStates); + } catch (e) { + continue; // Skip failed state machines + } + } + + shouldShowPointer = foundHoverStates.length > 0; + } catch (e) { + // Ignore errors and default to no pointer + } } + + canvas.style.cursor = shouldShowPointer ? 'pointer' : 'default'; + + } catch (e) { + canvas.style.cursor = 'default'; } }); + + canvas.addEventListener('mouseleave', () => { + canvas.style.cursor = 'default'; + }); } // Show fallback images when Rive fails to load @@ -174,13 +218,12 @@ // Initialize predefined animations function initializeAnimations() { - // SDK Animation (your current one) + // SDK Animation with native Rive interactions createRiveAnimation({ canvasSelector: '#sdk-rive-canvas', riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_sdk-rive.riv', aspectRatio: 369/93, stateMachine: "State Machine 1", - interactiveStates: ['Ruby', 'PHP', 'NET', 'Java', 'GO', 'TypeScript', 'Python'], fallbackImages: [ { src: '/learn/home/images/sdks-preview-light.svg', @@ -201,7 +244,6 @@ riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_579220ccfc15555b0fe23088ea2cb9bc_docs_animation.riv', aspectRatio: 404/262, stateMachine: "State Machine 1", - interactiveStates: [], fallbackImages: [ { src: '/learn/home/images/docs-preview-light.svg', @@ -219,10 +261,9 @@ // AI Animation createRiveAnimation({ canvasSelector: '#ai-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_bded8d9202f9014c85204d35ebaed64e_ai_animation.riv', - aspectRatio: 400/99, + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_afec146ef95157cbef522bcdee1ba8d9_ai_animation.riv', + aspectRatio: 371/99, stateMachine: "State Machine 1", - interactiveStates: [], fallbackImages: [ { src: '/learn/home/images/ai-preview-light.svg', @@ -237,15 +278,7 @@ ] }); - // Add your other Rive animations here: - // createRiveAnimation({ - // canvasSelector: '#docs-rive-canvas', - // riveUrl: 'https://your-cdn.com/docs-animation.riv', - // aspectRatio: 16/9, - // stateMachine: "Main State Machine", - // interactiveStates: ['Button1', 'Button2'], - // fallbackImages: [...] - // }); + // Add additional Rive animations by calling createRiveAnimation() with your config } // Cleanup function to dispose of Rive instances From 1fdfee8991e78a1caf48a5fe46e9e3fbbab449cf Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 16:48:34 -0400 Subject: [PATCH 04/10] Cleaned up event handler --- fern/rive-animation.js | 51 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 1c68b12e7..4e754fa23 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -1,9 +1,9 @@ -// Reusable Rive Animation Integration - Uses Native Rive Interactions Only +// Reusable Rive Animation Integration - Native Interactions + Event Handling (function() { let riveRuntimeLoaded = false; let pendingAnimations = []; - // Create a Rive animation with automatic cursor detection - all interactions handled by Rive natively + // Create a Rive animation with cursor detection, native interactions, and event handling function createRiveAnimation(config) { const { canvasSelector, // CSS selector for canvas (e.g., '#rive-canvas') @@ -11,7 +11,8 @@ aspectRatio, // e.g., 369/93 or 16/9 stateMachine = "State Machine 1", fallbackImages = [], // Array of {src, className, alt} - fit = 'Contain' // Rive fit mode + fit = 'Contain', // Rive fit mode + eventHandlers = {} // Custom event handlers: {'Event Name': (eventData) => {...}} } = config; const canvas = document.querySelector(canvasSelector); @@ -87,6 +88,17 @@ // Set up dynamic cursor changes based on Rive's interactive areas setupDynamicCursor(canvas, r, stateMachine); + + // Set up Rive event handling (primary method) + try { + if (r.on && rive.EventType && rive.EventType.RiveEvent) { + r.on(rive.EventType.RiveEvent, (riveEvent) => { + handleRiveEvent(riveEvent, eventHandlers); + }); + } + } catch (e) { + console.warn('Could not set up Rive event listeners:', e); + } }, onLoadError: (err) => { console.error('Rive load error:', err); @@ -202,6 +214,26 @@ }); } + + + // Handle any Rive event (explicit handlers only) + function handleRiveEvent(riveEvent, eventHandlers = {}) { + try { + const eventData = riveEvent.data; + const eventName = eventData.name; + + // Only handle events that have explicitly defined handlers + if (eventHandlers[eventName]) { + eventHandlers[eventName](eventData); + } + // If no handler is defined, the event is ignored (no fallbacks) + } catch (e) { + console.error('Error handling Rive event:', e); + } + } + + + // Show fallback images when Rive fails to load function showFallbackImages(canvas, fallbackImages) { if (fallbackImages.length === 0) return; @@ -236,6 +268,7 @@ alt: 'SDK Generation Preview' } ] + // No eventHandlers - this animation only uses native interactions }); // Docs Animation @@ -256,9 +289,10 @@ alt: 'Docs Animation Preview' } ] + // No eventHandlers - this animation only uses native interactions }); - // AI Animation + // AI Animation with custom event handling createRiveAnimation({ canvasSelector: '#ai-rive-canvas', riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_afec146ef95157cbef522bcdee1ba8d9_ai_animation.riv', @@ -275,7 +309,14 @@ className: 'ai-preview-img hidden dark:block', alt: 'AI Animation Preview' } - ] + ], + eventHandlers: { + 'Open URL': (eventData) => { + // Custom URL handling for AI animation + console.log('AI animation URL event:', eventData.url); + window.open(eventData.url, '_blank', 'noopener,noreferrer'); + } + } }); // Add additional Rive animations by calling createRiveAnimation() with your config From 3de5495c835d6fad55337b357d43a520006b2a81 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 16:56:43 -0400 Subject: [PATCH 05/10] Resize and sizing cleanup --- fern/rive-animation.js | 89 ++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 60 deletions(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 4e754fa23..ad7d2b879 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -11,7 +11,7 @@ aspectRatio, // e.g., 369/93 or 16/9 stateMachine = "State Machine 1", fallbackImages = [], // Array of {src, className, alt} - fit = 'Contain', // Rive fit mode + fit = 'Contain', // Rive fit mode (Contain, Cover, FitWidth, FitHeight, None) eventHandlers = {} // Custom event handlers: {'Event Name': (eventData) => {...}} } = config; @@ -44,40 +44,25 @@ } try { - // Calculate correct dimensions with high-DPI support - const rect = riveContainer.getBoundingClientRect(); - - // Use available width, fallback to a reasonable default if container has no width yet - const width = rect.width > 0 ? rect.width : 800; - const height = width / aspectRatio; // Force correct aspect ratio - - // Set container dimensions to prevent layout shift - riveContainer.style.width = width + 'px'; - riveContainer.style.height = height + 'px'; - - // Also ensure the parent container accommodates the content - if (sdkRiveContainer) { - sdkRiveContainer.style.minHeight = height + 'px'; + // Set container aspect ratio while respecting existing layout + const currentWidth = riveContainer.offsetWidth || riveContainer.clientWidth; + if (currentWidth > 0) { + // Use existing width, set height based on aspect ratio + riveContainer.style.height = (currentWidth / aspectRatio) + 'px'; + } else { + // Fallback: use CSS aspect-ratio if container has no initial width + riveContainer.style.aspectRatio = aspectRatio.toString(); + riveContainer.style.width = '100%'; } - // Force minimum 2x resolution for crispness (even on non-retina displays) - const dpr = Math.max(window.devicePixelRatio || 1, 2); - canvas.width = width * dpr; - canvas.height = height * dpr; - - // Set explicit CSS size to prevent stretching - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; - canvas.style.display = 'block'; - - // Create Rive instance + // Let Rive handle canvas sizing internally - much simpler! const r = new rive.Rive({ src: riveUrl, canvas: canvas, autoplay: true, stateMachines: stateMachine, layout: new rive.Layout({ - fit: rive.Fit[fit], + fit: rive.Fit[fit], // Configurable fit mode alignment: rive.Alignment.Center }), shouldDisableRiveListeners: false, // Enable native Rive interactions (hover, click, etc.) @@ -86,6 +71,9 @@ canvas.style.pointerEvents = 'auto'; canvas.style.userSelect = 'none'; + // Critical: Resize drawing surface to canvas after load + r.resizeDrawingSurfaceToCanvas(); + // Set up dynamic cursor changes based on Rive's interactive areas setupDynamicCursor(canvas, r, stateMachine); @@ -110,38 +98,19 @@ // Store instance for cleanup canvas._riveInstance = r; - // Add resize observer for responsive behavior - const resizeObserver = new ResizeObserver(entries => { - for (let entry of entries) { - if (entry.target === riveContainer) { - const newWidth = entry.contentRect.width; - if (newWidth > 0) { - const newHeight = newWidth / aspectRatio; - - // Update container size - riveContainer.style.height = newHeight + 'px'; - if (sdkRiveContainer) { - sdkRiveContainer.style.minHeight = newHeight + 'px'; - } - - // Update canvas size - const dpr = Math.max(window.devicePixelRatio || 1, 2); - canvas.width = newWidth * dpr; - canvas.height = newHeight * dpr; - canvas.style.width = newWidth + 'px'; - canvas.style.height = newHeight + 'px'; - - // Resize Rive animation - if (r && r.resizeDrawingSurfaceToCanvas) { - r.resizeDrawingSurfaceToCanvas(); - } - } - } + // Simple resize handling (following Rive best practices) + const windowResizeHandler = () => { + // Let Rive handle the canvas sizing internally + if (r && r.resizeDrawingSurfaceToCanvas) { + r.resizeDrawingSurfaceToCanvas(); } - }); + }; + + // Add window resize listener (as per Rive documentation) + window.addEventListener('resize', windowResizeHandler, false); - resizeObserver.observe(riveContainer); - canvas._resizeObserver = resizeObserver; + // Store cleanup function + canvas._windowResizeHandler = windowResizeHandler; } catch (error) { console.error('Rive creation error:', error); @@ -329,9 +298,9 @@ canvas._riveInstance.cleanup(); canvas._riveInstance = null; } - if (canvas._resizeObserver) { - canvas._resizeObserver.disconnect(); - canvas._resizeObserver = null; + if (canvas._windowResizeHandler) { + window.removeEventListener('resize', canvas._windowResizeHandler, false); + canvas._windowResizeHandler = null; } }); } From a25e38cf627f2fb5901e6ea8d66c78f33ce8818a Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 17:12:36 -0400 Subject: [PATCH 06/10] Update rive-animation.js --- fern/rive-animation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index ad7d2b879..a5a8571f9 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -243,7 +243,7 @@ // Docs Animation createRiveAnimation({ canvasSelector: '#docs-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_579220ccfc15555b0fe23088ea2cb9bc_docs_animation.riv', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_8014c1e8d9aa904e6d659541b198df0f_docs_animation.riv', aspectRatio: 404/262, stateMachine: "State Machine 1", fallbackImages: [ From 4628630bcd91173b13a2f7494b374e18ee0fa1f2 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 19:13:08 -0400 Subject: [PATCH 07/10] Update all animations for Light and Dark support --- fern/assets/styles.css | 4 +- fern/rive-animation.js | 185 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 182 insertions(+), 7 deletions(-) diff --git a/fern/assets/styles.css b/fern/assets/styles.css index 5656f21e1..3ef53dbb7 100644 --- a/fern/assets/styles.css +++ b/fern/assets/styles.css @@ -854,14 +854,14 @@ a[href*="changelog"] svg { display: block; overflow: visible; /* Changed from hidden - was clipping interactive areas */ transform: translateY(0px); - transition: transform 1s ease-out !important; + transition: transform 250ms ease-out !important; will-change: transform; /* Ensure click events can reach the canvas */ pointer-events: auto; } .sdk-rive:hover { - transform: translateY(-4px) !important; + transform: translateY(-3px) !important; } /* Ensure the canvas can receive clicks and is properly layered */ diff --git a/fern/rive-animation.js b/fern/rive-animation.js index a5a8571f9..7d0df1d40 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -61,6 +61,7 @@ canvas: canvas, autoplay: true, stateMachines: stateMachine, + autoBind: false, // Manual binding for theme switching layout: new rive.Layout({ fit: rive.Fit[fit], // Configurable fit mode alignment: rive.Alignment.Center @@ -77,6 +78,12 @@ // Set up dynamic cursor changes based on Rive's interactive areas setupDynamicCursor(canvas, r, stateMachine); + // Initialize with current site theme + if (r.viewModelCount > 0) { + const currentIsDark = detectSiteTheme(); + setRiveTheme(r, currentIsDark, 'Palette'); + } + // Set up Rive event handling (primary method) try { if (r.on && rive.EventType && rive.EventType.RiveEvent) { @@ -222,7 +229,7 @@ // SDK Animation with native Rive interactions createRiveAnimation({ canvasSelector: '#sdk-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_sdk-rive.riv', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_12235f640f9ad3339b42e8d026c6345a_sdk-rive.riv', aspectRatio: 369/93, stateMachine: "State Machine 1", fallbackImages: [ @@ -243,7 +250,7 @@ // Docs Animation createRiveAnimation({ canvasSelector: '#docs-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_8014c1e8d9aa904e6d659541b198df0f_docs_animation.riv', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825994c55f0eece04ce4e2_d261956a0f627eb6b94c39aa9fcc26f0_docs_animation.riv', aspectRatio: 404/262, stateMachine: "State Machine 1", fallbackImages: [ @@ -264,7 +271,7 @@ // AI Animation with custom event handling createRiveAnimation({ canvasSelector: '#ai-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_afec146ef95157cbef522bcdee1ba8d9_ai_animation.riv', + riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68825e97fd6225e1c8a7488c_b8d233d3b43c3da6eff8cc65874d7b49_ai_animation.riv', aspectRatio: 371/99, stateMachine: "State Machine 1", fallbackImages: [ @@ -289,6 +296,13 @@ }); // Add additional Rive animations by calling createRiveAnimation() with your config + + // Enable automatic site theme synchronization + setupRiveSiteThemeSync(); + + // Or manually control themes: + // switchRiveThemes(true); // Switch to dark theme + // switchRiveThemes(false); // Switch to light theme } // Cleanup function to dispose of Rive instances @@ -323,6 +337,167 @@ } }).observe(document, { subtree: true, childList: true }); - // Expose function globally for manual use + // Data Binding approach using View Model Instances (Official Rive API) + function setRiveTheme(riveInstance, isDark, viewModelName = 'Palette') { + try { + // Get the view model by name + const viewModel = riveInstance.viewModelByName(viewModelName); + if (!viewModel) { + console.warn(`View model '${viewModelName}' not found.`); + return false; + } + + // Check if instances exist + if (viewModel.instanceCount === 0) { + console.warn(`No instances found in view model '${viewModelName}'. Please create instances in the Rive editor.`); + return false; + } + + let viewModelInstance = null; + const instanceName = isDark ? 'Dark' : 'Light'; + + // Try 1: Get instance by name (if names are set) + try { + viewModelInstance = viewModel.instanceByName(instanceName); + } catch (e) { + // Name-based access failed, continue to index-based + } + + // Try 2: Get instance by index (fallback for unnamed instances) + if (!viewModelInstance) { + const instanceIndex = isDark ? 1 : 0; // Assume: 0=Light, 1=Dark + if (instanceIndex < viewModel.instanceCount) { + viewModelInstance = viewModel.instanceByIndex(instanceIndex); + console.log(`Using instance by index: ${instanceIndex} (${isDark ? 'Dark' : 'Light'} theme)`); + } else { + console.warn(`Instance index ${instanceIndex} not available. Only ${viewModel.instanceCount} instances found.`); + return false; + } + } + + if (!viewModelInstance) { + console.warn(`Could not get ${instanceName} theme instance.`); + return false; + } + + // Bind the instance to the Rive file (this switches the theme) + riveInstance.bindViewModelInstance(viewModelInstance); + console.log(`Successfully switched to ${isDark ? 'Dark' : 'Light'} theme`); + return true; + + } catch (e) { + console.warn('Could not switch Rive theme instance:', e); + return false; + } + } + + // Switch all animations to light/dark theme using Data Binding + function switchAllRiveThemes(isDark) { + document.querySelectorAll('canvas[id$="rive-canvas"]').forEach(canvas => { + if (canvas._riveInstance) { + setRiveTheme(canvas._riveInstance, isDark); + } + }); + } + + // Detect current site theme using multiple methods + function detectSiteTheme() { + // Method 1: Check HTML/body classes (most common) + if (document.documentElement.classList.contains('dark') || + document.body.classList.contains('dark') || + document.documentElement.classList.contains('dark-mode') || + document.body.classList.contains('dark-mode')) { + return true; + } + + // Method 2: Check data attributes + if (document.documentElement.getAttribute('data-theme') === 'dark' || + document.body.getAttribute('data-theme') === 'dark' || + document.documentElement.getAttribute('theme') === 'dark') { + return true; + } + + // Method 3: Check CSS custom properties (if available) + if (typeof getComputedStyle !== 'undefined') { + const style = getComputedStyle(document.documentElement); + const colorScheme = style.getPropertyValue('color-scheme'); + if (colorScheme.includes('dark')) { + return true; + } + } + + // Method 4: Fallback to system preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return true; + } + + return false; // Default to light theme + } + + // Set up comprehensive theme detection and syncing + function setupRiveSiteThemeSync() { + // Set initial theme based on current site state + const initialIsDark = detectSiteTheme(); + switchAllRiveThemes(initialIsDark); + console.log('Rive themes initialized:', initialIsDark ? 'Dark' : 'Light'); + + // Method 1: Watch for class changes on html/body (most reliable) + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + (mutation.attributeName === 'class' || + mutation.attributeName === 'data-theme' || + mutation.attributeName === 'theme')) { + const isDark = detectSiteTheme(); + switchAllRiveThemes(isDark); + console.log('Site theme changed, Rive themes updated:', isDark ? 'Dark' : 'Light'); + } + }); + }); + + // Observe both html and body for attribute changes + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'data-theme', 'theme'] + }); + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'data-theme', 'theme'] + }); + + // Method 2: Listen for system color scheme changes (backup) + if (window.matchMedia) { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + darkModeMediaQuery.addEventListener('change', (e) => { + // Only apply system preference if no explicit site theme is set + if (!document.documentElement.classList.contains('dark') && + !document.documentElement.classList.contains('light') && + !document.documentElement.getAttribute('data-theme')) { + switchAllRiveThemes(e.matches); + console.log('System theme changed, Rive themes updated:', e.matches ? 'Dark' : 'Light'); + } + }); + } + + // Method 3: Listen for custom theme events (if your site dispatches them) + document.addEventListener('themeChanged', (e) => { + if (e.detail && typeof e.detail.isDark === 'boolean') { + switchAllRiveThemes(e.detail.isDark); + console.log('Custom theme event received, Rive themes updated:', e.detail.isDark ? 'Dark' : 'Light'); + } + }); + + return observer; // Return observer for cleanup if needed + } + + // Legacy function for backward compatibility + function setupRiveSystemThemeDetection() { + return setupRiveSiteThemeSync(); + } + + // Expose functions globally for manual use window.createRiveAnimation = createRiveAnimation; -})(); \ No newline at end of file + window.switchRiveThemes = switchAllRiveThemes; + window.setupRiveSystemThemeDetection = setupRiveSystemThemeDetection; + window.setupRiveSiteThemeSync = setupRiveSiteThemeSync; +})(); \ No newline at end of file From ab47b416719e914595aa2fd25c42bf555102fd56 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Thu, 24 Jul 2025 19:28:13 -0400 Subject: [PATCH 08/10] Updated fallback image --- fern/rive-animation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 7d0df1d40..25360e3d0 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -276,12 +276,12 @@ stateMachine: "State Machine 1", fallbackImages: [ { - src: '/learn/home/images/ai-preview-light.svg', + src: '/learn/home/images/ai-search-preview-light.svg', className: 'ai-preview-img dark:hidden', alt: 'AI Animation Preview' }, { - src: '/learn/home/images/ai-preview-dark.svg', + src: '/learn/home/images/ai-search-preview-dark.svg', className: 'ai-preview-img hidden dark:block', alt: 'AI Animation Preview' } From e7c17460171384f0c5b2a2a5bbcf58758031761f Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Fri, 25 Jul 2025 10:06:34 -0400 Subject: [PATCH 09/10] Update rive-animation.js --- fern/rive-animation.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 25360e3d0..6e8c7fe80 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -226,6 +226,11 @@ // Initialize predefined animations function initializeAnimations() { + // Only run on home product page + if (!window.location.pathname.includes('/home')) { + return; + } + // SDK Animation with native Rive interactions createRiveAnimation({ canvasSelector: '#sdk-rive-canvas', @@ -234,12 +239,12 @@ stateMachine: "State Machine 1", fallbackImages: [ { - src: '/learn/home/images/sdks-preview-light.svg', + src: './images/sdks-preview-light.svg', className: 'sdks-preview-img dark:hidden', alt: 'SDK Generation Preview' }, { - src: '/learn/home/images/sdks-preview-dark.svg', + src: './images/sdks-preview-dark.svg', className: 'sdks-preview-img hidden dark:block', alt: 'SDK Generation Preview' } @@ -255,12 +260,12 @@ stateMachine: "State Machine 1", fallbackImages: [ { - src: '/learn/home/images/docs-preview-light.svg', + src: './images/docs-preview-light.svg', className: 'docs-preview-img dark:hidden', alt: 'Docs Animation Preview' }, { - src: '/learn/home/images/docs-preview-dark.svg', + src: './images/docs-preview-dark.svg', className: 'docs-preview-img hidden dark:block', alt: 'Docs Animation Preview' } @@ -276,19 +281,19 @@ stateMachine: "State Machine 1", fallbackImages: [ { - src: '/learn/home/images/ai-search-preview-light.svg', + src: './images/ai-search-preview-light.svg', className: 'ai-preview-img dark:hidden', alt: 'AI Animation Preview' }, { - src: '/learn/home/images/ai-search-preview-dark.svg', + src: './images/ai-search-preview-dark.svg', className: 'ai-preview-img hidden dark:block', alt: 'AI Animation Preview' } ], eventHandlers: { 'Open URL': (eventData) => { - // Custom URL handling for AI animation + // Custom URL handling for AI animation, URL is defined in the Rive file console.log('AI animation URL event:', eventData.url); window.open(eventData.url, '_blank', 'noopener,noreferrer'); } From 94640b441c134c87dfffb578bc0cfbdcb22019d2 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Fri, 25 Jul 2025 10:15:56 -0400 Subject: [PATCH 10/10] Cleanup theme switching methods --- fern/rive-animation.js | 83 ++++-------------------------------------- 1 file changed, 8 insertions(+), 75 deletions(-) diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 6e8c7fe80..3244bb9fc 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -373,7 +373,6 @@ const instanceIndex = isDark ? 1 : 0; // Assume: 0=Light, 1=Dark if (instanceIndex < viewModel.instanceCount) { viewModelInstance = viewModel.instanceByIndex(instanceIndex); - console.log(`Using instance by index: ${instanceIndex} (${isDark ? 'Dark' : 'Light'} theme)`); } else { console.warn(`Instance index ${instanceIndex} not available. Only ${viewModel.instanceCount} instances found.`); return false; @@ -387,7 +386,6 @@ // Bind the instance to the Rive file (this switches the theme) riveInstance.bindViewModelInstance(viewModelInstance); - console.log(`Successfully switched to ${isDark ? 'Dark' : 'Light'} theme`); return true; } catch (e) { @@ -405,104 +403,39 @@ }); } - // Detect current site theme using multiple methods + // Detect current site theme for Tailwind CSS class-based approach function detectSiteTheme() { - // Method 1: Check HTML/body classes (most common) - if (document.documentElement.classList.contains('dark') || - document.body.classList.contains('dark') || - document.documentElement.classList.contains('dark-mode') || - document.body.classList.contains('dark-mode')) { - return true; - } - - // Method 2: Check data attributes - if (document.documentElement.getAttribute('data-theme') === 'dark' || - document.body.getAttribute('data-theme') === 'dark' || - document.documentElement.getAttribute('theme') === 'dark') { - return true; - } - - // Method 3: Check CSS custom properties (if available) - if (typeof getComputedStyle !== 'undefined') { - const style = getComputedStyle(document.documentElement); - const colorScheme = style.getPropertyValue('color-scheme'); - if (colorScheme.includes('dark')) { - return true; - } - } - - // Method 4: Fallback to system preference - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - return true; - } - - return false; // Default to light theme + // Check for 'dark' class on html element (Tailwind CSS approach) + return document.documentElement.classList.contains('dark'); } - // Set up comprehensive theme detection and syncing + // Set up theme detection and syncing function setupRiveSiteThemeSync() { // Set initial theme based on current site state const initialIsDark = detectSiteTheme(); switchAllRiveThemes(initialIsDark); - console.log('Rive themes initialized:', initialIsDark ? 'Dark' : 'Light'); - // Method 1: Watch for class changes on html/body (most reliable) + // Watch for class changes on html element const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - (mutation.attributeName === 'class' || - mutation.attributeName === 'data-theme' || - mutation.attributeName === 'theme')) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const isDark = detectSiteTheme(); switchAllRiveThemes(isDark); - console.log('Site theme changed, Rive themes updated:', isDark ? 'Dark' : 'Light'); } }); }); - // Observe both html and body for attribute changes + // Observe html element for class changes observer.observe(document.documentElement, { attributes: true, - attributeFilter: ['class', 'data-theme', 'theme'] - }); - observer.observe(document.body, { - attributes: true, - attributeFilter: ['class', 'data-theme', 'theme'] - }); - - // Method 2: Listen for system color scheme changes (backup) - if (window.matchMedia) { - const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - darkModeMediaQuery.addEventListener('change', (e) => { - // Only apply system preference if no explicit site theme is set - if (!document.documentElement.classList.contains('dark') && - !document.documentElement.classList.contains('light') && - !document.documentElement.getAttribute('data-theme')) { - switchAllRiveThemes(e.matches); - console.log('System theme changed, Rive themes updated:', e.matches ? 'Dark' : 'Light'); - } - }); - } - - // Method 3: Listen for custom theme events (if your site dispatches them) - document.addEventListener('themeChanged', (e) => { - if (e.detail && typeof e.detail.isDark === 'boolean') { - switchAllRiveThemes(e.detail.isDark); - console.log('Custom theme event received, Rive themes updated:', e.detail.isDark ? 'Dark' : 'Light'); - } + attributeFilter: ['class'] }); return observer; // Return observer for cleanup if needed } - // Legacy function for backward compatibility - function setupRiveSystemThemeDetection() { - return setupRiveSiteThemeSync(); - } - // Expose functions globally for manual use window.createRiveAnimation = createRiveAnimation; window.switchRiveThemes = switchAllRiveThemes; - window.setupRiveSystemThemeDetection = setupRiveSystemThemeDetection; window.setupRiveSiteThemeSync = setupRiveSiteThemeSync; })(); \ No newline at end of file