diff --git a/packages/serverless-workflow-diagram-editor/README.md b/packages/serverless-workflow-diagram-editor/README.md index 5120843b..07f0a4c4 100644 --- a/packages/serverless-workflow-diagram-editor/README.md +++ b/packages/serverless-workflow-diagram-editor/README.md @@ -40,6 +40,52 @@ pnpm run build:storybook pnpm run start ``` +## shadcn/ui + +This package uses [shadcn/ui](https://ui.shadcn.com/) for UI primitives. Configuration lives in `components.json` at the package root. + +### Key settings + +- **Style**: `new-york` — compact spacing and sharper corners +- **Tailwind prefix**: `dec:` — all generated classes are prefixed to avoid conflicts with host applications +- **CSS target**: `src/components/ui/shadcn.css` — where shadcn injects its CSS variables +- **Path aliases**: `@/components`, `@/lib`, `@/hooks` — resolved via `tsconfig.json` paths and Vite's `tsconfigPaths` +- **Icon library**: `lucide` (generates `lucide-react` imports) + +### Adding a new shadcn component + +The shadcn CLI doesn't understand pnpm catalogs, so adding a component requires a manual step. + +1. **Generate the component** + + ```bash + cd packages/serverless-workflow-diagram-editor + pnpm dlx shadcn@latest add + ``` + + This creates the component in `src/components/ui/` and adds any new dependencies (e.g. `@radix-ui/*`) to `package.json` with a pinned version. + +2. **Move the dependency to the pnpm catalog** + + The CLI writes something like `"@radix-ui/react-tooltip": "^1.2.3"` directly into `package.json`. To follow the workspace convention: + - Add the package and version to the `catalog:` section in the **root** `pnpm-workspace.yaml` + - Replace the pinned version in the editor's `package.json` with `"catalog:"` + +3. **Verify consistency** + + ```bash + # From the repo root + pnpm dependencies:check + ``` + + This runs syncpack to confirm all workspace packages use the catalog. If you missed a dependency, run `pnpm dependencies:fix`. + +4. **Install** + + ```bash + pnpm install + ``` + ## Package Structure ``` diff --git a/packages/serverless-workflow-diagram-editor/components.json b/packages/serverless-workflow-diagram-editor/components.json new file mode 100644 index 00000000..54ca48f7 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/components/ui/shadcn.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "dec" + }, + "aliases": { + "components": "@/components", + "ui": "@/components/ui", + "lib": "@/lib", + "utils": "@/lib/utils", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/serverless-workflow-diagram-editor/package.json b/packages/serverless-workflow-diagram-editor/package.json index d26d1944..50ccf70c 100644 --- a/packages/serverless-workflow-diagram-editor/package.json +++ b/packages/serverless-workflow-diagram-editor/package.json @@ -44,7 +44,10 @@ "@serverlessworkflow/i18n": "workspace:*", "@serverlessworkflow/sdk": "catalog:", "@xyflow/react": "catalog:", - "js-yaml": "catalog:" + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "js-yaml": "catalog:", + "radix-ui": "catalog:" }, "devDependencies": { "@chromatic-com/storybook": "catalog:", diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/button.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/button.tsx new file mode 100644 index 00000000..89b6c373 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/button.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "dec:inline-flex dec:shrink-0 dec:items-center dec:justify-center dec:gap-2 dec:rounded-md dec:text-sm dec:font-medium dec:whitespace-nowrap dec:transition-all dec:outline-none dec:focus-visible:border-ring dec:focus-visible:ring-[3px] dec:focus-visible:ring-ring/50 dec:disabled:pointer-events-none dec:disabled:opacity-50 dec:aria-invalid:border-destructive dec:aria-invalid:ring-destructive/20 dec:dark:aria-invalid:ring-destructive/40 dec:[&_svg]:pointer-events-none dec:[&_svg]:shrink-0 dec:[&_svg:not([class*=size-])]:size-4", + { + variants: { + variant: { + default: "dec:bg-primary dec:text-primary-foreground dec:hover:bg-primary/90", + destructive: + "dec:bg-destructive dec:text-white dec:hover:bg-destructive/90 dec:focus-visible:ring-destructive/20 dec:dark:bg-destructive/60 dec:dark:focus-visible:ring-destructive/40", + outline: + "dec:border dec:bg-background dec:shadow-xs dec:hover:bg-accent dec:hover:text-accent-foreground dec:dark:border-input dec:dark:bg-input/30 dec:dark:hover:bg-input/50", + secondary: "dec:bg-secondary dec:text-secondary-foreground dec:hover:bg-secondary/80", + ghost: "dec:hover:bg-accent dec:hover:text-accent-foreground dec:dark:hover:bg-accent/50", + link: "dec:text-primary dec:underline-offset-4 dec:hover:underline", + }, + size: { + default: "dec:h-9 dec:px-4 dec:py-2 dec:has-[>svg]:px-3", + xs: "dec:h-6 dec:gap-1 dec:rounded-md dec:px-2 dec:text-xs dec:has-[>svg]:px-1.5 dec:[&_svg:not([class*=size-])]:size-3", + sm: "dec:h-8 dec:gap-1.5 dec:rounded-md dec:px-3 dec:has-[>svg]:px-2.5", + lg: "dec:h-10 dec:rounded-md dec:px-6 dec:has-[>svg]:px-4", + icon: "dec:size-9", + "icon-xs": "dec:size-6 dec:rounded-md dec:[&_svg:not([class*=size-])]:size-3", + "icon-sm": "dec:size-8", + "icon-lg": "dec:size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot.Root : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/input.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/input.tsx new file mode 100644 index 00000000..779b63ba --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/input.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/separator.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/separator.tsx new file mode 100644 index 00000000..a3346f94 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/separator.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import { Separator as SeparatorPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/shadcn.css b/packages/serverless-workflow-diagram-editor/src/components/ui/shadcn.css new file mode 100644 index 00000000..a38626eb --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/shadcn.css @@ -0,0 +1,79 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + shadcn/ui component styles. + + The shadcn CLI (via components.json) points here for CSS variable + injection. Keep all shadcn-specific CSS in this file so it stays + isolated from the editor's own global styles in styles.css. + + Imported by styles.css. +*/ + +.dec-root { + --sidebar: hsl(0 0% 98%); + + --sidebar-foreground: hsl(240 5.3% 26.1%); + + --sidebar-primary: hsl(240 5.9% 10%); + + --sidebar-primary-foreground: hsl(0 0% 98%); + + --sidebar-accent: hsl(240 4.8% 95.9%); + + --sidebar-accent-foreground: hsl(240 5.9% 10%); + + --sidebar-border: hsl(220 13% 91%); + + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +.dec-root.dark { + --sidebar: #2d3748; + + --sidebar-foreground: hsl(240 4.8% 95.9%); + + --sidebar-primary: hsl(224.3 76.3% 48%); + + --sidebar-primary-foreground: hsl(0 0% 100%); + + --sidebar-accent: #3d4a5c; + + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + + --sidebar-border: #3d4a5c; + + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +@theme inline { + --color-sidebar: var(--sidebar); + + --color-sidebar-foreground: var(--sidebar-foreground); + + --color-sidebar-primary: var(--sidebar-primary); + + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + + --color-sidebar-accent: var(--sidebar-accent); + + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + + --color-sidebar-border: var(--sidebar-border); + + --color-sidebar-ring: var(--sidebar-ring); +} diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sidebar.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/sidebar.tsx new file mode 100644 index 00000000..56a477dd --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sidebar.tsx @@ -0,0 +1,649 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use client"; + +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelRightIcon } from "lucide-react"; +import { Slot } from "radix-ui"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; + +const SIDEBAR_WIDTH = "20rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContextProps = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return setOpen((open) => !open); + }, [setOpen]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + toggleSidebar, + }), + [state, open, setOpen, toggleSidebar], + ); + + return ( + + +
+ {children} +
+
+
+ ); +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; +}) { + const { state } = useSidebar(); + + if (collapsible === "none") { + return ( +
+ {children} +
+ ); + } + + return ( +
+
+ {children} +
+
+ ); +} + +function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar(); + + return ( +