ai-design-system 0.1.72 → 0.1.73

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.
Files changed (41) hide show
  1. package/components/ai-elements/file-tree.tsx +12 -32
  2. package/components/ai-elements/node.tsx +32 -0
  3. package/components/ai-elements/prompt-input.tsx +8 -4
  4. package/components/ai-elements/tool.tsx +17 -3
  5. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +2 -0
  6. package/components/blocks/WorkflowCanvas/interfaces.ts +6 -1
  7. package/components/composites/ApprovalCard/ApprovalCard.tsx +8 -3
  8. package/components/composites/DefaultSwitcher/DefaultSwitcher.tsx +5 -1
  9. package/components/composites/PromptInput/PromptInput.tsx +16 -5
  10. package/components/composites/SpatialContainerNode/SpatialContainerNode.stories.tsx +169 -0
  11. package/components/composites/SpatialContainerNode/SpatialContainerNode.tsx +316 -0
  12. package/components/composites/SpatialContainerNode/index.ts +6 -0
  13. package/components/composites/StateNode/StateNode.tsx +3 -1
  14. package/components/composites/StreamingMarkdown.tsx +42 -3
  15. package/components/composites/TransitionNode/TransitionNode.tsx +3 -1
  16. package/components/composites/TriggerNode/TriggerNode.tsx +3 -1
  17. package/components/composites/index.ts +4 -0
  18. package/components/features/{RefinementPanel/RefinementPanel.behaviors.stories.tsx → ChatPanel/ChatPanel.behaviors.stories.tsx} +6 -6
  19. package/components/features/{RefinementPanel/RefinementPanel.mocks.ts → ChatPanel/ChatPanel.mocks.ts} +1 -1
  20. package/components/features/ChatPanel/ChatPanel.stories.tsx +214 -0
  21. package/components/features/{RefinementPanel/RefinementPanel.tsx → ChatPanel/ChatPanel.tsx} +53 -44
  22. package/components/features/ChatPanel/index.ts +28 -0
  23. package/components/features/{RefinementPanel/useRefinementPanel.d.ts → ChatPanel/useChatPanel.d.ts} +12 -50
  24. package/components/features/{RefinementPanel/useRefinementPanel.mock.ts → ChatPanel/useChatPanel.mock.ts} +6 -6
  25. package/components/features/NodeEditor/NodeEditor.mocks.ts +268 -0
  26. package/components/features/NodeEditor/NodeEditor.stories.tsx +22 -1
  27. package/components/features/NodeEditor/NodeEditor.tsx +8 -5
  28. package/components/features/PageLayout/PageLayout.mocks.ts +1 -1
  29. package/components/features/TextEditor/TextEditor.tsx +44 -12
  30. package/components/features/index.ts +3 -3
  31. package/dist/index.cjs +572 -102
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.css +782 -0
  34. package/dist/index.d.ts +240 -192
  35. package/dist/index.js +573 -104
  36. package/dist/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/utils/markdown-formatter.ts +78 -0
  39. package/components/features/RefinementPanel/RefinementPanel.stories.tsx +0 -287
  40. package/components/features/RefinementPanel/index.ts +0 -28
  41. /package/components/features/{RefinementPanel → ChatPanel}/README.md +0 -0
