From f77c83347fa382dd99540b35aaa9e4bba6330c24 Mon Sep 17 00:00:00 2001 From: Evgheni Poleacov Date: Fri, 15 May 2026 19:54:12 +0300 Subject: [PATCH 1/3] add examples/demo workspace and theme-aware editor --- .gitignore | 3 +- .yarnrc.yml | 1 + README.md | 17 + examples/demo/README.md | 46 + examples/demo/index.html | 12 + examples/demo/package.json | 27 + examples/demo/postcss.config.js | 6 + examples/demo/src/App.tsx | 112 ++ examples/demo/src/adapter.ts | 23 + examples/demo/src/index.css | 78 + examples/demo/src/main.tsx | 19 + examples/demo/src/sample.ts | 24 + examples/demo/src/samples/composition.yaml | 23 + examples/demo/src/samples/crossplane.yaml | 10 + examples/demo/src/samples/layout.json | 6 + examples/demo/src/samples/xrd.yaml | 28 + examples/demo/src/vite-env.d.ts | 6 + examples/demo/tailwind.config.ts | 67 + examples/demo/tsconfig.json | 25 + examples/demo/vite.config.ts | 24 + package.json | 7 +- .../Editor/ComposerEditor/ComposerEditor.tsx | 2 +- .../Editor/EditorArea/EditorArea.tsx | 23 +- src/components/ui/sidebar.tsx | 2 +- src/styles/editor.css | 2 + yarn.lock | 1500 ++++++++++++++++- 26 files changed, 2074 insertions(+), 19 deletions(-) create mode 100644 .yarnrc.yml create mode 100644 examples/demo/README.md create mode 100644 examples/demo/index.html create mode 100644 examples/demo/package.json create mode 100644 examples/demo/postcss.config.js create mode 100644 examples/demo/src/App.tsx create mode 100644 examples/demo/src/adapter.ts create mode 100644 examples/demo/src/index.css create mode 100644 examples/demo/src/main.tsx create mode 100644 examples/demo/src/sample.ts create mode 100644 examples/demo/src/samples/composition.yaml create mode 100644 examples/demo/src/samples/crossplane.yaml create mode 100644 examples/demo/src/samples/layout.json create mode 100644 examples/demo/src/samples/xrd.yaml create mode 100644 examples/demo/src/vite-env.d.ts create mode 100644 examples/demo/tailwind.config.ts create mode 100644 examples/demo/tsconfig.json create mode 100644 examples/demo/vite.config.ts diff --git a/.gitignore b/.gitignore index 33946f6..7cc67a7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules dist *.tsbuildinfo +examples/**/dist + .DS_Store Thumbs.db @@ -11,4 +13,3 @@ Thumbs.db !.yarn/releases !.yarn/sdks !.yarn/versions -.pnp.* diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index d7ae673..4ac519a 100644 --- a/README.md +++ b/README.md @@ -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 +`` 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 diff --git a/examples/demo/README.md b/examples/demo/README.md new file mode 100644 index 0000000..52abf5e --- /dev/null +++ b/examples/demo/README.md @@ -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 `` 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. diff --git a/examples/demo/index.html b/examples/demo/index.html new file mode 100644 index 0000000..d146106 --- /dev/null +++ b/examples/demo/index.html @@ -0,0 +1,12 @@ + + + + + + Composer Demo + + +
+ + + diff --git a/examples/demo/package.json b/examples/demo/package.json new file mode 100644 index 0000000..f2c4869 --- /dev/null +++ b/examples/demo/package.json @@ -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" + } +} diff --git a/examples/demo/postcss.config.js b/examples/demo/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/examples/demo/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/demo/src/App.tsx b/examples/demo/src/App.tsx new file mode 100644 index 0000000..375f0b4 --- /dev/null +++ b/examples/demo/src/App.tsx @@ -0,0 +1,112 @@ +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(null); + const [lastSave, setLastSave] = useState(null); + const [showPayload, setShowPayload] = useState(false); + const [theme, setTheme] = useState(() => + 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); + // eslint-disable-next-line no-console + console.log('[composer-demo] onSave payload', payload); + }; + + return ( +
+
+
+

