├── src ├── types │ ├── tag.ts │ └── generics.ts ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── loading.tsx │ ├── workflow │ │ ├── [id] │ │ │ ├── page.tsx │ │ │ └── loading.tsx │ │ └── _components │ │ │ └── flow-builder.tsx │ ├── layout.tsx │ ├── page.tsx │ └── globals.css ├── contants │ └── symbols.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── textarea.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── sonner.tsx │ │ ├── badge.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── button-theme-toggle.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── accordion.tsx │ │ ├── card-workflow.tsx │ │ ├── dialog.tsx │ │ ├── alert-dialog.tsx │ │ ├── command.tsx │ │ ├── dropdown-menu.tsx │ │ └── context-menu.tsx │ ├── theme-provider.tsx │ ├── generics │ │ ├── on-mounted.tsx │ │ ├── for-each.tsx │ │ ├── whenever.tsx │ │ ├── switch-case.tsx │ │ └── check.tsx │ └── flow-builder │ │ ├── components │ │ ├── blocks │ │ │ ├── sidebar │ │ │ │ ├── components │ │ │ │ │ ├── sidebar-switch-panel.tsx │ │ │ │ │ ├── sidebar-panel-wrapper.tsx │ │ │ │ │ ├── sidebar-panel-heading.tsx │ │ │ │ │ └── sidebar-button-item.tsx │ │ │ │ ├── constants │ │ │ │ │ └── panels.tsx │ │ │ │ ├── sidebar-module.tsx │ │ │ │ ├── panels │ │ │ │ │ ├── node-properties │ │ │ │ │ │ ├── constants │ │ │ │ │ │ │ └── property-panels.ts │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ └── use-node-list.ts │ │ │ │ │ │ ├── property-panels │ │ │ │ │ │ │ ├── introduction-property-panel.tsx │ │ │ │ │ │ │ ├── unavailable-property-panel.tsx │ │ │ │ │ │ │ ├── text-message-property-panel.tsx │ │ │ │ │ │ │ ├── tags-property.tsx │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ └── menu-option-property.tsx │ │ │ │ │ │ │ └── menu-property-panel.tsx │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── node-propery-panel.tsx │ │ │ │ │ │ │ └── node-list-item.tsx │ │ │ │ │ │ └── node-properties-panel.tsx │ │ │ │ │ └── available-nodes │ │ │ │ │ │ ├── available-nodes-panel.tsx │ │ │ │ │ │ └── components │ │ │ │ │ │ └── node-preview-draggable.tsx │ │ │ │ └── fragments │ │ │ │ │ └── desktop-sidebar-fragment.tsx │ │ │ ├── nodes │ │ │ │ ├── menu-node │ │ │ │ │ ├── components │ │ │ │ │ │ └── node-option.tsx │ │ │ │ │ └── menu.node.tsx │ │ │ │ ├── start.node.tsx │ │ │ │ ├── end.node.tsx │ │ │ │ ├── text-message-node │ │ │ │ │ └── text-message.node.tsx │ │ │ │ └── tags-node │ │ │ │ │ └── tags.node.tsx │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── ui │ │ │ ├── header-with-icon.tsx │ │ │ ├── save-flow-buttom.tsx │ │ │ └── node-card.tsx │ │ ├── controls │ │ │ ├── custom-control-button.tsx │ │ │ └── custom-controls.tsx │ │ ├── edges │ │ │ └── custom-deletable-edge.tsx │ │ ├── add-node-floating-menu │ │ │ ├── components │ │ │ │ └── node-list.tsx │ │ │ └── add-node-floating-menu.tsx │ │ └── handles │ │ │ └── custom-handler.tsx │ │ └── flow-builder.tsx ├── providers │ └── query-client-provider.tsx ├── lib │ └── utils.ts ├── hooks │ ├── use-delete-key-code.ts │ ├── use-on-nodes-delete.ts │ ├── use-delete-node.ts │ ├── use-insert-node.ts │ ├── use-drag-drop-flow-builder.ts │ ├── use-is-valid-connection.ts │ ├── use-flow-validator.ts │ ├── use-add-node-on-edge-drop.ts │ └── use-node-auto-adjust.ts ├── stores │ ├── add-node-on-edge-drop-state.ts │ └── flow-store.ts └── services │ ├── get-workflow.ts │ └── get-workflows.ts ├── public └── screen.jpeg ├── next.config.mjs ├── postcss.config.mjs ├── .eslintrc.json ├── .vscode └── launch.json ├── .gitignore ├── components.json ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.ts └── README.md /src/types/tag.ts: -------------------------------------------------------------------------------- 1 | export type Tag = Record<"value" | "label" | "color", string>; 2 | -------------------------------------------------------------------------------- /public/screen.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobruf/shadcn-next-workflows/HEAD/public/screen.jpeg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobruf/shadcn-next-workflows/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/contants/symbols.ts: -------------------------------------------------------------------------------- 1 | export const NODE_TYPE_DRAG_DATA_FORMAT = "application/flow-builder.node-type"; 2 | -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobruf/shadcn-next-workflows/HEAD/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobruf/shadcn-next-workflows/HEAD/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off", 5 | "@typescript-eslint/no-unused-vars": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/generics.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { [P in keyof T]?: DeepPartial }; 2 | export type DeepReadonly = { readonly [P in keyof T]: DeepReadonly }; 3 | export type DeepWritable = { -readonly [P in keyof T]: DeepWritable }; 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@iconify/react/dist/iconify.js"; 4 | import React from "react"; 5 | 6 | export default function Loading() { 7 | return
8 | 9 |
; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/workflow/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { getWorkflow } from "@/services/get-workflow"; 3 | import { FlowBuilderPage } from "../_components/flow-builder"; 4 | 5 | export default async function Workflow({ params }: { params: { id: string } }) { 6 | const workflow = await getWorkflow(params.id); 7 | 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/app/workflow/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@iconify/react/dist/iconify.js"; 4 | import React from "react"; 5 | 6 | export default function Loading() { 7 | return
8 | 9 |
; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/generics/on-mounted.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, useEffect, useState } from "react"; 2 | 3 | type OnMountedProps = Readonly<{ children: ReactNode }>; 4 | 5 | export function OnMounted({ children }: OnMountedProps) { 6 | const [mounted, setMounted] = useState(false); 7 | 8 | useEffect(() => { 9 | setMounted(true); 10 | }, []); 11 | 12 | return <>{mounted && children}; 13 | } 14 | -------------------------------------------------------------------------------- /src/providers/query-client-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider as ReactQueryProvider } from "@tanstack/react-query"; 3 | 4 | export const QueryClientProvider = ({ children }: { children: React.ReactNode }) => { 5 | const queryClient = new QueryClient(); 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /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 | 8 | export function truncateMiddle(text: string, maxLength: number) { 9 | if (text.length <= maxLength) return text; 10 | 11 | const half = maxLength / 2; 12 | return `${text.slice(0, half)}...${text.slice(-half)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/components/sidebar-switch-panel.tsx: -------------------------------------------------------------------------------- 1 | import { PANEL_COMPONENTS } from "../constants/panels"; 2 | 3 | type SwitchSidebarPanelProps = Readonly<{ 4 | active: "node-properties" | "available-nodes" | "none"; 5 | }>; 6 | 7 | export function SwitchSidebarPanel({ active }: SwitchSidebarPanelProps) { 8 | const PanelComponent = PANEL_COMPONENTS[active]; 9 | return PanelComponent ? : null; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/constants/panels.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from "react"; 2 | import AvailableNodesPanel from "../panels/available-nodes/available-nodes-panel"; 3 | import { NodePropertiesPanel } from "../panels/node-properties/node-properties-panel"; 4 | 5 | export const PANEL_COMPONENTS: Record< 6 | "node-properties" | "available-nodes" | "none", 7 | ComponentType 8 | > = { 9 | "available-nodes": AvailableNodesPanel, 10 | "node-properties": NodePropertiesPanel, 11 | none: () => null, 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/components/sidebar-panel-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | import type { ComponentPropsWithoutRef } from "react"; 4 | 5 | type SidebarPanelWrapperProps = Readonly>; 6 | 7 | export default function SidebarPanelWrapper({ 8 | children, 9 | className, 10 | ...props 11 | }: SidebarPanelWrapperProps) { 12 | return ( 13 |
17 | {children} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/sidebar-module.tsx: -------------------------------------------------------------------------------- 1 | import { useFlowStore } from "@/stores/flow-store"; 2 | import { useShallow } from "zustand/shallow"; 3 | import { DesktopSidebarFragment } from "./fragments/desktop-sidebar-fragment"; 4 | 5 | export function SidebarModule() { 6 | const [activePanel, setActivePanel] = useFlowStore( 7 | useShallow((s) => [s.workflow.sidebar.active, s.actions.sidebar.setActivePanel]) 8 | ); 9 | 10 | return ( 11 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/ui/header-with-icon.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify/react"; 2 | 3 | interface HeaderWithIconProps { 4 | icon: string; 5 | title: string; 6 | } 7 | 8 | export const HeaderWithIcon = ({ icon, title }: HeaderWithIconProps) => { 9 | return ( 10 |
11 | 12 |
13 | {title} 14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/panels/node-properties/constants/property-panels.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from "react"; 2 | import { NODES } from "../../../.."; 3 | import { BuilderNodeType } from "../../../../types"; 4 | 5 | export const NODE_PROPERTY_PANEL_COMPONENTS: Partial< 6 | Record< 7 | BuilderNodeType, 8 | ComponentType<{ 9 | id: string; 10 | type: BuilderNodeType; 11 | data: any; 12 | updateData: (data: Partial) => void; 13 | }> 14 | > 15 | > = NODES.reduce((acc, node) => { 16 | acc[node.type] = node.propertyPanel; 17 | return acc; 18 | }, {} as any); 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "blocks": "@/components/flow-builder/components/blocks", 16 | "flow-builder-ui": "@/components/flow-builder/components/ui", 17 | "utils": "@/lib/utils", 18 | "ui": "@/components/ui", 19 | "lib": "@/lib", 20 | "hooks": "@/hooks" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/flow-builder/components/blocks/sidebar/components/sidebar-panel-heading.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { ComponentPropsWithoutRef } from "react"; 3 | 4 | type SidebarPanelHeadingProps = Readonly>; 5 | 6 | export default function SidebarPanelHeading({ 7 | children, 8 | className, 9 | ...props 10 | }: SidebarPanelHeadingProps) { 11 | return ( 12 |
19 | {children} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/use-delete-key-code.ts: -------------------------------------------------------------------------------- 1 | import { type KeyCode, useOnSelectionChange } from "@xyflow/react"; 2 | import { useState } from "react"; 3 | 4 | export function useDeleteKeyCode(): KeyCode | null { 5 | const [deleteKeyCode, setDeleteKeyCode] = useState(null); 6 | 7 | useOnSelectionChange({ 8 | onChange: ({ nodes }) => { 9 | if (nodes.length === 0) { 10 | setDeleteKeyCode(["Backspace", "Delete", "Del"]); 11 | } else { 12 | const areNodesNotDeletable = nodes.some( 13 | (node) => node.data.deletable === false 14 | ); 15 | setDeleteKeyCode( 16 | areNodesNotDeletable ? null : ["Backspace", "Delete", "Del"] 17 | ); 18 | } 19 | }, 20 | }); 21 | 22 | return deleteKeyCode; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Textarea = React.forwardRef< 5 | HTMLTextAreaElement, 6 | React.TextareaHTMLAttributes 7 | >(({ className, ...props }, ref) => { 8 | return ( 9 |