├── src ├── vite-env.d.ts ├── types │ └── index.ts ├── components │ ├── edges │ │ └── index.ts │ ├── nodes │ │ ├── instruments │ │ │ ├── pad-utils │ │ │ │ ├── index.ts │ │ │ │ ├── pad-button.tsx │ │ │ │ ├── modifiers.tsx │ │ │ │ └── button-utils.ts │ │ │ ├── custom-node.tsx │ │ │ ├── beat-machine-node.tsx │ │ │ └── arpeggiator-node.tsx │ │ ├── effects │ │ │ ├── palindrome-node.tsx │ │ │ ├── rev-node.tsx │ │ │ ├── fm-node.tsx │ │ │ ├── crush-node.tsx │ │ │ ├── attack-node.tsx │ │ │ ├── sustain-node.tsx │ │ │ ├── postgain-node.tsx │ │ │ ├── release-node.tsx │ │ │ ├── fast-node.tsx │ │ │ ├── slow-node.tsx │ │ │ ├── gain-node.tsx │ │ │ ├── distort-node.tsx │ │ │ ├── jux-node.tsx │ │ │ ├── lpf-node.tsx │ │ │ ├── pan-node.tsx │ │ │ ├── phaser-node.tsx │ │ │ ├── room-node.tsx │ │ │ ├── ply-node.tsx │ │ │ ├── mask-node.tsx │ │ │ └── late-node.tsx │ │ ├── synths │ │ │ ├── synth-select-node.tsx │ │ │ └── drum-sounds-node.tsx │ │ └── workflow-node.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── collapsible.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── popover.tsx │ │ ├── tooltip.tsx │ │ ├── slider.tsx │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ └── sheet.tsx │ ├── layouts │ │ └── sidebar-layout │ │ │ └── index.tsx │ ├── base-node.tsx │ ├── base-handle.tsx │ ├── delete-edge.tsx │ ├── button-edge.tsx │ ├── button-handle.tsx │ ├── workflow │ │ ├── useDragAndDrop.ts │ │ ├── index.tsx │ │ └── controls.tsx │ ├── app-dropdown-menu.tsx │ ├── cpm.tsx │ ├── pattern-panel.tsx │ ├── save-project-dialog.tsx │ ├── pattern-popup.tsx │ ├── zoom-slider.tsx │ ├── share-url-popover.tsx │ ├── app-info-popover.tsx │ └── node-header.tsx ├── lib │ ├── utils.ts │ ├── node-registry.ts │ ├── graph-utils.ts │ ├── state-serialization.ts │ └── strudel.ts ├── store │ ├── strudel-store.ts │ ├── app-context.ts │ ├── index.tsx │ └── app-store.ts ├── data │ ├── workflow-data.ts │ ├── icon-mapping.ts │ └── css │ │ ├── themes.ts │ │ ├── theme-doom-64.css │ │ ├── theme-mono.css │ │ └── theme-neo-brutalism.css ├── hooks │ ├── use-mobile.tsx │ ├── use-dropdown.tsx │ ├── use-theme-css.tsx │ ├── use-url-state.tsx │ ├── use-global-playback.tsx │ └── use-workflow-runner.tsx └── main.tsx ├── .github ├── FUNDING.yml └── dependabot.yml ├── public ├── favicon.ico └── strudel-flow-og.png ├── .prettierrc ├── tsconfig.node.json ├── config.json ├── .gitignore ├── .eslintrc.cjs ├── components.json ├── vite.config.js ├── tsconfig.json ├── package.json ├── index.html └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: xyflow 2 | open_collective: xyflow 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyflow/strudel-flow/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /public/strudel-flow-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyflow/strudel-flow/HEAD/public/strudel-flow-og.png -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // This file can be used for other type definitions if needed in the future 2 | export {}; 3 | -------------------------------------------------------------------------------- /src/components/edges/index.ts: -------------------------------------------------------------------------------- 1 | import DeleteEdge from '@/components/delete-edge'; 2 | 3 | export const edgeTypes = { 4 | default: DeleteEdge, 5 | deleteEdge: DeleteEdge, 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "strudel-flow", 3 | "name": "Strudel Flow", 4 | "skipBuild": true, 5 | "description": "A visual drum machine and pattern sequencer built with Strudel and React Flow.", 6 | "tags": [], 7 | "hidden": true, 8 | "published": false 9 | } 10 | -------------------------------------------------------------------------------- /src/components/nodes/instruments/pad-utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pad node utilities for step sequencing and pattern modification 3 | */ 4 | 5 | // Utilities 6 | export * from './button-utils'; 7 | 8 | // UI Components and Types 9 | export * from './modifiers'; 10 | export * from './pad-button'; 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # docs: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'npm' 7 | directory: '/' 8 | schedule: 9 | interval: 'daily' 10 | allow: 11 | - dependency-name: '@xyflow/react' 12 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/nodes/effects/palindrome-node.tsx: -------------------------------------------------------------------------------- 1 | import WorkflowNode from '@/components/nodes/workflow-node'; 2 | import { WorkflowNodeProps, AppNode } from '..'; 3 | 4 | export function PalindromeNode({ id, data }: WorkflowNodeProps) { 5 | return ; 6 | } 7 | 8 | PalindromeNode.strudelOutput = (_: AppNode, strudelString: string) => { 9 | const palindromeCall = `palindrome()`; 10 | return strudelString ? `${strudelString}.${palindromeCall}` : palindromeCall; 11 | }; 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/nodes/effects/rev-node.tsx: -------------------------------------------------------------------------------- 1 | import WorkflowNode from '@/components/nodes/workflow-node'; 2 | import { WorkflowNodeProps, AppNode } from '..'; 3 | 4 | export function RevNode({ id, data }: WorkflowNodeProps) { 5 | return ; 6 | } 7 | 8 | RevNode.strudelOutput = (_: AppNode, strudelString: string) => { 9 | // Rev effect is always active when node exists 10 | const revCall = `rev()`; 11 | return strudelString ? `${strudelString}.${revCall}` : revCall; 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/strudel-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type StrudelStore = { 4 | pattern: string; 5 | cpm: string; 6 | bpc: string; 7 | setPattern: (pattern: string) => void; 8 | setCpm: (cpm: string) => void; 9 | setBpc: (bpc: string) => void; 10 | }; 11 | 12 | export const useStrudelStore = create((set) => ({ 13 | pattern: '', 14 | cpm: '120', 15 | bpc: '4', 16 | setPattern: (pattern: string) => set({ pattern: pattern }), 17 | setCpm: (cpm: string) => set({ cpm: cpm }), 18 | setBpc: (bpc: string) => set({ bpc: bpc }), 19 | })); 20 | -------------------------------------------------------------------------------- /src/lib/node-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Registry for node types and their strudel output methods 3 | */ 4 | 5 | import { nodeTypes } from '@/components/nodes'; 6 | import { AppNode } from '@/components/nodes'; 7 | 8 | // Type for components that have strudelOutput method 9 | type NodeWithStrudelOutput = { 10 | strudelOutput?: (node: AppNode, strudelString: string) => string; 11 | }; 12 | 13 | export function getNodeStrudelOutput(nodeType: string) { 14 | const NodeComponent = nodeTypes[nodeType as keyof typeof nodeTypes] as NodeWithStrudelOutput; 15 | return NodeComponent?.strudelOutput; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/layouts/sidebar-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppSidebar } from '@/components/layouts/sidebar-layout/app-sidebar'; 2 | import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; 3 | 4 | export default function SidebarLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | title?: string; 9 | }) { 10 | return ( 11 | 12 | 13 |
14 | 15 | {children} 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/data/workflow-data.ts: -------------------------------------------------------------------------------- 1 | import { AppNode, createNodeByType } from '@/components/nodes'; 2 | import { Edge } from '@xyflow/react'; 3 | 4 | export const initialNodes: AppNode[] = [ 5 | createNodeByType({ 6 | type: 'pad-node', 7 | id: 'padNode_1', 8 | position: { x: 0, y: 0 }, 9 | }), 10 | createNodeByType({ 11 | type: 'synth-select-node', 12 | id: 'synthSelectNode_1', 13 | position: { x: 0, y: 600 }, 14 | }), 15 | ]; 16 | 17 | export const initialEdges: Edge[] = [ 18 | { 19 | id: 'edge_1', 20 | source: 'padNode_1', 21 | target: 'synthSelectNode_1', 22 | type: 'default', 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/components/base-node.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const BaseNode = forwardRef< 6 | HTMLDivElement, 7 | HTMLAttributes & { selected?: boolean } 8 | >(({ className, selected, ...props }, ref) => ( 9 |
20 | )); 21 | 22 | BaseNode.displayName = 'BaseNode'; 23 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import tailwindcss from '@tailwindcss/vite'; 5 | import { fileURLToPath } from 'url'; 6 | import process from 'node:process'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(() => ({ 10 | plugins: [react(), tailwindcss()], 11 | resolve: { 12 | alias: { 13 | '@': path.resolve(fileURLToPath(new URL('.', import.meta.url)), 'src'), 14 | }, 15 | }, 16 | base: 17 | process.env.VERCEL_ENV === 'production' 18 | ? 'https://flow-machine-xyflow.vercel.app/' 19 | : '/', 20 | })); 21 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = useState(undefined); 7 | 8 | useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 12 | }; 13 | mql.addEventListener('change', onChange); 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 15 | 16 | return () => mql.removeEventListener('change', onChange); 17 | }, []); 18 | 19 | return !!isMobile; 20 | } 21 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { ReactFlowProvider } from '@xyflow/react'; 4 | import { AppStoreProvider } from '@/store'; 5 | import { defaultState } from '@/store/app-store'; 6 | import SidebarLayout from '@/components/layouts/sidebar-layout'; 7 | import Workflow from '@/components/workflow'; 8 | 9 | import './index.css'; 10 | 11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /src/components/base-handle.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { Handle, HandleProps } from '@xyflow/react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | export type BaseHandleProps = HandleProps; 7 | 8 | export const BaseHandle = forwardRef( 9 | ({ className, children, ...props }, ref) => { 10 | return ( 11 | 20 | {children} 21 | 22 | ); 23 | } 24 | ); 25 | 26 | BaseHandle.displayName = 'BaseHandle'; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /src/store/app-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { useStore } from 'zustand'; 3 | 4 | import { AppState, type AppStore, createAppStore } from '@/store/app-store'; 5 | 6 | export type AppStoreApi = ReturnType; 7 | 8 | export const AppStoreContext = createContext( 9 | undefined 10 | ); 11 | 12 | export interface AppStoreProviderProps { 13 | children: React.ReactNode; 14 | initialState?: AppState; 15 | } 16 | 17 | export const useAppStore = (selector: (store: AppStore) => T): T => { 18 | const appStoreContext = useContext(AppStoreContext); 19 | 20 | if (!appStoreContext) { 21 | throw new Error(`useAppStore must be used within AppStoreProvider`); 22 | } 23 | 24 | return useStore(appStoreContext, selector); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Separator({ 7 | className, 8 | orientation = 'horizontal', 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ); 24 | } 25 | 26 | export { Separator }; 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |