@agent-native/toolkit 0.8.3 → 0.9.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.
Files changed (99) hide show
  1. package/README.md +42 -3
  2. package/agent-native.eject.json +33 -0
  3. package/dist/chat-history/ChatHistoryRail.d.ts +19 -0
  4. package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -0
  5. package/dist/chat-history/ChatHistoryRail.js +22 -0
  6. package/dist/chat-history/ChatHistoryRail.js.map +1 -0
  7. package/dist/chat-history/ChatHistoryRail.spec.d.ts +2 -0
  8. package/dist/chat-history/ChatHistoryRail.spec.d.ts.map +1 -0
  9. package/dist/chat-history/ChatHistoryRail.spec.js +81 -0
  10. package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -0
  11. package/dist/chat-history/index.d.ts +1 -0
  12. package/dist/chat-history/index.d.ts.map +1 -1
  13. package/dist/chat-history/index.js +1 -0
  14. package/dist/chat-history/index.js.map +1 -1
  15. package/dist/chat-history.css +59 -0
  16. package/dist/composer/RealtimeVoiceMode.d.ts +5 -1
  17. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  18. package/dist/composer/RealtimeVoiceMode.js +27 -8
  19. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  20. package/dist/composer/RealtimeVoiceMode.spec.js +51 -11
  21. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
  22. package/dist/composer/VoiceButton.d.ts +2 -0
  23. package/dist/composer/VoiceButton.d.ts.map +1 -1
  24. package/dist/composer/VoiceButton.js +33 -1
  25. package/dist/composer/VoiceButton.js.map +1 -1
  26. package/dist/composer/VoiceButton.spec.js +9 -1
  27. package/dist/composer/VoiceButton.spec.js.map +1 -1
  28. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -1
  29. package/dist/composer/realtime-voice-audio-level.js +2 -1
  30. package/dist/composer/realtime-voice-audio-level.js.map +1 -1
  31. package/dist/composer/realtime-voice-audio-level.spec.js +7 -0
  32. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -1
  33. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  34. package/dist/composer/useRealtimeVoiceMode.js +4 -4
  35. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  36. package/dist/dashboard/DataTable.d.ts +20 -0
  37. package/dist/dashboard/DataTable.d.ts.map +1 -0
  38. package/dist/dashboard/DataTable.js +87 -0
  39. package/dist/dashboard/DataTable.js.map +1 -0
  40. package/dist/dashboard/DateRangePicker.d.ts +15 -0
  41. package/dist/dashboard/DateRangePicker.d.ts.map +1 -0
  42. package/dist/dashboard/DateRangePicker.js +15 -0
  43. package/dist/dashboard/DateRangePicker.js.map +1 -0
  44. package/dist/dashboard/GenericChartPanel.d.ts +57 -0
  45. package/dist/dashboard/GenericChartPanel.d.ts.map +1 -0
  46. package/dist/dashboard/GenericChartPanel.js +119 -0
  47. package/dist/dashboard/GenericChartPanel.js.map +1 -0
  48. package/dist/dashboard/GenericChartPanel.spec.d.ts +2 -0
  49. package/dist/dashboard/GenericChartPanel.spec.d.ts.map +1 -0
  50. package/dist/dashboard/GenericChartPanel.spec.js +41 -0
  51. package/dist/dashboard/GenericChartPanel.spec.js.map +1 -0
  52. package/dist/dashboard/MetricCard.d.ts +14 -0
  53. package/dist/dashboard/MetricCard.d.ts.map +1 -0
  54. package/dist/dashboard/MetricCard.js +10 -0
  55. package/dist/dashboard/MetricCard.js.map +1 -0
  56. package/dist/dashboard/StatsCard.d.ts +16 -0
  57. package/dist/dashboard/StatsCard.d.ts.map +1 -0
  58. package/dist/dashboard/StatsCard.js +8 -0
  59. package/dist/dashboard/StatsCard.js.map +1 -0
  60. package/dist/dashboard/dashboard-layout.d.ts +49 -0
  61. package/dist/dashboard/dashboard-layout.d.ts.map +1 -0
  62. package/dist/dashboard/dashboard-layout.js +231 -0
  63. package/dist/dashboard/dashboard-layout.js.map +1 -0
  64. package/dist/dashboard/dashboard-layout.spec.d.ts +2 -0
  65. package/dist/dashboard/dashboard-layout.spec.d.ts.map +1 -0
  66. package/dist/dashboard/dashboard-layout.spec.js +52 -0
  67. package/dist/dashboard/dashboard-layout.spec.js.map +1 -0
  68. package/dist/dashboard/index.d.ts +7 -0
  69. package/dist/dashboard/index.d.ts.map +1 -0
  70. package/dist/dashboard/index.js +7 -0
  71. package/dist/dashboard/index.js.map +1 -0
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +1 -0
  75. package/dist/index.js.map +1 -1
  76. package/dist/styles.css +35 -0
  77. package/package.json +15 -1
  78. package/src/chat-history/ChatHistoryRail.spec.tsx +138 -0
  79. package/src/chat-history/ChatHistoryRail.tsx +89 -0
  80. package/src/chat-history/index.ts +5 -0
  81. package/src/chat-history.css +59 -0
  82. package/src/composer/RealtimeVoiceMode.spec.tsx +116 -11
  83. package/src/composer/RealtimeVoiceMode.tsx +106 -36
  84. package/src/composer/VoiceButton.spec.ts +17 -1
  85. package/src/composer/VoiceButton.tsx +49 -0
  86. package/src/composer/realtime-voice-audio-level.spec.ts +15 -0
  87. package/src/composer/realtime-voice-audio-level.ts +2 -1
  88. package/src/composer/useRealtimeVoiceMode.tsx +5 -3
  89. package/src/dashboard/DataTable.tsx +273 -0
  90. package/src/dashboard/DateRangePicker.tsx +57 -0
  91. package/src/dashboard/GenericChartPanel.spec.tsx +53 -0
  92. package/src/dashboard/GenericChartPanel.tsx +461 -0
  93. package/src/dashboard/MetricCard.tsx +54 -0
  94. package/src/dashboard/StatsCard.tsx +49 -0
  95. package/src/dashboard/dashboard-layout.spec.ts +79 -0
  96. package/src/dashboard/dashboard-layout.ts +329 -0
  97. package/src/dashboard/index.ts +6 -0
  98. package/src/index.ts +1 -0
  99. package/src/styles.css +35 -0
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildDashboardPanelGroups, clampDashboardColumns, columnExpansionForDropSlot, movePanelToDropSlot, removePanelFromLayout, } from "./dashboard-layout.js";
3
+ function panel(id, width = 1) {
4
+ return { id, width, kind: "chart" };
5
+ }
6
+ const options = { isSection: (item) => item.kind === "section" };
7
+ describe("dashboard layout", () => {
8
+ it("groups a section and balances persisted widths per visible row", () => {
9
+ const panels = [
10
+ panel("a"),
11
+ { id: "section", kind: "section", columns: 3 },
12
+ panel("b"),
13
+ panel("c"),
14
+ panel("d"),
15
+ ];
16
+ const groups = buildDashboardPanelGroups(panels, 2, options);
17
+ expect(groups.map((group) => group.rows.map((row) => row.panels.map((item) => item.id)))).toEqual([[["a"]], [["b", "c", "d"]]]);
18
+ expect(groups[1].columns).toBe(3);
19
+ });
20
+ it("removes without backfilling a later row", () => {
21
+ const next = removePanelFromLayout(["a", "b", "c", "d", "e"].map((id) => panel(id)), "b", 3, options);
22
+ expect(next.map((item) => [item.id, item.width])).toEqual([
23
+ ["a", 2],
24
+ ["c", 1],
25
+ ["d", 2],
26
+ ["e", 1],
27
+ ]);
28
+ });
29
+ it("moves into a column slot and grows the target group as needed", () => {
30
+ const panels = ["a", "b", "c"].map((id) => panel(id));
31
+ const slot = {
32
+ type: "column",
33
+ groupKey: "intro",
34
+ rowIndex: 0,
35
+ columnIndex: 1,
36
+ };
37
+ const groups = buildDashboardPanelGroups(panels, 2, options);
38
+ expect(columnExpansionForDropSlot(groups, "c", slot)).toEqual({
39
+ columns: 3,
40
+ sectionPanelId: null,
41
+ });
42
+ const next = movePanelToDropSlot(panels, "c", slot, 2, options);
43
+ expect(next.map((item) => item.id)).toEqual(["a", "c", "b"]);
44
+ expect(next.map((item) => item.width)).toEqual([1, 1, 1]);
45
+ });
46
+ it("clamps malformed and out-of-range column counts", () => {
47
+ expect(clampDashboardColumns("4")).toBe(2);
48
+ expect(clampDashboardColumns(-3)).toBe(1);
49
+ expect(clampDashboardColumns(99)).toBe(6);
50
+ });
51
+ });
52
+ //# sourceMappingURL=dashboard-layout.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-layout.spec.js","sourceRoot":"","sources":["../../src/dashboard/dashboard-layout.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export * from "./DataTable.js";
2
+ export * from "./DateRangePicker.js";
3
+ export * from "./GenericChartPanel.js";
4
+ export * from "./MetricCard.js";
5
+ export * from "./StatsCard.js";
6
+ export * from "./dashboard-layout.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export * from "./DataTable.js";
2
+ export * from "./DateRangePicker.js";
3
+ export * from "./GenericChartPanel.js";
4
+ export * from "./MetricCard.js";
5
+ export * from "./StatsCard.js";
6
+ export * from "./dashboard-layout.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./app-shell/index.js";
2
2
  export * from "./collab-ui/index.js";