@@ -142,19 +142,13 @@ export const FileTreeFolder = ({
142
142
  children,
143
143
  ...props
144
144
  }: FileTreeFolderProps) => {
145
- const { expandedPaths, togglePath, selectedPath, onSelect } =
146
- useContext(FileTreeContext);
145
+ const { expandedPaths, togglePath } = useContext(FileTreeContext);
147
146
  const isExpanded = expandedPaths.has(path);
148
- const isSelected = selectedPath === path;
149
147
 
150
148
  const handleOpenChange = useCallback(() => {
151
149
  togglePath(path);
152
150
  }, [togglePath, path]);
153
151
 
154
- const handleSelect = useCallback(() => {
155
- onSelect?.(path);
156
- }, [onSelect, path]);
157
-
158
152
  const folderContextValue = useMemo(
159
153
  () => ({ isExpanded, name, path }),
160
154
  [isExpanded, name, path]
@@ -169,30 +163,16 @@ export const FileTreeFolder = ({
169
163
  tabIndex={0}
170
164
  {...props}
171
165
  >
172
- <div
173
- className={cn(
174
- "flex w-full items-center gap-1 rounded px-2 py-1 text-left transition-colors hover:bg-muted/50",
175
- isSelected && "bg-muted"
176
- )}
177
- >
178
- <CollapsibleTrigger asChild>
179
- <button
180
- className="flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0"
181
- type="button"
182
- >
183
- <ChevronRightIcon
184
- className={cn(
185
- "size-4 shrink-0 text-muted-foreground transition-transform",
186
- isExpanded && "rotate-90"
187
- )}
188
- />
189
- </button>
190
- </CollapsibleTrigger>
191
- <button
192
- className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left"
193
- onClick={handleSelect}
194
- type="button"
166
+ <CollapsibleTrigger asChild>
167
+ <div
168
+ className="flex w-full cursor-pointer items-center gap-1 rounded px-2 py-1 text-left transition-colors hover:bg-muted/50 select-none"
195
169
  >
170
+ <ChevronRightIcon
171
+ className={cn(
172
+ "size-4 shrink-0 text-muted-foreground transition-transform",
173
+ isExpanded && "rotate-90"
174
+ )}
175
+ />
196
176
  <FileTreeIcon>
197
177
  {isExpanded ? (
198
178
  <FolderOpenIcon className="size-4 text-blue-500" />
@@ -201,8 +181,8 @@ export const FileTreeFolder = ({
201
181
  )}
202
182
  </FileTreeIcon>
203
183
  <FileTreeName>{name}</FileTreeName>
204
- </button>
205
- </div>
184
+ </div>
185
+ </CollapsibleTrigger>
206
186
  <CollapsibleContent>
207
187
  <div className="ml-4 border-l pl-2">{children}</div>
208
188
  </CollapsibleContent>
@@ -88,3 +88,35 @@ export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
88
88
  {...props}
89
89
  />
90
90
  );
91
+
92
+ export function getHighlightIconColor(
93
+ status?: string,
94
+ highlightStatus?: string,
95
+ defaultColorClass: string = "text-primary"
96
+ ): string {
97
+ const effectiveHighlight =
98
+ highlightStatus ??
99
+ (status === "success" || status === "completed" || status === "done"
100
+ ? "done"
101
+ : status === "pending_hitl" || status === "pending"
102
+ ? "pending"
103
+ : status === "running" || status === "active"
104
+ ? "active"
105
+ : status === "error" || status === "failed"
106
+ ? "error"
107
+ : undefined);
108
+
109
+ switch (effectiveHighlight) {
110
+ case "done":
111
+ return "text-green-500";
112
+ case "pending":
113
+ return "text-amber-500";
114
+ case "active":
115
+ return "text-indigo-400";
116
+ case "error":
117
+ return "text-red-500";
118
+ default:
119
+ return defaultColorClass;
120
+ }
121
+ }
122
+
@@ -963,22 +963,26 @@ export const PromptInputSubmit = ({
963
963
  ...props
964
964
  }: PromptInputSubmitProps) => {
965
965
  let Icon = <SendIcon className="size-4" />;
966
+ let buttonVariant = variant;
967
+ let customClass = className;
966
968
 
967
969
  if (status === "submitted") {
968
970
  Icon = <Loader2Icon className="size-4 animate-spin" />;
969
971
  } else if (status === "streaming") {
970
- Icon = <SquareIcon className="size-4" />;
972
+ Icon = <SquareIcon className="size-3.5 fill-current" />;
973
+ buttonVariant = "default";
974
+ customClass = cn("rounded-full", className);
971
975
  } else if (status === "error") {
972
976
  Icon = <XIcon className="size-4" />;
973
977
  }
974
978
 
975
979
  return (
976
980
  <InputGroupButton
977
- aria-label="Submit"
978
- className={cn(className)}
981
+ aria-label={status === "streaming" ? "Stop" : "Submit"}
982
+ className={customClass}
979
983
  size={size}
980
984
  type="submit"
981
- variant={variant}
985
+ variant={buttonVariant}
982
986
  {...props}
983
987
  >
984
988
  {children ?? Icon}
@@ -36,6 +36,8 @@ import type { ComponentProps, ReactNode } from "react";
36
36
  import { isValidElement } from "react";
37
37
  import { CodeBlock } from "./code-block";
38
38
 
39
+ import { Shimmer } from "@/components/ai-elements/shimmer";
40
+
39
41
  export type ToolProps = ComponentProps<typeof Collapsible>;
40
42
 
41
43
  export const Tool = ({ className, ...props }: ToolProps) => (
@@ -65,7 +67,7 @@ const getStatusBadge = (status: ToolUIPart["state"]) => {
65
67
 
66
68
  const icons: Record<ToolUIPart["state"], ReactNode> = {
67
69
  "input-streaming": <CircleIcon className="size-4" />,
68
- "input-available": <ClockIcon className="size-4 animate-pulse" />,
70
+ "input-available": <ClockIcon className="size-4 animate-spin text-primary" />,
69
71
  "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
70
72
  "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
71
73
  "output-available": <CheckCircleIcon className="size-4 text-green-600" />,
@@ -73,10 +75,22 @@ const getStatusBadge = (status: ToolUIPart["state"]) => {
73
75
  "output-denied": <XCircleIcon className="size-4 text-orange-600" />,
74
76
  };
75
77
 
78
+ const isRunning = status === "input-available";
79
+
76
80
  return (
77
- <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
81
+ <Badge
82
+ className={cn(
83
+ "gap-1.5 rounded-full text-xs font-medium transition-all relative overflow-hidden",
84
+ isRunning && "border border-primary/30 bg-primary/10 text-primary shadow-xs"
85
+ )}
86
+ variant={isRunning ? "outline" : "secondary"}
87
+ >
78
88
  {icons[status]}
79
- {labels[status]}
89
+ {isRunning ? (
90
+ <Shimmer className="text-xs font-medium text-foreground inline-block">Running</Shimmer>
91
+ ) : (
92
+ labels[status]
93
+ )}
80
94
  </Badge>
81
95
  );
82
96
  };
@@ -16,6 +16,7 @@ import { Panel } from "@/components/ai-elements/panel";
16
16
  import { StateNode } from "@/components/composites/StateNode";
17
17
  import { TransitionNode } from "@/components/composites/TransitionNode";
18
18
  import { TriggerNode } from "@/components/composites/TriggerNode";
19
+ import { SpatialContainerNode } from "@/components/composites/SpatialContainerNode";
19
20
  import type { WorkflowCanvasProps, WorkflowEdge } from "./interfaces";
20
21
  import "@xyflow/react/dist/style.css";
21
22
 
@@ -29,6 +30,7 @@ const nodeTypes = {
29
30
  state: StateNode,
30
31
  transition: TransitionNode,
31
32
  trigger: TriggerNode,
33
+ spatialContainer: SpatialContainerNode,
32
34
  };
33
35
 
34
36
  function WorkflowCanvasInner({
@@ -3,8 +3,13 @@ import type React from "react";
3
3
  import type { StateNodeData } from "@/components/composites/StateNode";
4
4
  import type { TransitionNodeData } from "@/components/composites/TransitionNode";
5
5
  import type { TriggerNodeData } from "@/components/composites/TriggerNode";
6
+ import type { SpatialContainerNodeData } from "@/components/composites/SpatialContainerNode";
6
7
 
7
- export type WorkflowNodeData = StateNodeData | TransitionNodeData | TriggerNodeData;
8
+ export type WorkflowNodeData =
9
+ | StateNodeData
10
+ | TransitionNodeData
11
+ | TriggerNodeData
12
+ | SpatialContainerNodeData;
8
13
  export type WorkflowNode = Node<WorkflowNodeData>;
9
14
 
10
15
  export interface WorkflowEdge {
@@ -13,6 +13,7 @@ import {
13
13
  ConfirmationActions,
14
14
  ConfirmationAction,
15
15
  } from "@/components/ai-elements/confirmation";
16
+ import { StreamingMarkdown } from "@/components/composites/StreamingMarkdown";
16
17
 
17
18
  export interface ActionRequest {
18
19
  name: string;
@@ -209,10 +210,14 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
209
210
  className={cn("w-full", className)}
210
211
  >
211
212
  <ConfirmationTitle>
212
- <div className="flex items-start justify-between gap-4 select-none">
213
- <div className="flex items-start gap-3">
213
+ <div className="flex items-start justify-between gap-4">
214
+ <div className="flex items-start gap-3 min-w-0 flex-1">
214
215
  <QuestionIcon className="h-5 w-5 text-muted-foreground mt-0.5 flex-shrink-0" />
215
- <span>{currentQuestion.question}</span>
216
+ <div className="flex-1 min-w-0 max-h-60 overflow-y-auto pr-1 text-sm leading-relaxed text-foreground [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
217
+ <StreamingMarkdown controls={false}>
218
+ {currentQuestion.question}
219
+ </StreamingMarkdown>
220
+ </div>
216
221
  </div>
217
222
  {questions.length > 1 && (
218
223
  <div className="flex items-center gap-1.5 text-xs text-muted-foreground flex-shrink-0 font-normal">
@@ -66,7 +66,11 @@ export const DefaultSwitcher = React.memo<DefaultSwitcherProps>(
66
66
  </SelectTrigger>
67
67
  <SelectContent>
68
68
  {themes.map((theme) => (
69
- <SelectItem key={theme.value} value={theme.value}>
69
+ <SelectItem
70
+ key={theme.value}
71
+ value={theme.value}
72
+ onClick={() => onValueChange?.(theme.value)}
73
+ >
70
74
  {theme.label}
71
75
  </SelectItem>
72
76
  ))}
@@ -32,6 +32,7 @@ export interface PromptInputBlockProps
32
32
  ) => void | Promise<void>;
33
33
  dialog?: ReactNode;
34
34
  loading?: boolean;
35
+ onStop?: () => void;
35
36
  }
36
37
 
37
38
  export const PromptInput = React.memo<PromptInputBlockProps>(
@@ -43,17 +44,21 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
43
44
  onSubmit,
44
45
  dialog,
45
46
  loading = false,
47
+ onStop,
46
48
  ...props
47
49
  }) => {
48
50
  const handleSubmit = React.useCallback(
49
51
  (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
50
- if (disabled) {
52
+ if (disabled || (loading && onStop)) {
51
53
  event.preventDefault();
54
+ if (loading && onStop) {
55
+ onStop();
56
+ }
52
57
  return;
53
58
  }
54
59
  onSubmit(message, event);
55
60
  },
56
- [disabled, onSubmit]
61
+ [disabled, loading, onStop, onSubmit]
57
62
  );
58
63
 
59
64
  const isControlled = value !== undefined && onChange !== undefined;
@@ -71,22 +76,28 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
71
76
  return <>{dialog}</>;
72
77
  }
73
78
 
79
+ const isStopping = loading && Boolean(onStop);
80
+
74
81
  const promptInputContent = (
75
82
  <AIPromptInput onSubmit={handleSubmit} {...props}>
76
83
  <PromptInputBody>
77
84
  <PromptInputTextarea
78
85
  placeholder={placeholder}
79
- disabled={false}
86
+ disabled={disabled}
80
87
  onChange={isControlled ? handleControlledChange : undefined}
81
88
  />
82
89
  </PromptInputBody>
83
90
  <PromptInputFooter>
84
91
  <PromptInputTools>
85
- <Button variant="ghost" size="icon" className="h-8 w-8 rounded-full" type="button" disabled={disabled}>
92
+ <Button variant="ghost" size="icon" className="h-8 w-8 rounded-full" type="button" disabled={disabled || loading}>
86
93
  <Icon name="plus" size="sm" />
87
94
  </Button>
88
95
  </PromptInputTools>
89
- <PromptInputSubmit disabled={disabled} status={loading ? "submitted" : undefined} />
96
+ <PromptInputSubmit
97
+ disabled={disabled || (loading && !onStop)}
98
+ status={loading ? (onStop ? "streaming" : "submitted") : undefined}
99
+ onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
100
+ />
90
101
  </PromptInputFooter>
91
102
  </AIPromptInput>
92
103
  );
@@ -0,0 +1,169 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { SpatialContainerNode } from "./SpatialContainerNode";
3
+
4
+ /**
5
+ * SpatialContainerNode Composite Stories
6
+ *
7
+ * SpatialContainerNode represents a 2.5D visual container or spatial grid region
8
+ * in node editors and workflow canvas components. It acts as a parent node for grouping
9
+ * sub-nodes, cards, and sticky notes inside defined spatial domains.
10
+ *
11
+ * ## Features
12
+ * - 2.5D Extruded 3D visual styling, flat border regions, or glassmorphism aesthetics
13
+ * - Built-in Resizer (`NodeResizer`) for interactive sizing
14
+ * - Themed color variants (Blue, Indigo, Emerald, Amber, Purple, Rose, Cyan, Slate)
15
+ * - Category badges and node action toolbars
16
+ * - Built-in connection handles for inter-container relationship routing
17
+ */
18
+ const meta = {
19
+ title: "Composites/SpatialContainerNode",
20
+ component: SpatialContainerNode,
21
+ tags: ["autodocs"],
22
+ parameters: {
23
+ layout: "centered",
24
+ docs: {
25
+ description: {
26
+ component:
27
+ "Visual container node for grouping sub-nodes in spatial layouts, Business Model Canvas grids, and architectural region diagrams.",
28
+ },
29
+ },
30
+ },
31
+ } satisfies Meta<typeof SpatialContainerNode>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ id: "container-1",
39
+ selected: false,
40
+ width: 340,
41
+ height: 220,
42
+ data: {
43
+ label: "Value Propositions",
44
+ description: "Core products, services, and bundle offerings for target customers",
45
+ type: "spatialContainer",
46
+ themeColor: "indigo",
47
+ variant: "extruded3d",
48
+ icon: "box",
49
+ badgeText: "3 Services",
50
+ },
51
+ },
52
+ };
53
+
54
+ export const Extruded3D: Story = {
55
+ args: {
56
+ id: "container-2",
57
+ selected: true,
58
+ width: 340,
59
+ height: 220,
60
+ data: {
61
+ label: "Key Activities",
62
+ description: "Production, problem-solving, and platform maintenance tasks",
63
+ type: "spatialContainer",
64
+ themeColor: "emerald",
65
+ variant: "extruded3d",
66
+ icon: "activity",
67
+ badgeText: "Active",
68
+ },
69
+ },
70
+ };
71
+
72
+ export const Glassmorphism: Story = {
73
+ args: {
74
+ id: "container-3",
75
+ selected: false,
76
+ width: 340,
77
+ height: 220,
78
+ data: {
79
+ label: "Customer Relationships",
80
+ description: "Automated onboarding, dedicated personal assistance, and community support",
81
+ type: "spatialContainer",
82
+ themeColor: "purple",
83
+ variant: "glassmorphism",
84
+ icon: "heart",
85
+ badgeText: "High Touch",
86
+ },
87
+ },
88
+ };
89
+
90
+ export const FlatRegion: Story = {
91
+ args: {
92
+ id: "container-4",
93
+ selected: false,
94
+ width: 340,
95
+ height: 220,
96
+ data: {
97
+ label: "Cost Structure",
98
+ description: "Fixed cloud infrastructure, developer salaries, and R&D licenses",
99
+ type: "spatialContainer",
100
+ themeColor: "rose",
101
+ variant: "flat",
102
+ icon: "wallet",
103
+ badgeText: "$120k/mo",
104
+ },
105
+ },
106
+ };
107
+
108
+ export const ColorVariants: Story = {
109
+ render: () => (
110
+ <div className="grid grid-cols-2 gap-6 p-4">
111
+ <SpatialContainerNode
112
+ id="c-blue"
113
+ selected={false}
114
+ width={300}
115
+ height={180}
116
+ data={{
117
+ label: "Key Partners",
118
+ type: "spatialContainer",
119
+ themeColor: "blue",
120
+ variant: "extruded3d",
121
+ icon: "handshake",
122
+ badgeText: "Suppliers",
123
+ }}
124
+ />
125
+ <SpatialContainerNode
126
+ id="c-emerald"
127
+ selected={false}
128
+ width={300}
129
+ height={180}
130
+ data={{
131
+ label: "Key Resources",
132
+ type: "spatialContainer",
133
+ themeColor: "emerald",
134
+ variant: "extruded3d",
135
+ icon: "database",
136
+ badgeText: "Assets",
137
+ }}
138
+ />
139
+ <SpatialContainerNode
140
+ id="c-amber"
141
+ selected={false}
142
+ width={300}
143
+ height={180}
144
+ data={{
145
+ label: "Channels",
146
+ type: "spatialContainer",
147
+ themeColor: "amber",
148
+ variant: "extruded3d",
149
+ icon: "truck",
150
+ badgeText: "Distribution",
151
+ }}
152
+ />
153
+ <SpatialContainerNode
154
+ id="c-purple"
155
+ selected={false}
156
+ width={300}
157
+ height={180}
158
+ data={{
159
+ label: "Customer Segments",
160
+ type: "spatialContainer",
161
+ themeColor: "purple",
162
+ variant: "extruded3d",
163
+ icon: "users",
164
+ badgeText: "B2B & B2C",
165
+ }}
166
+ />
167
+ </div>
168
+ ),
169
+ };