ai-design-system 0.1.95 → 0.1.97

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.
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { StoriesBar, type StoryItem } from "./StoriesBar";
3
+ import { fn } from "@storybook/test";
4
+
5
+ /**
6
+ * StoriesBar Composite Stories
7
+ *
8
+ * Displays circular story avatar bubbles with status indicators and titles.
9
+ */
10
+ const meta = {
11
+ title: "Composites/StoriesBar",
12
+ component: StoriesBar,
13
+ tags: ["autodocs"],
14
+ parameters: { layout: "padded" },
15
+ args: {
16
+ onStoryClick: fn(),
17
+ },
18
+ } satisfies Meta<typeof StoriesBar>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ const mockStories: StoryItem[] = [
24
+ {
25
+ id: "story-1",
26
+ title: "Devin iOS",
27
+ status: "ready",
28
+ },
29
+ {
30
+ id: "story-2",
31
+ title: "Waypoint",
32
+ status: "ready",
33
+ },
34
+ {
35
+ id: "story-3",
36
+ title: "Design Sys",
37
+ status: "generating",
38
+ },
39
+ {
40
+ id: "story-4",
41
+ title: "Workspace",
42
+ status: "ready",
43
+ },
44
+ {
45
+ id: "story-5",
46
+ title: "ZeroOps",
47
+ status: "draft",
48
+ },
49
+ ];
50
+
51
+ /**
52
+ * Default usage with active story
53
+ */
54
+ export const Default: Story = {
55
+ args: {
56
+ activeStoryId: "story-1",
57
+ stories: mockStories,
58
+ },
59
+ };
60
+
61
+ /**
62
+ * Empty stories bar
63
+ */
64
+ export const EmptyState: Story = {
65
+ args: {
66
+ activeStoryId: null,
67
+ stories: [],
68
+ },
69
+ };
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Button } from "@/components/primitives/Button";
5
+ import { Icon } from "@/components/primitives/Icon";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export interface StoryItem {
9
+ id: string;
10
+ sessionId?: string;
11
+ title: string;
12
+ videoTitle?: string;
13
+ subtitle?: string;
14
+ icon?: string;
15
+ time?: string;
16
+ status?: "ready" | "generating" | "draft";
17
+ thumbnailUrl?: string;
18
+ avatarColor?: string;
19
+ avatarInitials?: string;
20
+ }
21
+
22
+ export interface StoriesBarProps extends React.HTMLAttributes<HTMLDivElement> {
23
+ stories: StoryItem[];
24
+ activeStoryId?: string | null;
25
+ onStoryClick: (story: StoryItem) => void;
26
+ defaultIcon?: string;
27
+ }
28
+
29
+ function getStoryStatusStyle(status?: StoryItem["status"]) {
30
+ switch (status) {
31
+ case "generating":
32
+ return {
33
+ iconColor: "text-amber-500",
34
+ badgeBg: "bg-amber-500",
35
+ };
36
+ case "ready":
37
+ return {
38
+ iconColor: "text-emerald-500",
39
+ badgeBg: "bg-emerald-500",
40
+ };
41
+ case "draft":
42
+ default:
43
+ return {
44
+ iconColor: "text-muted-foreground",
45
+ badgeBg: "bg-muted-foreground",
46
+ };
47
+ }
48
+ }
49
+
50
+ export const StoriesBar = React.forwardRef<HTMLDivElement, StoriesBarProps>(
51
+ ({ stories, activeStoryId, onStoryClick, defaultIcon = "clapperboard", className, ...props }, ref) => {
52
+ return (
53
+ <div
54
+ ref={ref}
55
+ className={cn("w-full border-b border-border bg-background shrink-0", className)}
56
+ data-testid="stories-bar"
57
+ {...props}
58
+ >
59
+ <div className="flex items-center gap-2.5 px-3 py-1.5 overflow-x-auto no-scrollbar scroll-smooth">
60
+ {stories.map((story) => {
61
+ const isActive = story.id === activeStoryId;
62
+ const isGenerating = story.status === "generating";
63
+ const statusStyle = getStoryStatusStyle(story.status);
64
+ const iconName = story.icon || defaultIcon;
65
+
66
+ return (
67
+ <Button
68
+ key={story.id}
69
+ variant="ghost"
70
+ onClick={() => onStoryClick(story)}
71
+ className="flex flex-col items-center gap-1 h-auto p-0 hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg cursor-pointer group shrink-0"
72
+ data-testid={`btn-story-${story.id}`}
73
+ aria-label={`Open story: ${story.title}`}
74
+ >
75
+ <div className="relative w-9 h-9 shrink-0 aspect-square rounded-full transition-transform group-hover:scale-105">
76
+ <div
77
+ className={cn(
78
+ "w-full h-full rounded-full flex items-center justify-center border transition-colors overflow-hidden select-none",
79
+ isActive
80
+ ? "border-primary bg-muted/60"
81
+ : "border-border/50 bg-muted/40"
82
+ )}
83
+ >
84
+ {story.thumbnailUrl ? (
85
+ <img
86
+ src={story.thumbnailUrl}
87
+ alt={story.title}
88
+ className="w-full h-full object-cover"
89
+ />
90
+ ) : (
91
+ <Icon
92
+ name={iconName}
93
+ className={cn("w-4 h-4 transition-colors", statusStyle.iconColor)}
94
+ />
95
+ )}
96
+ </div>
97
+
98
+ {isGenerating ? (
99
+ <span className="absolute bottom-0 right-0 flex h-2 w-2">
100
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-500 opacity-75" />
101
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500" />
102
+ </span>
103
+ ) : (
104
+ <span
105
+ className={cn(
106
+ "absolute bottom-0 right-0 inline-flex rounded-full h-2 w-2 ring-1.5 ring-background",
107
+ statusStyle.badgeBg
108
+ )}
109
+ />
110
+ )}
111
+ </div>
112
+
113
+ <span
114
+ className={cn(
115
+ "text-[10px] leading-tight truncate max-w-[56px] text-center",
116
+ isActive
117
+ ? "font-semibold text-foreground"
118
+ : "text-muted-foreground group-hover:text-foreground"
119
+ )}
120
+ title={story.title}
121
+ >
122
+ {story.title}
123
+ </span>
124
+ </Button>
125
+ );
126
+ })}
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+ );
132
+
133
+ StoriesBar.displayName = "StoriesBar";
@@ -0,0 +1,2 @@
1
+ export { StoriesBar } from "./StoriesBar";
2
+ export type { StoriesBarProps, StoryItem } from "./StoriesBar";
@@ -201,8 +201,11 @@ export const TaskQueue = React.memo<TaskQueueProps>(
201
201
  <div className="flex items-center gap-2.5 min-w-0 flex-1">
202
202
  {getStatusIcon(task.status)}
203
203
 
204
- <QueueItemContent className="font-mono text-sm text-foreground select-all">
205
- {task.command || task.title}
204
+ <QueueItemContent
205
+ className="font-mono text-sm text-foreground select-all"
206
+ title={task.runId ? `Run ID: ${task.runId}` : undefined}
207
+ >
208
+ {task.title || task.command}
206
209
  </QueueItemContent>
207
210
 
208
211
  {task.duration && (
@@ -219,7 +222,7 @@ export const TaskQueue = React.memo<TaskQueueProps>(
219
222
  e.stopPropagation();
220
223
  onStop(task.id);
221
224
  }}
222
- aria-label={`Stop ${task.command || task.title}`}
225
+ aria-label={`Stop ${task.title || task.command}`}
223
226
  title="Stop task"
224
227
  className="size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100"
225
228
  >
@@ -233,7 +236,7 @@ export const TaskQueue = React.memo<TaskQueueProps>(
233
236
  e.stopPropagation();
234
237
  onCancel(task.id);
235
238
  }}
236
- aria-label={`Cancel ${task.command || task.title}`}
239
+ aria-label={`Cancel ${task.title || task.command}`}
237
240
  title="Cancel task"
238
241
  className="size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100"
239
242
  >
@@ -247,7 +250,7 @@ export const TaskQueue = React.memo<TaskQueueProps>(
247
250
  e.stopPropagation();
248
251
  onRetry(task.id);
249
252
  }}
250
- aria-label={`Retry ${task.command || task.title}`}
253
+ aria-label={`Retry ${task.title || task.command}`}
251
254
  title="Retry task"
252
255
  className="size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100"
253
256
  >
@@ -35,6 +35,10 @@ export interface TaskItem {
35
35
  * Optional error message if the task failed
36
36
  */
37
37
  error?: string;
38
+ /**
39
+ * Optional underlying run/job identifier (e.g. for tooltip/diagnostics)
40
+ */
41
+ runId?: string;
38
42
  }
39
43
 
40
44
  export interface TaskQueueProps {
@@ -280,3 +280,8 @@ export type { MediaPreviewProps, MediaFileLike } from './MediaPreview'
280
280
  // WorkspaceEmptyState Composite
281
281
  export { WorkspaceEmptyState } from './WorkspaceEmptyState'
282
282
  export type { WorkspaceEmptyStateProps } from './WorkspaceEmptyState'
283
+
284
+ // StoriesBar Composite
285
+ export { StoriesBar } from './StoriesBar'
286
+ export type { StoriesBarProps, StoryItem } from './StoriesBar'
287
+
@@ -199,6 +199,11 @@ export interface ChatPanelProps {
199
199
  * @default true
200
200
  */
201
201
  enableSpeech?: boolean;
202
+ /**
203
+ * Whether to display the session header at the top of the chat panel.
204
+ * @default true
205
+ */
206
+ showHeader?: boolean;
202
207
  }
203
208
 
204
209
  /**
@@ -243,6 +248,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
243
248
  onTaskStopAll,
244
249
  onTaskClick,
245
250
  enableSpeech = true,
251
+ showHeader = true,
246
252
  }) => {
247
253
  // File change queue state
248
254
  const [fileChangeState, setFileChangeState] = React.useState<
@@ -397,14 +403,16 @@ export const ChatPanel = React.memo<ChatPanelProps>(
397
403
  return (
398
404
  <div className={`relative flex h-full flex-col ${className || ""}`}>
399
405
  {/* Chat Session Header */}
400
- <SessionHeader
401
- sessions={sessions}
402
- activeSessionId={activeSessionId}
403
- onNewSession={onNewSession}
404
- onCloseSession={onCloseSession}
405
- onSelectSession={onSelectSession}
406
- onDownloadSession={onDownloadSession || (async () => exportMessagesToMarkdownFile(messages as ExportableMessage[]))}
407
- />
406
+ {showHeader && (
407
+ <SessionHeader
408
+ sessions={sessions}
409
+ activeSessionId={activeSessionId}
410
+ onNewSession={onNewSession}
411
+ onCloseSession={onCloseSession}
412
+ onSelectSession={onSelectSession}
413
+ onDownloadSession={onDownloadSession || (async () => exportMessagesToMarkdownFile(messages as ExportableMessage[]))}
414
+ />
415
+ )}
408
416
 
409
417
  <AIConversation
410
418
  messages={messages}
@@ -33,8 +33,11 @@ export {
33
33
  ContextCacheUsage,
34
34
  TaskQueue,
35
35
  SpeechInput,
36
+ StoriesBar,
36
37
  } from './composites';
37
38
  export type {
39
+ StoriesBarProps,
40
+ StoryItem,
38
41
  SpeechInputProps,
39
42
  ApprovalCardProps,
40
43
  ActionRequest,
package/dist/index.cjs CHANGED
@@ -1487,6 +1487,16 @@ var defaultIcons = {
1487
1487
  { type: "circle", attrs: { cx: "9", cy: "9", r: "2" } },
1488
1488
  { type: "path", attrs: { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" } }
1489
1489
  ]
1490
+ },
1491
+ "clapperboard": {
1492
+ name: "clapperboard",
1493
+ viewBox: "0 0 24 24",
1494
+ elements: [
1495
+ { type: "path", attrs: { d: "M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z" } },
1496
+ { type: "path", attrs: { d: "m6.2 5.3 3.1 3.9" } },
1497
+ { type: "path", attrs: { d: "m12.4 3.4 3.1 4" } },
1498
+ { type: "path", attrs: { d: "M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" } }
1499
+ ]
1490
1500
  }
1491
1501
  };
1492
1502
  var IconRegistry = class {
@@ -7169,7 +7179,14 @@ var TaskQueue = React3__namespace.memo(
7169
7179
  children: [
7170
7180
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 min-w-0 flex-1", children: [
7171
7181
  getStatusIcon(task.status),
7172
- /* @__PURE__ */ jsxRuntime.jsx(QueueItemContent, { className: "font-mono text-sm text-foreground select-all", children: task.command || task.title }),
7182
+ /* @__PURE__ */ jsxRuntime.jsx(
7183
+ QueueItemContent,
7184
+ {
7185
+ className: "font-mono text-sm text-foreground select-all",
7186
+ title: task.runId ? `Run ID: ${task.runId}` : void 0,
7187
+ children: task.title || task.command
7188
+ }
7189
+ ),
7173
7190
  task.duration && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground/60 font-sans ml-1 flex-shrink-0", children: task.duration })
7174
7191
  ] }),
7175
7192
  /* @__PURE__ */ jsxRuntime.jsxs(QueueItemActions, { children: [
@@ -7180,7 +7197,7 @@ var TaskQueue = React3__namespace.memo(
7180
7197
  e.stopPropagation();
7181
7198
  onStop(task.id);
7182
7199
  },
7183
- "aria-label": `Stop ${task.command || task.title}`,
7200
+ "aria-label": `Stop ${task.title || task.command}`,
7184
7201
  title: "Stop task",
7185
7202
  className: "size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100",
7186
7203
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "circle-stop", size: "sm" })
@@ -7193,7 +7210,7 @@ var TaskQueue = React3__namespace.memo(
7193
7210
  e.stopPropagation();
7194
7211
  onCancel(task.id);
7195
7212
  },
7196
- "aria-label": `Cancel ${task.command || task.title}`,
7213
+ "aria-label": `Cancel ${task.title || task.command}`,
7197
7214
  title: "Cancel task",
7198
7215
  className: "size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100",
7199
7216
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "x", size: "sm" })
@@ -7206,7 +7223,7 @@ var TaskQueue = React3__namespace.memo(
7206
7223
  e.stopPropagation();
7207
7224
  onRetry(task.id);
7208
7225
  },
7209
- "aria-label": `Retry ${task.command || task.title}`,
7226
+ "aria-label": `Retry ${task.title || task.command}`,
7210
7227
  title: "Retry task",
7211
7228
  className: "size-6 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 opacity-100 group-hover:opacity-100",
7212
7229
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "refresh-cw", size: "sm" })
@@ -8021,7 +8038,8 @@ var ChatPanel = React3__namespace.memo(
8021
8038
  onTaskRetry,
8022
8039
  onTaskStopAll,
8023
8040
  onTaskClick,
8024
- enableSpeech = true
8041
+ enableSpeech = true,
8042
+ showHeader = true
8025
8043
  }) => {
8026
8044
  const [fileChangeState, setFileChangeState] = React3__namespace.useState("approval-requested");
8027
8045
  const [approval, setApproval] = React3__namespace.useState({});
@@ -8140,7 +8158,7 @@ var ChatPanel = React3__namespace.memo(
8140
8158
  );
8141
8159
  }, [loading, messages]);
