diff --git a/src/components/features/Demo.tsx b/src/components/features/Demo.tsx index 38b4ecf..d0b484a 100644 --- a/src/components/features/Demo.tsx +++ b/src/components/features/Demo.tsx @@ -1,8 +1,7 @@ 'use client' import React, { useState, useEffect, useRef, useMemo } from 'react' -import { ChevronRight, Upload, Play, FileCode2, Zap, Package, Code2, Terminal, GitBranch } from 'lucide-react' -import CopyButton from '../ui/CopyButton' +import { ChevronLeft, ChevronRight, Upload, Play, FileCode2, Zap, Package, Code2, Terminal, GitBranch } from 'lucide-react' import { ctaInvertedPrimaryClasses } from '../ui/ctaInvertedPrimaryClasses' import { colorizeTerminalText } from '@/lib/terminal-colorize' import { DemoShikiEditor, DemoShikiStatic, inferDemoLang } from './DemoShiki' @@ -648,6 +647,34 @@ export default function Demo() { } }, [activeContextView, contextMain, contextJsons, contextBundle]) + type ContextViewTab = 'bundle' | 'folder' | 'main' + + const contextViewOrder = useMemo((): ContextViewTab[] => { + const order: ContextViewTab[] = [] + if (contextBundle) order.push('bundle') + if (contextJsons) order.push('folder') + if (contextMain) order.push('main') + return order + }, [contextBundle, contextJsons, contextMain]) + + const showContextViewTabs = contextViewOrder.length > 1 + + const cycleContextView = (direction: -1 | 1) => { + if (contextViewOrder.length === 0) return + const i = contextViewOrder.indexOf(activeContextView as ContextViewTab) + const from = i >= 0 ? i : 0 + const next = + (from + direction + contextViewOrder.length) % contextViewOrder.length + setActiveContextView(contextViewOrder[next]) + } + + useEffect(() => { + if (contextViewOrder.length === 0) return + if (!contextViewOrder.includes(activeContextView as ContextViewTab)) { + setActiveContextView(contextViewOrder[0]) + } + }, [contextViewOrder, activeContextView]) + // Intersection observer hooks for animations // Reset contentRef and ctaRef animation when switching tabs const { ref: headerRef, inView: headerInView } = useInView(0.1) @@ -695,15 +722,33 @@ export default function Demo() { await new Promise(resolve => setTimeout(resolve, 200)) if (uploadedFiles.length > 1) { - // Generate multi-file context bundles + // Generate multi-file context bundles + first file’s component bundle (same tab UX as single-file) const { contextJsons: folderContexts, contextMain: projectContext } = generateMultiFileContextBundle(uploadedFiles, includeStyle) setContextJsons(folderContexts) setContextMain(projectContext) + setContextBundle(generateContextBundle(uploadedFiles[0].content, includeStyle)) setActiveContextView('main') } else { - // Single file - use original generation + // Single file — component bundle plus project/folder views so tabs match multi-file demo const bundle = generateContextBundle(userCode, includeStyle) setContextBundle(bundle) + const demoRelPath = + uploadedFiles.length === 1 + ? uploadedFiles[0].path.includes('/') + ? uploadedFiles[0].path + : `src/components/${uploadedFiles[0].name}` + : fileName === 'page.tsx' + ? 'src/app/dashboard/page.tsx' + : fileName.endsWith('.ts') && !fileName.endsWith('.tsx') + ? `src/services/${fileName}` + : `src/components/${fileName}` + const { contextJsons: folderContexts, contextMain: projectContext } = + generateMultiFileContextBundle( + [{ name: fileName, content: userCode, path: demoRelPath }], + includeStyle + ) + setContextJsons(folderContexts) + setContextMain(projectContext) setActiveContextView('bundle') } @@ -717,6 +762,8 @@ export default function Demo() { setShowOutput(false) setTerminalOutput([]) setContextBundle(null) + setContextJsons(null) + setContextMain(null) // Update filename based on example if (example === 'react') { @@ -760,6 +807,8 @@ export default function Demo() { setShowOutput(false) setTerminalOutput([]) setContextBundle(null) + setContextJsons(null) + setContextMain(null) } // Workflow handler - runs all steps automatically @@ -1040,12 +1089,12 @@ export default function Demo() { {/* Main content area */}
{/* Code editor panel */} -
+
{/* Files ready for context generation indicator */} {filesReadyForContextGeneration && uploadedFiles.length > 0 && (
)} -
+
@@ -1102,16 +1151,13 @@ export default function Demo() { ))}
)} -
+
-
- -
@@ -1170,7 +1216,7 @@ export default function Demo() {
{terminalOutput.map((line, index) => line.type === 'empty' ? ( @@ -1208,38 +1254,72 @@ export default function Demo() { 'context.json'}
-
- -
- {/* Context view toggle buttons */} - {contextMain && contextJsons && ( -
+ {/* Context view: tabs + side chevrons to cycle bundle / folder / project */} + {showContextViewTabs && ( +
+
+ {contextBundle && ( + + )} + {contextJsons && ( + + )} + {contextMain && ( + + )} +
)} -
+
{workflowOutput.map((line: any, index: number) => { if (line.type === 'empty') { @@ -1439,7 +1519,6 @@ export default function Demo() { npm install -g logicstamp-context -
npm install -g logicstamp-context -
{/* MCP Installation */} @@ -1489,7 +1567,6 @@ export default function Demo() { npm install -g logicstamp-mcp -
diff --git a/src/components/features/DemoShiki.tsx b/src/components/features/DemoShiki.tsx index 30f30cb..b4de63f 100644 --- a/src/components/features/DemoShiki.tsx +++ b/src/components/features/DemoShiki.tsx @@ -77,8 +77,13 @@ type EditorProps = { disabled?: boolean } -const EDITOR_LAYER = - 'absolute inset-0 h-full w-full overflow-auto p-6 font-mono text-sm leading-6 [tab-size:2]' +/** Highlight mirror: no scrollbars — scroll position is synced from the textarea only. */ +const HIGHLIGHT_LAYER = + 'pointer-events-none absolute inset-0 h-full w-full overflow-hidden p-6 font-mono text-sm leading-6 [tab-size:2]' + +/** Single scroll container for both axes (avoids stacked scrollbars from two overflow:auto layers). */ +const TEXTAREA_LAYER = + 'absolute inset-0 z-[1] h-full w-full min-h-0 min-w-0 resize-none overflow-auto overscroll-contain border-0 bg-transparent p-6 font-mono text-sm leading-6 whitespace-pre [tab-size:2] [-webkit-overflow-scrolling:touch] text-transparent caret-zinc-900 focus:outline-none focus:ring-0 dark:caret-zinc-100 selection:bg-sky-500/25 dark:selection:bg-sky-400/30 placeholder:text-gray-400 dark:placeholder:text-gray-500' /** Editable textarea with Shiki highlight layer (synced scroll). */ export function DemoShikiEditor({ @@ -120,15 +125,15 @@ export function DemoShikiEditor({ }, []) return ( -
+
{html ? (
) : ( @@ -143,7 +148,9 @@ export function DemoShikiEditor({ disabled={disabled} placeholder={placeholder} spellCheck={false} - className={`z-[1] ${EDITOR_LAYER} resize-none border-0 bg-transparent text-transparent caret-zinc-900 focus:outline-none focus:ring-0 dark:caret-zinc-100 selection:bg-sky-500/25 dark:selection:bg-sky-400/30 placeholder:text-gray-400 dark:placeholder:text-gray-500`} + autoCapitalize="off" + autoCorrect="off" + className={TEXTAREA_LAYER} />
)