3
+ export * from "./dashboard/index.js";
3
4
  export * from "./hooks/index.js";
4
5
  export * from "./onboarding/index.js";
5
6
  export * from "./provider.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./app-shell/index.js";
2
2
  export * from "./collab-ui/index.js";
3
+ export * from "./dashboard/index.js";
3
4
  export * from "./hooks/index.js";
4
5
  export * from "./onboarding/index.js";
5
6
  export * from "./provider.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
package/dist/styles.css CHANGED
@@ -19,3 +19,38 @@
19
19
  * compiled `.js` next to it for consumers that only install `dist/`.
20
20
  */
21
21
  @source "./**/*.{js,ts,tsx}";
22
+
23
+ .agent-realtime-voice-working::after {
24
+ position: absolute;
25
+ z-index: 0;
26
+ inset: 0;
27
+ border-radius: inherit;
28
+ pointer-events: none;
29
+ content: "";
30
+ }
31
+
32
+ @media (prefers-reduced-motion: no-preference) {
33
+ .agent-realtime-voice-working::after {
34
+ background: linear-gradient(
35
+ 90deg,
36
+ transparent 0%,
37
+ transparent 36%,
38
+ hsl(var(--foreground) / 0.14) 50%,
39
+ transparent 64%,
40
+ transparent 100%
41
+ );
42
+ background-repeat: repeat;
43
+ background-size: 14rem 100%;
44
+ animation: agent-realtime-voice-shine 2.6s linear infinite;
45
+ }
46
+
47
+ @keyframes agent-realtime-voice-shine {
48
+ from {
49
+ background-position: 0 0;
50
+ }
51
+
52
+ to {
53
+ background-position: 14rem 0;
54
+ }
55
+ }
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/toolkit",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Reusable app-building UI and helpers for Agent-Native apps.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -61,6 +61,20 @@
61
61
  "import": "./dist/collab-ui/*.js",
62
62
  "default": "./dist/collab-ui/*.js"
63
63
  },
