diff --git a/.github/workflows/interactdocs.yml b/.github/workflows/interactdocs.yml new file mode 100644 index 00000000..830ffe22 --- /dev/null +++ b/.github/workflows/interactdocs.yml @@ -0,0 +1,66 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: + - master + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + + - name: Enable Corepack + run: corepack enable + run: corepack prepare yarn@4.10.3 --activate + run: yarn set version 4.10.3 + run: NPQ_PKG_MGR=yarn npx npq install + + - name: Build interact package + run: yarn workspace @wix/interact build + + - name: Build docs app + run: yarn workspace @wix/interact-docs build + env: + # Set base path for GitHub Pages deployment under /docs + VITE_BASE: /docs/ + + - name: Prepare deployment directory + run: | + mkdir -p _site/docs + cp -r apps/docs/dist/* _site/docs/ + + - name: Setup Pages + uses: actions/configure-pages@6509e525a06f43847064050610066357391e6390 # v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@3a27866f6480871542049080632d70930d695383 # v3 + with: + path: _site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }}docs/ + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@939173330191535487491557568637540315448 # v4 diff --git a/apps/docs/package.json b/apps/docs/package.json index 2b4fe383..96bb59cf 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && vite build && node scripts/copy-docs.js", "preview": "vite preview", "lint": "tsc --noEmit", "test": "echo 'No tests yet'" @@ -13,7 +13,11 @@ "dependencies": { "@wix/interact": "*", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^7.1.1", + "rehype-highlight": "^7.0.1", + "remark-gfm": "^4.0.0" }, "devDependencies": { "@types/react": "^18.3.2", diff --git a/apps/docs/scripts/copy-docs.js b/apps/docs/scripts/copy-docs.js new file mode 100644 index 00000000..e726df05 --- /dev/null +++ b/apps/docs/scripts/copy-docs.js @@ -0,0 +1,28 @@ +import { cpSync, mkdirSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const srcDocs = resolve(__dirname, '../../../packages/interact/docs'); +const destDocs = resolve(__dirname, '../dist/docs'); + +const srcRules = resolve(__dirname, '../../../packages/interact/rules'); +const destRules = resolve(__dirname, '../dist/rules'); + +// Create dest directory if it doesn't exist +if (!existsSync(dirname(destDocs))) { + mkdirSync(dirname(destDocs), { recursive: true }); +} + +// Copy docs to dist +cpSync(srcDocs, destDocs, { recursive: true, force: true }); + +console.log('✓ Docs copied to dist/docs'); + +// Copy rules to dist +cpSync(srcRules, destRules, { recursive: true, force: true }); + +console.log('✓ Rules copied to dist/rules'); + diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx index 765f03d9..2b3d1ea5 100644 --- a/apps/docs/src/App.tsx +++ b/apps/docs/src/App.tsx @@ -1,17 +1,17 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Layout } from './components/Layout'; -import { Introduction } from './pages/Introduction'; -import { ComponentExamples } from './pages/ComponentExamples'; -import { ApiCheatsheet } from './pages/ApiCheatsheet'; +import { MarkdownPage } from './components/MarkdownPage'; function App() { return ( - - - - - + + + + } /> + + + ); } export default App; - diff --git a/apps/docs/src/components/InteractShowcase.tsx b/apps/docs/src/components/InteractShowcase.tsx deleted file mode 100644 index 030b1387..00000000 --- a/apps/docs/src/components/InteractShowcase.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import { Interact, type InteractConfig } from '@wix/interact'; - -const demoConfig: InteractConfig = { - interactions: [ - { - trigger: 'viewEnter', - key: 'docs-hero', - params: { type: 'once', threshold: 0.6 }, - effects: [{ effectId: 'hero-reveal' }] - }, - { - trigger: 'hover', - key: 'docs-card-primary', - effects: [{ effectId: 'card-lift' }] - }, - { - trigger: 'click', - key: 'docs-card-secondary', - params: { method: 'toggle' }, - effects: [{ effectId: 'card-press' }] - } - ], - effects: { - 'hero-reveal': { - key: 'docs-hero', - duration: 900, - easing: 'cubic-bezier(0.22, 1, 0.36, 1)', - keyframeEffect: { - name: 'hero-reveal', - keyframes: [ - { opacity: 0, transform: 'translateY(28px)' }, - { opacity: 1, transform: 'translateY(0px)' } - ] - } - }, - 'card-lift': { - key: 'docs-card-primary', - transitionProperties: [ - { - name: 'transform', - value: 'translateY(-8px)', - duration: 220, - easing: 'cubic-bezier(0.4, 0, 0.2, 1)' - }, - { - name: 'box-shadow', - value: '0 24px 50px rgba(15, 23, 42, 0.18)', - duration: 220, - easing: 'cubic-bezier(0.4, 0, 0.2, 1)' - } - ] - }, - 'card-press': { - key: 'docs-card-secondary', - duration: 500, - easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', - keyframeEffect: { - name: 'card-press', - keyframes: [ - { transform: 'scale(1)', opacity: 1 }, - { transform: 'scale(0.95)', opacity: 0.9 }, - { transform: 'scale(1)', opacity: 1 } - ] - } - } - } -}; - -const cards = [ - { - key: 'docs-card-primary', - title: 'Hero CTA', - description: 'Hover to preview transition-based lift + depth.' - }, - { - key: 'docs-card-secondary', - title: 'Click Pulse', - description: 'Click to trigger the custom keyframe effect powered by motion.' - } -]; - -export const InteractShowcase = () => { - const config = useMemo(() => demoConfig, []); - - useEffect(() => { - const interactInstance = Interact.create(config); - - return () => { - interactInstance.destroy(); - }; - }, [config]); - - return ( -
- -
-

