Skip to content

Commit 8f50d66

Browse files
authored
feat(demo): support multiple context views with dynamic UI handling (#222)
- add context view cycling (bundle / folder / main) - conditionally render context tabs based on available contexts - improve handling of single vs multi-file uploads - refine Demo layout for responsiveness and usability - enhance DemoShikiEditor scroll behavior and textarea props
1 parent 99d913d commit 8f50d66

2 files changed

Lines changed: 127 additions & 43 deletions

File tree

src/components/features/Demo.tsx

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use client'
22

33
import React, { useState, useEffect, useRef, useMemo } from 'react'
4-
import { ChevronRight, Upload, Play, FileCode2, Zap, Package, Code2, Terminal, GitBranch } from 'lucide-react'
5-
import CopyButton from '../ui/CopyButton'
4+
import { ChevronLeft, ChevronRight, Upload, Play, FileCode2, Zap, Package, Code2, Terminal, GitBranch } from 'lucide-react'
65
import { ctaInvertedPrimaryClasses } from '../ui/ctaInvertedPrimaryClasses'
76
import { colorizeTerminalText } from '@/lib/terminal-colorize'
87
import { DemoShikiEditor, DemoShikiStatic, inferDemoLang } from './DemoShiki'
@@ -648,6 +647,34 @@ export default function Demo() {
648647
}
649648
}, [activeContextView, contextMain, contextJsons, contextBundle])
650649