64
+ "./dashboard": {
65
+ "types": "./dist/dashboard/index.d.ts",
66
+ "browser": "./dist/dashboard/index.js",
67
+ "development": "./dist/dashboard/index.js",
68
+ "import": "./dist/dashboard/index.js",
69
+ "default": "./dist/dashboard/index.js"
70
+ },
71
+ "./dashboard/*": {
72
+ "types": "./dist/dashboard/*.d.ts",
73
+ "browser": "./dist/dashboard/*.js",
74
+ "development": "./dist/dashboard/*.js",
75
+ "import": "./dist/dashboard/*.js",
76
+ "default": "./dist/dashboard/*.js"
77
+ },
64
78
  "./chat-history": {
65
79
  "types": "./dist/chat-history/index.d.ts",
66
80
  "browser": "./dist/chat-history/index.js",
@@ -0,0 +1,138 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import type { ChatHistoryItem } from "./ChatHistoryList.js";
8
+ import { ChatHistoryRail } from "./ChatHistoryRail.js";
9
+
10
+ const railLabels = {
11
+ newChat: "New chat",
12
+ showMore: "Show more chats",
13
+ showLess: "Show fewer chats",
14
+ };
15
+
16
+ function makeItems(count: number): ChatHistoryItem[] {
17
+ return Array.from({ length: count }, (_, index) => ({
18
+ id: `thread-${index + 1}`,
19
+ title: `Chat ${index + 1}`,
20
+ }));
21
+ }
22
+
23
+ describe("ChatHistoryRail", () => {
24
+ let container: HTMLDivElement;
25
+ let root: Root;
26
+
27
+ beforeEach(() => {
28
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
29
+ container = document.createElement("div");
30
+ document.body.appendChild(container);
31
+ root = createRoot(container);
32
+ });
33
+
34
+ afterEach(() => {
35
+ act(() => root.unmount());
36
+ container.remove();
37
+ vi.restoreAllMocks();
38
+ vi.unstubAllGlobals();
39
+ });
40
+
41
+ it("shows five recent chats before progressively disclosing up to fifteen", () => {
42
+ act(() => {
43
+ root.render(
44
+ <ChatHistoryRail
45
+ items={makeItems(20)}
46
+ onSelect={() => {}}
47
+ onNewChat={() => {}}
48
+ railLabels={railLabels}
49
+ />,
50
+ );
51
+ });
52
+
53
+ expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
54
+ const disclosure = container.querySelector<HTMLButtonElement>(
55
+ ".an-chat-history-rail__disclosure",
56
+ );
57
+ expect(disclosure?.getAttribute("aria-label")).toBe("Show more chats");
58
+
59
+ act(() => disclosure?.click());
60
+ expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(15);
61
+ expect(disclosure?.getAttribute("aria-label")).toBe("Show fewer chats");
62
+
63
+ act(() => disclosure?.click());
64
+ expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
65
+ });
66
+
67
+ it("keeps the disclosure to the right of new chat and calls its handler", () => {
68
+ const onNewChat = vi.fn();
69
+ act(() => {
70
+ root.render(
71
+ <ChatHistoryRail
72
+ items={makeItems(6)}
73
+ onSelect={() => {}}
74
+ onNewChat={onNewChat}
75
+ railLabels={railLabels}
76
+ />,
77
+ );
78
+ });
79
+
80
+ const disclosure = container.querySelector<HTMLButtonElement>(
81
+ ".an-chat-history-rail__disclosure",
82
+ );
83
+ const newChat = container.querySelector<HTMLButtonElement>(
84
+ ".an-chat-history-rail__new-chat",
85
+ );
86
+ expect(newChat?.textContent).toBe("New chat");
87
+ expect(newChat?.nextElementSibling).toBe(disclosure);
88
+ expect(newChat?.parentElement?.lastElementChild).toBe(disclosure);
89
+
90
+ act(() => newChat?.click());
91
+ expect(onNewChat).toHaveBeenCalledOnce();
92
+ });
93
+
94
+ it("lets new chat fill the footer when there are no more chats", () => {
95
+ act(() => {
96
+ root.render(
97
+ <ChatHistoryRail
98
+ items={makeItems(3)}
99
+ onSelect={() => {}}
100
+ onNewChat={() => {}}
101
+ railLabels={railLabels}
102
+ />,
103
+ );
104
+ });
105
+
106
+ expect(
107
+ container.querySelector(".an-chat-history-rail__disclosure"),
108
+ ).toBeNull();
109
+ expect(
110
+ container.querySelector(".an-chat-history-rail__new-chat"),
111
+ ).not.toBeNull();
112
+ });
113
+
114
+ it("preserves list actions for progressively disclosed rows", () => {
115
+ const onSelect = vi.fn();
116
+ act(() => {
117
+ root.render(
118
+ <ChatHistoryRail
119
+ items={makeItems(6)}
120
+ onSelect={onSelect}
121
+ onNewChat={() => {}}
122
+ railLabels={railLabels}
123
+ />,
124
+ );
125
+ });
126
+
127
+ act(() => {
128
+ container
129
+ .querySelector<HTMLButtonElement>(".an-chat-history-rail__disclosure")
130
+ ?.click();
131
+ });
132
+ const rows = container.querySelectorAll<HTMLButtonElement>(
133
+ ".an-chat-history-row__button",
134
+ );
135
+ act(() => rows[5]?.click());
136
+ expect(onSelect).toHaveBeenCalledWith("thread-6");
137
+ });
138
+ });
@@ -0,0 +1,89 @@
1
+ import { IconDots, IconPlus } from "@tabler/icons-react";
2
+ import { useEffect, useMemo, useState } from "react";
3
+
4
+ import {
5
+ ChatHistoryList,
6
+ type ChatHistoryItem,
7
+ type ChatHistoryListProps,
8
+ } from "./ChatHistoryList.js";
9
+
10
+ export interface ChatHistoryRailLabels {
11
+ newChat: string;
12
+ showMore: string;
13
+ showLess: string;
14
+ }
15
+
16
+ export interface ChatHistoryRailProps extends Omit<
17
+ ChatHistoryListProps,
18
+ "footer" | "items" | "sections" | "variant"
19
+ > {
20
+ items: ChatHistoryItem[];
21
+ onNewChat: () => void;
22
+ railLabels: ChatHistoryRailLabels;
23
+ previewCount?: number;
24
+ expandedCount?: number;
25
+ }
26
+
27
+ /**
28
+ * Compact recent-chat rail for app sidebars. Hosts own thread persistence,
29
+ * sorting, routing, and mutations; the rail only owns progressive disclosure.
30
+ */
31
+ export function ChatHistoryRail({
32
+ items,
33
+ onNewChat,
34
+ railLabels,
35
+ previewCount = 5,
36
+ expandedCount = 15,
37
+ className,
38
+ emptyLabel,
39
+ ...listProps
40
+ }: ChatHistoryRailProps) {
41
+ const [expanded, setExpanded] = useState(false);
42
+ const collapsedLimit = Math.max(1, previewCount);
43
+ const expandedLimit = Math.max(collapsedLimit, expandedCount);
44
+ const canExpand = items.length > collapsedLimit;
45
+ const visibleItems = useMemo(
46
+ () => items.slice(0, expanded ? expandedLimit : collapsedLimit),
47
+ [collapsedLimit, expanded, expandedLimit, items],
48
+ );
49
+
50
+ useEffect(() => {
51
+ if (!canExpand) setExpanded(false);
52
+ }, [canExpand]);
53
+
54
+ const footer = (
55
+ <div className="an-chat-history-rail__footer">
56
+ <button
57
+ type="button"
58
+ className="an-chat-history-rail__new-chat"
59
+ onClick={onNewChat}
60
+ >
61
+ <IconPlus size={13} strokeWidth={1.8} aria-hidden="true" />
62
+ <span>{railLabels.newChat}</span>
63
+ </button>
64
+ {canExpand && (
65
+ <button
66
+ type="button"
67
+ className="an-chat-history-rail__disclosure"
68
+ onClick={() => setExpanded((current) => !current)}
69
+ aria-expanded={expanded}
70
+ aria-label={expanded ? railLabels.showLess : railLabels.showMore}
71
+ title={expanded ? railLabels.showLess : railLabels.showMore}
72
+ >
73
+ <IconDots size={14} strokeWidth={1.8} aria-hidden="true" />
74
+ </button>
75
+ )}
76
+ </div>
77
+ );
78
+
79
+ return (
80
+ <ChatHistoryList
81
+ {...listProps}
82
+ items={visibleItems}
83
+ footer={footer}
84
+ emptyLabel={emptyLabel ?? null}
85
+ variant="rail"
86
+ className={["an-chat-history-rail", className].filter(Boolean).join(" ")}
87
+ />
88
+ );
89
+ }
@@ -5,3 +5,8 @@ export {
5
5
  type ChatHistoryListProps,
6
6
  type ChatHistorySection,
7
7
  } from "./ChatHistoryList.js";
8
+ export {
9
+ ChatHistoryRail,
10
+ type ChatHistoryRailLabels,
11
+ type ChatHistoryRailProps,
12
+ } from "./ChatHistoryRail.js";
@@ -16,6 +16,7 @@
16
16
  .an-chat-history {
17
17
  display: flex;
18
18
  flex-direction: column;
19
+ min-width: 0;
19
20
  min-height: 0;
20
21
  }
21
22
 
@@ -49,6 +50,7 @@
49
50
 
50
51
  .an-chat-history__list {
51
52
  overflow-y: auto;
53
+ min-width: 0;
52
54
  padding: 4px 0;
53
55
  max-height: 16rem;
54
56
  }
@@ -59,6 +61,10 @@
59
61
  min-height: 0;
60
62
  }
