Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ node_modules
dist
*.tsbuildinfo

examples/**/dist

.DS_Store
Thumbs.db

Expand All @@ -11,4 +13,3 @@ Thumbs.db
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ yarn typecheck # type-only check

Outputs land in `dist/`: ESM (`.js`), CJS (`.cjs`), declarations (`.d.ts`), and the editor stylesheet.

## Demo app

A tiny Vite playground lives in [`examples/demo`](./examples/demo). It mounts
`<ComposerEditor />` against a stub adapter with a minimal XRD + Composition so
you can drive the UI end-to-end without a host application. It's wired up as a
Yarn workspace, so `yarn install` at the repo root sets it up alongside the
library.

```bash
yarn install # installs the library + demo workspace
yarn demo # starts the Vite dev server
yarn demo:build # production build of the demo
```

The demo imports the library straight from `../../src`, so editing files under
`src/` hot-reloads inside the demo.

## License

MIT
11 changes: 9 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import globals from 'globals';

export default tseslint.config(
{
ignores: ['dist/**', 'node_modules/**', '.yarn/**', '.claude/**'],
ignores: [
'dist/**',
'node_modules/**',
'.yarn/**',
'.claude/**',
'examples/**/dist/**',
'examples/**/node_modules/**',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
react.configs.flat.recommended,
{
files: ['src/**/*.{ts,tsx}'],
files: ['src/**/*.{ts,tsx}', 'examples/**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
Expand Down
46 changes: 46 additions & 0 deletions examples/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# composer-demo

Tiny Vite + React app for poking at `@overlock-studio/composer` without a host.
It's a Yarn workspace under the parent repo and imports the library directly
from `../../src`, so edits to the library hot-reload.

## Run

From the repo root:

```bash
yarn install # installs the workspace, hoists everything to ./node_modules
yarn demo # vite dev server, opens http://localhost:5173
yarn demo:build
```

Or from inside this folder:

```bash
yarn dev
yarn build
```

## What it does

- Loads a minimal Crossplane bundle (`crossplane.yaml` + XRD + one Composition with a `nop` managed resource).
- Mounts `<ComposerEditor />` with a stubbed `EditorDataAdapter`.
- Captures the `onSave` payload and renders it in a side panel for inspection.
- A header button calls the imperative `editorRef.current?.save()` to exercise the ref handle.

## Editing the library

Source resolution goes through Vite aliases in `vite.config.ts`:

| Import | Resolves to |
| ----------------------------------------------- | ------------------------------------ |
| `@overlock-studio/composer` | `../../src/index.ts` |
| `@overlock-studio/composer/styles/editor.css` | `../../src/styles/editor.css` |

So any change in `composer/src/**` triggers HMR in the demo.

## Tailwind

The library ships utility classes. Tailwind is configured locally
(`tailwind.config.ts`) to scan both demo and library sources, and
`src/index.css` defines the shadcn-style HSL CSS variables the library expects.
12 changes: 12 additions & 0 deletions examples/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Composer Demo</title>
</head>
<body class="h-screen w-screen overflow-hidden bg-background text-foreground">
<div id="root" class="h-full w-full"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@overlock-studio/composer-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@overlock-studio/composer": "workspace:*",
"@xyflow/react": "^12.10.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.1.10",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"vite": "^6.0.7"
}
}
6 changes: 6 additions & 0 deletions examples/demo/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
111 changes: 111 additions & 0 deletions examples/demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useEffect, useRef, useState } from 'react';
import {
Button,
ComposerEditor,
type ComposerEditorHandle,
type ComposerSavePayload,
} from '@overlock-studio/composer';
import '@overlock-studio/composer/styles/editor.css';
import '@xyflow/react/dist/style.css';

import { demoAdapter } from './adapter';
import { sampleFiles, sampleHashes, sampleLayout } from './sample';

type Theme = 'light' | 'dark';

export default function App() {
const editorRef = useRef<ComposerEditorHandle>(null);
const [lastSave, setLastSave] = useState<ComposerSavePayload | null>(null);
const [showPayload, setShowPayload] = useState(false);
const [theme, setTheme] = useState<Theme>(() =>
document.documentElement.classList.contains('dark') ? 'dark' : 'light',
);

useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('composer-demo-theme', theme);
}, [theme]);

const handleSave = (payload: ComposerSavePayload) => {
setLastSave(payload);
setShowPayload(true);
console.log('[composer-demo] onSave payload', payload);
};

return (
<div className="flex h-full w-full flex-col">
<header className="flex items-center justify-between bg-sidebar px-4 py-2">
<div className="flex items-baseline gap-3">
<h1 className="text-sm font-semibold">Composer demo</h1>
<span className="text-xs text-muted-foreground">
Mounts ComposerEditor with a minimal XRD + Composition.
</span>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => editorRef.current?.save()}
>
Trigger save() via ref
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowPayload((v) => !v)}
>
{showPayload ? 'Hide' : 'Show'} last payload
</Button>
<Button
variant="ghost"
size="sm"
aria-label="Toggle theme"
onClick={() =>
setTheme((t) => (t === 'dark' ? 'light' : 'dark'))
}
>
{theme === 'dark' ? 'Light' : 'Dark'} mode
</Button>
</div>
</header>

