ai-design-system 0.1.116 → 0.1.118

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.
@@ -55,7 +55,7 @@ export const AccountButton = React.forwardRef<HTMLButtonElement, AccountButtonPr
55
55
  <div
56
56
  className={cn(
57
57
  "w-6 h-6 rounded-full flex items-center justify-center text-xs font-semibold select-none text-white",
58
- avatarColor ? "" : "bg-emerald-700"
58
+ avatarColor ? "" : "bg-gradient-to-br from-teal-800 to-emerald-600"
59
59
  )}
60
60
  style={avatarColor ? { backgroundColor: avatarColor } : undefined}
61
61
  >
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { useState, useRef, useEffect } from "react";
5
5
  import { Icon } from "@/components/primitives/Icon";
6
+ import { Button } from "@/components/primitives/Button";
6
7
  import { Input } from "@/components/primitives/Input";
7
8
  import { Textarea } from "@/components/primitives/Textarea";
8
9
  import { Label } from "@/components/primitives/Label";
@@ -48,6 +49,12 @@ export interface ApprovalCardProps {
48
49
  state?: ToolUIState;
49
50
  approval?: ToolApproval;
50
51
  className?: string;
52
+ /**
53
+ * Opens the file a question is about (`args.file_path`). With it, the
54
+ * question shows the file's name as a link; without it, or when the question
55
+ * names no file, nothing is shown.
56
+ */
57
+ onOpenFile?: (path: string) => void;
51
58
  }
52
59
 
53
60
  function formatValue(value: unknown): string {
@@ -92,6 +99,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
92
99
  state = "approval-requested",
93
100
  approval = {},
94
101
  className,
102
+ onOpenFile,
95
103
  }) => {
96
104
  const [isEditing, setIsEditing] = useState(false);
97
105
  const [prevArgs, setPrevArgs] = useState(actionRequest.args);
@@ -99,6 +107,12 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
99
107
  const [rejectReason, setRejectReason] = useState("");
100
108
  const [showRejectInput, setShowRejectInput] = useState(false);
101
109
 
110
+ // The file the question is about, when the asker named one.
111
+ const questionFile =
112
+ typeof actionRequest.args?.file_path === "string" && actionRequest.args.file_path.trim()
113
+ ? actionRequest.args.file_path
114
+ : null;
115
+
102
116
  const hideSkipButton = (actionRequest.args?.hide_skip_button as boolean) ?? false;
103
117
  const submitButtonText = (actionRequest.args?.button_submit_text as string) || 'Continue';
104
118
  const skipButtonText = (actionRequest.args?.button_skip_text as string) || 'Skip';
@@ -267,29 +281,45 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
267
281
  <StreamingMarkdown controls={false}>
268
282
  {currentQuestion.question}
269
283
  </StreamingMarkdown>
284
+ {questionFile && onOpenFile && (
285
+ <Button
286
+ type="button"
287
+ variant="link"
288
+ onClick={() => onOpenFile(questionFile)}
289
+ className="mt-2 h-auto p-0 inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-primary hover:underline"
290
+ data-testid="approval-card-file-link"
291
+ >
292
+ <Icon name="file-text" className="h-4 w-4 flex-shrink-0" />
293
+ <span className="truncate">{questionFile.split("/").pop()}</span>
294
+ </Button>
295
+ )}
270
296
  </div>
271
297
  </div>
272
298
  {questions.length > 1 && (
273
299
  <div className="flex items-center gap-1.5 text-xs text-muted-foreground flex-shrink-0 font-normal">
274
- <button
300
+ <Button
275
301
  type="button"
302
+ variant="ghost"
303
+ size="icon-sm"
276
304
  onClick={() => setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))}
277
305
  disabled={currentQuestionIndex === 0}
278
- className="hover:text-foreground disabled:opacity-30 p-0.5 transition-colors"
306
+ className="h-6 w-6 p-0 hover:text-foreground disabled:opacity-30 transition-colors"
279
307
  >
280
308
  <Icon name="chevron-left" size="sm" />
281
- </button>
309
+ </Button>
282
310
  <span className="font-medium">{currentQuestionIndex + 1} of {questions.length}</span>
283
- <button
311
+ <Button
284
312
  type="button"
313
+ variant="ghost"
314
+ size="icon-sm"
285
315
  onClick={() =>
286
316
  setCurrentQuestionIndex((prev) => Math.min(questions.length - 1, prev + 1))
287
317
  }
288
318
  disabled={currentQuestionIndex === questions.length - 1}
289
- className="hover:text-foreground disabled:opacity-30 p-0.5 transition-colors"
319
+ className="h-6 w-6 p-0 hover:text-foreground disabled:opacity-30 transition-colors"
290
320
  >
291
321
  <Icon name="chevron-right" size="sm" />
292
- </button>
322
+ </Button>
293
323
  </div>
294
324
  )}