61
63
 
64
+ .an-chat-history-rail .an-chat-history__state:empty {
65
+ display: none;
66
+ }
67
+
62
68
  .an-chat-history__state {
63
69
  padding: 16px 12px;
64
70
  text-align: center;
@@ -72,6 +78,55 @@
72
78
  color: hsl(38 92% 50%);
73
79
  }
74
80
 
81
+ .an-chat-history-rail__footer {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 2px;
85
+ min-width: 0;
86
+ padding: 1px 4px 0;
87
+ }
88
+
89
+ .an-chat-history-rail__disclosure,
90
+ .an-chat-history-rail__new-chat {
91
+ display: flex;
92
+ align-items: center;
93
+ border: 0;
94
+ border-radius: calc(var(--radius) - 2px);
95
+ background: transparent;
96
+ color: hsl(var(--muted-foreground) / 0.72);
97
+ cursor: pointer;
98
+ }
99
+
100
+ .an-chat-history-rail__disclosure:hover,
101
+ .an-chat-history-rail__new-chat:hover {
102
+ background: hsl(var(--accent) / 0.5);
103
+ color: hsl(var(--accent-foreground));
104
+ }
105
+
106
+ .an-chat-history-rail__disclosure:focus-visible,
107
+ .an-chat-history-rail__new-chat:focus-visible {
108
+ outline: 2px solid hsl(var(--ring));
109
+ outline-offset: -2px;
110
+ }
111
+
112
+ .an-chat-history-rail__disclosure {
113
+ justify-content: center;
114
+ flex: 0 0 auto;
115
+ width: 30px;
116
+ min-height: 30px;
117
+ }
118
+
119
+ .an-chat-history-rail__new-chat {
120
+ flex: 1 1 auto;
121
+ gap: 8px;
122
+ min-width: 0;
123
+ min-height: 30px;
124
+ padding: 6px 10px;
125
+ font-size: 12px;
126
+ font-weight: 500;
127
+ text-align: start;
128
+ }
129
+
75
130
  .an-chat-history__section-label {
76
131
  padding: 6px 12px 4px;
77
132
  font-size: 10px;
@@ -83,12 +138,14 @@
83
138
  .an-chat-history--rail .an-chat-history__section {
84
139
  display: grid;
85
140
  gap: 1px;
141
+ min-width: 0;
86
142
  }
87
143
 
88
144
  /* ── Row ──────────────────────────────────────────────────────────────── */
89
145
 
90
146
  .an-chat-history-row {
91
147
  position: relative;
148
+ min-width: 0;
92
149
  }
93
150
 
94
151
  .an-chat-history--rail .an-chat-history-row {
@@ -99,6 +156,7 @@
99
156
  .an-chat-history-row__button {
100
157
  display: block;
101
158
  width: 100%;
159
+ min-width: 0;
102
160
  padding: 8px 12px;
103
161
  border: 0;
104
162
  background: transparent;
@@ -141,6 +199,7 @@
141
199
  justify-content: space-between;
142
200
  gap: 8px;
143
201
  min-width: 0;
202
+ overflow: hidden;
144
203
  }
145
204
 
146
205
  .an-chat-history-row__title {