ai-design-system 0.1.113 → 0.1.115

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.
@@ -23,6 +23,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
23
23
  className,
24
24
  padded = true,
25
25
  mobileBehavior = "tabs",
26
+ hideMobileTabs = false,
26
27
  activeSectionId,
27
28
  onActiveSectionChange,
28
29
  ...props
@@ -58,6 +59,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
58
59
  className={className}
59
60
  padded={padded}
60
61
  mobileBehavior={mobileBehavior}
62
+ hideMobileTabs={hideMobileTabs}
61
63
  activeSectionId={activeSectionId}
62
64
  onActiveSectionChange={onActiveSectionChange}
63
65
  {...props}
@@ -25,6 +25,7 @@ export interface SectionLayoutProps extends React.ComponentPropsWithoutRef<"div"
25
25
  dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted";
26
26
  padded?: boolean;
27
27
  mobileBehavior?: "tabs" | "stack" | "none";
28
+ hideMobileTabs?: boolean;
28
29
  activeSectionId?: string;
29
30
  onActiveSectionChange?: (sectionId: string) => void;
30
31
  }
@@ -7,7 +7,7 @@ import {
7
7
  type Node,
8
8
  useReactFlow,
9
9
  } from "@xyflow/react";
10
- import { useCallback, useEffect } from "react";
10
+ import { useCallback, useEffect, useRef } from "react";
11
11
  import { Canvas } from "@/components/ai-elements/canvas";
12
12
  import { Connection as ConnectionLine } from "@/components/ai-elements/connection";
13
13
  import { Controls } from "@/components/ai-elements/controls";