295
325
  </div>
@@ -301,8 +331,8 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
301
331
  const isOther = index === allOptions.length - 1;
302
332
  const isSelected = isMultiSelect
303
333
  ? (selectedAnswers[currentQuestionIndex] as string[])?.includes(
304
- isOther ? otherTexts[currentQuestionIndex] || "" : option
305
- )
334
+ isOther ? otherTexts[currentQuestionIndex] || "" : option
335
+ )
306
336
  : selectedOptionIndices[currentQuestionIndex] === index;
307
337
 
308
338
  return (
@@ -500,7 +530,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
500
530
  }}
501
531
  disabled={isProcessing}
502
532
  >
503
- <Icon name="check" size="sm" className="mr-2" />
533
+ <Icon name="check" size="sm" className="mr-2" />
504
534
  Save & Approve
505
535
  </ConfirmationAction>
506
536
  </ConfirmationActions>
@@ -576,7 +606,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
576
606
  }}
577
607
  disabled={isProcessing}
578
608
  >
579
- <Icon name="x" size="sm" className="mr-2" />
609
+ <Icon name="x" size="sm" className="mr-2" />
580
610
  Confirm Rejection
581
611
  </ConfirmationAction>
582
612
  </>
@@ -589,7 +619,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
589
619
  onClick={() => setShowRejectInput(true)}
590
620
  disabled={isProcessing}
591
621
  >
592
- <Icon name="x" size="sm" className="mr-2" />
622
+ <Icon name="x" size="sm" className="mr-2" />
593
623
  {skipButtonText}
594
624
  </ConfirmationAction>
595
625
  )}
@@ -610,7 +640,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
610
640
  onClick={onApprove}
611
641
  disabled={isProcessing}
612
642
  >
613
- <Icon name="check" size="sm" className="mr-2" />
643
+ <Icon name="check" size="sm" className="mr-2" />
614
644
  {submitButtonText}
615
645
  </ConfirmationAction>
616
646
  </>
@@ -145,7 +145,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
145
145
  className={cn(
146
146
  "[&>[data-slot=input-group]]:rounded-2xl",
147
147
  className?.includes("border") &&
148
- "[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
148
+ "[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
149
149
  className
150
150
  )}
151
151
  {...props}
@@ -155,15 +155,15 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
155
155
  placeholder={placeholder}
156
156
  disabled={disabled}
157
157
  onChange={isControlled ? handleControlledChange : undefined}
158
- className="min-h-9 py-2 px-3 text-sm resize-none border-0 shadow-none focus-visible:ring-0 bg-transparent flex-1 field-sizing-content"
158
+ className="min-h-12 py-3 pl-3 pr-2 text-sm resize-none border-0 shadow-none focus-visible:ring-0 bg-transparent flex-1 field-sizing-content"
159
159
  />
