ai-design-system 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -269
- package/components/ai-elements/canvas.tsx +2 -1
- package/components/ai-elements/edge.tsx +28 -20
- package/components/ai-elements/node.tsx +23 -4
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.stories.tsx +279 -0
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +180 -0
- package/components/blocks/WorkflowCanvas/index.ts +2 -0
- package/components/blocks/index.ts +15 -0
- package/components/composites/StateNode/StateNode.stories.tsx +281 -0
- package/components/composites/StateNode/StateNode.tsx +100 -0
- package/components/composites/StateNode/index.ts +2 -0
- package/components/composites/TransitionNode/TransitionNode.stories.tsx +260 -0
- package/components/composites/TransitionNode/TransitionNode.tsx +80 -0
- package/components/composites/TransitionNode/index.ts +2 -0
- package/components/composites/WorkflowToolbar/WorkflowToolbar.stories.tsx +352 -0
- package/components/composites/WorkflowToolbar/WorkflowToolbar.tsx +195 -0
- package/components/composites/WorkflowToolbar/index.ts +7 -0
- package/components/composites/index.ts +13 -1
- package/components/features/AIDocEditor/README.md +127 -0
- package/components/features/PageLayout/README.md +122 -0
- package/components/features/PageLayout/usePageLayout.d.ts +2 -0
- package/components/features/RefinementPanel/RefinementPanel.stories.tsx +2 -2
- package/components/features/RefinementPanel/index.ts +1 -1
- package/components/features/RefinementPanel/useRefinementPanel.mock.ts +1 -1
- package/components/features/SpecNavigator/README.md +116 -0
- package/components/features/SpecNavigator/SpecNavigator.stories.tsx +2 -2
- package/components/features/SpecNavigator/useSpecNavigator.mock.ts +1 -1
- package/components/features/WorkflowBuilder/README.md +159 -0
- package/components/features/WorkflowBuilder/WorkflowBuilder.behaviors.stories.tsx +136 -0
- package/components/features/WorkflowBuilder/WorkflowBuilder.mocks.ts +36 -0
- package/components/features/WorkflowBuilder/WorkflowBuilder.stories.tsx +135 -0
- package/components/features/WorkflowBuilder/WorkflowBuilder.tsx +97 -0
- package/components/features/WorkflowBuilder/index.ts +2 -0
- package/components/features/WorkflowBuilder/useWorkflowBuilder.d.ts +44 -0
- package/components/features/WorkflowBuilder/useWorkflowBuilder.mock.ts +116 -0
- package/components/features/index.ts +8 -0
- package/components/index.ts +1 -1
- package/components/primitives/ButtonGroup/ButtonGroup.stories.tsx +35 -0
- package/components/primitives/ButtonGroup/ButtonGroup.tsx +39 -0
- package/components/primitives/ButtonGroup/index.ts +3 -0
- package/components/primitives/index.ts +1 -0
- package/components/ui/animated-border.tsx +79 -0
- package/dist/index.cjs +897 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +99 -0
- package/dist/index.js +886 -169
- package/dist/index.js.map +1 -1
- package/package.json +14 -12
package/README.md
CHANGED
|
@@ -36,272 +36,3 @@ pnpm storybook
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Open [http://localhost:6006](http://localhost:6006) to view the component library.
|
|
39
|
-
|
|
40
|
-
## Design System Governance
|
|
41
|
-
|
|
42
|
-
### Pre-Commit Hooks
|
|
43
|
-
|
|
44
|
-
The design system enforces strict quality standards through automated pre-commit hooks:
|
|
45
|
-
|
|
46
|
-
**Master Validation Script**
|
|
47
|
-
- Runs all design system validations in a single command
|
|
48
|
-
- Run manually: `bash scripts/run-all-validations.sh` or `pnpm run prebuild`
|
|
49
|
-
|
|
50
|
-
**Layer Import Architecture**
|
|
51
|
-
- Validates that components follow the layered architecture pattern
|
|
52
|
-
- Ensures primitives don't import from patterns or features
|
|
53
|
-
- Ensures patterns don't import from features
|
|
54
|
-
- Run manually: `python3 scripts/validate-layer-imports.py`
|
|
55
|
-
|
|
56
|
-
**Storybook Coverage**
|
|
57
|
-
- Ensures every component has a corresponding `.stories.tsx` file
|
|
58
|
-
- Validates story file naming conventions
|
|
59
|
-
- Run manually: `node scripts/validate-storybook-coverage.js`
|
|
60
|
-
|
|
61
|
-
**Adding New Validations**
|
|
62
|
-
To add a new validation to the master script, edit `scripts/run-all-validations.sh`:
|
|
63
|
-
1. Add a new section with clear header and description
|
|
64
|
-
2. Run your validation script and capture exit code
|
|
65
|
-
3. Add the exit code to the final results check
|
|
66
|
-
4. The script provides clear guidance with comments for adding new validations
|
|
67
|
-
|
|
68
|
-
**Bypassing Hooks (Not Recommended)**
|
|
69
|
-
```bash
|
|
70
|
-
git commit --no-verify
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Only bypass hooks if you have a valid reason and plan to fix violations immediately.
|
|
74
|
-
|
|
75
|
-
## Project Structure
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
packages/ui-lib/
|
|
79
|
-
├── components/
|
|
80
|
-
│ ├── primitives/ # Base components (buttons, inputs, etc.)
|
|
81
|
-
│ ├── patterns/ # Composite components (cards, forms, etc.)
|
|
82
|
-
│ └── features/ # Feature-specific components
|
|
83
|
-
├── scripts/
|
|
84
|
-
│ ├── hooks/ # Git hooks (tracked in version control)
|
|
85
|
-
│ │ └── pre-commit # Pre-commit validation hook
|
|
86
|
-
│ ├── setup-hooks.sh # Hook installation script
|
|
87
|
-
│ ├── run-all-validations.sh # Master validation script
|
|
88
|
-
│ ├── validate-layer-imports.py
|
|
89
|
-
│ └── validate-storybook-coverage.js
|
|
90
|
-
├── styles/ # Global styles and themes
|
|
91
|
-
└── tokens/ # Design tokens
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Layer Architecture
|
|
95
|
-
|
|
96
|
-
The design system follows a strict three-layer architecture:
|
|
97
|
-
|
|
98
|
-
1. **Primitives** - Base components with no dependencies on other layers
|
|
99
|
-
- Examples: Button, Input, Textarea, Badge
|
|
100
|
-
- Can only import from external libraries and utilities
|
|
101
|
-
|
|
102
|
-
2. **Patterns** - Composite components built from primitives
|
|
103
|
-
- Examples: Card, Form, Dialog
|
|
104
|
-
- Can import from primitives and external libraries
|
|
105
|
-
|
|
106
|
-
3. **Features** - Feature-specific components
|
|
107
|
-
- Examples: AIConversation, RefinementPanel
|
|
108
|
-
- Can import from primitives, patterns, and external libraries
|
|
109
|
-
|
|
110
|
-
**Import Rules:**
|
|
111
|
-
- Primitives → No internal imports
|
|
112
|
-
- Patterns → Can import Primitives
|
|
113
|
-
- Features → Can import Primitives + Patterns
|
|
114
|
-
|
|
115
|
-
## Available Scripts
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
pnpm dev # Start development server
|
|
119
|
-
pnpm build # Build for production
|
|
120
|
-
pnpm start # Start production server
|
|
121
|
-
pnpm lint # Run ESLint
|
|
122
|
-
pnpm tokens:build # Build design tokens
|
|
123
|
-
pnpm tokens:watch # Watch design tokens
|
|
124
|
-
pnpm storybook # Start Storybook dev server
|
|
125
|
-
pnpm build-storybook # Build Storybook for production
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Creating New Components
|
|
129
|
-
|
|
130
|
-
### 1. Choose the Correct Layer
|
|
131
|
-
|
|
132
|
-
Determine which layer your component belongs to:
|
|
133
|
-
- **Primitive**: Standalone, reusable UI element
|
|
134
|
-
- **Pattern**: Combination of primitives
|
|
135
|
-
- **Feature**: Domain-specific component
|
|
136
|
-
|
|
137
|
-
### 2. Create Component File
|
|
138
|
-
|
|
139
|
-
Create your component following the established structure:
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
// components/primitives/YourComponent/YourComponent.tsx
|
|
143
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
144
|
-
import { cn } from "@/lib/utils";
|
|
145
|
-
|
|
146
|
-
const yourComponentVariants = cva(
|
|
147
|
-
"base-classes",
|
|
148
|
-
{
|
|
149
|
-
variants: {
|
|
150
|
-
variant: {
|
|
151
|
-
default: "variant-classes",
|
|
152
|
-
secondary: "variant-classes",
|
|
153
|
-
},
|
|
154
|
-
size: {
|
|
155
|
-
default: "size-classes",
|
|
156
|
-
sm: "size-classes",
|
|
157
|
-
lg: "size-classes",
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
defaultVariants: {
|
|
161
|
-
variant: "default",
|
|
162
|
-
size: "default",
|
|
163
|
-
},
|
|
164
|
-
}
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
export interface YourComponentProps
|
|
168
|
-
extends React.HTMLAttributes<HTMLElement>,
|
|
169
|
-
VariantProps<typeof yourComponentVariants> {
|
|
170
|
-
// Additional props
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function YourComponent({
|
|
174
|
-
className,
|
|
175
|
-
variant,
|
|
176
|
-
size,
|
|
177
|
-
...props
|
|
178
|
-
}: YourComponentProps) {
|
|
179
|
-
return (
|
|
180
|
-
<div
|
|
181
|
-
className={cn(yourComponentVariants({ variant, size, className }))}
|
|
182
|
-
{...props}
|
|
183
|
-
/>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### 3. Create Index File
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
// components/primitives/YourComponent/index.ts
|
|
192
|
-
export { YourComponent } from "./YourComponent";
|
|
193
|
-
export type { YourComponentProps } from "./YourComponent";
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### 4. Create Storybook Story (REQUIRED)
|
|
197
|
-
|
|
198
|
-
Every component must have a `.stories.tsx` file:
|
|
199
|
-
|
|
200
|
-
```tsx
|
|
201
|
-
// components/primitives/YourComponent/YourComponent.stories.tsx
|
|
202
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
203
|
-
import { YourComponent } from "./YourComponent";
|
|
204
|
-
|
|
205
|
-
const meta = {
|
|
206
|
-
title: "Primitives/YourComponent",
|
|
207
|
-
component: YourComponent,
|
|
208
|
-
tags: ["autodocs"],
|
|
209
|
-
parameters: {
|
|
210
|
-
layout: "centered",
|
|
211
|
-
},
|
|
212
|
-
} satisfies Meta<typeof YourComponent>;
|
|
213
|
-
|
|
214
|
-
export default meta;
|
|
215
|
-
type Story = StoryObj<typeof meta>;
|
|
216
|
-
|
|
217
|
-
export const Default: Story = {
|
|
218
|
-
args: {
|
|
219
|
-
// default props
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export const Variant: Story = {
|
|
224
|
-
args: {
|
|
225
|
-
variant: "secondary",
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### 5. Validate
|
|
231
|
-
|
|
232
|
-
Run validation scripts to ensure compliance:
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
# Run all validations (recommended)
|
|
236
|
-
bash scripts/run-all-validations.sh
|
|
237
|
-
# or
|
|
238
|
-
pnpm run prebuild
|
|
239
|
-
|
|
240
|
-
# Run individual validations
|
|
241
|
-
python3 scripts/validate-layer-imports.py # Layer architecture
|
|
242
|
-
node scripts/validate-storybook-coverage.js # Storybook coverage
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Design Tokens
|
|
246
|
-
|
|
247
|
-
The design system uses Style Dictionary for design tokens management:
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
# Build tokens once
|
|
251
|
-
pnpm tokens:build
|
|
252
|
-
|
|
253
|
-
# Watch for changes
|
|
254
|
-
pnpm tokens:watch
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Tokens are defined in the `tokens/` directory and compiled to CSS custom properties.
|
|
258
|
-
|
|
259
|
-
## Technology Stack
|
|
260
|
-
|
|
261
|
-
- **Framework**: Next.js 16 with App Router
|
|
262
|
-
- **UI**: React 19
|
|
263
|
-
- **Styling**: Tailwind CSS 4
|
|
264
|
-
- **Component Variants**: Class Variance Authority (CVA)
|
|
265
|
-
- **Component Foundation**: Radix UI primitives
|
|
266
|
-
- **Documentation**: Storybook 10
|
|
267
|
-
- **Type Safety**: TypeScript 5 (strict mode)
|
|
268
|
-
- **Icons**: Lucide React
|
|
269
|
-
- **Animation**: Motion (Framer Motion)
|
|
270
|
-
|
|
271
|
-
## Accessibility
|
|
272
|
-
|
|
273
|
-
All components must meet WCAG 2.1 Level AA standards:
|
|
274
|
-
- Keyboard navigation support
|
|
275
|
-
- Screen reader compatibility
|
|
276
|
-
- Proper ARIA attributes
|
|
277
|
-
- Focus management
|
|
278
|
-
- Color contrast requirements
|
|
279
|
-
|
|
280
|
-
## Theming
|
|
281
|
-
|
|
282
|
-
The design system supports:
|
|
283
|
-
- Light and dark modes
|
|
284
|
-
- Design token-based theming
|
|
285
|
-
- CSS custom properties
|
|
286
|
-
- Responsive design across all breakpoints
|
|
287
|
-
|
|
288
|
-
## Contributing
|
|
289
|
-
|
|
290
|
-
1. Set up git hooks: `./scripts/setup-hooks.sh`
|
|
291
|
-
2. Create components following the layer architecture
|
|
292
|
-
3. Include Storybook stories for all components
|
|
293
|
-
4. Ensure all validations pass before committing
|
|
294
|
-
5. Document accessibility features and keyboard shortcuts
|
|
295
|
-
|
|
296
|
-
## Learn More
|
|
297
|
-
|
|
298
|
-
- [Next.js Documentation](https://nextjs.org/docs)
|
|
299
|
-
- [Tailwind CSS](https://tailwindcss.com)
|
|
300
|
-
- [Radix UI](https://www.radix-ui.com)
|
|
301
|
-
- [shadcn/ui](https://ui.shadcn.com)
|
|
302
|
-
- [Storybook](https://storybook.js.org)
|
|
303
|
-
- [CVA](https://cva.style)
|
|
304
|
-
|
|
305
|
-
## Support
|
|
306
|
-
|
|
307
|
-
For questions or issues with the design system, contact the Design System Guardian or open an issue in the repository.
|
|
@@ -9,13 +9,14 @@ type CanvasProps = ReactFlowProps & {
|
|
|
9
9
|
|
|
10
10
|
export const Canvas = ({ children, ...props }: CanvasProps) => (
|
|
11
11
|
<ReactFlow
|
|
12
|
+
{...props}
|
|
12
13
|
deleteKeyCode={["Backspace", "Delete"]}
|
|
13
14
|
fitView
|
|
14
15
|
panOnDrag={false}
|
|
15
16
|
panOnScroll
|
|
17
|
+
proOptions={{ hideAttribution: true }}
|
|
16
18
|
selectionOnDrag={true}
|
|
17
19
|
zoomOnDoubleClick={false}
|
|
18
|
-
{...props}
|
|
19
20
|
>
|
|
20
21
|
<Background bgColor="var(--sidebar)" />
|
|
21
22
|
<Controls />
|
|
@@ -41,11 +41,9 @@ const Temporary = ({
|
|
|
41
41
|
|
|
42
42
|
const getHandleCoordsByPosition = (
|
|
43
43
|
node: InternalNode<Node>,
|
|
44
|
-
handlePosition: Position
|
|
44
|
+
handlePosition: Position,
|
|
45
|
+
handleType: "source" | "target"
|
|
45
46
|
) => {
|
|
46
|
-
// Choose the handle type based on position - Left is for target, Right is for source
|
|
47
|
-
const handleType = handlePosition === Position.Left ? "target" : "source";
|
|
48
|
-
|
|
49
47
|
const handle = node.internals.handleBounds?.[handleType]?.find(
|
|
50
48
|
(h) => h.position === handlePosition
|
|
51
49
|
);
|
|
@@ -57,9 +55,6 @@ const getHandleCoordsByPosition = (
|
|
|
57
55
|
let offsetX = handle.width / 2;
|
|
58
56
|
let offsetY = handle.height / 2;
|
|
59
57
|
|
|
60
|
-
// this is a tiny detail to make the markerEnd of an edge visible.
|
|
61
|
-
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
|
62
|
-
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
|
63
58
|
switch (handlePosition) {
|
|
64
59
|
case Position.Left:
|
|
65
60
|
offsetX = 0;
|
|
@@ -87,19 +82,32 @@ const getEdgeParams = (
|
|
|
87
82
|
source: InternalNode<Node>,
|
|
88
83
|
target: InternalNode<Node>
|
|
89
84
|
) => {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
85
|
+
const sx = source.internals.positionAbsolute.x + (source.measured?.width ?? 0) / 2;
|
|
86
|
+
const sy = source.internals.positionAbsolute.y + (source.measured?.height ?? 0) / 2;
|
|
87
|
+
const tx = target.internals.positionAbsolute.x + (target.measured?.width ?? 0) / 2;
|
|
88
|
+
const ty = target.internals.positionAbsolute.y + (target.measured?.height ?? 0) / 2;
|
|
89
|
+
|
|
90
|
+
const dx = tx - sx;
|
|
91
|
+
const dy = ty - sy;
|
|
92
|
+
|
|
93
|
+
// Pick source/target positions based on dominant direction
|
|
94
|
+
let sourcePos: Position;
|
|
95
|
+
let targetPos: Position;
|
|
96
|
+
|
|
97
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
98
|
+
// Horizontal dominant
|
|
99
|
+
sourcePos = dx > 0 ? Position.Right : Position.Left;
|
|
100
|
+
targetPos = dx > 0 ? Position.Left : Position.Right;
|
|
101
|
+
} else {
|
|
102
|
+
// Vertical dominant
|
|
103
|
+
sourcePos = dy > 0 ? Position.Bottom : Position.Top;
|
|
104
|
+
targetPos = dy > 0 ? Position.Top : Position.Bottom;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const [srcX, srcY] = getHandleCoordsByPosition(source, sourcePos, "source");
|
|
108
|
+
const [tgtX, tgtY] = getHandleCoordsByPosition(target, targetPos, "target");
|
|
109
|
+
|
|
110
|
+
return { sx: srcX, sy: srcY, tx: tgtX, ty: tgtY, sourcePos, targetPos };
|
|
103
111
|
};
|
|
104
112
|
|
|
105
113
|
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CardHeader,
|
|
8
8
|
CardTitle,
|
|
9
9
|
} from "@/components/ui/card";
|
|
10
|
+
import { AnimatedBorder } from "@/components/ui/animated-border";
|
|
10
11
|
import { cn } from "@/lib/utils";
|
|
11
12
|
import { Handle, Position } from "@xyflow/react";
|
|
12
13
|
import type { ComponentProps } from "react";
|
|
@@ -16,18 +17,36 @@ export type NodeProps = ComponentProps<typeof Card> & {
|
|
|
16
17
|
target: boolean;
|
|
17
18
|
source: boolean;
|
|
18
19
|
};
|
|
20
|
+
status?: "idle" | "running" | "success" | "error";
|
|
19
21
|
};
|
|
20
22
|
|
|
21
|
-
export const Node = ({ handles, className, ...props }: NodeProps) => (
|
|
23
|
+
export const Node = ({ handles, className, status, ...props }: NodeProps) => (
|
|
22
24
|
<Card
|
|
23
25
|
className={cn(
|
|
24
|
-
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
|
26
|
+
"node-container relative size-full h-auto w-sm gap-0 rounded-md bg-card p-0 transition-all duration-200",
|
|
27
|
+
status === "success" && "border-green-500 border-2",
|
|
28
|
+
status === "error" && "border-red-500 border-2",
|
|
25
29
|
className
|
|
26
30
|
)}
|
|
27
31
|
{...props}
|
|
28
32
|
>
|
|
29
|
-
{
|
|
30
|
-
{handles.
|
|
33
|
+
{status === "running" && <AnimatedBorder />}
|
|
34
|
+
{handles.target && (
|
|
35
|
+
<>
|
|
36
|
+
<Handle id="target-top" position={Position.Top} type="target" />
|
|
37
|
+
<Handle id="target-right" position={Position.Right} type="target" />
|
|
38
|
+
<Handle id="target-bottom" position={Position.Bottom} type="target" />
|
|
39
|
+
<Handle id="target-left" position={Position.Left} type="target" />
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
{handles.source && (
|
|
43
|
+
<>
|
|
44
|
+
<Handle id="source-top" position={Position.Top} type="source" />
|
|
45
|
+
<Handle id="source-right" position={Position.Right} type="source" />
|
|
46
|
+
<Handle id="source-bottom" position={Position.Bottom} type="source" />
|
|
47
|
+
<Handle id="source-left" position={Position.Left} type="source" />
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
31
50
|
{props.children}
|
|
32
51
|
</Card>
|
|
33
52
|
);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import type { NodeChange, EdgeChange, Connection } from "@xyflow/react";
|
|
4
|
+
import { applyNodeChanges, applyEdgeChanges } from "@xyflow/react";
|
|
5
|
+
import { WorkflowCanvas, type WorkflowNode, type WorkflowEdge } from "./WorkflowCanvas";
|
|
6
|
+
import "@xyflow/react/dist/style.css";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WorkflowCanvas Stories
|
|
10
|
+
*
|
|
11
|
+
* WorkflowCanvas is a complete workflow canvas composite with ReactFlow integration.
|
|
12
|
+
* It provides a visual canvas for building workflow diagrams with state and transition nodes.
|
|
13
|
+
*
|
|
14
|
+
* ## Features
|
|
15
|
+
* - ReactFlow canvas with grid background
|
|
16
|
+
* - State nodes and Transition nodes
|
|
17
|
+
* - Bezier curve edges with animation
|
|
18
|
+
* - Pan and zoom controls
|
|
19
|
+
* - Minimap toggle
|
|
20
|
+
* - Node drag and drop
|
|
21
|
+
* - Edge creation by dragging from handles
|
|
22
|
+
* - Duplicate connection prevention
|
|
23
|
+
* - Delete nodes/edges with Backspace/Delete
|
|
24
|
+
* - Keyboard shortcuts (Cmd+/ for fit view)
|
|
25
|
+
*
|
|
26
|
+
* ## Usage Guidelines
|
|
27
|
+
*
|
|
28
|
+
* ### Do's
|
|
29
|
+
* - Provide nodes and edges data
|
|
30
|
+
* - Handle onNodesChange and onEdgesChange for state updates
|
|
31
|
+
* - Use onConnect to handle new connections
|
|
32
|
+
* - Set showMinimap for large workflows
|
|
33
|
+
*
|
|
34
|
+
* ### Don'ts
|
|
35
|
+
* - Don't use without proper height container
|
|
36
|
+
* - Don't mix node types incorrectly
|
|
37
|
+
*/
|
|
38
|
+
const meta = {
|
|
39
|
+
title: "Blocks/WorkflowCanvas",
|
|
40
|
+
component: WorkflowCanvas,
|
|
41
|
+
tags: ["autodocs"],
|
|
42
|
+
parameters: {
|
|
43
|
+
layout: "fullscreen",
|
|
44
|
+
docs: {
|
|
45
|
+
description: {
|
|
46
|
+
component:
|
|
47
|
+
"Complete workflow canvas composite with ReactFlow integration for building visual workflow diagrams.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} satisfies Meta<typeof WorkflowCanvas>;
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof meta>;
|
|
55
|
+
|
|
56
|
+
const initialNodes: WorkflowNode[] = [
|
|
57
|
+
{
|
|
58
|
+
id: "1",
|
|
59
|
+
type: "transition",
|
|
60
|
+
position: { x: 300, y: 50 },
|
|
61
|
+
data: { label: "Start", type: "transition", status: "idle" },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "2",
|
|
65
|
+
type: "state",
|
|
66
|
+
position: { x: 300, y: 130 },
|
|
67
|
+
data: { label: "Approve Order", description: "Approve the order", type: "state", status: "idle" },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "3",
|
|
71
|
+
type: "transition",
|
|
72
|
+
position: { x: 300, y: 210 },
|
|
73
|
+
data: { label: "Order Approved", type: "transition", status: "idle" },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "4",
|
|
77
|
+
type: "state",
|
|
78
|
+
position: { x: 300, y: 290 },
|
|
79
|
+
data: { label: "Process Payment", description: "Process payment", type: "state", status: "idle" },
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const initialEdges: WorkflowEdge[] = [
|
|
84
|
+
{ id: "e1-2", source: "1", target: "2", type: "animated" },
|
|
85
|
+
{ id: "e2-3", source: "2", target: "3", type: "animated" },
|
|
86
|
+
{ id: "e3-4", source: "3", target: "4", type: "animated" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Default workflow canvas with basic nodes and edges
|
|
91
|
+
*/
|
|
92
|
+
export const Default: Story = {
|
|
93
|
+
render: () => {
|
|
94
|
+
const [nodes, setNodes] = useState<WorkflowNode[]>(initialNodes);
|
|
95
|
+
const [edges, setEdges] = useState<WorkflowEdge[]>(initialEdges);
|
|
96
|
+
|
|
97
|
+
const onNodesChange = (changes: NodeChange[]) => {
|
|
98
|
+
setNodes((nds) => applyNodeChanges(changes, nds) as WorkflowNode[]);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onEdgesChange = (changes: EdgeChange[]) => {
|
|
102
|
+
setEdges((eds) => applyEdgeChanges(changes, eds) as WorkflowEdge[]);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onConnect = (connection: Connection) => {
|
|
106
|
+
setEdges((eds) => [
|
|
107
|
+
...eds,
|
|
108
|
+
{
|
|
109
|
+
id: `e${connection.source}-${connection.target}`,
|
|
110
|
+
source: connection.source!,
|
|
111
|
+
target: connection.target!,
|
|
112
|
+
type: "animated",
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div style={{ width: "100vw", height: "100vh" }}>
|
|
119
|
+
<WorkflowCanvas
|
|
120
|
+
nodes={nodes}
|
|
121
|
+
edges={edges}
|
|
122
|
+
onNodesChange={onNodesChange}
|
|
123
|
+
onEdgesChange={onEdgesChange}
|
|
124
|
+
onConnect={onConnect}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Workflow canvas with minimap enabled
|
|
133
|
+
*/
|
|
134
|
+
export const WithMinimap: Story = {
|
|
135
|
+
render: () => {
|
|
136
|
+
const [nodes, setNodes] = useState<WorkflowNode[]>(initialNodes);
|
|
137
|
+
const [edges, setEdges] = useState<WorkflowEdge[]>(initialEdges);
|
|
138
|
+
|
|
139
|
+
const onNodesChange = (changes: NodeChange[]) => {
|
|
140
|
+
setNodes((nds) => applyNodeChanges(changes, nds) as WorkflowNode[]);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const onEdgesChange = (changes: EdgeChange[]) => {
|
|
144
|
+
setEdges((eds) => applyEdgeChanges(changes, eds) as WorkflowEdge[]);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const onConnect = (connection: Connection) => {
|
|
148
|
+
setEdges((eds) => [
|
|
149
|
+
...eds,
|
|
150
|
+
{
|
|
151
|
+
id: `e${connection.source}-${connection.target}`,
|
|
152
|
+
source: connection.source!,
|
|
153
|
+
target: connection.target!,
|
|
154
|
+
type: "animated",
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div style={{ width: "100vw", height: "100vh" }}>
|
|
161
|
+
<WorkflowCanvas
|
|
162
|
+
nodes={nodes}
|
|
163
|
+
edges={edges}
|
|
164
|
+
onNodesChange={onNodesChange}
|
|
165
|
+
onEdgesChange={onEdgesChange}
|
|
166
|
+
onConnect={onConnect}
|
|
167
|
+
showMinimap={true}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Complex workflow with multiple branches
|
|
176
|
+
*/
|
|
177
|
+
export const ComplexWorkflow: Story = {
|
|
178
|
+
render: () => {
|
|
179
|
+
const complexNodes: WorkflowNode[] = [
|
|
180
|
+
{ id: "start", type: "transition", position: { x: 300, y: 0 }, data: { label: "Start", type: "transition", status: "idle" } },
|
|
181
|
+
{ id: "req", type: "transition", position: { x: 300, y: 100 }, data: { label: "Order Requested", type: "transition", status: "idle" } },
|
|
182
|
+
{ id: "approve", type: "state", position: { x: 100, y: 200 }, data: { label: "Approve Order", type: "state", status: "idle" } },
|
|
183
|
+
{ id: "reject", type: "state", position: { x: 500, y: 200 }, data: { label: "Reject Order", type: "state", status: "idle" } },
|
|
184
|
+
{ id: "cancel", type: "state", position: { x: 0, y: 320 }, data: { label: "Cancel Order", type: "state", status: "idle" } },
|
|
185
|
+
{ id: "approved", type: "transition", position: { x: 200, y: 320 }, data: { label: "Order Approved", type: "transition", status: "idle" } },
|
|
186
|
+
{ id: "rejected", type: "transition", position: { x: 500, y: 320 }, data: { label: "Order Rejected", type: "transition", status: "idle" } },
|
|
187
|
+
{ id: "start_proc", type: "state", position: { x: 300, y: 420 }, data: { label: "Start Order Processing", type: "state", status: "idle" } },
|
|
188
|
+
{ id: "cancelled", type: "transition", position: { x: 0, y: 440 }, data: { label: "Order Cancelled", type: "transition", status: "idle" } },
|
|
189
|
+
{ id: "complete", type: "state", position: { x: 150, y: 540 }, data: { label: "Complete Order Processing",type: "state", status: "idle" } },
|
|
190
|
+
{ id: "processing", type: "transition", position: { x: 400, y: 540 }, data: { label: "Order Processing", type: "transition", status: "idle" } },
|
|
191
|
+
{ id: "hold", type: "state", position: { x: 600, y: 540 }, data: { label: "Place Order On Hold", type: "state", status: "idle" } },
|
|
192
|
+
{ id: "resume", type: "state", position: { x: 750, y: 420 }, data: { label: "Resume Order Processing", type: "state", status: "idle" } },
|
|
193
|
+
{ id: "ready", type: "transition", position: { x: 150, y: 660 }, data: { label: "Order Ready for Delivery",type: "transition", status: "idle" } },
|
|
194
|
+
{ id: "on_hold", type: "transition", position: { x: 600, y: 660 }, data: { label: "Order On Hold", type: "transition", status: "idle" } },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const complexEdges: WorkflowEdge[] = [
|
|
198
|
+
{ id: "e1", source: "start", target: "req", type: "animated" },
|
|
199
|
+
{ id: "e2", source: "req", target: "approve", type: "animated" },
|
|
200
|
+
{ id: "e3", source: "req", target: "reject", type: "animated" },
|
|
201
|
+
{ id: "e4", source: "approve", target: "cancel", type: "animated" },
|
|
202
|
+
{ id: "e5", source: "approve", target: "approved", type: "animated" },
|
|
203
|
+
{ id: "e6", source: "reject", target: "rejected", type: "animated" },
|
|
204
|
+
{ id: "e7", source: "approved", target: "start_proc", type: "animated" },
|
|
205
|
+
{ id: "e8", source: "cancel", target: "cancelled", type: "animated" },
|
|
206
|
+
{ id: "e9", source: "start_proc",target: "complete", type: "animated" },
|
|
207
|
+
{ id: "e10", source: "start_proc",target: "processing", type: "animated" },
|
|
208
|
+
{ id: "e11", source: "processing",target: "hold", type: "animated" },
|
|
209
|
+
{ id: "e12", source: "hold", target: "resume", type: "animated" },
|
|
210
|
+
{ id: "e13", source: "complete", target: "ready", type: "animated" },
|
|
211
|
+
{ id: "e14", source: "hold", target: "on_hold", type: "animated" },
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const [nodes, setNodes] = useState<WorkflowNode[]>(complexNodes);
|
|
215
|
+
const [edges, setEdges] = useState<WorkflowEdge[]>(complexEdges);
|
|
216
|
+
|
|
217
|
+
const onNodesChange = (changes: NodeChange[]) => {
|
|
218
|
+
setNodes((nds) => applyNodeChanges(changes, nds) as WorkflowNode[]);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const onEdgesChange = (changes: EdgeChange[]) => {
|
|
222
|
+
setEdges((eds) => applyEdgeChanges(changes, eds) as WorkflowEdge[]);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const onConnect = (connection: Connection) => {
|
|
226
|
+
setEdges((eds) => [
|
|
227
|
+
...eds,
|
|
228
|
+
{
|
|
229
|
+
id: `e${connection.source}-${connection.target}`,
|
|
230
|
+
source: connection.source!,
|
|
231
|
+
target: connection.target!,
|
|
232
|
+
type: "animated",
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div style={{ width: "100vw", height: "100vh" }}>
|
|
239
|
+
<WorkflowCanvas
|
|
240
|
+
nodes={nodes}
|
|
241
|
+
edges={edges}
|
|
242
|
+
onNodesChange={onNodesChange}
|
|
243
|
+
onEdgesChange={onEdgesChange}
|
|
244
|
+
onConnect={onConnect}
|
|
245
|
+
showMinimap={true}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Empty workflow canvas
|
|
254
|
+
*/
|
|
255
|
+
export const Empty: Story = {
|
|
256
|
+
render: () => {
|
|
257
|
+
const [nodes, setNodes] = useState<WorkflowNode[]>([]);
|
|
258
|
+
const [edges, setEdges] = useState<WorkflowEdge[]>([]);
|
|
259
|
+
|
|
260
|
+
const onNodesChange = (changes: NodeChange[]) => {
|
|
261
|
+
setNodes((nds) => applyNodeChanges(changes, nds) as WorkflowNode[]);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const onEdgesChange = (changes: EdgeChange[]) => {
|
|
265
|
+
setEdges((eds) => applyEdgeChanges(changes, eds) as WorkflowEdge[]);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div style={{ width: "100vw", height: "100vh" }}>
|
|
270
|
+
<WorkflowCanvas
|
|
271
|
+
nodes={nodes}
|
|
272
|
+
edges={edges}
|
|
273
|
+
onNodesChange={onNodesChange}
|
|
274
|
+
onEdgesChange={onEdgesChange}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|