650+
type ContextViewTab = 'bundle' | 'folder' | 'main'
651+
652+
const contextViewOrder = useMemo((): ContextViewTab[] => {
653+
const order: ContextViewTab[] = []
654+
if (contextBundle) order.push('bundle')
655+
if (contextJsons) order.push('folder')
656+
if (contextMain) order.push('main')
657+
return order
658+
}, [contextBundle, contextJsons, contextMain])
659+
660+
const showContextViewTabs = contextViewOrder.length > 1
661+
662+
const cycleContextView = (direction: -1 | 1) => {
663+
if (contextViewOrder.length === 0) return
664+
const i = contextViewOrder.indexOf(activeContextView as ContextViewTab)
665+
const from = i >= 0 ? i : 0
666+
const next =
667+
(from + direction + contextViewOrder.length) % contextViewOrder.length
668+
setActiveContextView(contextViewOrder[next])
669+
}
670+
671+
useEffect(() => {
672+
if (contextViewOrder.length === 0) return
673+
if (!contextViewOrder.includes(activeContextView as ContextViewTab)) {
674+
setActiveContextView(contextViewOrder[0])
675+
}
676+
}, [contextViewOrder, activeContextView])
677+
651678
// Intersection observer hooks for animations
652679
// Reset contentRef and ctaRef animation when switching tabs
653680
const { ref: headerRef, inView: headerInView } = useInView(0.1)
@@ -695,15 +722,33 @@ export default function Demo() {
695722
await new Promise(resolve => setTimeout(resolve, 200))
696723

697724
if (uploadedFiles.length > 1) {
698-
// Generate multi-file context bundles
725+
// Generate multi-file context bundles + first file’s component bundle (same tab UX as single-file)
699726
const { contextJsons: folderContexts, contextMain: projectContext } = generateMultiFileContextBundle(uploadedFiles, includeStyle)
700727
setContextJsons(folderContexts)
701728
setContextMain(projectContext)
729+
setContextBundle(generateContextBundle(uploadedFiles[0].content, includeStyle))
702730
setActiveContextView('main')
703731
} else {
704-
// Single file - use original generation
732+
// Single file — component bundle plus project/folder views so tabs match multi-file demo
705733
const bundle = generateContextBundle(userCode, includeStyle)
706734
setContextBundle(bundle)
735+
const demoRelPath =
736+
uploadedFiles.length === 1
737+
? uploadedFiles[0].path.includes('/')
738+
? uploadedFiles[0].path
739+
: `src/components/${uploadedFiles[0].name}`
740+
: fileName === 'page.tsx'
741+
? 'src/app/dashboard/page.tsx'
742+
: fileName.endsWith('.ts') && !fileName.endsWith('.tsx')
743+
? `src/services/${fileName}`
744+
: `src/components/${fileName}`
745+
const { contextJsons: folderContexts, contextMain: projectContext } =
746+
generateMultiFileContextBundle(
747+
[{ name: fileName, content: userCode, path: demoRelPath }],
748+
includeStyle
749+
)
750+
setContextJsons(folderContexts)
751+
setContextMain(projectContext)
707752
setActiveContextView('bundle')
708753
}
709754

@@ -717,6 +762,8 @@ export default function Demo() {
717762
setShowOutput(false)
718763
setTerminalOutput([])
719764
setContextBundle(null)
765+
setContextJsons(null)
766+
setContextMain(null)
720767

721768
// Update filename based on example
722769
if (example === 'react') {
@@ -760,6 +807,8 @@ export default function Demo() {
760807
setShowOutput(false)
761808
setTerminalOutput([])
762809
setContextBundle(null)
810+
setContextJsons(null)
811+
setContextMain(null)
763812
}
764813

765814
// Workflow handler - runs all steps automatically
@@ -1040,12 +1089,12 @@ export default function Demo() {
10401089
{/* Main content area */}
10411090
<div
10421091
ref={contentRef}
1043-
className={`grid lg:grid-cols-2 gap-6 transition-all duration-1000 delay-300 ${
1092+
className={`grid min-w-0 max-w-full lg:grid-cols-2 gap-6 transition-all duration-1000 delay-300 ${
10441093
contentInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
10451094
}`}
10461095
>
10471096
{/* Code editor panel */}
1048-
<div className="relative">
1097+
<div className="relative min-w-0">
10491098
{/* Files ready for context generation indicator */}
10501099
{filesReadyForContextGeneration && uploadedFiles.length > 0 && (
10511100
<div
@@ -1060,7 +1109,7 @@ export default function Demo() {
10601109
</div>
10611110
)}
10621111

1063-
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl ring-1 ring-gray-200/50 dark:ring-gray-800/50 overflow-hidden">
1112+
<div className="min-w-0 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl ring-1 ring-gray-200/50 dark:ring-gray-800/50 overflow-hidden">
10641113
<div className="bg-gray-50 dark:bg-gray-800 px-6 py-3 flex items-center justify-between border-b border-gray-200 dark:border-gray-700">
10651114
<div className="flex items-center gap-3">
10661115
<Code2 className="w-4 h-4 text-green-400 flex-shrink-0" />
@@ -1102,16 +1151,13 @@ export default function Demo() {
11021151
))}
11031152
</div>
11041153
)}
1105-
<div className="relative overflow-hidden">
1154+
<div className="relative min-h-0 min-w-0">
11061155
<DemoShikiEditor
11071156
value={userCode}
11081157
onChange={setUserCode}
11091158
lang={inferDemoLang(fileName)}
11101159
placeholder="Paste your React/TypeScript code here..."
11111160
/>
1112-
<div className="absolute top-4 right-4 z-[2]">
1113-
<CopyButton text={userCode} />
1114-
</div>
11151161
</div>
11161162
</div>
11171163

@@ -1170,7 +1216,7 @@ export default function Demo() {
11701216
</div>
11711217
<div
11721218
ref={terminalScrollRef}
1173-
className="h-[280px] overflow-y-auto overflow-x-auto p-6 font-mono text-sm bg-gray-900 sidebar-scrollable"
1219+
className="h-[280px] lg:h-[420px] overflow-y-auto overflow-x-auto p-6 font-mono text-sm bg-gray-900 sidebar-scrollable"
11741220
>
11751221
{terminalOutput.map((line, index) =>
11761222
line.type === 'empty' ? (
@@ -1208,38 +1254,72 @@ export default function Demo() {
12081254
'context.json'}
12091255
</span>
12101256
</div>
1211-
<div className="flex-shrink-0">
1212-
<CopyButton text={displayContextJson} />
1213-
</div>
12141257
</div>
12151258

1216-
{/* Context view toggle buttons */}
1217-
{contextMain && contextJsons && (
1218-
<div className="bg-gray-100 dark:bg-gray-800/50 px-2 sm:px-4 py-2 flex items-center gap-1.5 sm:gap-2 overflow-x-auto border-b border-gray-200 dark:border-gray-700">
1259+
{/* Context view: tabs + side chevrons to cycle bundle / folder / project */}
1260+
{showContextViewTabs && (
1261+
<div className="bg-gray-100 dark:bg-gray-800/50 px-2 sm:px-3 py-2 flex items-center gap-1 sm:gap-2 border-b border-gray-200 dark:border-gray-700">
12191262
<button
1220-
onClick={() => setActiveContextView('main')}
1221-
className={`px-2 sm:px-3 py-1.5 rounded-lg text-[10px] sm:text-xs font-medium transition-all whitespace-nowrap flex-shrink-0 ${
1222-
activeContextView === 'main'
1223-
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
1224-
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50'
1225-
}`}
1263+
type="button"
1264+
onClick={() => cycleContextView(-1)}
1265+
className="flex-shrink-0 p-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50 transition-colors"
1266+
aria-label="Previous context view"
12261267
>
1227-
context_main.json
1268+
<ChevronLeft className="w-4 h-4 sm:w-5 sm:h-5" />
12281269
</button>
1270+
<div className="flex flex-1 items-center gap-1.5 sm:gap-2 overflow-x-auto min-w-0">
1271+
{contextBundle && (
1272+
<button
1273+
type="button"
1274+
onClick={() => setActiveContextView('bundle')}
1275+
className={`px-2 sm:px-3 py-1.5 rounded-lg text-[10px] sm:text-xs font-medium transition-all whitespace-nowrap flex-shrink-0 ${
1276+
activeContextView === 'bundle'
1277+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
1278+
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50'
1279+
}`}
1280+
>
1281+
context.json
1282+
</button>
1283+
)}
1284+
{contextJsons && (
1285+
<button
1286+
type="button"
1287+
onClick={() => setActiveContextView('folder')}
1288+
className={`px-2 sm:px-3 py-1.5 rounded-lg text-[10px] sm:text-xs font-medium transition-all whitespace-nowrap flex-shrink-0 ${
1289+
activeContextView === 'folder'
1290+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
1291+
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50'
1292+
}`}
1293+
>
1294+
context.json (per folder)
1295+
</button>
1296+
)}
1297+
{contextMain && (
1298+
<button
1299+
type="button"
1300+
onClick={() => setActiveContextView('main')}
1301+
className={`px-2 sm:px-3 py-1.5 rounded-lg text-[10px] sm:text-xs font-medium transition-all whitespace-nowrap flex-shrink-0 ${
1302+
activeContextView === 'main'
1303+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
1304+
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50'
1305+
}`}
1306+
>
1307+
context_main.json
1308+
</button>
1309+
)}
1310+
</div>
12291311
<button
1230-
onClick={() => setActiveContextView('folder')}
1231-
className={`px-2 sm:px-3 py-1.5 rounded-lg text-[10px] sm:text-xs font-medium transition-all whitespace-nowrap flex-shrink-0 ${
1232-
activeContextView === 'folder'
1233-
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
1234-
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50'
1235-
}`}
1312+
type="button"
1313+
onClick={() => cycleContextView(1)}
1314+
className="flex-shrink-0 p-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/50 transition-colors"
1315+
aria-label="Next context view"
12361316
>
1237-
context.json (per folder)
1317+
<ChevronRight className="w-4 h-4 sm:w-5 sm:h-5" />
12381318
</button>
12391319
</div>
12401320
)}
12411321

1242-
<div className="h-[280px] overflow-y-auto overflow-x-auto p-3 sm:p-6 bg-gray-50 dark:bg-gray-900 sidebar-scrollable w-full">
1322+
<div className="h-[280px] lg:h-[420px] overflow-y-auto overflow-x-auto p-3 sm:p-6 bg-gray-50 dark:bg-gray-900 sidebar-scrollable w-full">
12431323
<DemoShikiStatic
12441324
code={displayContextJson}
12451325
lang="json"
@@ -1325,7 +1405,7 @@ export default function Demo() {
13251405
</div>
13261406
<div
13271407
ref={workflowScrollRef}
1328-
className="h-[500px] overflow-y-auto overflow-x-auto p-6 font-mono text-sm bg-gray-900 sidebar-scrollable"
1408+
className="h-[500px] lg:h-[600px] overflow-y-auto overflow-x-auto p-6 font-mono text-sm bg-gray-900 sidebar-scrollable"
13291409
>
13301410
{workflowOutput.map((line: any, index: number) => {
13311411
if (line.type === 'empty') {
@@ -1439,7 +1519,6 @@ export default function Demo() {
14391519
<code className="text-sm sm:text-base lg:text-lg font-mono font-semibold text-gray-900 dark:text-gray-100" aria-label="Installation command">
14401520
npm install -g logicstamp-context
14411521
</code>
1442-
<CopyButton text="npm install -g logicstamp-context" className="ml-2" />
14431522
</div>
14441523
</div>
14451524
<a
@@ -1477,7 +1556,6 @@ export default function Demo() {
14771556
<code className="text-sm sm:text-base lg:text-lg font-mono font-semibold text-gray-900 dark:text-gray-100" aria-label="Installation command">
14781557
npm install -g logicstamp-context
14791558
</code>
1480-
<CopyButton text="npm install -g logicstamp-context" className="ml-2" />
14811559
</div>
14821560
</div>
14831561
{/* MCP Installation */}
@@ -1489,7 +1567,6 @@ export default function Demo() {
14891567
<code className="text-sm sm:text-base lg:text-lg font-mono font-semibold text-gray-900 dark:text-gray-100" aria-label="Installation command">
14901568
npm install -g logicstamp-mcp
14911569
</code>
1492-
<CopyButton text="npm install -g logicstamp-mcp" className="ml-2" />
14931570
</div>
14941571
</div>
14951572
</div>

src/components/features/DemoShiki.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,13 @@ type EditorProps = {
7777
disabled?: boolean
7878
}
7979

80-
const EDITOR_LAYER =
81-
'absolute inset-0 h-full w-full overflow-auto p-6 font-mono text-sm leading-6 [tab-size:2]'
80+
/** Highlight mirror: no scrollbars — scroll position is synced from the textarea only. */
81+
const HIGHLIGHT_LAYER =
82+
'pointer-events-none absolute inset-0 h-full w-full overflow-hidden p-6 font-mono text-sm leading-6 [tab-size:2]'
83+
84+
/** Single scroll container for both axes (avoids stacked scrollbars from two overflow:auto layers). */
85+
const TEXTAREA_LAYER =
86+
'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'
8287

8388
/** Editable textarea with Shiki highlight layer (synced scroll). */
8489
export function DemoShikiEditor({
@@ -120,15 +125,15 @@ export function DemoShikiEditor({
120125
}, [])
121126

122127
return (
123-
<div className="relative h-[400px] lg:h-[350px] w-full">
128+
<div className="relative h-[min(400px,_70vh)] min-h-[240px] w-full min-w-0 lg:h-[min(350px,_65vh)]">
124129
<div
125130
ref={highlightRef}
126-
className={`pointer-events-none ${EDITOR_LAYER} bg-gray-50 dark:bg-gray-900`}
131+
className={`${HIGHLIGHT_LAYER} bg-gray-50 dark:bg-gray-900`}
127132
aria-hidden
128133
>
129134
{html ? (
130135
<div
131-
className={`min-h-full min-w-max ${shikiPreReset} [&_pre.shiki]:text-sm [&_pre.shiki]:leading-6`}
136+
className={`min-h-full min-w-max ${shikiPreReset} [&_pre.shiki]:!overflow-hidden [&_pre.shiki]:text-sm [&_pre.shiki]:leading-6`}
132137
dangerouslySetInnerHTML={{ __html: html }}
133138
/>
134139
) : (
@@ -143,7 +148,9 @@ export function DemoShikiEditor({
143148
disabled={disabled}
144149
placeholder={placeholder}
145150
spellCheck={false}
146-
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`}
151+
autoCapitalize="off"
152+
autoCorrect="off"
153+
className={TEXTAREA_LAYER}
147154
/>
148155
</div>
149156
)

0 commit comments

Comments
 (0)