160
- <InputGroupAddon align="inline-end" className="self-end pb-1 pr-1.5 shrink-0">
160
+ <InputGroupAddon align="inline-end" className="self-end pb-3 pr-3 pt-3 shrink-0 has-[>button]:mr-0 !mr-0">
161
161
  <PromptInputSubmit
162
162
  disabled={disabled || (loading && !onStop)}
163
163
  status={loading ? (onStop ? "streaming" : "submitted") : undefined}
164
164
  onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
165
165
  className={cn(
166
- "rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition-colors",
166
+ "rounded-lg text-white transition-colors",
167
167
  isStopping && "bg-transparent hover:bg-accent"
168
168
  )}
169
169
  >
@@ -181,7 +181,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
181
181
  onError={onError}
182
182
  className={cn(
183
183
  className?.includes("border") &&
184
- "[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
184
+ "[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
185
185
  className
186
186
  )}
187
187
  {...props}
@@ -218,7 +218,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
218
218
  status={loading ? (onStop ? "streaming" : "submitted") : undefined}
219
219
  onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
220
220
  className={cn(
221
- "rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition-colors",
221
+ "rounded-lg text-white transition-colors",
222
222
  isStopping && "bg-transparent hover:bg-accent"
223
223
  )}
224
224
  >
@@ -17,10 +17,24 @@ export interface ChatSessionInfo {
17
17
  }
18
18
 
19
19
  import type { FileDownloadResult } from '@/components/composites/FileTreeExplorer';
20
+ import { formatDayLabel } from '@/lib/date';
20
21
 
21
22
 
22
23
  export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
23
24
  title?: string;
25
+ /**
26
+ * What a session is called.
27
+ * - "title": the session's own title (default).
28
+ * - "date": the day it was created, as "21 Aug 2025", in the reader's time
29
+ * zone. For a surface that keeps one session per day, where the day is
30
+ * what tells sessions apart and a first message is not.
31
+ * @default "title"
32
+ */
33
+ titleFormat?: 'title' | 'date';
34
+ /**
35
+ * IANA time zone for "date" titles. Defaults to the reader's own.
36
+ */
37
+ timeZone?: string;
24
38
  sessions?: ChatSessionInfo[];
25
39
  activeSessionId?: string | null;
26
40
  onNewSession?: () => void;
@@ -48,7 +62,7 @@ export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement>
48
62
  * and a dropdown history of past sessions.
49
63
  */
50
64
  export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps>(
51
- ({ title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
65
+ ({ title, titleFormat = 'title', timeZone, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
52
66
  const activeSession = sessions?.find(s => s.id === activeSessionId);
53
67
  const downloadRef = React.useRef<HTMLAnchorElement>(null);
54
68
 
@@ -66,9 +80,16 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
66
80
  URL.revokeObjectURL(url);
67
81
  }, [onDownloadSession]);
68
82
 
69
- const displayTitle = title || (activeSessionId
70
- ? (activeSession?.title || 'Untitled Session')
71
- : 'New Session');
83
+ const byDate = titleFormat === 'date';
84
+ const sessionLabel = (session: ChatSessionInfo | undefined): string => {
85
+ if (!byDate) return session?.title || 'Untitled Session';
86
+ // A session not yet in the list is the one being started now: today's.
87
+ return formatDayLabel(session?.created_at ?? new Date(), { timeZone }) || 'Untitled Session';
88
+ };
89
+
90
+ const displayTitle = title || (activeSessionId
91
+ ? sessionLabel(activeSession)
92
+ : byDate ? formatDayLabel(new Date(), { timeZone }) : 'New Session');
72
93
 
73
94
  return (
74
95
  <div
@@ -121,8 +142,11 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
121
142
  onClick={() => onSelectSession?.(session.id)}
122
143
  className="flex flex-col items-start py-2 cursor-pointer"
123
144
  >
124
- <span className="text-sm font-medium truncate w-full">{session.title || 'Untitled Session'}</span>
125
- <span className="text-xs text-muted-foreground">{new Date(session.created_at).toLocaleString()}</span>
145
+ <span className="text-sm font-medium truncate w-full">{sessionLabel(session)}</span>
146
+ {/* The date is the title in "date" mode; repeating it underneath says nothing. */}
147
+ {!byDate && (
148
+ <span className="text-xs text-muted-foreground">{new Date(session.created_at).toLocaleString()}</span>
149
+ )}
126
150
  </DropdownMenuItem>
127
151
  ))
128
152
  ) : (
@@ -21,31 +21,12 @@ export default meta;
21
21
  type Story = StoryObj<typeof meta>;
22
22
 
23
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
- },
24
+ { id: "story-1", title: "Devin iOS", status: "ready", unread: true },
25
+ { id: "story-2", title: "Waypoint", status: "ready" },
26
+ { id: "story-3", title: "Design Sys", status: "working" },
27
+ { id: "story-4", title: "Workspace", status: "working", unread: true },
28
+ { id: "story-5", title: "Render", status: "failed", unread: true },
29
+ { id: "story-6", title: "ZeroOps", status: "stopped" },
49
30
  ];