- Powered by Interact -

-

Configurable animation without the fog

-

- Drive orchestrated hover, scroll, and pointer interactions using a single JSON file. -

-
-
- -
- {cards.map((card) => ( - -
-

{card.title}

-

{card.description}

-
-
- ))} -
-
- ); -}; - diff --git a/apps/docs/src/components/Layout.tsx b/apps/docs/src/components/Layout.tsx index fcf8fc05..5152f552 100644 --- a/apps/docs/src/components/Layout.tsx +++ b/apps/docs/src/components/Layout.tsx @@ -1,12 +1,15 @@ import type { PropsWithChildren } from 'react'; -import { DocsSidebar } from './Sidebar'; +import { Sidebar } from './Sidebar'; -export const Layout = ({ children }: PropsWithChildren) => { +export function Layout({ children }: PropsWithChildren) { return ( -
- -
{children}
+
+ +
+
+ {children} +
+
); -}; - +} diff --git a/apps/docs/src/components/MarkdownPage.tsx b/apps/docs/src/components/MarkdownPage.tsx new file mode 100644 index 00000000..e2a13a26 --- /dev/null +++ b/apps/docs/src/components/MarkdownPage.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from 'react'; +import { useLocation, Link } from 'react-router-dom'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeHighlight from 'rehype-highlight'; +import { getMarkdownPath } from '../config/navigation'; + +export function MarkdownPage() { + const location = useLocation(); + const [content, setContent] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadMarkdown = async () => { + setLoading(true); + setError(null); + + const mdPath = getMarkdownPath(location.pathname); + + try { + // Use the /docs endpoint which serves from packages/interact/docs + const response = await fetch(`/docs/${mdPath}`); + + if (!response.ok) { + throw new Error(`Failed to load: ${mdPath}`); + } + + const text = await response.text(); + setContent(text); + } catch (err) { + console.error('Error loading markdown:', err); + setError(`Could not load documentation for: ${location.pathname}`); + } finally { + setLoading(false); + } + }; + + loadMarkdown(); + // Scroll to top on navigation + window.scrollTo(0, 0); + }, [location.pathname]); + + if (loading) { + return ( +
+
+ Loading documentation... +
+ ); + } + + if (error) { + return ( +
+

Page Not Found

+

{error}

+ ← Back to Home +
+ ); + } + + return ( +
+ { + // External links open in new tab + if (href?.startsWith('http')) { + return {children}; + } + // Anchor links (hash only) + if (href?.startsWith('#')) { + return {children}; + } + // Internal markdown links - convert to router paths + if (href && (href.endsWith('.md') || href.startsWith('./') || href.startsWith('../') || href.startsWith('/') || !href.includes('://'))) { + // Separate hash from path + const [pathPart, hashPart] = href.split('#'); + + // Build absolute path from current location + let absolutePath = pathPart; + if (pathPart && !pathPart.startsWith('/')) { + // Relative path - resolve against current location + // For index pages (like /api), treat them as directories + const currentPath = location.pathname; + const currentDir = currentPath.endsWith('/') + ? currentPath.slice(0, -1) + : currentPath; // Treat /api as directory /api for relative resolution + absolutePath = `${currentDir}/${pathPart}`.replace(/\/+/g, '/'); + } + + // Clean up: remove .md extension and README, normalize path + let cleanPath = (absolutePath || '') + .replace(/\.md$/, '') + .replace(/\/README$/i, '') + .replace(/\/\.\//g, '/') // Remove ./ + .replace(/\/+/g, '/') // Remove duplicate slashes + .replace(/^\/+/, '/'); // Ensure single leading slash + + // Handle ../ segments + const segments = cleanPath.split('/').filter(Boolean); + const resolved: string[] = []; + for (const seg of segments) { + if (seg === '..') { + resolved.pop(); + } else if (seg !== '.') { + resolved.push(seg); + } + } + cleanPath = '/' + resolved.join('/'); + + // Re-add hash if present + const finalPath = hashPart ? `${cleanPath}#${hashPart}` : cleanPath; + return {children}; + } + return {children}; + }, + // Add copy button to code blocks + pre: ({ children, ...props }) => ( +
+
{children}
+
+ ), + // Style tables nicely + table: ({ children, ...props }) => ( +
+ {children}
+
+ ), + }} + > + {content} +
+
+ ); +} diff --git a/apps/docs/src/components/Section.tsx b/apps/docs/src/components/Section.tsx deleted file mode 100644 index a6e39124..00000000 --- a/apps/docs/src/components/Section.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { PropsWithChildren, ReactNode } from 'react'; - -type SectionProps = PropsWithChildren<{ - id: string; - title: string; - kicker?: string; - description?: ReactNode; -}>; - -export const Section = ({ id, title, kicker, description, children }: SectionProps) => { - return ( -
- {kicker ? ( -

- {kicker} -

- ) : null} -

{title}

- {description ? ( -
- {description} -
- ) : null} - {children} -
- ); -}; - diff --git a/apps/docs/src/components/Sidebar.tsx b/apps/docs/src/components/Sidebar.tsx index e881417f..457e9c9e 100644 --- a/apps/docs/src/components/Sidebar.tsx +++ b/apps/docs/src/components/Sidebar.tsx @@ -1,30 +1,62 @@ -const sections = [ - { href: '#introduction', label: 'Introduction' }, - { href: '#examples', label: 'Interactive Examples' }, - { href: '#api', label: 'API Cheatsheet' } -]; +import { NavLink, useLocation } from 'react-router-dom'; +import { navigation, type NavSection } from '../config/navigation'; -export const DocsSidebar = () => { +function NavSectionComponent({ section }: { section: NavSection }) { + const location = useLocation(); + return ( -