8142
8160
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative flex h-full flex-col ${className || ""}`, children: [
8143
- /* @__PURE__ */ jsxRuntime.jsx(
8161
+ showHeader && /* @__PURE__ */ jsxRuntime.jsx(
8144
8162
  SessionHeader,
8145
8163
  {
8146
8164
  sessions,
@@ -14375,6 +14393,108 @@ var WorkspaceEmptyState = React3__namespace.memo(
14375
14393
  }
14376
14394
  );
14377
14395
  WorkspaceEmptyState.displayName = "WorkspaceEmptyState";
14396
+ function getStoryStatusStyle(status) {
14397
+ switch (status) {
14398
+ case "generating":
14399
+ return {
14400
+ iconColor: "text-amber-500",
14401
+ badgeBg: "bg-amber-500"
14402
+ };
14403
+ case "ready":
14404
+ return {
14405
+ iconColor: "text-emerald-500",
14406
+ badgeBg: "bg-emerald-500"
14407
+ };
14408
+ case "draft":
14409
+ default:
14410
+ return {
14411
+ iconColor: "text-muted-foreground",
14412
+ badgeBg: "bg-muted-foreground"
14413
+ };
14414
+ }
14415
+ }
14416
+ var StoriesBar = React3__namespace.forwardRef(
14417
+ (_a, ref) => {
14418
+ var _b = _a, { stories, activeStoryId, onStoryClick, defaultIcon = "clapperboard", className } = _b, props = __objRest(_b, ["stories", "activeStoryId", "onStoryClick", "defaultIcon", "className"]);
14419
+ return /* @__PURE__ */ jsxRuntime.jsx(
14420
+ "div",
14421
+ __spreadProps(__spreadValues({
14422
+ ref,
14423
+ className: cn("w-full border-b border-border bg-background shrink-0", className),
14424
+ "data-testid": "stories-bar"
14425
+ }, props), {
14426
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2.5 px-3 py-1.5 overflow-x-auto no-scrollbar scroll-smooth", children: stories.map((story) => {
14427
+ const isActive = story.id === activeStoryId;
14428
+ const isGenerating = story.status === "generating";
14429
+ const statusStyle = getStoryStatusStyle(story.status);
14430
+ const iconName = story.icon || defaultIcon;
14431
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14432
+ Button2,
14433
+ {
14434
+ variant: "ghost",
14435
+ onClick: () => onStoryClick(story),
14436
+ className: "flex flex-col items-center gap-1 h-auto p-0 hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg cursor-pointer group shrink-0",
14437
+ "data-testid": `btn-story-${story.id}`,
14438
+ "aria-label": `Open story: ${story.title}`,
14439
+ children: [
14440
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-9 h-9 shrink-0 aspect-square rounded-full transition-transform group-hover:scale-105", children: [
14441
+ /* @__PURE__ */ jsxRuntime.jsx(
14442
+ "div",
14443
+ {
14444
+ className: cn(
14445
+ "w-full h-full rounded-full flex items-center justify-center border transition-colors overflow-hidden select-none",
14446
+ isActive ? "border-primary bg-muted/60" : "border-border/50 bg-muted/40"
14447
+ ),
14448
+ children: story.thumbnailUrl ? /* @__PURE__ */ jsxRuntime.jsx(
14449
+ "img",
14450
+ {
14451
+ src: story.thumbnailUrl,
14452
+ alt: story.title,
14453
+ className: "w-full h-full object-cover"
14454
+ }
14455
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
14456
+ Icon,
14457
+ {
14458
+ name: iconName,
14459
+ className: cn("w-4 h-4 transition-colors", statusStyle.iconColor)
14460
+ }
14461
+ )
14462
+ }
14463
+ ),
14464
+ isGenerating ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute bottom-0 right-0 flex h-2 w-2", children: [
14465
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-500 opacity-75" }),
14466
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2 w-2 bg-amber-500" })
14467
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
14468
+ "span",
14469
+ {
14470
+ className: cn(
14471
+ "absolute bottom-0 right-0 inline-flex rounded-full h-2 w-2 ring-1.5 ring-background",
14472
+ statusStyle.badgeBg
14473
+ )
14474
+ }
14475
+ )
14476
+ ] }),
14477
+ /* @__PURE__ */ jsxRuntime.jsx(
14478
+ "span",
14479
+ {
14480
+ className: cn(
14481
+ "text-[10px] leading-tight truncate max-w-[56px] text-center",
14482
+ isActive ? "font-semibold text-foreground" : "text-muted-foreground group-hover:text-foreground"
14483
+ ),
14484
+ title: story.title,
14485
+ children: story.title
14486
+ }
14487
+ )
14488
+ ]
14489
+ },
14490
+ story.id
14491
+ );
14492
+ }) })
14493
+ })
14494
+ );
14495
+ }
14496
+ );
14497
+ StoriesBar.displayName = "StoriesBar";
14378
14498
  var SpecNavigator = React3__namespace.memo(
14379
14499
  ({ groups, selectedFileId, onFileSelect, className }) => {
14380
14500
  var _a, _b;
@@ -17105,6 +17225,7 @@ exports.SectionLayout = SectionLayout;
17105
17225
  exports.SessionHeader = SessionHeader;
17106
17226
  exports.SpecNavigator = SpecNavigator;
17107
17227
  exports.SpeechInput = SpeechInput;
17228
+ exports.StoriesBar = StoriesBar;
17108
17229
  exports.TaskQueue = TaskQueue;
17109
17230
  exports.TextEditor = TextEditor;
17110
17231
  exports.WorkflowCanvas = WorkflowCanvas;