50
31
 
51
32
  /**
@@ -13,7 +13,20 @@ export interface StoryItem {
13
13
  subtitle?: string;
14
14
  icon?: string;
15
15
  time?: string;
16
- status?: "ready" | "generating" | "draft";
16
+ /**
17
+ * The video's state, drawn as the story's colour:
18
+ * - "failed": red — the video has stopped on an error
19
+ * - "ready": green — the finished video is available
20
+ * - "stopped": grey — not ready, and nothing is running to finish it
21
+ * - "working": orange — in progress
22
+ * "generating" and "draft" are the earlier names of "working" and "stopped".
23
+ */
24
+ status?: "failed" | "ready" | "stopped" | "working" | "generating" | "draft";
25
+ /**
26
+ * Whether the story holds a notification the user has not seen. Shown as the
27
+ * badge, on any status, and only then.
28
+ */
29
+ unread?: boolean;
17
30
  thumbnailUrl?: string;
18
31
  avatarColor?: string;
19
32
  avatarInitials?: string;
@@ -28,22 +41,17 @@ export interface StoriesBarProps extends React.HTMLAttributes<HTMLDivElement> {
28
41
 
29
42
  function getStoryStatusStyle(status?: StoryItem["status"]) {
30
43
  switch (status) {
31
- case "generating":
32
- return {
33
- iconColor: "text-amber-500",
34
- badgeBg: "bg-amber-500",
35
- };
44
+ case "failed":
45
+ return { iconColor: "text-red-500", badgeBg: "bg-red-500" };
36
46
  case "ready":
37
- return {
38
- iconColor: "text-emerald-500",
39
- badgeBg: "bg-emerald-500",
40
- };
47
+ return { iconColor: "text-emerald-500", badgeBg: "bg-emerald-500" };
48
+ case "working":
49
+ case "generating":
50
+ return { iconColor: "text-amber-500", badgeBg: "bg-amber-500" };
51
+ case "stopped":
41
52
  case "draft":
42
53
  default:
43
- return {
44
- iconColor: "text-muted-foreground",
45
- badgeBg: "bg-muted-foreground",
46
- };
54
+ return { iconColor: "text-muted-foreground", badgeBg: "bg-muted-foreground" };
47
55
  }
48
56
  }
49
57
 
@@ -61,7 +69,6 @@ export const StoriesBar = React.forwardRef<HTMLDivElement, StoriesBarProps>(
61
69
  <div className="flex items-center gap-2.5 px-3 py-1.5 overflow-x-auto no-scrollbar scroll-smooth">
62
70
  {stories.map((story) => {
63
71
  const isActive = story.id === activeStoryId;
64
- const isGenerating = story.status === "generating";
65
72
  const statusStyle = getStoryStatusStyle(story.status);
66
73
  const iconName = story.icon || defaultIcon;
67
74
 
@@ -100,17 +107,15 @@ export const StoriesBar = React.forwardRef<HTMLDivElement, StoriesBarProps>(
100
107
  )}
101
108
  </div>
102
109
 
103
- {isGenerating ? (
104
- <span className="absolute bottom-0 right-0 flex h-2 w-2">
105
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-500 opacity-75" />
106
- <span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500" />
107
- </span>
108
- ) : (
110
+ {/* The badge: an unseen notification, on any status. */}
111
+ {story.unread && (
109
112
  <span
110
113
  className={cn(
111
114
  "absolute bottom-0 right-0 inline-flex rounded-full h-2 w-2 ring-1.5 ring-background",
112
115
  statusStyle.badgeBg
113
116
  )}
117
+ data-testid={`story-badge-${story.id}`}
118
+ aria-label="New notification"
114
119
  />
115
120
  )}
116
121
  </div>
@@ -95,6 +95,8 @@ export interface ChatPanelProps {
95
95
  * Handler to edit HITL approval request arguments
96
96
  */
97
97
  onApprovalEdit?: (editedArgs: Record<string, unknown>) => void;
98
+ /** Opens the file a pending question is about (its `file_path`). The card shows the link only when this is given. */
99
+ onOpenFile?: (path: string) => void;
98
100
  /**
99
101
  * Processing state for HITL approval request
100
102
  */
@@ -220,6 +222,17 @@ export interface ChatPanelProps {
220
222
  * Title override for the session header
221
223
  */
222
224
  sessionTitle?: string;
225
+ /**
226
+ * What sessions are called in the header and the history list: their own
227
+ * title ("title", default) or the day they were created ("date", shown as
228
+ * "21 Aug 2025" in the reader's time zone).
229
+ * @default "title"
230
+ */
231
+ sessionTitleFormat?: 'title' | 'date';
232
+ /**
233
+ * IANA time zone for "date" session titles. Defaults to the reader's own.
234
+ */
235
+ sessionTimeZone?: string;
223
236
  /**
224
237
  * Whether to show action buttons in the session header
225
238
  * @default true
@@ -273,6 +286,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
273
286
  onApprovalApprove,
274
287
  onApprovalReject,
275
288
  onApprovalEdit,
289
+ onOpenFile,
276
290
  isApprovalProcessing = false,
277
291
  loading = false,
278
292
  onStop,
@@ -304,6 +318,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
304
318
  enableSpeech = true,
305
319
  showHeader = true,
306
320
  sessionTitle,
321
+ sessionTitleFormat = 'title',
322
+ sessionTimeZone,
307
323
  showSessionActions = true,
308
324
  showNewSession = true,
309
325
  showDownloadSession = true,
@@ -433,6 +449,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
433
449
  isProcessing={isApprovalProcessing}
434
450
  state={approvalCardState}
435
451
  approval={approvalCardApproval}
452
+ onOpenFile={onOpenFile}
436
453
  />
437
454
  </div>
438
455
  );
@@ -468,6 +485,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
468
485
  {showHeader && (
469
486
  <SessionHeader
470
487
  title={sessionTitle}
488
+ titleFormat={sessionTitleFormat}
489
+ timeZone={sessionTimeZone}
471
490
  showActions={showSessionActions}
472
491
  showNewSession={showNewSession}
473
492
  showDownloadSession={showDownloadSession}
@@ -94,6 +94,9 @@ export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
94
94
 
95
95
  // Utilities
96
96
  export { cn } from '@/lib/utils';
97
+ // Calendar days in the reader's time zone
98
+ export { formatDayLabel, dayKey, isSameDay, userTimeZone } from '@/lib/date';
99
+ export type { DayOptions } from '@/lib/date';
97
100
  export { ButtonSwitcher } from './composites';
98
101
  export type { ButtonSwitcherProps, ButtonSwitcherItem } from './composites';
99
102