Documentation Index
Fetch the complete documentation index at: https://docs.noclick.com/llms.txt
Use this file to discover all available pages before exploring further.
Custom Components
Custom components are React applications that render inside the NoClick interface tab. They run in sandboxed iframes with access to the full @noclick/sdk API, Tailwind CSS, and any npm package.
Creating a Component
From the Canvas
- Open the Interface tab in the workflow editor
- Drag Custom Component from the UX panel onto the grid
- Click the node on the canvas to open its config
- Write JSX in the code editor
From MCP (Claude Code, Cursor)
<add_node type="interface-html-react" name="dashboard" label="Dashboard" />
<update_config id="dashboard" operation="jsx" fullscreen="true" />
<update_config id="dashboard" field="jsx_source">
import React from 'react';
import ReactDOM from 'react-dom/client';
function App() {
return <div className="p-4 text-white">Hello from NoClick!</div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</update_config>
How It Works
- You write JSX/TSX in the node’s
jsx_source config field
- When the node executes, the backend transpiles it via Sucrase (runs in QuickJS, ~15ms)
- An import map is generated with React 19 from esm.sh, Tailwind CSS CDN, and any npm packages you import
- The
@noclick/sdk is injected as a base64 data URI in the import map
- Everything is assembled into an HTML document (
srcdoc) and rendered in an iframe
Using npm Packages
Just import them — the transpiler auto-detects imports and maps them to esm.sh:
import { BarChart, Bar, XAxis, YAxis } from 'recharts';
import { format } from 'date-fns';
import { motion } from 'framer-motion';
Packages are bundled with &bundle to avoid transitive dependency issues.
Fullscreen Mode
Set fullscreen="true" in the node config to render the component as a full-viewport tab instead of a grid block:
<update_config id="my-component" fullscreen="true" />
When multiple fullscreen components exist, tabs appear below the main tab bar. Tabs are:
- Draggable — reorder by dragging
- Renamable — click the active tab label to edit
- Persistent — tab order survives page reloads
Styling
Tailwind CSS is included automatically. Use standard Tailwind classes:
<div className="flex flex-col items-center gap-4 p-8 bg-zinc-950 text-white min-h-screen">
<h1 className="text-2xl font-bold">Dashboard</h1>
<button className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg">
Click me
</button>
</div>
Using the SDK
Import any SDK namespace directly:
import { nodes, execution, state, auth, resources, dataset } from '@noclick/sdk';
Read Node Output
const output = await nodes.getOutput('data-fetcher-node-id');
Run a Node and Get Results
// Fire and forget
execution.runNodesInBackground(['node-id']);
// Run with temporary config overrides
execution.runNodesInBackground([
{ id: 'http-node', config: { url: userInput } }
]);
// Stream results as nodes complete
const stream = execution.runNodesAndGetOutput(['fetcher'], ['chart-data', 'summary']);
stream.on('output', (nodeId, data) => {
if (nodeId === 'chart-data') setChartData(data);
});
await stream.all();
Persistent State
Requires a State Manager node in the workflow:
await state.set('counter', 42);
const val = await state.get('counter');
await state.update('items', list => [...(list || []), newItem]);
await state.del('counter');
// Subscribe to changes
state.onChange('counter', newVal => setCounter(newVal));
OAuth and Credentials
// Check if Gmail is connected
const hasGmail = await auth.hasCredential('google_gmail_oauth');
// Trigger OAuth popup (scopes auto-resolved from node schemas)
if (!hasGmail) {
const cred = await auth.requestCredential('google_gmail_oauth');
}
// Create API key credential
const cred = await auth.createCredential('telegram_bot_token', { token: key });
File Upload
const { resourceId, uploadUrl } = await resources.upload('photo.jpg', 'image/jpeg', file.size);
await fetch(uploadUrl, { method: 'PUT', body: file });
const downloadUrl = await resources.getUrl(resourceId);
Dataset CRUD
const dsId = await dataset.create('User Submissions');
await dataset.appendRows(dsId, [{ name: 'Alice', score: 95 }]);
const page = await dataset.getRows(dsId, { limit: 100 });
await dataset.deleteRows(dsId, [rowId]);
Caching
Compiled srcdoc is cached in IndexedDB (one slot per node). Components render instantly on tab switch and page reload without re-transpilation. The cache auto-invalidates when jsx_source changes. Copied/duplicated nodes share cache via source hash matching.
Troubleshooting
| Issue | Solution |
|---|
| Gray iframe on first load | esm.sh cold start (~3-5s). Packages are cached after first load. |
< in stored JSX | Use field="jsx_source" body syntax in MCP XML. Entity escaping is handled automatically. |
{{ }} in JSX stripped | Fixed — unresolvable {{ }} patterns (like JSX inline styles) are preserved as-is. |
| Drag/resize stuck on canvas | Fixed — pointer-events: none on iframes during drag/resize via CSS. |
| SDK method hangs | All requests timeout after 30s with a descriptive error. |