@@ -58,6 +58,36 @@ function WorkflowCanvasInner({
58
58
  className,
59
59
  }: WorkflowCanvasProps) {
60
60
  const { fitView } = useReactFlow();
61
+ const containerRef = useRef<HTMLDivElement>(null);
62
+ const wasZeroSizeRef = useRef<boolean>(true);
63
+
64
+ // Automatically fit view when the canvas container transitions from hidden (0x0)
65
+ // to visible (e.g. mobile Overview icon clicked or tab activated)
66
+ useEffect(() => {
67
+ const el = containerRef.current;
68
+ if (!el || typeof ResizeObserver === "undefined") return;
69
+
70
+ const observer = new ResizeObserver((entries) => {
71
+ for (const entry of entries) {
72
+ const { width, height } = entry.contentRect;
73
+ if (width > 0 && height > 0) {
74
+ if (wasZeroSizeRef.current) {
75
+ wasZeroSizeRef.current = false;
76
+ // Delay slightly so xyflow's internal node measurements can settle
77
+ const timer = setTimeout(() => {
78
+ fitView({ padding: 0.2, duration: 300 });
79
+ }, 60);
80
+ return () => clearTimeout(timer);
81
+ }
82
+ } else {
83
+ wasZeroSizeRef.current = true;
84
+ }
85
+ }
86
+ });
87
+
88
+ observer.observe(el);
89
+ return () => observer.disconnect();
90
+ }, [fitView]);
61
91
 
62
92
  // Opt-in re-fit, e.g. after the device-preview toolbar's Route select
63
93
  // switches to "All" and the node count/extent changes underneath the
@@ -67,20 +97,16 @@ function WorkflowCanvasInner({
67
97
  // every such change would override the user's own pan/zoom constantly.
68
98
  useEffect(() => {
69
99
  if (fitViewSignal === undefined) return;
70
- // A plain synchronous call, and even a double-rAF, both raced xyflow's
71
- // own internal node measurement — verified live in both cases: the
72
- // resulting scale was far smaller than the two device-preview nodes'
73
- // true combined width needed (computed against their real DOM rects:
74
- // ~1.2 would tightly fit them, but fitView produced ~0.57 and one node
75
- // still landed partly outside the pane). xyflow measures each node via
76
- // ResizeObserver, which fires as a separate, unsynchronized callback —
77
- // not guaranteed to have run even two animation frames after the nodes
78
- // prop change lands. A short real-time delay is the standard, if
79
- // inelegant, workaround for this exact class of race.
80
- const timer = setTimeout(() => {
100
+ const timer1 = setTimeout(() => {
81
101
  fitView({ padding: 0.2, duration: 300 });
82
102
  }, 50);
83
- return () => clearTimeout(timer);
103
+ const timer2 = setTimeout(() => {
104
+ fitView({ padding: 0.2, duration: 300 });
105
+ }, 200);
106
+ return () => {
107
+ clearTimeout(timer1);
108
+ clearTimeout(timer2);
109
+ };
84
110
  }, [fitViewSignal, fitView]);
85
111
 
86
112
  useEffect(() => {
@@ -105,6 +131,7 @@ function WorkflowCanvasInner({
105
131
 
106
132
  return (
107
133
  <div
134
+ ref={containerRef}
108
135
  className={className}
109
136
  data-testid="workflow-canvas"
110
137
  style={{
@@ -31,6 +31,12 @@ export interface AdjustableLayoutProps extends React.ComponentPropsWithoutRef<"d
31
31
  * @default 'tabs'
32
32
  */
33
33
  mobileBehavior?: "tabs" | "stack" | "none"
34
+ /**
35
+ * Whether to hide the mobile tab bar when mobileBehavior is "tabs".
36
+ * Used when switching between panels is driven by internal controls (e.g. Overview / Show Chat).
37
+ * @default false
38
+ */
39
+ hideMobileTabs?: boolean
34
40
  /**
35
41
  * Active section ID in mobile tabs mode (for controlled usage).
36
42
  */
@@ -67,6 +73,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
67
73
  className,
68
74
  padded = false,
69
75
  mobileBehavior = "tabs",
76
+ hideMobileTabs = false,
70
77
  activeSectionId,
71
78
  onActiveSectionChange,
72
79
  ...props
@@ -384,12 +391,12 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
384
391
  ref={containerRef}
385
392
  className={cn(
386
393
  "flex flex-col overflow-hidden h-full min-w-0 w-full",
387
- padded && "p-2 sm:p-4 gap-2",
394
+ padded && (!hideMobileTabs ? "p-2 sm:p-4 gap-2" : "p-2 sm:p-4"),
388
395
  className
389
396
  )}
390
397
  {...props}
391
398
  >
392
- {sections.length > 1 && (
399
+ {sections.length > 1 && !hideMobileTabs && (
393
400
  <div className="flex-none pb-1">
394
401
  <Tabs
395
402
  value={currentActiveId}
@@ -88,7 +88,6 @@ export const DocumentTabBar = React.memo<DocumentTabBarProps>(
88
88
  onClick={onToggleExplorer}
89
89
  className="h-10 w-10 shrink-0 rounded-none border-r border-border/50 text-muted-foreground hover:text-foreground md:hidden"
90
90
  aria-label="Toggle file explorer"
91
- title="Toggle file explorer"
92
91
  >
93
92
  <Icon name="folder" size="xs" />
94
93
  </Button>
@@ -30,6 +30,15 @@ export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement>
30
30
  showActions?: boolean;
31
31
  showNewSession?: boolean;
32
32
  showDownloadSession?: boolean;
33
+ /**
34
+ * Handler to switch to editor/overview in mobile view
35
+ */
36
+ onOverview?: () => void;
37
+ /**
38
+ * Whether to show the overview button in the session header
39
+ * @default true
40
+ */
41
+ showOverview?: boolean;
33
42
  }
34
43
 
35
44
  /**
@@ -39,7 +48,7 @@ export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement>
39
48
  * and a dropdown history of past sessions.
40
49
  */
41
50
  export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps>(
42
- ({ title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, className, ...props }, ref) => {
51
+ ({ title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
43
52
  const activeSession = sessions?.find(s => s.id === activeSessionId);
44
53
  const downloadRef = React.useRef<HTMLAnchorElement>(null);
45
54
 
@@ -74,6 +83,17 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
74
83
  </div>
75
84
  {showActions && (
76
85
  <div className="flex items-center space-x-1 flex-none">
86
+ {Boolean(onOverview && showOverview !== false) && (
87
+ <Button
88
+ variant="ghost"
89
+ size="icon"
90
+ onClick={onOverview}
91
+ className="h-8 w-8 md:hidden"
92
+ aria-label="Overview"
93
+ >
94
+ <Icon name="overview" className="h-4 w-4" />
95
+ </Button>
96
+ )}
77
97
  {Boolean(onDownloadSession && showDownloadSession) && (
78
98
  <Button variant="ghost" size="icon" onClick={handleDownloadClick} className="h-8 w-8" title="Download Chat History">
79
99
  <Icon name="download" className="h-4 w-4" />
@@ -235,6 +235,15 @@ export interface ChatPanelProps {
235
235
  * @default true
236
236
  */
237
237
  showDownloadSession?: boolean;
238
+ /**
239
+ * Handler to switch to editor/overview in mobile view
240
+ */
241
+ onOverview?: () => void;
242
+ /**
243
+ * Whether to show the overview button in the session header
244
+ * @default true
245
+ */
246
+ showOverview?: boolean;
238
247
  /**
239
248
  * Presentation variant for ChatPanel:
240
249
  * - "default": standard full flex-1 panel (default)
@@ -298,6 +307,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
298
307
  showSessionActions = true,
299
308
  showNewSession = true,
300
309
  showDownloadSession = true,
310
+ onOverview,
311
+ showOverview = true,
301
312
  variant = "default",
302
313
  onClose,
303
314
  }) => {
@@ -460,6 +471,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
460
471
  showActions={showSessionActions}
461
472
  showNewSession={showNewSession}
462
473
  showDownloadSession={showDownloadSession}
474
+ onOverview={onOverview}
475
+ showOverview={showOverview}
463
476
  sessions={sessions}
464
477
  activeSessionId={activeSessionId}
465
478
  onNewSession={onNewSession}
package/dist/index.cjs CHANGED
@@ -1039,6 +1039,16 @@ var defaultIcons = {
1039
1039
  viewBox: "0 0 24 24",
1040
1040
  path: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6M16 13H8m8 4H8m8-8H8"
1041
1041
  },
1042
+ "overview": {
1043
+ name: "overview",
1044
+ viewBox: "0 0 24 24",
1045
+ elements: [
1046
+ { type: "rect", attrs: { x: "3", y: "3", width: "18", height: "18", rx: "4" } },
1047
+ { type: "line", attrs: { x1: "7", y1: "8", x2: "17", y2: "8" } },
1048
+ { type: "line", attrs: { x1: "7", y1: "12", x2: "17", y2: "12" } },
1049
+ { type: "line", attrs: { x1: "7", y1: "16", x2: "12", y2: "16" } }
1050
+ ]
1051
+ },
1042
1052
  "message-square": {
1043
1053
  name: "message-square",
1044
1054
  viewBox: "0 0 24 24",
@@ -2043,6 +2053,7 @@ var AdjustableLayout = React3__namespace.memo(
2043
2053
  className,
2044
2054
  padded = false,
2045
2055
  mobileBehavior = "tabs",
2056
+ hideMobileTabs = false,
2046
2057
  activeSectionId,
2047
2058
  onActiveSectionChange
2048
2059
  } = _b, props = __objRest(_b, [
@@ -2054,6 +2065,7 @@ var AdjustableLayout = React3__namespace.memo(
2054
2065
  "className",
2055
2066
  "padded",
2056
2067
  "mobileBehavior",
2068
+ "hideMobileTabs",
2057
2069
  "activeSectionId",
2058
2070
  "onActiveSectionChange"
2059
2071
  ]);
@@ -2314,12 +2326,12 @@ var AdjustableLayout = React3__namespace.memo(
2314
2326
  ref: containerRef,
2315
2327
  className: cn(
2316
2328
  "flex flex-col overflow-hidden h-full min-w-0 w-full",
2317
- padded && "p-2 sm:p-4 gap-2",
2329
+ padded && (!hideMobileTabs ? "p-2 sm:p-4 gap-2" : "p-2 sm:p-4"),
2318
2330
  className
2319
2331
  )
2320
2332
  }, props), {
2321
2333
  children: [
2322
- sections.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none pb-1", children: /* @__PURE__ */ jsxRuntime.jsx(
2334
+ sections.length > 1 && !hideMobileTabs && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none pb-1", children: /* @__PURE__ */ jsxRuntime.jsx(
2323
2335
  Tabs2,
2324
2336
  {
2325
2337
  value: currentActiveId,
@@ -2547,6 +2559,7 @@ var SectionLayout = React3__namespace.memo(
2547
2559
  className,
2548
2560
  padded = true,
2549
2561
  mobileBehavior = "tabs",
2562
+ hideMobileTabs = false,
2550
2563
  activeSectionId,
2551
2564
  onActiveSectionChange
2552
2565
  } = _b, props = __objRest(_b, [
@@ -2559,6 +2572,7 @@ var SectionLayout = React3__namespace.memo(
2559
2572
  "className",
2560
2573
  "padded",
2561
2574
  "mobileBehavior",
2575
+ "hideMobileTabs",
2562
2576
  "activeSectionId",
2563
2577
  "onActiveSectionChange"
2564
2578
  ]);
@@ -2584,6 +2598,7 @@ var SectionLayout = React3__namespace.memo(
2584
2598
  className,
2585
2599
  padded,
2586
2600
  mobileBehavior,
2601
+ hideMobileTabs,
2587
2602
  activeSectionId,
2588
2603
  onActiveSectionChange
2589
2604
  }, props)
@@ -8120,7 +8135,7 @@ var ApprovalCard = React3__namespace.memo(
8120
8135
  ApprovalCard.displayName = "ApprovalCard";
8121
8136
  var SessionHeader = React3__namespace.forwardRef(
8122
8137
  (_a, ref) => {
8123
- var _b = _a, { title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, className } = _b, props = __objRest(_b, ["title", "sessions", "activeSessionId", "onNewSession", "onCloseSession", "onSelectSession", "onDownloadSession", "showActions", "showNewSession", "showDownloadSession", "className"]);
8138
+ var _b = _a, { title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className } = _b, props = __objRest(_b, ["title", "sessions", "activeSessionId", "onNewSession", "onCloseSession", "onSelectSession", "onDownloadSession", "showActions", "showNewSession", "showDownloadSession", "onOverview", "showOverview", "className"]);
8124
8139
  const activeSession = sessions == null ? void 0 : sessions.find((s) => s.id === activeSessionId);
8125
8140
  const downloadRef = React3__namespace.useRef(null);
8126
8141
  const handleDownloadClick = React3__namespace.useCallback(async () => {
@@ -8146,6 +8161,17 @@ var SessionHeader = React3__namespace.forwardRef(
8146
8161
  children: [
8147
8162
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center space-x-2 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium truncate", children: displayTitle }) }),
8148
8163
  showActions && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-1 flex-none", children: [
8164
+ Boolean(onOverview && showOverview !== false) && /* @__PURE__ */ jsxRuntime.jsx(
8165
+ Button2,
8166
+ {
8167
+ variant: "ghost",
8168
+ size: "icon",
8169
+ onClick: onOverview,
8170
+ className: "h-8 w-8 md:hidden",
8171
+ "aria-label": "Overview",
8172
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "overview", className: "h-4 w-4" })
8173
+ }
8174
+ ),
8149
8175
  Boolean(onDownloadSession && showDownloadSession) && /* @__PURE__ */ jsxRuntime.jsx(Button2, { variant: "ghost", size: "icon", onClick: handleDownloadClick, className: "h-8 w-8", title: "Download Chat History", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "download", className: "h-4 w-4" }) }),
8150
8176
  Boolean(onNewSession && showNewSession) && /* @__PURE__ */ jsxRuntime.jsx(Button2, { variant: "ghost", size: "icon", onClick: onNewSession, className: "h-8 w-8", title: "New Session", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "h-4 w-4" }) }),
8151
8177
  /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu2, { children: [
@@ -8381,6 +8407,8 @@ var ChatPanel = React3__namespace.memo(
8381
8407
  showSessionActions = true,
8382
8408
  showNewSession = true,
8383
8409
  showDownloadSession = true,
8410
+ onOverview,
8411
+ showOverview = true,
8384
8412
  variant = "default",
8385
8413
  onClose
8386
8414
  }) => {
@@ -8508,6 +8536,8 @@ var ChatPanel = React3__namespace.memo(
8508
8536
  showActions: showSessionActions,
8509
8537
  showNewSession,
8510
8538
  showDownloadSession,
8539
+ onOverview,
8540
+ showOverview,
8511
8541
  sessions,
8512
8542
  activeSessionId,
8513
8543
  onNewSession,
@@ -11050,7 +11080,6 @@ var DocumentTabBar = React3__namespace.default.memo(
11050
11080
  onClick: onToggleExplorer,
11051
11081
  className: "h-10 w-10 shrink-0 rounded-none border-r border-border/50 text-muted-foreground hover:text-foreground md:hidden",
11052
11082
  "aria-label": "Toggle file explorer",
11053
- title: "Toggle file explorer",
11054
11083
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "folder", size: "xs" })
11055
11084
  }
11056
11085
  ),
@@ -16203,12 +16232,42 @@ function WorkflowCanvasInner({
16203
16232
  className
16204
16233
  }) {
16205
16234
  const { fitView } = react.useReactFlow();
16235
+ const containerRef = React3.useRef(null);
16236
+ const wasZeroSizeRef = React3.useRef(true);
16237
+ React3.useEffect(() => {
16238
+ const el = containerRef.current;
16239
+ if (!el || typeof ResizeObserver === "undefined") return;
16240
+ const observer = new ResizeObserver((entries) => {
16241
+ for (const entry of entries) {
16242
+ const { width, height } = entry.contentRect;
16243
+ if (width > 0 && height > 0) {
16244
+ if (wasZeroSizeRef.current) {
16245
+ wasZeroSizeRef.current = false;
16246
+ const timer = setTimeout(() => {
16247
+ fitView({ padding: 0.2, duration: 300 });
16248
+ }, 60);
16249
+ return () => clearTimeout(timer);
16250
+ }
16251
+ } else {
16252
+ wasZeroSizeRef.current = true;
16253
+ }
16254
+ }
16255
+ });
16256
+ observer.observe(el);
16257
+ return () => observer.disconnect();
16258
+ }, [fitView]);
16206
16259
  React3.useEffect(() => {
16207
16260
  if (fitViewSignal === void 0) return;
16208
- const timer = setTimeout(() => {
16261
+ const timer1 = setTimeout(() => {
16209
16262
  fitView({ padding: 0.2, duration: 300 });
16210
16263
  }, 50);
16211
- return () => clearTimeout(timer);
16264
+ const timer2 = setTimeout(() => {
16265
+ fitView({ padding: 0.2, duration: 300 });
16266
+ }, 200);
16267
+ return () => {
16268
+ clearTimeout(timer1);
16269
+ clearTimeout(timer2);
16270
+ };
16212
16271
  }, [fitViewSignal, fitView]);
16213
16272
  React3.useEffect(() => {
16214
16273
  const handleKeyDown = (event) => {
@@ -16231,6 +16290,7 @@ function WorkflowCanvasInner({
16231
16290
  return /* @__PURE__ */ jsxRuntime.jsx(
16232
16291
  "div",
16233
16292
  {
16293
+ ref: containerRef,
16234
16294
  className,
16235
16295
  "data-testid": "workflow-canvas",
16236
16296
  style: {