<div className="flex min-h-0 flex-1">
<div className="flex min-w-0 flex-1 flex-col">
<ComposerEditor
ref={editorRef}
files={sampleFiles}
crossplaneFile="crossplane.yaml"
hashes={sampleHashes}
layout={sampleLayout}
adapter={demoAdapter}
onSave={handleSave}
/>
</div>
{showPayload && (
<aside className="w-[28rem] shrink-0 overflow-auto border-l border-border/70 bg-muted/40 p-3 text-xs">
<div className="mb-2 flex items-center justify-between">
<strong>onSave payload</strong>
<Button
variant="ghost"
size="sm"
onClick={() => setShowPayload(false)}
>
close
</Button>
</div>
{lastSave ? (
<pre className="whitespace-pre-wrap break-words font-mono">
{JSON.stringify(lastSave, null, 2)}
</pre>
) : (
<p className="text-muted-foreground">
Click save (header icon inside the editor, or the button above)
to capture a payload.
</p>
)}
</aside>
)}
</div>
</div>
);
}
79 changes: 79 additions & 0 deletions examples/demo/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
crossplaneCoreBlockTypes,
isCrossplaneCoreUrl,
type BlockType,
type ConfigurationDB,
type CrossplaneProviderDB,
type EditorDataAdapter,
} from '@overlock-studio/composer';

import { sampleDependencies } from './sample';

const providers: CrossplaneProviderDB[] = sampleDependencies
.filter((d) => d.kind === 'provider')
.map((d, idx) => ({
_id: `${idx + 1}`,
title: d.package.split('/').pop() ?? d.package,
description: `Demo provider entry parsed from crossplane.yaml (${d.package})`,
icon: '',
family: d.package.split('/').slice(-2, -1)[0] ?? 'provider',
familyName: d.package.split('/').slice(-2, -1)[0] ?? undefined,
url: d.package,
version: d.version.replace(/^[>=<~^ ]+/, '') || undefined,
}));

const configuration: ConfigurationDB = {
_id: 'composer',
name: 'demo-configuration',
providers: providers.map((p) => p._id),
functions: [],
deployId: null,
};

const fetchFromServer = async (url: string): Promise<BlockType[]> => {
const res = await fetch(
`/api/blocktypes?url=${encodeURIComponent(url)}`,
);
if (!res.ok) {
const body = await res.text();
throw new Error(
`/api/blocktypes ${res.status}: ${body || res.statusText}`,
);
}
return (await res.json()) as BlockType[];
};

export const demoAdapter: EditorDataAdapter = {
getBlocks: async () => [],
updateBlocks: async () => true,
getBlockTypes: async (url) => {
if (isCrossplaneCoreUrl(url)) {
return crossplaneCoreBlockTypes;
}
try {
return await fetchFromServer(url);
} catch (err) {
console.warn(`[composer-demo] getBlockTypes(${url}) failed:`, err);
return [];
}
},
getConfiguration: async (id) =>
id === configuration._id ? configuration : null,
getTemplate: async () => null,
listCrossplaneProviders: async () => ({
crossplaneProviders: providers,
totalCount: providers.length,
}),
getConfigurationData: async () => ({
compositions: [],
xrdBlockType: [],
providerUrls: providers.map((p) =>
p.version ? `${p.url}:${p.version}` : p.url,
),
functionUrls: [],
}),
createConfiguration: async () => configuration._id,
updateConfiguration: async () => undefined,
createProvidersFromUrls: async (urls) => urls,
createFunctionsFromUrls: async (urls) => urls,
};
Loading
Loading