From 993515e750babc487f0e62422d11b12c5e48cbf3 Mon Sep 17 00:00:00 2001 From: harang Date: Sat, 27 Jun 2026 15:35:53 +0900 Subject: [PATCH 1/5] docs: add Expo Snack demo embed Expose the public Snack alongside the example project in README and docs. Add the same Rspress Snack embed pattern used by the reference docs. Constraint: Do not change GitHub Actions setup in this follow-up Confidence: high Scope-risk: narrow Tested: CI=true pnpm docs:typecheck Tested: CI=true pnpm docs:build Tested: CI=true pnpm format:check targeted docs files Tested: git diff --check --- README.ko.md | 1 + README.md | 1 + docs/components/ExpoSnackEmbed.tsx | 70 +++++++++++++++++++ .../en/guide/getting-started/quick-start.mdx | 10 ++- .../ko/guide/getting-started/quick-start.mdx | 10 ++- docs/expo-snack.d.ts | 6 ++ docs/tsconfig.json | 3 +- 7 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 docs/components/ExpoSnackEmbed.tsx create mode 100644 docs/expo-snack.d.ts diff --git a/README.ko.md b/README.ko.md index aaf6a03..73ea9f0 100644 --- a/README.ko.md +++ b/README.ko.md @@ -35,6 +35,7 @@ Reanimated, Worklets, Gesture Handler를 기반으로 card stack, like/pass butt ### 예제 & 데모 - [📁 예제 프로젝트](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - 다양한 use case를 담은 실제 구현 코드입니다 +- [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - Expo Snack에서 바로 체험할 수 있습니다 ### 🤖 AI diff --git a/README.md b/README.md index 1179f2d..f47f74e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Full documentation is available at: ### Examples & Demo - [📁 Example Project](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - Real implementation code with various use cases +- [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - Try it instantly on Expo Snack ### 🤖 AI diff --git a/docs/components/ExpoSnackEmbed.tsx b/docs/components/ExpoSnackEmbed.tsx new file mode 100644 index 0000000..c231733 --- /dev/null +++ b/docs/components/ExpoSnackEmbed.tsx @@ -0,0 +1,70 @@ +import { useEffect, useRef } from 'react'; + +type Props = { + snackId: string; + platform?: 'web' | 'ios' | 'android'; + preview?: boolean; + height?: number | `${number}px`; +}; + +export function ExpoSnackEmbed({ + snackId, + platform = 'web', + preview = true, + height = '505px', +}: Props) { + const containerRef = useRef(null); + + useEffect(() => { + const loadAndInitialize = async () => { + try { + if (!document.querySelector('script[src="https://snack.expo.dev/embed.js"]')) { + const script = document.createElement('script'); + script.src = 'https://snack.expo.dev/embed.js'; + script.async = true; + document.head.appendChild(script); + + await new Promise((resolve, reject) => { + script.addEventListener('load', resolve); + script.addEventListener('error', reject); + }); + } + } catch (error) { + console.error('Failed to load Expo Snack embed:', error); + } + + setTimeout(() => { + if (window.ExpoSnack && containerRef.current) { + const existingEmbed = containerRef.current.querySelector('iframe'); + + if (existingEmbed) { + existingEmbed.remove(); + } + + if (window.ExpoSnack?.initialize) { + window.ExpoSnack?.initialize?.(); + } + } + }, 100); + }; + + loadAndInitialize(); + }, [snackId]); + + return ( +
+ ); +} diff --git a/docs/docs/1.x/en/guide/getting-started/quick-start.mdx b/docs/docs/1.x/en/guide/getting-started/quick-start.mdx index 7f0c327..94cb945 100644 --- a/docs/docs/1.x/en/guide/getting-started/quick-start.mdx +++ b/docs/docs/1.x/en/guide/getting-started/quick-start.mdx @@ -1,3 +1,5 @@ +import { ExpoSnackEmbed } from '../../../../../components/ExpoSnackEmbed'; + # Quick Start Create one typed deck family for your item type. `Root`, `Card`, hooks, actions, @@ -5,8 +7,12 @@ and events from that factory share the same registry namespace. ## Examples & Demo -[📁 Example Project](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - -Real implementation code with various use cases. +- [📁 Example Project](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - + Real implementation code with various use cases. +- [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - + Try it instantly on Expo Snack. + + ```tsx import { Text, View } from 'react-native'; diff --git a/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx b/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx index 5ffc522..3000f13 100644 --- a/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx +++ b/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx @@ -1,3 +1,5 @@ +import { ExpoSnackEmbed } from '../../../../../components/ExpoSnackEmbed'; + # 빠른 시작 Item type에 맞는 typed deck family를 하나 만드세요. 같은 factory의 `Root`, `Card`, @@ -5,8 +7,12 @@ hook, action, event가 같은 registry namespace를 공유합니다. ## 예제 & 데모 -[📁 예제 프로젝트](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - -다양한 use case를 담은 실제 구현 코드입니다. +- [📁 예제 프로젝트](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - + 다양한 use case를 담은 실제 구현 코드입니다. +- [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - + Expo Snack에서 바로 체험할 수 있습니다. + + ```tsx import { Text, View } from 'react-native'; diff --git a/docs/expo-snack.d.ts b/docs/expo-snack.d.ts new file mode 100644 index 0000000..41525ba --- /dev/null +++ b/docs/expo-snack.d.ts @@ -0,0 +1,6 @@ +interface Window { + ExpoSnack?: { + initialize?: () => void; + embed?: (element: HTMLElement) => void; + }; +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json index fa4849d..2ce14f9 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -13,10 +13,11 @@ "useDefineForClassFields": true, "allowImportingTsExtensions": true, "paths": { + "@components": ["./components"], "@theme": ["./theme"] } }, - "include": ["docs/**/*", "theme/**/*", "rspress.config.ts"], + "include": ["docs/**/*", "theme/**/*", "rspress.config.ts", "components/**/*", "expo-snack.d.ts"], "mdx": { "checkMdx": true } From 7889727a0a6fccc50859551676e35ec9ee90a653 Mon Sep 17 00:00:00 2001 From: harang Date: Sat, 27 Jun 2026 16:37:12 +0900 Subject: [PATCH 2/5] fix: keep docs dev dependencies isolated Rspress dev should resolve React Refresh from pnpm's isolated graph. The hoisted linker made the refresh runtime get transformed by itself. The embedded Snack preview now opens on iOS for the docs demo. Constraint: pnpm hoisting changed the Rspress dev bundle runtime path Rejected: Disable HMR | masks the React Refresh resolution issue Confidence: high Scope-risk: moderate Tested: CI=true pnpm install --frozen-lockfile Tested: CI=true pnpm docs:typecheck Tested: CI=true pnpm docs:build Tested: git diff --check --cached Not-tested: Manual browser interaction with the embedded Snack --- docs/docs/1.x/en/guide/getting-started/quick-start.mdx | 2 +- docs/docs/1.x/ko/guide/getting-started/quick-start.mdx | 2 +- pnpm-workspace.yaml | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/docs/1.x/en/guide/getting-started/quick-start.mdx b/docs/docs/1.x/en/guide/getting-started/quick-start.mdx index 94cb945..7e7c2b9 100644 --- a/docs/docs/1.x/en/guide/getting-started/quick-start.mdx +++ b/docs/docs/1.x/en/guide/getting-started/quick-start.mdx @@ -12,7 +12,7 @@ and events from that factory share the same registry namespace. - [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - Try it instantly on Expo Snack. - + ```tsx import { Text, View } from 'react-native'; diff --git a/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx b/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx index 3000f13..62352d6 100644 --- a/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx +++ b/docs/docs/1.x/ko/guide/getting-started/quick-start.mdx @@ -12,7 +12,7 @@ hook, action, event가 같은 registry namespace를 공유합니다. - [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - Expo Snack에서 바로 체험할 수 있습니다. - + ```tsx import { Text, View } from 'react-native'; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 23a34d6..182d1fd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,7 +2,6 @@ packages: - . - example - docs -nodeLinker: hoisted autoInstallPeers: true allowBuilds: lefthook: true From 2a27b664fa5ad0fd5a4a80335dc788e6fa021717 Mon Sep 17 00:00:00 2001 From: harang Date: Sat, 27 Jun 2026 16:40:16 +0900 Subject: [PATCH 3/5] chore: add docs demo changeset Record the README and docs quick start updates as a patch changeset. This keeps the package release notes aligned with the published docs and Snack demo. Constraint: README changes affect npm-facing package documentation Confidence: high Scope-risk: narrow Tested: git diff --check Not-tested: changeset version generation --- .changeset/fresh-readme-demo.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-readme-demo.md diff --git a/.changeset/fresh-readme-demo.md b/.changeset/fresh-readme-demo.md new file mode 100644 index 0000000..393e725 --- /dev/null +++ b/.changeset/fresh-readme-demo.md @@ -0,0 +1,5 @@ +--- +'@react-native-motion-kit/swipe-deck': patch +--- + +Update the README and docs quick start links for the published docs site and Expo Snack demo. From 11a10a6ed808e2e7c7cb6d1c6c61e27696156d0e Mon Sep 17 00:00:00 2001 From: harang Date: Sun, 28 Jun 2026 11:21:39 +0900 Subject: [PATCH 4/5] fix: support pnpm jest transforms Move Jest configuration out of package.json so the pnpm-specific transform allowlist is readable and keeps React Native test dependencies compiled. Constraint: pnpm stores dependencies under node_modules/.pnpm paths Rejected: Keep a long inline package.json regex | harder to review and maintain Confidence: high Scope-risk: narrow Tested: CI=true pnpm test Tested: CI=true pnpm format:check package.json jest.config.js Tested: CI=true pnpm typecheck Tested: CI=true pnpm build Tested: CI=true pnpm docs:build Tested: git diff --check --- jest.config.js | 26 ++++++++++++++++++++++++++ package.json | 17 ----------------- 2 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..3d14b48 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,26 @@ +const reactNativePackages = [ + '(jest-)?react-native', + '@react-native(-community)?', + 'react-native-gesture-handler', + 'react-native-reanimated', + 'react-native-worklets', +]; + +const pnpmReactNativePackages = [ + '(jest-)?react-native', + 'react-native-gesture-handler', + 'react-native-reanimated', + 'react-native-worklets', +]; + +module.exports = { + modulePathIgnorePatterns: ['/example/node_modules', '/lib/'], + preset: '@react-native/jest-preset', + resolver: 'react-native-worklets/jest/resolver', + setupFiles: ['react-native-gesture-handler/jestSetup.js'], + setupFilesAfterEnv: ['/jest-setup.js'], + transformIgnorePatterns: [ + `node_modules/(?!(${reactNativePackages.join('|')}|\\.pnpm)/)`, + `node_modules/.pnpm/(?!(${pnpmReactNativePackages.join('|')})@|@react-native\\+|@react-native-community\\+)`, + ], +}; diff --git a/package.json b/package.json index 0f3dfdc..764484f 100644 --- a/package.json +++ b/package.json @@ -120,23 +120,6 @@ "@commitlint/config-conventional" ] }, - "jest": { - "modulePathIgnorePatterns": [ - "/example/node_modules", - "/lib/" - ], - "preset": "@react-native/jest-preset", - "resolver": "react-native-worklets/jest/resolver", - "setupFiles": [ - "react-native-gesture-handler/jestSetup.js" - ], - "setupFilesAfterEnv": [ - "/jest-setup.js" - ], - "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?|react-native-gesture-handler|react-native-reanimated|react-native-worklets)/)" - ] - }, "packageManager": "pnpm@11.7.0", "create-react-native-library": { "type": "library", From d848eb3ebafbbee3d33d7b37b954e238e4c3b199 Mon Sep 17 00:00:00 2001 From: harang Date: Sun, 28 Jun 2026 14:11:35 +0900 Subject: [PATCH 5/5] fix: prevent blank Snack embeds after docs navigation Rspress client navigation can remount the MDX page after Snack already loaded. The embed script scans only at load time, so each mount now recreates the current iframe. Constraint: Expo Snack embed script initializes by scanning DOM at load time Rejected: Re-run document-wide initialize | broader than needed and timing-sensitive Confidence: high Scope-risk: narrow Tested: CI=true pnpm docs:typecheck Tested: CI=true pnpm format:check targeted Snack files Tested: git diff --check targeted Snack files Tested: CI=true pnpm docs:build --- docs/components/ExpoSnackEmbed.tsx | 88 ++++++++++++++++++++---------- docs/expo-snack.d.ts | 3 +- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/docs/components/ExpoSnackEmbed.tsx b/docs/components/ExpoSnackEmbed.tsx index c231733..267d100 100644 --- a/docs/components/ExpoSnackEmbed.tsx +++ b/docs/components/ExpoSnackEmbed.tsx @@ -1,5 +1,9 @@ import { useEffect, useRef } from 'react'; +const SNACK_EMBED_SCRIPT_SRC = 'https://snack.expo.dev/embed.js'; + +let snackEmbedScriptPromise: Promise | undefined; + type Props = { snackId: string; platform?: 'web' | 'ios' | 'android'; @@ -7,6 +11,40 @@ type Props = { height?: number | `${number}px`; }; +function loadSnackEmbedScript() { + if (window.ExpoSnack) { + return Promise.resolve(); + } + + if (snackEmbedScriptPromise) { + return snackEmbedScriptPromise; + } + + snackEmbedScriptPromise = new Promise((resolve, reject) => { + const existingScript = document.querySelector( + `script[src="${SNACK_EMBED_SCRIPT_SRC}"]`, + ); + const script = existingScript ?? document.createElement('script'); + + const handleLoad = () => resolve(); + const handleError = () => { + snackEmbedScriptPromise = undefined; + reject(new Error('Failed to load Expo Snack embed script.')); + }; + + script.addEventListener('load', handleLoad, { once: true }); + script.addEventListener('error', handleError, { once: true }); + + if (!existingScript) { + script.src = SNACK_EMBED_SCRIPT_SRC; + script.async = true; + document.head.appendChild(script); + } + }); + + return snackEmbedScriptPromise; +} + export function ExpoSnackEmbed({ snackId, platform = 'web', @@ -16,40 +54,32 @@ export function ExpoSnackEmbed({ const containerRef = useRef(null); useEffect(() => { - const loadAndInitialize = async () => { - try { - if (!document.querySelector('script[src="https://snack.expo.dev/embed.js"]')) { - const script = document.createElement('script'); - script.src = 'https://snack.expo.dev/embed.js'; - script.async = true; - document.head.appendChild(script); - - await new Promise((resolve, reject) => { - script.addEventListener('load', resolve); - script.addEventListener('error', reject); - }); - } - } catch (error) { - console.error('Failed to load Expo Snack embed:', error); - } + const container = containerRef.current; + let isDisposed = false; - setTimeout(() => { - if (window.ExpoSnack && containerRef.current) { - const existingEmbed = containerRef.current.querySelector('iframe'); + const initializeEmbed = async () => { + await loadSnackEmbedScript(); - if (existingEmbed) { - existingEmbed.remove(); - } + if (isDisposed || !container) { + return; + } - if (window.ExpoSnack?.initialize) { - window.ExpoSnack?.initialize?.(); - } - } - }, 100); + window.ExpoSnack?.remove?.(container); + window.ExpoSnack?.append?.(container); }; - loadAndInitialize(); - }, [snackId]); + initializeEmbed().catch((error) => { + console.error('Failed to initialize Expo Snack embed:', error); + }); + + return () => { + isDisposed = true; + + if (container) { + window.ExpoSnack?.remove?.(container); + } + }; + }, [platform, preview, snackId]); return (
void; initialize?: () => void; - embed?: (element: HTMLElement) => void; + remove?: (element: HTMLElement) => void; }; }