@agent-native/toolkit 0.9.1 → 0.10.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.
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +15 -1
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +26 -13
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.spec.js +77 -1
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -1
- package/dist/chat-history/index.d.ts +2 -1
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.js +36 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
- package/dist/chat-history.css +2 -0
- package/dist/composer/ComposerPlusMenu.js +1 -1
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts +1 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +26 -5
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/PromptComposer.spec.js +8 -1
- package/dist/composer/PromptComposer.spec.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +56 -11
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +59 -7
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +110 -6
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +74 -2
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
- package/dist/conformance/conformance.spec.d.ts +2 -0
- package/dist/conformance/conformance.spec.d.ts.map +1 -0
- package/dist/conformance/conformance.spec.js +87 -0
- package/dist/conformance/conformance.spec.js.map +1 -0
- package/dist/conformance/index.d.ts +4 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/conformance/runner.d.ts +12 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +515 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +26 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +13 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance/version.d.ts +6 -0
- package/dist/conformance/version.d.ts.map +1 -0
- package/dist/conformance/version.js +14 -0
- package/dist/conformance/version.js.map +1 -0
- package/dist/design-system/components.d.ts +64 -0
- package/dist/design-system/components.d.ts.map +1 -0
- package/dist/design-system/components.js +50 -0
- package/dist/design-system/components.js.map +1 -0
- package/dist/design-system/context.d.ts +11 -0
- package/dist/design-system/context.d.ts.map +1 -0
- package/dist/design-system/context.js +17 -0
- package/dist/design-system/context.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts +3 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -0
- package/dist/design-system/default-adapter.js +166 -0
- package/dist/design-system/default-adapter.js.map +1 -0
- package/dist/design-system/default-adapter.spec.d.ts +2 -0
- package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
- package/dist/design-system/default-adapter.spec.js +96 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -0
- package/dist/design-system/definition.d.ts +9 -0
- package/dist/design-system/definition.d.ts.map +1 -0
- package/dist/design-system/definition.js +4 -0
- package/dist/design-system/definition.js.map +1 -0
- package/dist/design-system/design-system.spec.d.ts +2 -0
- package/dist/design-system/design-system.spec.d.ts.map +1 -0
- package/dist/design-system/design-system.spec.js +121 -0
- package/dist/design-system/design-system.spec.js.map +1 -0
- package/dist/design-system/error-boundary.d.ts +18 -0
- package/dist/design-system/error-boundary.d.ts.map +1 -0
- package/dist/design-system/error-boundary.js +16 -0
- package/dist/design-system/error-boundary.js.map +1 -0
- package/dist/design-system/index.d.ts +8 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +8 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/theme.d.ts +30 -0
- package/dist/design-system/theme.d.ts.map +1 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/design-system/theme.js.map +1 -0
- package/dist/design-system/theme.spec.d.ts +2 -0
- package/dist/design-system/theme.spec.d.ts.map +1 -0
- package/dist/design-system/theme.spec.js +43 -0
- package/dist/design-system/theme.spec.js.map +1 -0
- package/dist/design-system/types.d.ts +288 -0
- package/dist/design-system/types.d.ts.map +1 -0
- package/dist/design-system/types.js +2 -0
- package/dist/design-system/types.js.map +1 -0
- package/dist/editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/editor/SharedRichEditor.js +3 -2
- package/dist/editor/SharedRichEditor.js.map +1 -1
- package/dist/editor/SlashCommandMenu.d.ts +8 -0
- package/dist/editor/SlashCommandMenu.d.ts.map +1 -1
- package/dist/editor/SlashCommandMenu.js +6 -0
- package/dist/editor/SlashCommandMenu.js.map +1 -1
- package/dist/editor/SlashCommandMenu.spec.d.ts +2 -0
- package/dist/editor/SlashCommandMenu.spec.d.ts.map +1 -0
- package/dist/editor/SlashCommandMenu.spec.js +20 -0
- package/dist/editor/SlashCommandMenu.spec.js.map +1 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/provider.d.ts +4 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -4
- package/dist/provider.js.map +1 -1
- package/dist/styles.css +52 -23
- package/dist/ui/button.d.ts +5 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +25 -4
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/dialog.d.ts +4 -0
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/popover.d.ts +1 -0
- package/dist/ui/popover.d.ts.map +1 -1
- package/dist/ui/popover.js +2 -2
- package/dist/ui/popover.js.map +1 -1
- package/dist/ui/select.d.ts +3 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/sidebar.d.ts +6 -2
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +12 -15
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/tooltip.d.ts +3 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +2 -2
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +31 -1
- package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
- package/src/chat-history/ChatHistoryRail.tsx +100 -39
- package/src/chat-history/index.ts +9 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +2 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.spec.ts +12 -1
- package/src/composer/PromptComposer.tsx +32 -7
- package/src/composer/RealtimeVoiceMode.spec.tsx +117 -9
- package/src/composer/RealtimeVoiceMode.tsx +119 -31
- package/src/composer/useRealtimeVoiceMode.spec.ts +101 -1
- package/src/composer/useRealtimeVoiceMode.tsx +124 -9
- package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
- package/src/conformance/conformance.spec.tsx +143 -0
- package/src/conformance/index.ts +3 -0
- package/src/conformance/runner.tsx +862 -0
- package/src/conformance/types.ts +45 -0
- package/src/conformance/version.ts +22 -0
- package/src/design-system/components.tsx +146 -0
- package/src/design-system/context.tsx +31 -0
- package/src/design-system/default-adapter.spec.tsx +143 -0
- package/src/design-system/default-adapter.tsx +915 -0
- package/src/design-system/definition.ts +14 -0
- package/src/design-system/design-system.spec.tsx +183 -0
- package/src/design-system/error-boundary.tsx +37 -0
- package/src/design-system/index.ts +7 -0
- package/src/design-system/theme.spec.ts +61 -0
- package/src/design-system/theme.ts +145 -0
- package/src/design-system/types.ts +363 -0
- package/src/editor/SharedRichEditor.tsx +13 -2
- package/src/editor/SlashCommandMenu.spec.ts +33 -0
- package/src/editor/SlashCommandMenu.tsx +20 -0
- package/src/editor/index.ts +2 -0
- package/src/provider.tsx +98 -5
- package/src/styles.css +52 -23
- package/src/ui/button.tsx +95 -6
- package/src/ui/dialog.tsx +48 -23
- package/src/ui/popover.tsx +16 -2
- package/src/ui/select.tsx +7 -3
- package/src/ui/sidebar.tsx +47 -48
- package/src/ui/tooltip.tsx +7 -3
package/dist/ui/tooltip.js
CHANGED
|
@@ -9,9 +9,9 @@ function normalizeTooltipText(text) {
|
|
|
9
9
|
const decoded = text.replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
10
10
|
return decoded.replace(/\b([A-Za-z][A-Za-z ]*?)\((?=(?:⌘|⌃|⌥|⇧|Ctrl|Alt|Shift|Cmd))/g, (_match, label) => `${label.charAt(0).toUpperCase()}${label.slice(1)} (`);
|
|
11
11
|
}
|
|
12
|
-
const TooltipContent = React.forwardRef(({ className, sideOffset = 6, children, ...props }, ref) => {
|
|
12
|
+
const TooltipContent = React.forwardRef(({ className, sideOffset = 6, children, container, ...props }, ref) => {
|
|
13
13
|
const normalizedChildren = typeof children === "string" ? normalizeTooltipText(children) : children;
|
|
14
|
-
return (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, "data-agent-native-tooltip": "true", className: cn("z-[300] overflow-hidden rounded-md border border-border bg-popover px-2 py-1 text-[11px] text-foreground shadow-md origin-[var(--radix-tooltip-content-transform-origin)] data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=delayed-open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=delayed-open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:duration-150 data-[state=closed]:duration-100 motion-reduce:data-[state=delayed-open]:zoom-in-100 motion-reduce:data-[state=closed]:zoom-out-100", className), ...props, children: normalizedChildren }) }));
|
|
14
|
+
return (_jsx(TooltipPrimitive.Portal, { container: container, children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, "data-agent-native-tooltip": "true", className: cn("z-[300] overflow-hidden rounded-md border border-border bg-popover px-2 py-1 text-[11px] text-foreground shadow-md origin-[var(--radix-tooltip-content-transform-origin)] data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=delayed-open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=delayed-open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:duration-150 data-[state=closed]:duration-100 motion-reduce:data-[state=delayed-open]:zoom-in-100 motion-reduce:data-[state=closed]:zoom-out-100", className), ...props, children: normalizedChildren }) }));
|
|
15
15
|
});
|
|
16
16
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
17
17
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, normalizeTooltipText, };
|
package/dist/ui/tooltip.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip.js","sourceRoot":"","sources":["../../src/ui/tooltip.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,gBAAgB,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,MAAM,eAAe,GAAG,gBAAgB,CAAC,QAAQ,CAAC;AAElD,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC;AAEtC,MAAM,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;AAEhD,SAAS,oBAAoB,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CACnE,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAC9C,CAAC;IACF,OAAO,OAAO,CAAC,OAAO,CACpB,8DAA8D,EAC9D,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzE,CAAC;AACJ,CAAC;AAED,MAAM,cAAc,GAAG,KAAK,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"tooltip.js","sourceRoot":"","sources":["../../src/ui/tooltip.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,gBAAgB,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,MAAM,eAAe,GAAG,gBAAgB,CAAC,QAAQ,CAAC;AAElD,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC;AAEtC,MAAM,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;AAEhD,SAAS,oBAAoB,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CACnE,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAC9C,CAAC;IACF,OAAO,OAAO,CAAC,OAAO,CACpB,8DAA8D,EAC9D,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzE,CAAC;AACJ,CAAC;AAED,MAAM,cAAc,GAAG,KAAK,CAAC,UAAU,CAOrC,CAAC,EAAE,SAAS,EAAE,UAAU,GAAG,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IACtE,MAAM,kBAAkB,GACtB,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC3E,OAAO,CACL,KAAC,gBAAgB,CAAC,MAAM,IAAC,SAAS,EAAE,SAAS,YAC3C,KAAC,gBAAgB,CAAC,OAAO,IACvB,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,UAAU,+BACI,MAAM,EAChC,SAAS,EAAE,EAAE,CACX,miBAAmiB,EACniB,SAAS,CACV,KACG,KAAK,YAER,kBAAkB,GACM,GACH,CAC3B,CAAC;AACJ,CAAC,CAAC,CAAC;AACH,cAAc,CAAC,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC;AAElE,OAAO,EACL,OAAO,EACP,cAAc,EACd,cAAc,EACd,eAAe,EACf,oBAAoB,GACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|
|
@@ -103,6 +103,20 @@
|
|
|
103
103
|
"import": "./dist/composer/*.js",
|
|
104
104
|
"default": "./dist/composer/*.js"
|
|
105
105
|
},
|
|
106
|
+
"./conformance": {
|
|
107
|
+
"types": "./dist/conformance/index.d.ts",
|
|
108
|
+
"browser": "./dist/conformance/index.js",
|
|
109
|
+
"development": "./dist/conformance/index.js",
|
|
110
|
+
"import": "./dist/conformance/index.js",
|
|
111
|
+
"default": "./dist/conformance/index.js"
|
|
112
|
+
},
|
|
113
|
+
"./conformance/*": {
|
|
114
|
+
"types": "./dist/conformance/*.d.ts",
|
|
115
|
+
"browser": "./dist/conformance/*.js",
|
|
116
|
+
"development": "./dist/conformance/*.js",
|
|
117
|
+
"import": "./dist/conformance/*.js",
|
|
118
|
+
"default": "./dist/conformance/*.js"
|
|
119
|
+
},
|
|
106
120
|
"./context-ui": {
|
|
107
121
|
"types": "./dist/context-ui/index.d.ts",
|
|
108
122
|
"browser": "./dist/context-ui/index.js",
|
|
@@ -117,6 +131,20 @@
|
|
|
117
131
|
"import": "./dist/design-tweaks/index.js",
|
|
118
132
|
"default": "./dist/design-tweaks/index.js"
|
|
119
133
|
},
|
|
134
|
+
"./design-system": {
|
|
135
|
+
"types": "./dist/design-system/index.d.ts",
|
|
136
|
+
"browser": "./dist/design-system/index.js",
|
|
137
|
+
"development": "./dist/design-system/index.js",
|
|
138
|
+
"import": "./dist/design-system/index.js",
|
|
139
|
+
"default": "./dist/design-system/index.js"
|
|
140
|
+
},
|
|
141
|
+
"./design-system/*": {
|
|
142
|
+
"types": "./dist/design-system/*.d.ts",
|
|
143
|
+
"browser": "./dist/design-system/*.js",
|
|
144
|
+
"development": "./dist/design-system/*.js",
|
|
145
|
+
"import": "./dist/design-system/*.js",
|
|
146
|
+
"default": "./dist/design-system/*.js"
|
|
147
|
+
},
|
|
120
148
|
"./editor": {
|
|
121
149
|
"types": "./dist/editor/index.d.ts",
|
|
122
150
|
"browser": "./dist/editor/index.js",
|
|
@@ -261,6 +289,7 @@
|
|
|
261
289
|
"class-variance-authority": "^0.7.1",
|
|
262
290
|
"clsx": "^2.1.1",
|
|
263
291
|
"cmdk": "^1.1.1",
|
|
292
|
+
"culori": "^4.0.2",
|
|
264
293
|
"date-fns": "^4.4.0",
|
|
265
294
|
"embla-carousel-react": "^8.6.0",
|
|
266
295
|
"highlight.js": "^11.11.1",
|
|
@@ -279,6 +308,7 @@
|
|
|
279
308
|
"yjs": "^13.6.31"
|
|
280
309
|
},
|
|
281
310
|
"devDependencies": {
|
|
311
|
+
"@types/culori": "^4.0.1",
|
|
282
312
|
"@types/react": "^19.2.14",
|
|
283
313
|
"@types/react-dom": "^19.2.3",
|
|
284
314
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
@@ -4,6 +4,9 @@ import { act } from "react";
|
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
+
import { ActionButton, IconButton } from "../design-system/components.js";
|
|
8
|
+
import { defineDesignSystem } from "../design-system/definition.js";
|
|
9
|
+
import { ToolkitProvider } from "../provider.js";
|
|
7
10
|
import type { ChatHistoryItem } from "./ChatHistoryList.js";
|
|
8
11
|
import { ChatHistoryRail } from "./ChatHistoryRail.js";
|
|
9
12
|
|
|
@@ -135,4 +138,175 @@ describe("ChatHistoryRail", () => {
|
|
|
135
138
|
act(() => rows[5]?.click());
|
|
136
139
|
expect(onSelect).toHaveBeenCalledWith("thread-6");
|
|
137
140
|
});
|
|
141
|
+
|
|
142
|
+
it("lets a design system replace the whole view without replacing its controller", () => {
|
|
143
|
+
const onNewChat = vi.fn();
|
|
144
|
+
const CustomActionButton = vi.fn(
|
|
145
|
+
({ children, onPress }: Parameters<typeof ActionButton>[0]) => (
|
|
146
|
+
<button
|
|
147
|
+
data-acme-action
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={(event) => onPress?.(event)}
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</button>
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const designSystem = defineDesignSystem({
|
|
156
|
+
name: "Acme",
|
|
157
|
+
components: { ActionButton: CustomActionButton },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
act(() => {
|
|
161
|
+
root.render(
|
|
162
|
+
<ToolkitProvider designSystem={designSystem}>
|
|
163
|
+
<ChatHistoryRail
|
|
164
|
+
items={makeItems(20)}
|
|
165
|
+
onSelect={() => {}}
|
|
166
|
+
onNewChat={onNewChat}
|
|
167
|
+
railLabels={railLabels}
|
|
168
|
+
renderRail={({ controller }) => (
|
|
169
|
+
<section data-acme-rail>
|
|
170
|
+
<output>{controller.visibleItems.length}</output>
|
|
171
|
+
<ActionButton onPress={controller.onNewChat}>
|
|
172
|
+
{controller.newChatLabel}
|
|
173
|
+
</ActionButton>
|
|
174
|
+
<ActionButton onPress={controller.toggleExpanded}>
|
|
175
|
+
{controller.disclosureLabel}
|
|
176
|
+
</ActionButton>
|
|
177
|
+
</section>
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
</ToolkitProvider>,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const actions =
|
|
185
|
+
container.querySelectorAll<HTMLButtonElement>("[data-acme-action]");
|
|
186
|
+
expect(container.querySelector("[data-acme-rail]")).not.toBeNull();
|
|
187
|
+
expect(container.querySelector("output")?.textContent).toBe("5");
|
|
188
|
+
expect(actions[1]?.textContent).toBe("Show more chats");
|
|
189
|
+
|
|
190
|
+
act(() => actions[1]?.click());
|
|
191
|
+
expect(container.querySelector("output")?.textContent).toBe("15");
|
|
192
|
+
expect(actions[1]?.textContent).toBe("Show fewer chats");
|
|
193
|
+
|
|
194
|
+
act(() => actions[0]?.click());
|
|
195
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
196
|
+
expect(CustomActionButton).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("routes the default footer through the semantic action bridge", () => {
|
|
200
|
+
const onNewChat = vi.fn();
|
|
201
|
+
const CustomActionButton = vi.fn(
|
|
202
|
+
({ children, onPress }: Parameters<typeof ActionButton>[0]) => (
|
|
203
|
+
<button
|
|
204
|
+
data-semantic-action
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={(event) => onPress?.(event)}
|
|
207
|
+
>
|
|
208
|
+
{children}
|
|
209
|
+
</button>
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
const CustomIconButton = vi.fn(
|
|
213
|
+
({ icon, label, onPress }: Parameters<typeof IconButton>[0]) => (
|
|
214
|
+
<button
|
|
215
|
+
data-semantic-icon
|
|
216
|
+
type="button"
|
|
217
|
+
aria-label={label}
|
|
218
|
+
onClick={(event) => onPress?.(event)}
|
|
219
|
+
>
|
|
220
|
+
{icon}
|
|
221
|
+
</button>
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
const designSystem = defineDesignSystem({
|
|
225
|
+
name: "Acme",
|
|
226
|
+
components: {
|
|
227
|
+
ActionButton: CustomActionButton,
|
|
228
|
+
IconButton: CustomIconButton,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
root.render(
|
|
234
|
+
<ToolkitProvider designSystem={designSystem}>
|
|
235
|
+
<ChatHistoryRail
|
|
236
|
+
items={makeItems(6)}
|
|
237
|
+
onSelect={() => {}}
|
|
238
|
+
onNewChat={onNewChat}
|
|
239
|
+
railLabels={railLabels}
|
|
240
|
+
/>
|
|
241
|
+
</ToolkitProvider>,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const newChat = container.querySelector<HTMLButtonElement>(
|
|
246
|
+
"[data-semantic-action]",
|
|
247
|
+
);
|
|
248
|
+
const disclosure = container.querySelector<HTMLButtonElement>(
|
|
249
|
+
"[data-semantic-icon]",
|
|
250
|
+
);
|
|
251
|
+
expect(newChat).not.toBeNull();
|
|
252
|
+
expect(disclosure?.getAttribute("aria-label")).toBe("Show more chats");
|
|
253
|
+
expect(CustomActionButton.mock.calls[0]?.[0]).toMatchObject({
|
|
254
|
+
emphasis: "ghost",
|
|
255
|
+
size: "compact",
|
|
256
|
+
leadingIcon: expect.anything(),
|
|
257
|
+
});
|
|
258
|
+
expect(CustomIconButton.mock.calls[0]?.[0]).toMatchObject({
|
|
259
|
+
size: "compact",
|
|
260
|
+
"aria-expanded": false,
|
|
261
|
+
label: "Show more chats",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
act(() => newChat?.click());
|
|
265
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps native button semantics and focus in the default view", () => {
|
|
269
|
+
const onNewChat = vi.fn();
|
|
270
|
+
act(() => {
|
|
271
|
+
root.render(
|
|
272
|
+
<ChatHistoryRail
|
|
273
|
+
items={makeItems(6)}
|
|
274
|
+
onSelect={() => {}}
|
|
275
|
+
onNewChat={onNewChat}
|
|
276
|
+
railLabels={railLabels}
|
|
277
|
+
/>,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const newChat = container.querySelector<HTMLButtonElement>(
|
|
282
|
+
".an-chat-history-rail__new-chat",
|
|
283
|
+
);
|
|
284
|
+
newChat?.focus();
|
|
285
|
+
act(() => newChat?.click());
|
|
286
|
+
|
|
287
|
+
expect(newChat?.type).toBe("button");
|
|
288
|
+
expect(document.activeElement).toBe(newChat);
|
|
289
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("falls back to the default rail when a product renderer fails", () => {
|
|
293
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
294
|
+
act(() => {
|
|
295
|
+
root.render(
|
|
296
|
+
<ChatHistoryRail
|
|
297
|
+
items={makeItems(6)}
|
|
298
|
+
onSelect={() => {}}
|
|
299
|
+
onNewChat={() => {}}
|
|
300
|
+
railLabels={railLabels}
|
|
301
|
+
renderRail={() => {
|
|
302
|
+
throw new Error("broken company rail");
|
|
303
|
+
}}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(
|
|
309
|
+
container.querySelector(".an-chat-history-rail__new-chat"),
|
|
310
|
+
).not.toBeNull();
|
|
311
|
+
});
|
|
138
312
|
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { IconDots, IconPlus } from "@tabler/icons-react";
|
|
2
|
-
import {
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import { ActionButton, IconButton } from "../design-system/components.js";
|
|
5
|
+
import { DesignSystemErrorBoundary } from "../design-system/error-boundary.js";
|
|
4
6
|
import {
|
|
5
7
|
ChatHistoryList,
|
|
6
8
|
type ChatHistoryItem,
|
|
7
9
|
type ChatHistoryListProps,
|
|
8
10
|
} from "./ChatHistoryList.js";
|
|
11
|
+
import {
|
|
12
|
+
type ChatHistoryRailController,
|
|
13
|
+
useChatHistoryRailController,
|
|
14
|
+
} from "./useChatHistoryRailController.js";
|
|
9
15
|
|
|
10
16
|
export interface ChatHistoryRailLabels {
|
|
11
17
|
newChat: string;
|
|
@@ -22,63 +28,71 @@ export interface ChatHistoryRailProps extends Omit<
|
|
|
22
28
|
railLabels: ChatHistoryRailLabels;
|
|
23
29
|
previewCount?: number;
|
|
24
30
|
expandedCount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Product-level presentation slot for design systems that want to replace
|
|
33
|
+
* the rail wholesale. State and actions still come from the same controller
|
|
34
|
+
* as the default view.
|
|
35
|
+
*/
|
|
36
|
+
renderRail?: (context: ChatHistoryRailRenderContext) => ReactNode;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
);
|
|
39
|
+
export interface ChatHistoryRailRenderContext {
|
|
40
|
+
controller: ChatHistoryRailController<ChatHistoryItem>;
|
|
41
|
+
listProps: Omit<
|
|
42
|
+
ChatHistoryListProps,
|
|
43
|
+
"footer" | "items" | "sections" | "variant"
|
|
44
|
+
>;
|
|
45
|
+
}
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
export type DefaultChatHistoryRailViewProps = ChatHistoryRailRenderContext;
|
|
48
|
+
|
|
49
|
+
export function DefaultChatHistoryRailView({
|
|
50
|
+
controller,
|
|
51
|
+
listProps,
|
|
52
|
+
}: ChatHistoryRailRenderContext) {
|
|
53
|
+
const {
|
|
54
|
+
canExpand,
|
|
55
|
+
disclosureLabel,
|
|
56
|
+
expanded,
|
|
57
|
+
newChatLabel,
|
|
58
|
+
onNewChat,
|
|
59
|
+
toggleExpanded,
|
|
60
|
+
visibleItems,
|
|
61
|
+
} = controller;
|
|
62
|
+
const { className, emptyLabel, ...rest } = listProps;
|
|
53
63
|
|
|
54
64
|
const footer = (
|
|
55
65
|
<div className="an-chat-history-rail__footer">
|
|
56
|
-
<
|
|
66
|
+
<ActionButton
|
|
57
67
|
type="button"
|
|
58
68
|
className="an-chat-history-rail__new-chat"
|
|
59
|
-
|
|
69
|
+
emphasis="ghost"
|
|
70
|
+
size="compact"
|
|
71
|
+
leadingIcon={
|
|
72
|
+
<IconPlus size={13} strokeWidth={1.8} aria-hidden="true" />
|
|
73
|
+
}
|
|
74
|
+
onPress={onNewChat}
|
|
60
75
|
>
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
</button>
|
|
76
|
+
<span>{newChatLabel}</span>
|
|
77
|
+
</ActionButton>
|
|
64
78
|
{canExpand && (
|
|
65
|
-
<
|
|
79
|
+
<IconButton
|
|
66
80
|
type="button"
|
|
67
81
|
className="an-chat-history-rail__disclosure"
|
|
68
|
-
|
|
82
|
+
size="compact"
|
|
83
|
+
icon={<IconDots size={14} strokeWidth={1.8} aria-hidden="true" />}
|
|
84
|
+
onPress={toggleExpanded}
|
|
69
85
|
aria-expanded={expanded}
|
|
70
|
-
|
|
71
|
-
title={
|
|
72
|
-
|
|
73
|
-
<IconDots size={14} strokeWidth={1.8} aria-hidden="true" />
|
|
74
|
-
</button>
|
|
86
|
+
label={disclosureLabel}
|
|
87
|
+
title={disclosureLabel}
|
|
88
|
+
/>
|
|
75
89
|
)}
|
|
76
90
|
</div>
|
|
77
91
|
);
|
|
78
92
|
|
|
79
93
|
return (
|
|
80
94
|
<ChatHistoryList
|
|
81
|
-
{...
|
|
95
|
+
{...rest}
|
|
82
96
|
items={visibleItems}
|
|
83
97
|
footer={footer}
|
|
84
98
|
emptyLabel={emptyLabel ?? null}
|
|
@@ -87,3 +101,50 @@ export function ChatHistoryRail({
|
|
|
87
101
|
/>
|
|
88
102
|
);
|
|
89
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compact recent-chat rail for app sidebars. Hosts own thread persistence,
|
|
107
|
+
* sorting, routing, and mutations; the rail only owns progressive disclosure.
|
|
108
|
+
*/
|
|
109
|
+
export function ChatHistoryRail({
|
|
110
|
+
items,
|
|
111
|
+
onNewChat,
|
|
112
|
+
railLabels,
|
|
113
|
+
previewCount = 5,
|
|
114
|
+
expandedCount = 15,
|
|
115
|
+
renderRail,
|
|
116
|
+
className,
|
|
117
|
+
emptyLabel,
|
|
118
|
+
...listProps
|
|
119
|
+
}: ChatHistoryRailProps) {
|
|
120
|
+
const controller = useChatHistoryRailController({
|
|
121
|
+
items,
|
|
122
|
+
onNewChat,
|
|
123
|
+
labels: railLabels,
|
|
124
|
+
previewCount,
|
|
125
|
+
expandedCount,
|
|
126
|
+
});
|
|
127
|
+
const context: ChatHistoryRailRenderContext = {
|
|
128
|
+
controller,
|
|
129
|
+
listProps: { ...listProps, className, emptyLabel },
|
|
130
|
+
};
|
|
131
|
+
const fallback = <DefaultChatHistoryRailView {...context} />;
|
|
132
|
+
|
|
133
|
+
return renderRail ? (
|
|
134
|
+
<DesignSystemErrorBoundary component="ChatHistoryRail" fallback={fallback}>
|
|
135
|
+
<ChatHistoryRailCustomView renderRail={renderRail} context={context} />
|
|
136
|
+
</DesignSystemErrorBoundary>
|
|
137
|
+
) : (
|
|
138
|
+
fallback
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ChatHistoryRailCustomView({
|
|
143
|
+
renderRail,
|
|
144
|
+
context,
|
|
145
|
+
}: {
|
|
146
|
+
renderRail: NonNullable<ChatHistoryRailProps["renderRail"]>;
|
|
147
|
+
context: ChatHistoryRailRenderContext;
|
|
148
|
+
}) {
|
|
149
|
+
return renderRail(context);
|
|
150
|
+
}
|
|
@@ -7,6 +7,15 @@ export {
|
|
|
7
7
|
} from "./ChatHistoryList.js";
|
|
8
8
|
export {
|
|
9
9
|
ChatHistoryRail,
|
|
10
|
+
DefaultChatHistoryRailView,
|
|
11
|
+
type DefaultChatHistoryRailViewProps,
|
|
10
12
|
type ChatHistoryRailLabels,
|
|
11
13
|
type ChatHistoryRailProps,
|
|
14
|
+
type ChatHistoryRailRenderContext,
|
|
12
15
|
} from "./ChatHistoryRail.js";
|
|
16
|
+
export {
|
|
17
|
+
type ChatHistoryRailController,
|
|
18
|
+
type ChatHistoryRailControllerLabels,
|
|
19
|
+
type UseChatHistoryRailControllerOptions,
|
|
20
|
+
useChatHistoryRailController,
|
|
21
|
+
} from "./useChatHistoryRailController.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
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 {
|
|
8
|
+
type ChatHistoryRailController,
|
|
9
|
+
type UseChatHistoryRailControllerOptions,
|
|
10
|
+
useChatHistoryRailController,
|
|
11
|
+
} from "./useChatHistoryRailController.js";
|
|
12
|
+
|
|
13
|
+
interface Item {
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const labels = {
|
|
18
|
+
newChat: "New chat",
|
|
19
|
+
showMore: "Show more chats",
|
|
20
|
+
showLess: "Show fewer chats",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function makeItems(count: number): Item[] {
|
|
24
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
25
|
+
id: `thread-${index + 1}`,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("useChatHistoryRailController", () => {
|
|
30
|
+
let container: HTMLDivElement;
|
|
31
|
+
let root: Root;
|
|
32
|
+
let controller: ChatHistoryRailController<Item>;
|
|
33
|
+
|
|
34
|
+
function Harness(props: UseChatHistoryRailControllerOptions<Item>) {
|
|
35
|
+
controller = useChatHistoryRailController(props);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(options: Partial<UseChatHistoryRailControllerOptions<Item>>) {
|
|
40
|
+
const props: UseChatHistoryRailControllerOptions<Item> = {
|
|
41
|
+
items: makeItems(20),
|
|
42
|
+
labels,
|
|
43
|
+
onNewChat: () => {},
|
|
44
|
+
...options,
|
|
45
|
+
};
|
|
46
|
+
act(() => root.render(<Harness {...props} />));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
51
|
+
container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
root = createRoot(container);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
act(() => root.unmount());
|
|
58
|
+
container.remove();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("progressively discloses the default five and fifteen item limits", () => {
|
|
64
|
+
const items = makeItems(20);
|
|
65
|
+
render({ items });
|
|
66
|
+
|
|
67
|
+
expect(controller.expanded).toBe(false);
|
|
68
|
+
expect(controller.collapsedLimit).toBe(5);
|
|
69
|
+
expect(controller.expandedLimit).toBe(15);
|
|
70
|
+
expect(controller.visibleItems).toEqual(items.slice(0, 5));
|
|
71
|
+
expect(controller.canExpand).toBe(true);
|
|
72
|
+
expect(controller.disclosureLabel).toBe("Show more chats");
|
|
73
|
+
|
|
74
|
+
act(() => controller.toggleExpanded());
|
|
75
|
+
|
|
76
|
+
expect(controller.expanded).toBe(true);
|
|
77
|
+
expect(controller.visibleItems).toEqual(items.slice(0, 15));
|
|
78
|
+
expect(controller.disclosureLabel).toBe("Show fewer chats");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("clamps finite limits while preserving legacy disclosure behavior", () => {
|
|
82
|
+
render({ previewCount: -2.5, expandedCount: 1.9 });
|
|
83
|
+
|
|
84
|
+
expect(controller.collapsedLimit).toBe(1);
|
|
85
|
+
expect(controller.expandedLimit).toBe(1);
|
|
86
|
+
expect(controller.visibleItems).toHaveLength(1);
|
|
87
|
+
expect(controller.canExpand).toBe(true);
|
|
88
|
+
|
|
89
|
+
act(() => controller.toggleExpanded());
|
|
90
|
+
expect(controller.expanded).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("uses defaults for non-finite limits and never puts expanded below preview", () => {
|
|
94
|
+
render({
|
|
95
|
+
previewCount: Number.NaN,
|
|
96
|
+
expandedCount: Number.POSITIVE_INFINITY,
|
|
97
|
+
});
|
|
98
|
+
expect(controller.collapsedLimit).toBe(5);
|
|
99
|
+
expect(controller.expandedLimit).toBe(15);
|
|
100
|
+
|
|
101
|
+
render({ previewCount: 8, expandedCount: 3 });
|
|
102
|
+
expect(controller.collapsedLimit).toBe(8);
|
|
103
|
+
expect(controller.expandedLimit).toBe(8);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("resets disclosure explicitly and when the collection becomes too short", () => {
|
|
107
|
+
render({});
|
|
108
|
+
act(() => controller.toggleExpanded());
|
|
109
|
+
expect(controller.expanded).toBe(true);
|
|
110
|
+
|
|
111
|
+
act(() => controller.resetExpanded());
|
|
112
|
+
expect(controller.expanded).toBe(false);
|
|
113
|
+
|
|
114
|
+
act(() => controller.toggleExpanded());
|
|
115
|
+
expect(controller.expanded).toBe(true);
|
|
116
|
+
|
|
117
|
+
render({ items: makeItems(3) });
|
|
118
|
+
expect(controller.expanded).toBe(false);
|
|
119
|
+
expect(controller.canExpand).toBe(false);
|
|
120
|
+
expect(controller.visibleItems).toHaveLength(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("exposes view labels and the host new-chat action without UI assumptions", () => {
|
|
124
|
+
const onNewChat = vi.fn();
|
|
125
|
+
render({ onNewChat });
|
|
126
|
+
|
|
127
|
+
expect(controller.newChatLabel).toBe("New chat");
|
|
128
|
+
act(() => controller.onNewChat());
|
|
129
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ChatHistoryRailControllerLabels {
|
|
4
|
+
newChat: string;
|
|
5
|
+
showMore: string;
|
|
6
|
+
showLess: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseChatHistoryRailControllerOptions<Item> {
|
|
10
|
+
items: readonly Item[];
|
|
11
|
+
onNewChat: () => void;
|
|
12
|
+
labels: ChatHistoryRailControllerLabels;
|
|
13
|
+
previewCount?: number;
|
|
14
|
+
expandedCount?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChatHistoryRailController<Item> {
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
collapsedLimit: number;
|
|
20
|
+
expandedLimit: number;
|
|
21
|
+
visibleItems: Item[];
|
|
22
|
+
canExpand: boolean;
|
|
23
|
+
newChatLabel: string;
|
|
24
|
+
disclosureLabel: string;
|
|
25
|
+
onNewChat: () => void;
|
|
26
|
+
toggleExpanded: () => void;
|
|
27
|
+
resetExpanded: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeLimit(value: number | undefined, fallback: number): number {
|
|
31
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
32
|
+
return Math.max(1, Math.floor(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useChatHistoryRailController<Item>({
|
|
36
|
+
items,
|
|
37
|
+
onNewChat,
|
|
38
|
+
labels,
|
|
39
|
+
previewCount,
|
|
40
|
+
expandedCount,
|
|
41
|
+
}: UseChatHistoryRailControllerOptions<Item>): ChatHistoryRailController<Item> {
|
|
42
|
+
const [expandedState, setExpandedState] = useState(false);
|
|
43
|
+
const collapsedLimit = normalizeLimit(previewCount, 5);
|
|
44
|
+
const expandedLimit = Math.max(
|
|
45
|
+
collapsedLimit,
|
|
46
|
+
normalizeLimit(expandedCount, 15),
|
|
47
|
+
);
|
|
48
|
+
const canExpand = items.length > collapsedLimit;
|
|
49
|
+
const expanded = canExpand && expandedState;
|
|
50
|
+
const visibleItems = useMemo(
|
|
51
|
+
() => items.slice(0, expanded ? expandedLimit : collapsedLimit),
|
|
52
|
+
[collapsedLimit, expanded, expandedLimit, items],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!canExpand) setExpandedState(false);
|
|
57
|
+
}, [canExpand]);
|
|
58
|
+
|
|
59
|
+
const toggleExpanded = useCallback(() => {
|
|
60
|
+
if (canExpand) setExpandedState((current) => !current);
|
|
61
|
+
}, [canExpand]);
|
|
62
|
+
const resetExpanded = useCallback(() => setExpandedState(false), []);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
expanded,
|
|
66
|
+
collapsedLimit,
|
|
67
|
+
expandedLimit,
|
|
68
|
+
visibleItems,
|
|
69
|
+
canExpand,
|
|
70
|
+
newChatLabel: labels.newChat,
|
|
71
|
+
disclosureLabel: expanded ? labels.showLess : labels.showMore,
|
|
72
|
+
onNewChat,
|
|
73
|
+
toggleExpanded,
|
|
74
|
+
resetExpanded,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/chat-history.css
CHANGED
|
@@ -113,12 +113,14 @@
|
|
|
113
113
|
justify-content: center;
|
|
114
114
|
flex: 0 0 auto;
|
|
115
115
|
width: 30px;
|
|
116
|
+
height: auto;
|
|
116
117
|
min-height: 30px;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
.an-chat-history-rail__new-chat {
|
|
120
121
|
flex: 1 1 auto;
|
|
121
122
|
gap: 8px;
|
|
123
|
+
height: auto;
|
|
122
124
|
min-width: 0;
|
|
123
125
|
min-height: 30px;
|
|
124
126
|
padding: 6px 10px;
|