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}
/>
)