Skip to content

Commit afe6446

Browse files
committed
fix(ui,registry): add runnable code snippet component
1 parent f7fa467 commit afe6446

File tree

11 files changed

+382
-30
lines changed

11 files changed

+382
-30
lines changed

packages/registry/registry.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@
107107
}
108108
]
109109
},
110+
{
111+
"name": "runnable-snippet",
112+
"type": "registry:ui",
113+
"title": "Runnable Snippet",
114+
"description": "Interactive code block with run button and output display",
115+
"dependencies": ["@openpkg-ts/ui", "lucide-react"],
116+
"files": [
117+
{
118+
"path": "registry/new-york/ui/runnable-snippet/runnable-snippet.tsx",
119+
"type": "registry:ui"
120+
}
121+
]
122+
},
110123
{
111124
"name": "use-method-from-spec",
112125
"type": "registry:hook",

packages/registry/registry/new-york/components/class-section/class-section.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { formatSchema } from '@openpkg-ts/sdk/browser';
44
import type { OpenPkg, SpecExport, SpecMember } from '@openpkg-ts/spec';
55
import { APIParameterItem, APISection, ParameterList } from '@openpkg-ts/ui/docskit';
66
import type { ReactNode } from 'react';
7+
import { ExpandableParameter } from '@/registry/new-york/components/expandable-parameter/expandable-parameter';
78
import {
89
buildImportStatement,
910
getLanguagesFromExamples,
1011
specExamplesToCodeExamples,
11-
specParamToAPIParam,
1212
} from './spec-to-docskit';
1313

1414
export interface ClassSectionProps {
@@ -110,18 +110,9 @@ export function ClassSection({ export: exp, spec }: ClassSectionProps): ReactNod
110110
>
111111
{constructorParams.length > 0 && (
112112
<ParameterList title="Constructor">
113-
{constructorParams.map((param, index) => {
114-
const apiParam = specParamToAPIParam(param);
115-
return (
116-
<APIParameterItem
117-
key={param.name ?? index}
118-
name={apiParam.name}
119-
type={apiParam.type}
120-
required={apiParam.required}
121-
description={apiParam.description}
122-
/>
123-
);
124-
})}
113+
{constructorParams.map((param, index) => (
114+
<ExpandableParameter key={param.name ?? index} parameter={param} />
115+
))}
125116
</ParameterList>
126117
)}
127118

packages/registry/registry/new-york/components/function-section/function-section.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { formatSchema } from '@openpkg-ts/sdk/browser';
44
import type { OpenPkg, SpecExport } from '@openpkg-ts/spec';
55
import { APIParameterItem, APISection, ParameterList, ResponseBlock } from '@openpkg-ts/ui/docskit';
66
import type { ReactNode } from 'react';
7+
import { ExpandableParameter } from '@/registry/new-york/components/expandable-parameter/expandable-parameter';
78
import {
89
buildImportStatement,
910
getLanguagesFromExamples,
1011
specExamplesToCodeExamples,
11-
specParamToAPIParam,
1212
} from './spec-to-docskit';
1313

1414
export interface FunctionSectionProps {
@@ -56,18 +56,9 @@ export function FunctionSection({ export: exp, spec }: FunctionSectionProps): Re
5656
>
5757
{hasParams && (
5858
<ParameterList title="Parameters">
59-
{sig.parameters?.map((param, index) => {
60-
const apiParam = specParamToAPIParam(param);
61-
return (
62-
<APIParameterItem
63-
key={param.name ?? index}
64-
name={apiParam.name}
65-
type={apiParam.type}
66-
required={apiParam.required}
67-
description={apiParam.description}
68-
/>
69-
);
70-
})}
59+
{sig.parameters?.map((param, index) => (
60+
<ExpandableParameter key={param.name ?? index} parameter={param} />
61+
))}
7162
</ParameterList>
7263
)}
7364

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client';
2+
3+
import { type RawCode } from 'codehike/code';
4+
import { CheckCircle2, Loader2, Play, XCircle } from 'lucide-react';
5+
import { useState } from 'react';
6+
import { cn } from '@/lib/utils';
7+
import { ClientDocsKitCode } from '@openpkg-ts/ui/docskit';
8+
import { CollapsiblePanel } from '@openpkg-ts/ui/docskit';
9+
10+
export interface RunnableSnippetProps {
11+
/** Code to display */
12+
code: string;
13+
/** Language for syntax highlighting */
14+
language?: string;
15+
/** Optional title */
16+
title?: string;
17+
/** For demo: mock execution state */
18+
mockState?: 'idle' | 'running' | 'success' | 'error';
19+
/** For demo: mock output data */
20+
mockOutput?: string;
21+
/** Custom className */
22+
className?: string;
23+
}
24+
25+
/**
26+
* Interactive code block with run button and output display.
27+
* Currently uses mock states; will be wired to sandbox execution in Phase 2.
28+
*/
29+
export function RunnableSnippet({
30+
code,
31+
language = 'typescript',
32+
title,
33+
mockState = 'idle',
34+
mockOutput,
35+
className,
36+
}: RunnableSnippetProps): React.ReactNode {
37+
const [state, setState] = useState<'idle' | 'running' | 'success' | 'error'>(mockState);
38+
const [output, setOutput] = useState<string | null>(mockOutput ?? null);
39+
const [duration, setDuration] = useState<number>(0);
40+
41+
const handleRun = () => {
42+
setState('running');
43+
setOutput(null);
44+
45+
// Mock execution - will be replaced with real sandbox in Phase 2
46+
const startTime = Date.now();
47+
setTimeout(() => {
48+
const elapsed = Date.now() - startTime;
49+
setDuration(elapsed);
50+
51+
if (mockState === 'error') {
52+
setState('error');
53+
setOutput(mockOutput ?? 'Error: Execution failed');
54+
} else {
55+
setState('success');
56+
setOutput(
57+
mockOutput ?? JSON.stringify({ result: 'success', timestamp: new Date().toISOString() }, null, 2),
58+
);
59+
}
60+
}, 1200);
61+
};
62+
63+
const codeblock: RawCode = {
64+
value: code,
65+
lang: language,
66+
meta: title,
67+
};
68+
69+
return (
70+
<div className={cn('relative', className)}>
71+
{/* Code display with run button */}
72+
<div className="relative group">
73+
<ClientDocsKitCode codeblock={codeblock} />
74+
75+
{/* Run button - positioned like CopyButton */}
76+
<button
77+
type="button"
78+
onClick={handleRun}
79+
disabled={state === 'running'}
80+
className={cn(
81+
'absolute right-3 top-3 z-10',
82+
'size-8 flex items-center justify-center',
83+
'rounded border border-openpkg-code-border bg-openpkg-code-bg',
84+
'text-openpkg-code-text-inactive',
85+
'cursor-pointer transition-opacity duration-200',
86+
'opacity-0 group-hover:opacity-100',
87+
state === 'running' && 'cursor-not-allowed opacity-100',
88+
)}
89+
aria-label="Run code"
90+
>
91+
{state === 'running' ? (
92+
<Loader2 size={16} className="animate-spin" />
93+
) : (
94+
<Play size={16} className="fill-current" />
95+
)}
96+
</button>
97+
</div>
98+
99+
{/* Output panel - shown after execution */}
100+
{output && (
101+
<div className="-mt-4">
102+
<CollapsiblePanel
103+
title={`${state === 'success' ? '✓ Success' : '✕ Error'} | ${duration}ms`}
104+
defaultExpanded={true}
105+
>
106+
<pre className="p-4 m-0 text-xs font-mono overflow-auto max-h-[400px]">{output}</pre>
107+
</CollapsiblePanel>
108+
</div>
109+
)}
110+
</div>
111+
);
112+
}

packages/ui/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# @openpkg-ts/ui
22

3+
## 0.6.2
4+
5+
### Patch Changes
6+
7+
- ## Bug Fixes
8+
9+
- **Fix diff/mark annotations not rendering**: Fixed handler order in `code.handlers.tsx` - `diff` and `mark` handlers must come before `line` handler to set CSS variables correctly
10+
- **Fix malformed className in code.line.tsx**: Moved CSS transition from className string to style object
11+
12+
## New Features
13+
14+
- **Add RunnableSnippet component**: Interactive code block with run button and output display (Phase 1 - mock execution, sandbox deferred)
15+
16+
## Registry Improvements
17+
18+
- **Auto-expandable parameters**: Updated `function-section` and `class-section` to use `ExpandableParameter` for all function/constructor parameters
19+
- Parameters with nested object properties now automatically show expand/collapse toggles
20+
- No manual wiring required - works out of the box when installed via `shadcn add`
21+
322
## 0.6.1
423

524
### Patch Changes
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# RunnableSnippet Component
2+
3+
Interactive code block with run button and output display.
4+
5+
## Installation
6+
7+
```bash
8+
shadcn add @openpkg-ts/runnable-snippet
9+
```
10+
11+
## Usage
12+
13+
### Basic Example
14+
15+
```tsx
16+
import { RunnableSnippet } from '@openpkg-ts/ui/docskit';
17+
18+
<RunnableSnippet
19+
code="console.log('Hello, world!');"
20+
language="javascript"
21+
/>
22+
```
23+
24+
### With Mock States (Phase 1)
25+
26+
```tsx
27+
<RunnableSnippet
28+
code={exampleCode}
29+
language="typescript"
30+
title="fetchUser.ts"
31+
mockState="idle"
32+
mockOutput='{"id":"usr_123","name":"Alice"}'
33+
/>
34+
```
35+
36+
### From Spec Examples
37+
38+
```tsx
39+
import { RunnableSnippet } from '@openpkg-ts/ui/docskit';
40+
import spec from './spec.json';
41+
42+
const fetchUserExport = spec.exports.find(e => e.name === 'fetchUser');
43+
const example = fetchUserExport.examples?.[0];
44+
45+
<RunnableSnippet
46+
code={typeof example === 'string' ? example : example.code}
47+
language={example.language || 'typescript'}
48+
title="fetchUser Example"
49+
mockState="idle"
50+
/>
51+
```
52+
53+
## Props
54+
55+
```typescript
56+
interface RunnableSnippetProps {
57+
/** Code to display */
58+
code: string;
59+
/** Language for syntax highlighting */
60+
language?: string;
61+
/** Optional title */
62+
title?: string;
63+
/** For demo: mock execution state */
64+
mockState?: 'idle' | 'running' | 'success' | 'error';
65+
/** For demo: mock output data */
66+
mockOutput?: string;
67+
/** Custom className */
68+
className?: string;
69+
}
70+
```
71+
72+
## Features
73+
74+
- **Code Display**: Uses `ClientDocsKitCode` for consistent syntax highlighting
75+
- **Run Button**: Floating button (appears on hover) positioned like CopyButton
76+
- **Output Panel**: `CollapsiblePanel` showing execution results
77+
- **Loading State**: Spinning loader icon during execution
78+
- **Success/Error States**: Visual indicators with execution duration
79+
80+
## Visual States
81+
82+
### Idle
83+
Code block with floating run button (visible on hover)
84+
85+
### Running
86+
Code block with disabled spinning button
87+
88+
### Success
89+
Code block + expanded output panel with ✓ and timing
90+
91+
### Error
92+
Code block + expanded output panel with ✕ and error message
93+
94+
## Phase 2: Real Execution (Future)
95+
96+
Replace `mockState` with real `onRun` handler:
97+
98+
```tsx
99+
<RunnableSnippet
100+
code="const x = 1 + 1;\nconsole.log(x);"
101+
onRun={sandboxExecute} // Real sandbox execution
102+
/>
103+
```
104+
105+
Will require:
106+
- iframe sandbox infrastructure
107+
- postMessage protocol
108+
- TypeScript transpilation
109+
- Console capture
110+
- Timeout handling

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openpkg-ts/ui",
3-
"version": "0.6.1",
3+
"version": "0.6.2",
44
"description": "UI primitives and components for OpenPkg documentation",
55
"homepage": "https://github.com/ryanwaits/openpkg-ts#readme",
66
"repository": {

packages/ui/src/docskit/code.handlers.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { wordWrap } from './word-wrap';
1616

1717
export function getHandlers(options: CodeOptions) {
1818
return [
19-
line,
20-
options.lineNumbers && lineNumbers,
19+
// Handlers that set CSS variables must come before 'line'
2120
mark,
2221
diff,
22+
// Line handler consumes --openpkg-line-bg and --openpkg-line-border
23+
line,
24+
options.lineNumbers && lineNumbers,
2325
link,
2426
callout,
2527
...collapse,

packages/ui/src/docskit/code.line.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ export const line: AnnotationHandler = {
99
style={{
1010
borderLeftColor: 'var(--openpkg-line-border, transparent)',
1111
backgroundColor: 'var(--openpkg-line-bg, transparent)',
12+
transition: 'background-color 0.3s ease',
1213
}}
13-
className="flex border-l-2 border-l-transparent background-color 0.3s ease"
14+
className="flex border-l-2 border-l-transparent"
1415
>
1516
<InnerLine merge={props} className="px-3 flex-1" />
1617
</div>

packages/ui/src/docskit/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ export {
9898
type TypeColor,
9999
typeBadgeVariants,
100100
} from './type-badge';
101+
export { RunnableSnippet, type RunnableSnippetProps } from './runnable-snippet';

0 commit comments

Comments
 (0)