Composer demo

+ + Mounts ComposerEditor with a minimal XRD + Composition. + +
+
+ + + +
+
+ +
+
+ +
+ {showPayload && ( + + )} +
+
+ ); +} diff --git a/examples/demo/src/adapter.ts b/examples/demo/src/adapter.ts new file mode 100644 index 0000000..97c735e --- /dev/null +++ b/examples/demo/src/adapter.ts @@ -0,0 +1,23 @@ +import type { EditorDataAdapter } from '@overlock-studio/composer'; + +export const demoAdapter: EditorDataAdapter = { + getBlocks: async () => [], + updateBlocks: async () => true, + getBlockTypes: async () => [], + getConfiguration: async () => null, + getTemplate: async () => null, + listCrossplaneProviders: async () => ({ + crossplaneProviders: [], + totalCount: 0, + }), + getConfigurationData: async () => ({ + compositions: [], + xrdBlockType: [], + providerUrls: [], + functionUrls: [], + }), + createConfiguration: async () => 'demo-configuration-id', + updateConfiguration: async () => undefined, + createProvidersFromUrls: async (urls) => urls, + createFunctionsFromUrls: async (urls) => urls, +}; diff --git a/examples/demo/src/index.css b/examples/demo/src/index.css new file mode 100644 index 0000000..989a1b0 --- /dev/null +++ b/examples/demo/src/index.css @@ -0,0 +1,78 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 5.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 5.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + html, + body, + #root { + height: 100%; + } + + body { + @apply bg-background text-foreground; + } +} diff --git a/examples/demo/src/main.tsx b/examples/demo/src/main.tsx new file mode 100644 index 0000000..3abae10 --- /dev/null +++ b/examples/demo/src/main.tsx @@ -0,0 +1,19 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const rootEl = document.getElementById('root'); +if (!rootEl) { + throw new Error('Missing #root element'); +} + +const stored = localStorage.getItem('composer-demo-theme'); +const initialTheme = stored === 'light' ? 'light' : 'dark'; +document.documentElement.classList.toggle('dark', initialTheme === 'dark'); + +createRoot(rootEl).render( + + + , +); diff --git a/examples/demo/src/sample.ts b/examples/demo/src/sample.ts new file mode 100644 index 0000000..99844e6 --- /dev/null +++ b/examples/demo/src/sample.ts @@ -0,0 +1,24 @@ +import type { + CrossplaneFile, + LayoutByComposition, +} from '@overlock-studio/composer'; + +import crossplaneYaml from './samples/crossplane.yaml?raw'; +import xrdYaml from './samples/xrd.yaml?raw'; +import compositionYaml from './samples/composition.yaml?raw'; +import layoutJson from './samples/layout.json'; + +export const sampleFiles: CrossplaneFile[] = [ + { name: 'crossplane.yaml', content: crossplaneYaml }, + { name: 'xrd.yaml', content: xrdYaml }, + { name: 'composition.yaml', content: compositionYaml }, +]; + +export const sampleLayout: LayoutByComposition = + layoutJson as LayoutByComposition; + +export const sampleHashes: Record = { + 'crossplane.yaml': 'h-crossplane-1', + 'xrd.yaml': 'h-xrd-1', + 'composition.yaml': 'h-composition-1', +}; diff --git a/examples/demo/src/samples/composition.yaml b/examples/demo/src/samples/composition.yaml new file mode 100644 index 0000000..46075bb --- /dev/null +++ b/examples/demo/src/samples/composition.yaml @@ -0,0 +1,23 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: demo-app-composition +spec: + compositeTypeRef: + apiVersion: demo.example.org/v1alpha1 + kind: XDemoApp + resources: + - name: nop-resource + base: + apiVersion: nop.crossplane.io/v1alpha1 + kind: NopResource + spec: + forProvider: + conditionAfter: + - conditionType: Ready + conditionStatus: "True" + time: 5s + patches: + - type: FromCompositeFieldPath + fromFieldPath: spec.size + toFieldPath: spec.forProvider.fields.size diff --git a/examples/demo/src/samples/crossplane.yaml b/examples/demo/src/samples/crossplane.yaml new file mode 100644 index 0000000..84ca1fb --- /dev/null +++ b/examples/demo/src/samples/crossplane.yaml @@ -0,0 +1,10 @@ +apiVersion: meta.pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: demo-configuration +spec: + crossplane: + version: ">=v1.14.0" + dependsOn: + - provider: xpkg.upbound.io/crossplane-contrib/provider-nop + version: ">=v0.4.0" diff --git a/examples/demo/src/samples/layout.json b/examples/demo/src/samples/layout.json new file mode 100644 index 0000000..10d447c --- /dev/null +++ b/examples/demo/src/samples/layout.json @@ -0,0 +1,6 @@ +{ + "demo-app-composition": { + "_self": { "x": 80, "y": 80, "width": 480, "height": 360 }, + "nop-resource": { "x": 40, "y": 80 } + } +} diff --git a/examples/demo/src/samples/xrd.yaml b/examples/demo/src/samples/xrd.yaml new file mode 100644 index 0000000..ce818a0 --- /dev/null +++ b/examples/demo/src/samples/xrd.yaml @@ -0,0 +1,28 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xdemoapps.demo.example.org +spec: + group: demo.example.org + names: + kind: XDemoApp + plural: xdemoapps + claimNames: + kind: DemoApp + plural: demoapps + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + size: + type: string + description: t-shirt size of the app + required: + - size diff --git a/examples/demo/src/vite-env.d.ts b/examples/demo/src/vite-env.d.ts new file mode 100644 index 0000000..8e9fc56 --- /dev/null +++ b/examples/demo/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.yaml?raw' { + const content: string; + export default content; +} diff --git a/examples/demo/tailwind.config.ts b/examples/demo/tailwind.config.ts new file mode 100644 index 0000000..7554d20 --- /dev/null +++ b/examples/demo/tailwind.config.ts @@ -0,0 +1,67 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + darkMode: ['class'], + content: [ + './index.html', + './src/**/*.{ts,tsx}', + '../../src/**/*.{ts,tsx}', + ], + theme: { + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [], +}; + +export default config; diff --git a/examples/demo/tsconfig.json b/examples/demo/tsconfig.json new file mode 100644 index 0000000..4c13859 --- /dev/null +++ b/examples/demo/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "allowImportingTsExtensions": false, + "baseUrl": ".", + "paths": { + "@overlock-studio/composer": ["../../src/index.ts"], + "@overlock-studio/composer/styles/editor.css": [ + "../../src/styles/editor.css" + ], + "@/*": ["../../src/*"] + } + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/demo/vite.config.ts b/examples/demo/vite.config.ts new file mode 100644 index 0000000..743c300 --- /dev/null +++ b/examples/demo/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'node:path'; + +const composerSrc = path.resolve(__dirname, '../../src'); + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@overlock-studio/composer/styles/editor.css': path.join( + composerSrc, + 'styles/editor.css', + ), + '@overlock-studio/composer': path.join(composerSrc, 'index.ts'), + '@': composerSrc, + }, + dedupe: ['react', 'react-dom', '@xyflow/react'], + }, + server: { + port: 5173, + open: true, + }, +}); diff --git a/package.json b/package.json index da76812..97220bf 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,9 @@ "dist", "src" ], + "workspaces": [ + "examples/*" + ], "sideEffects": [ "*.css" ], @@ -43,7 +46,9 @@ "clean": "rm -rf dist", "typecheck": "tsc --noEmit", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "demo": "yarn workspace @overlock-studio/composer-demo dev", + "demo:build": "yarn workspace @overlock-studio/composer-demo build" }, "peerDependencies": { "@xyflow/react": "^12.10.0", diff --git a/src/components/Editor/ComposerEditor/ComposerEditor.tsx b/src/components/Editor/ComposerEditor/ComposerEditor.tsx index 94a1339..7162447 100644 --- a/src/components/Editor/ComposerEditor/ComposerEditor.tsx +++ b/src/components/Editor/ComposerEditor/ComposerEditor.tsx @@ -253,7 +253,7 @@ function ComposerEditorBody({ return ( -
+