@agent-native/toolkit 0.212.1 → 0.214.0

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 (209) hide show
  1. package/agent-native.eject.json +9 -0
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +10 -4
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +13 -1
  6. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/context.js +3 -1
  8. package/dist/app/agentkit/react/context.js.map +1 -1
  9. package/dist/app/agentkit/react/index.d.ts +1 -0
  10. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/index.js +1 -0
  12. package/dist/app/agentkit/react/index.js.map +1 -1
  13. package/dist/app/agentkit/react/message-content-order.d.ts +21 -0
  14. package/dist/app/agentkit/react/message-content-order.d.ts.map +1 -0
  15. package/dist/app/agentkit/react/message-content-order.js +173 -0
  16. package/dist/app/agentkit/react/message-content-order.js.map +1 -0
  17. package/dist/app/agentkit/react/root.d.ts +3 -1
  18. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  19. package/dist/app/agentkit/react/root.js +2 -2
  20. package/dist/app/agentkit/react/root.js.map +1 -1
  21. package/dist/app/agentkit/react/styles.css +2 -2
  22. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  23. package/dist/app/chat/AgentKitAssistantChat.js +7 -2
  24. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  25. package/dist/app/chat/AgentPanel.d.ts +14 -2
  26. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  27. package/dist/app/chat/AgentPanel.js +22 -5
  28. package/dist/app/chat/AgentPanel.js.map +1 -1
  29. package/dist/app/chat/chat/surface-types.d.ts +8 -0
  30. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  31. package/dist/app/chat/chat/surface-types.js.map +1 -1
  32. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  33. package/dist/app/chat/chat/widgets/DataChartRenderer.js +127 -30
  34. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  35. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts +3 -2
  36. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  37. package/dist/app/chat/chat/widgets/DataChartWidget.js +14 -9
  38. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  39. package/dist/app/chat/chat/widgets/DataInsightsWidget.js +1 -1
  40. package/dist/app/chat/chat/widgets/DataInsightsWidget.js.map +1 -1
  41. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts +3 -2
  42. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts.map +1 -1
  43. package/dist/app/chat/chat/widgets/DataTableWidget.js +10 -66
  44. package/dist/app/chat/chat/widgets/DataTableWidget.js.map +1 -1
  45. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +11 -0
  46. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -0
  47. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +131 -0
  48. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -0
  49. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js +2 -2
  50. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js.map +1 -1
  51. package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  52. package/dist/app/chat/mcp-apps/McpAppRenderer.js +6 -5
  53. package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
  54. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts +2 -0
  55. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts.map +1 -0
  56. package/dist/app/chat/mcp-apps/mcp-app-theme.js +11 -0
  57. package/dist/app/chat/mcp-apps/mcp-app-theme.js.map +1 -0
  58. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  59. package/dist/app/i18n/catalogs/ar-SA.js +10 -0
  60. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  61. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  62. package/dist/app/i18n/catalogs/de-DE.js +10 -0
  63. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  64. package/dist/app/i18n/catalogs/en-US.d.ts +10 -0
  65. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  66. package/dist/app/i18n/catalogs/en-US.js +10 -0
  67. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  68. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  69. package/dist/app/i18n/catalogs/es-ES.js +10 -0
  70. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  71. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  72. package/dist/app/i18n/catalogs/fr-FR.js +10 -0
  73. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  74. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  75. package/dist/app/i18n/catalogs/hi-IN.js +10 -0
  76. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  77. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  78. package/dist/app/i18n/catalogs/ja-JP.js +10 -0
  79. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  80. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  81. package/dist/app/i18n/catalogs/ko-KR.js +10 -0
  82. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  83. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  84. package/dist/app/i18n/catalogs/pt-BR.js +10 -0
  85. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  86. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  87. package/dist/app/i18n/catalogs/zh-CN.js +10 -0
  88. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  89. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  90. package/dist/app/i18n/catalogs/zh-TW.js +10 -0
  91. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  92. package/dist/app/org/AuthenticationSection.d.ts +3 -0
  93. package/dist/app/org/AuthenticationSection.d.ts.map +1 -1
  94. package/dist/app/org/AuthenticationSection.js +9 -3
  95. package/dist/app/org/AuthenticationSection.js.map +1 -1
  96. package/dist/app/org/pages/OrgAuthenticationPage.d.ts.map +1 -1
  97. package/dist/app/org/pages/OrgAuthenticationPage.js +2 -2
  98. package/dist/app/org/pages/OrgAuthenticationPage.js.map +1 -1
  99. package/dist/app/providers/AppProviders.d.ts +2 -1
  100. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  101. package/dist/app/providers/AppProviders.js +4 -3
  102. package/dist/app/providers/AppProviders.js.map +1 -1
  103. package/dist/app/providers/ThemeFamilySync.d.ts +2 -0
  104. package/dist/app/providers/ThemeFamilySync.d.ts.map +1 -0
  105. package/dist/app/providers/ThemeFamilySync.js +6 -0
  106. package/dist/app/providers/ThemeFamilySync.js.map +1 -0
  107. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  108. package/dist/app/settings/account/PreferencesSettings.js +2 -1
  109. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  110. package/dist/app/settings/account/ThemeFamilySettings.d.ts +2 -0
  111. package/dist/app/settings/account/ThemeFamilySettings.d.ts.map +1 -0
  112. package/dist/app/settings/account/ThemeFamilySettings.js +31 -0
  113. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -0
  114. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  115. package/dist/app/settings/account/search-entries.js +6 -0
  116. package/dist/app/settings/account/search-entries.js.map +1 -1
  117. package/dist/app/shared/DefaultSpinner.d.ts.map +1 -1
  118. package/dist/app/shared/DefaultSpinner.js +4 -5
  119. package/dist/app/shared/DefaultSpinner.js.map +1 -1
  120. package/dist/app/shared/ErrorBoundary.d.ts +1 -0
  121. package/dist/app/shared/ErrorBoundary.d.ts.map +1 -1
  122. package/dist/app/shared/ErrorBoundary.js +27 -12
  123. package/dist/app/shared/ErrorBoundary.js.map +1 -1
  124. package/dist/app/shared/PoweredByBadge.d.ts.map +1 -1
  125. package/dist/app/shared/PoweredByBadge.js +8 -49
  126. package/dist/app/shared/PoweredByBadge.js.map +1 -1
  127. package/dist/app/styles/agent-conversation.css +0 -23
  128. package/dist/app/styles/agent-native.css +1 -1
  129. package/dist/app/styles/theme-families.css +66 -0
  130. package/dist/composer/ComposerContextBanner.d.ts +22 -0
  131. package/dist/composer/ComposerContextBanner.d.ts.map +1 -0
  132. package/dist/composer/ComposerContextBanner.js +9 -0
  133. package/dist/composer/ComposerContextBanner.js.map +1 -0
  134. package/dist/composer/ComposerModePicker.d.ts +26 -0
  135. package/dist/composer/ComposerModePicker.d.ts.map +1 -0
  136. package/dist/composer/ComposerModePicker.js +14 -0
  137. package/dist/composer/ComposerModePicker.js.map +1 -0
  138. package/dist/composer/index.d.ts +2 -0
  139. package/dist/composer/index.d.ts.map +1 -1
  140. package/dist/composer/index.js +2 -0
  141. package/dist/composer/index.js.map +1 -1
  142. package/dist/styles.css +1 -0
  143. package/package.json +3 -3
  144. package/src/app/agentkit/react/components.spec.tsx +12 -0
  145. package/src/app/agentkit/react/components.tsx +41 -22
  146. package/src/app/agentkit/react/context.tsx +17 -0
  147. package/src/app/agentkit/react/index.ts +1 -0
  148. package/src/app/agentkit/react/lifecycle.spec.tsx +116 -1
  149. package/src/app/agentkit/react/message-content-order.spec.ts +190 -0
  150. package/src/app/agentkit/react/message-content-order.ts +246 -0
  151. package/src/app/agentkit/react/root.tsx +4 -0
  152. package/src/app/agentkit/react/styles.css +2 -2
  153. package/src/app/chat/AgentKitAssistantChat.spec.tsx +19 -0
  154. package/src/app/chat/AgentKitAssistantChat.tsx +10 -1
  155. package/src/app/chat/AgentPanel.header.spec.ts +72 -14
  156. package/src/app/chat/AgentPanel.tsx +97 -53
  157. package/src/app/chat/chat/surface-types.ts +8 -0
  158. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +217 -0
  159. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +306 -68
  160. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +329 -0
  161. package/src/app/chat/chat/widgets/DataChartWidget.ssr.spec.tsx +25 -0
  162. package/src/app/chat/chat/widgets/DataChartWidget.tsx +48 -33
  163. package/src/app/chat/chat/widgets/DataInsightsWidget.tsx +2 -2
  164. package/src/app/chat/chat/widgets/DataTableWidget.spec.tsx +37 -3
  165. package/src/app/chat/chat/widgets/DataTableWidget.tsx +22 -86
  166. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +301 -0
  167. package/src/app/chat/chat/widgets/builtin-tool-renderers.tsx +5 -5
  168. package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -5
  169. package/src/app/chat/mcp-apps/mcp-app-theme.spec.ts +30 -0
  170. package/src/app/chat/mcp-apps/mcp-app-theme.ts +11 -0
  171. package/src/app/i18n/catalogs/ar-SA.ts +12 -0
  172. package/src/app/i18n/catalogs/de-DE.ts +13 -0
  173. package/src/app/i18n/catalogs/en-US.ts +12 -0
  174. package/src/app/i18n/catalogs/es-ES.ts +14 -0
  175. package/src/app/i18n/catalogs/fr-FR.ts +14 -0
  176. package/src/app/i18n/catalogs/hi-IN.ts +12 -0
  177. package/src/app/i18n/catalogs/ja-JP.ts +12 -0
  178. package/src/app/i18n/catalogs/ko-KR.ts +12 -0
  179. package/src/app/i18n/catalogs/pt-BR.ts +13 -0
  180. package/src/app/i18n/catalogs/zh-CN.ts +11 -0
  181. package/src/app/i18n/catalogs/zh-TW.ts +12 -0
  182. package/src/app/org/AuthenticationSection.spec.tsx +62 -0
  183. package/src/app/org/AuthenticationSection.tsx +47 -1
  184. package/src/app/org/pages/OrgAuthenticationPage.tsx +4 -0
  185. package/src/app/org/pages/OrgSettingsPages.spec.tsx +3 -0
  186. package/src/app/providers/AppProviders.spec.tsx +71 -0
  187. package/src/app/providers/AppProviders.tsx +10 -0
  188. package/src/app/providers/ThemeFamilySync.tsx +6 -0
  189. package/src/app/settings/account/PreferencesSettings.spec.tsx +13 -2
  190. package/src/app/settings/account/PreferencesSettings.tsx +8 -0
  191. package/src/app/settings/account/ThemeFamilySettings.tsx +61 -0
  192. package/src/app/settings/account/search-entries.ts +6 -0
  193. package/src/app/shared/DefaultSpinner.spec.tsx +14 -0
  194. package/src/app/shared/DefaultSpinner.tsx +4 -5
  195. package/src/app/shared/ErrorBoundary.spec.tsx +45 -2
  196. package/src/app/shared/ErrorBoundary.tsx +37 -12
  197. package/src/app/shared/PoweredByBadge.spec.tsx +16 -0
  198. package/src/app/shared/PoweredByBadge.tsx +8 -50
  199. package/src/app/styles/agent-conversation.css +0 -23
  200. package/src/app/styles/agent-native.css +1 -1
  201. package/src/app/styles/agent-native.spec.ts +18 -0
  202. package/src/app/styles/theme-families.css +66 -0
  203. package/src/app/styles/theme-families.spec.ts +29 -0
  204. package/src/composer/ComposerContextBanner.spec.tsx +74 -0
  205. package/src/composer/ComposerContextBanner.tsx +72 -0
  206. package/src/composer/ComposerModePicker.spec.tsx +101 -0
  207. package/src/composer/ComposerModePicker.tsx +98 -0
  208. package/src/composer/index.ts +9 -0
  209. package/src/styles.css +1 -0
@@ -25,6 +25,9 @@ vi.mock("../VoiceTranscriptionSection.js", () => ({
25
25
  <div data-testid="voice" data-compact={String(!!compact)} />
26
26
  ),
27
27
  }));
28
+ vi.mock("./ThemeFamilySettings.js", () => ({
29
+ ThemeFamilySettings: () => <div data-testid="theme-family-settings" />,
30
+ }));
28
31
 
29
32
  import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
30
33
 
@@ -70,12 +73,14 @@ describe("PreferencesSettings", () => {
70
73
  const groups = [...container.querySelectorAll("section")];
71
74
  expect(groups.map((group) => group.id)).toEqual([
72
75
  "language-region",
76
+ "appearance",
73
77
  "voice-input",
74
78
  ]);
75
79
  expect(groups[0]!.querySelector("h2")?.textContent).toBe(
76
80
  "Language and region",
77
81
  );
78
- expect(groups[1]!.querySelector("h2")?.textContent).toBe("Voice input");
82
+ expect(groups[1]!.querySelector("h2")?.textContent).toBe("Appearance");
83
+ expect(groups[2]!.querySelector("h2")?.textContent).toBe("Voice input");
79
84
 
80
85
  const language = container.querySelector("#interface-language");
81
86
  expect(language?.textContent).toContain("Interface language");
@@ -90,8 +95,14 @@ describe("PreferencesSettings", () => {
90
95
  "Used for timestamps and scheduled automations.",
91
96
  );
92
97
 
98
+ const themeFamily = container.querySelector("#theme-family");
99
+ expect(themeFamily?.textContent).toContain("Theme family");
100
+ expect(
101
+ themeFamily?.querySelector('[data-testid="theme-family-settings"]'),
102
+ ).not.toBeNull();
103
+
93
104
  expect(
94
- groups[1]!
105
+ groups[2]!
95
106
  .querySelector('[data-testid="voice"]')
96
107
  ?.getAttribute("data-compact"),
97
108
  ).toBe("true");
@@ -4,6 +4,7 @@ import { LanguagePicker } from "../../shared/index.js";
4
4
  import { SchedulingTimezoneField } from "../SchedulingTimezoneField.js";
5
5
  import { SettingsGroup, SettingsRow } from "../SettingsRow.js";
6
6
  import { VoiceTranscriptionSection } from "../VoiceTranscriptionSection.js";
7
+ import { ThemeFamilySettings } from "./ThemeFamilySettings.js";
7
8
 
8
9
  const key = (name: string) => `agentChat.settingsShell.account.${name}`;
9
10
 
@@ -37,6 +38,13 @@ export function PreferencesSettings() {
37
38
  control={<SchedulingTimezoneField compact />}
38
39
  />
39
40
  </SettingsGroup>
41
+ <SettingsGroup id="appearance" title={t(key("appearance"))}>
42
+ <SettingsRow
43
+ id="theme-family"
44
+ label={t(key("themeFamily"))}
45
+ control={<ThemeFamilySettings />}
46
+ />
47
+ </SettingsGroup>
40
48
  <SettingsGroup id="voice-input" title={t(key("voiceInput"))}>
41
49
  <VoiceTranscriptionSection compact />
42
50
  </SettingsGroup>
@@ -0,0 +1,61 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { useThemeFamily, type ThemeFamily } from "@agent-native/core/client/ui";
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@agent-native/toolkit/ui/select";
10
+ import { useState } from "react";
11
+
12
+ const key = (name: string) => `agentChat.settingsShell.account.${name}`;
13
+
14
+ export function ThemeFamilySettings() {
15
+ const t = useT();
16
+ const { family, status, setFamily } = useThemeFamily();
17
+ const [saving, setSaving] = useState(false);
18
+ const [saveFailed, setSaveFailed] = useState(false);
19
+
20
+ async function changeFamily(value: string) {
21
+ if (value !== "agent-native" && value !== "original") return;
22
+ if (value === family) return;
23
+
24
+ setSaving(true);
25
+ setSaveFailed(false);
26
+ try {
27
+ await setFamily(value as ThemeFamily);
28
+ } catch {
29
+ setSaveFailed(true);
30
+ } finally {
31
+ setSaving(false);
32
+ }
33
+ }
34
+
35
+ return (
36
+ <div className="w-full sm:w-56">
37
+ <Select
38
+ value={family}
39
+ onValueChange={(value) => void changeFamily(value)}
40
+ disabled={saving || status === "loading"}
41
+ >
42
+ <SelectTrigger aria-label={t(key("themeFamily"))}>
43
+ <SelectValue />
44
+ </SelectTrigger>
45
+ <SelectContent>
46
+ <SelectItem value="agent-native">
47
+ {t(key("themeFamilyAgentNative"))}
48
+ </SelectItem>
49
+ <SelectItem value="original">
50
+ {t(key("themeFamilyOriginal"))}
51
+ </SelectItem>
52
+ </SelectContent>
53
+ </Select>
54
+ {saveFailed || status === "error" ? (
55
+ <p className="mt-2 text-xs text-destructive" role="alert">
56
+ {t(key("themeFamilySaveError"))}
57
+ </p>
58
+ ) : null}
59
+ </div>
60
+ );
61
+ }
@@ -33,6 +33,12 @@ export const PREFERENCES_SEARCH_ENTRIES: readonly SettingsPageSearchEntry[] = [
33
33
  keywords: "language locale translation",
34
34
  anchor: "interface-language",
35
35
  },
36
+ {
37
+ id: "theme-family",
38
+ labelKey: key("themeFamily"),
39
+ keywords: "theme appearance style colors",
40
+ anchor: "theme-family",
41
+ },
36
42
  {
37
43
  id: "timezone",
38
44
  labelKey: key("timezone"),
@@ -0,0 +1,14 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { DefaultSpinner } from "./DefaultSpinner.js";
5
+
6
+ describe("DefaultSpinner theme styles", () => {
7
+ it("uses the selected dark theme instead of the operating system preference", () => {
8
+ const html = renderToStaticMarkup(<DefaultSpinner />);
9
+
10
+ expect(html).toContain("html.dark");
11
+ expect(html).toContain('html[data-theme="dark"]');
12
+ expect(html).not.toContain("prefers-color-scheme");
13
+ });
14
+ });
@@ -117,11 +117,10 @@ export function DefaultSpinner({
117
117
  background: hsl(var(--background, 0 0% 100%));
118
118
  color: hsl(var(--foreground, 240 10% 3.9%));
119
119
  }
120
- @media (prefers-color-scheme: dark) {
121
- html {
122
- background: hsl(var(--background, 240 10% 3.9%));
123
- color: hsl(var(--foreground, 0 0% 98%));
124
- }
120
+ html.dark,
121
+ html[data-theme="dark"]:not(.light) {
122
+ background: hsl(var(--background, 240 10% 3.9%));
123
+ color: hsl(var(--foreground, 0 0% 98%));
125
124
  }
126
125
  `}</style>
127
126
  </div>
@@ -1,6 +1,18 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { isExpectedRouteNotFound } from "./ErrorBoundary.js";
3
+ import {
4
+ isExpectedRouteNotFound,
5
+ resolveErrorBoundaryThemeClass,
6
+ } from "./ErrorBoundary.js";
7
+
8
+ function themeRoot(className: string, dataTheme: string | null) {
9
+ return {
10
+ classList: {
11
+ contains: (name: string) => className.split(" ").includes(name),
12
+ },
13
+ getAttribute: () => dataTheme,
14
+ } as unknown as Pick<HTMLElement, "classList" | "getAttribute">;
15
+ }
4
16
 
5
17
  describe("isExpectedRouteNotFound", () => {
6
18
  it("recognizes React Router 404 responses", () => {
@@ -26,3 +38,34 @@ describe("isExpectedRouteNotFound", () => {
26
38
  ).toBe(false);
27
39
  });
28
40
  });
41
+
42
+ describe("resolveErrorBoundaryThemeClass", () => {
43
+ it("honors an explicit data theme before storage or the system preference", () => {
44
+ const readStoredTheme = vi.fn(() => "dark");
45
+ const readSystemTheme = vi.fn(() => true);
46
+
47
+ expect(
48
+ resolveErrorBoundaryThemeClass(
49
+ themeRoot("", "light"),
50
+ readStoredTheme,
51
+ readSystemTheme,
52
+ ),
53
+ ).toBe("light");
54
+ expect(readStoredTheme).not.toHaveBeenCalled();
55
+ expect(readSystemTheme).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("uses the system theme only when no explicit theme is selected", () => {
59
+ const readStoredTheme = vi.fn(() => null);
60
+ const readSystemTheme = vi.fn(() => true);
61
+
62
+ expect(
63
+ resolveErrorBoundaryThemeClass(
64
+ themeRoot("", null),
65
+ readStoredTheme,
66
+ readSystemTheme,
67
+ ),
68
+ ).toBe("dark");
69
+ expect(readSystemTheme).toHaveBeenCalledOnce();
70
+ });
71
+ });
@@ -196,21 +196,46 @@ function useErrorCopy() {
196
196
  function useApplyThemeClass() {
197
197
  useEffect(() => {
198
198
  const root = document.documentElement;
199
- if (root.classList.contains("dark") || root.classList.contains("light"))
200
- return;
201
- try {
202
- const stored = localStorage.getItem("theme");
203
- if (stored === "dark") {
204
- root.classList.add("dark");
205
- } else if (stored === "light") {
206
- root.classList.add("light");
207
- } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
208
- root.classList.add("dark");
209
- }
210
- } catch {}
199
+ let storageUnreadable = false;
200
+ const theme = resolveErrorBoundaryThemeClass(
201
+ root,
202
+ () => {
203
+ try {
204
+ return localStorage.getItem("theme");
205
+ } catch {
206
+ storageUnreadable = true;
207
+ return null;
208
+ }
209
+ },
210
+ () =>
211
+ window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false,
212
+ );
213
+ if (storageUnreadable) {
214
+ root.setAttribute("data-theme-status", "unreadable");
215
+ } else {
216
+ root.removeAttribute("data-theme-status");
217
+ }
218
+ if (theme) root.classList.add(theme);
211
219
  }, []);
212
220
  }
213
221
 
222
+ export function resolveErrorBoundaryThemeClass(
223
+ root: Pick<HTMLElement, "classList" | "getAttribute">,
224
+ readStoredTheme: () => string | null,
225
+ readSystemTheme: () => boolean,
226
+ ): "dark" | "light" | undefined {
227
+ if (root.classList.contains("dark") || root.classList.contains("light")) {
228
+ return undefined;
229
+ }
230
+
231
+ const dataTheme = root.getAttribute("data-theme");
232
+ if (dataTheme === "dark" || dataTheme === "light") return dataTheme;
233
+
234
+ const storedTheme = readStoredTheme();
235
+ if (storedTheme === "dark" || storedTheme === "light") return storedTheme;
236
+ return readSystemTheme() ? "dark" : undefined;
237
+ }
238
+
214
239
  function errorMessageOf(error: unknown): string {
215
240
  if (error instanceof Error) return error.message;
216
241
  return typeof error === "string" ? error : "";
@@ -0,0 +1,16 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { PoweredByBadge } from "./PoweredByBadge.js";
5
+
6
+ describe("PoweredByBadge theme styles", () => {
7
+ it("follows the selected theme classes and data attribute", () => {
8
+ const html = renderToStaticMarkup(<PoweredByBadge />);
9
+
10
+ expect(html).toContain("html.dark");
11
+ expect(html).toContain('html[data-theme="dark"]');
12
+ expect(html).toContain("html.light");
13
+ expect(html).toContain('html[data-theme="light"]');
14
+ expect(html).not.toContain("prefers-color-scheme");
15
+ });
16
+ });
@@ -47,8 +47,6 @@ const containerStyle = (
47
47
  opacity: 0.82,
48
48
  });
49
49
 
50
- const darkQuery = "(prefers-color-scheme: dark)";
51
-
52
50
  export function PoweredByBadge({
53
51
  position = "bottom-right",
54
52
  variant = "badge",
@@ -91,54 +89,36 @@ export function PoweredByBadge({
91
89
  .an-powered-badge-plain:hover {
92
90
  opacity: 0.82 !important;
93
91
  }
94
- .dark .an-powered-badge:not(.an-powered-badge-plain) {
92
+ :is(html.dark, html[data-theme="dark"]:not(.light)) .an-powered-badge:not(.an-powered-badge-plain) {
95
93
  background: rgba(255, 255, 255, 0.06) !important;
96
94
  border-color: rgba(255, 255, 255, 0.08) !important;
97
95
  color: rgba(215, 215, 215, 0.94) !important;
98
96
  }
99
- .dark .an-powered-logo-light {
97
+ :is(html.dark, html[data-theme="dark"]:not(.light)) .an-powered-logo-light {
100
98
  display: none;
101
99
  }
102
- .dark .an-powered-logo-dark {
100
+ :is(html.dark, html[data-theme="dark"]:not(.light)) .an-powered-logo-dark {
103
101
  display: block;
104
102
  }
105
- @media ${darkQuery} {
106
- .an-powered-badge:not(.an-powered-badge-plain) {
107
- background: rgba(255, 255, 255, 0.06) !important;
108
- border-color: rgba(255, 255, 255, 0.08) !important;
109
- color: rgba(215, 215, 215, 0.94) !important;
110
- }
111
- .an-powered-logo-light {
112
- display: none;
113
- }
114
- .an-powered-logo-dark {
115
- display: block;
116
- }
117
- }
118
- .light .an-powered-badge:not(.an-powered-badge-plain) {
103
+ :is(html.light, html[data-theme="light"]:not(.dark)) .an-powered-badge:not(.an-powered-badge-plain) {
119
104
  background: rgba(0, 0, 0, 0.05) !important;
120
105
  border-color: rgba(0, 0, 0, 0.06) !important;
121
106
  color: rgba(95, 95, 95, 0.95) !important;
122
107
  }
123
- .light .an-powered-logo-light {
108
+ :is(html.light, html[data-theme="light"]:not(.dark)) .an-powered-logo-light {
124
109
  display: block;
125
110
  }
126
- .light .an-powered-logo-dark {
111
+ :is(html.light, html[data-theme="light"]:not(.dark)) .an-powered-logo-dark {
127
112
  display: none;
128
113
  }
129
114
  .an-powered-badge:not(.an-powered-badge-plain):hover {
130
115
  opacity: 1 !important;
131
116
  color: rgba(70, 70, 70, 1) !important;
132
117
  }
133
- @media ${darkQuery} {
134
- .an-powered-badge:not(.an-powered-badge-plain):hover {
135
- color: rgba(238, 238, 238, 1) !important;
136
- }
137
- }
138
- .dark .an-powered-badge:not(.an-powered-badge-plain):hover {
118
+ :is(html.dark, html[data-theme="dark"]:not(.light)) .an-powered-badge:not(.an-powered-badge-plain):hover {
139
119
  color: rgba(238, 238, 238, 1) !important;
140
120
  }
141
- .light .an-powered-badge:not(.an-powered-badge-plain):hover {
121
+ :is(html.light, html[data-theme="light"]:not(.dark)) .an-powered-badge:not(.an-powered-badge-plain):hover {
142
122
  color: rgba(70, 70, 70, 1) !important;
143
123
  }
144
124
  `}</style>
@@ -205,32 +185,10 @@ export function OpenSourceBadge({
205
185
  max-width: calc(100vw - 2rem);
206
186
  }
207
187
  }
208
- .dark .an-open-source-badge {
209
- color: #00B5FF !important;
210
- }
211
- @media ${darkQuery} {
212
- .an-open-source-badge {
213
- color: #00B5FF !important;
214
- }
215
- }
216
- .light .an-open-source-badge {
217
- color: #00B5FF !important;
218
- }
219
188
  .an-open-source-badge:hover {
220
189
  opacity: 1 !important;
221
190
  color: #33C4FF !important;
222
191
  }
223
- @media ${darkQuery} {
224
- .an-open-source-badge:hover {
225
- color: #33C4FF !important;
226
- }
227
- }
228
- .dark .an-open-source-badge:hover {
229
- color: #33C4FF !important;
230
- }
231
- .light .an-open-source-badge:hover {
232
- color: #33C4FF !important;
233
- }
234
192
  `}</style>
235
193
  <a
236
194
  href="https://github.com/BuilderIO/agent-native"
@@ -219,17 +219,6 @@
219
219
  background: var(--shiki-dark-bg);
220
220
  }
221
221
 
222
- @media (prefers-color-scheme: dark) {
223
- :root:not(.light) .agent-conversation-shiki pre {
224
- background: var(--shiki-dark-bg, hsl(var(--muted) / 0.72));
225
- color: var(--shiki-dark, hsl(var(--foreground)));
226
- }
227
- :root:not(.light) .agent-conversation-shiki pre span {
228
- color: var(--shiki-dark);
229
- background: var(--shiki-dark-bg);
230
- }
231
- }
232
-
233
222
  .agent-conversation-tool {
234
223
  width: 100%;
235
224
  border: 1px solid hsl(var(--border) / 0.9);
@@ -727,18 +716,6 @@
727
716
  background: var(--shiki-dark-bg);
728
717
  }
729
718
 
730
- @media (prefers-color-scheme: dark) {
731
- :root:not(.light) .agent-markdown-shiki pre {
732
- background: var(--shiki-dark-bg);
733
- color: var(--shiki-dark);
734
- }
735
-
736
- :root:not(.light) .agent-markdown-shiki pre span {
737
- color: var(--shiki-dark);
738
- background: var(--shiki-dark-bg);
739
- }
740
- }
741
-
742
719
  .agent-tool-code .agent-markdown-shiki {
743
720
  margin: 0;
744
721
  border-radius: 0;
@@ -27,7 +27,7 @@
27
27
  @import "./tokens/agent-kit.css";
28
28
  @import "../agentkit/react/styles.css";
29
29
 
30
- @custom-variant dark (&:is(.dark *));
30
+ @custom-variant dark (&:where(.dark, .dark *));
31
31
 
32
32
  @plugin "@tailwindcss/typography";
33
33
 
@@ -3,6 +3,24 @@ import { readFileSync } from "node:fs";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
5
  describe("agent-native shell surface tokens", () => {
6
+ it("uses the selected theme class for dark utilities and chat markdown", () => {
7
+ const toolkitCss = readFileSync(
8
+ new URL("./agent-native.css", import.meta.url),
9
+ { encoding: "utf8" },
10
+ );
11
+ const conversationCss = readFileSync(
12
+ new URL("./agent-conversation.css", import.meta.url),
13
+ { encoding: "utf8" },
14
+ );
15
+
16
+ expect(toolkitCss).toContain(
17
+ "@custom-variant dark (&:where(.dark, .dark *));",
18
+ );
19
+ expect(conversationCss).not.toContain("prefers-color-scheme: dark");
20
+ expect(conversationCss).toContain(".dark .agent-conversation-shiki pre");
21
+ expect(conversationCss).toContain(".dark .agent-markdown-shiki pre");
22
+ });
23
+
6
24
  it("keeps the composer surface opaque while preserving its muted blend", () => {
7
25
  const css = readFileSync(new URL("./agent-native.css", import.meta.url), {
8
26
  encoding: "utf8",
@@ -0,0 +1,66 @@
1
+ /*
2
+ * The agent-native family uses the shared Tailwind/shadcn palette from the
3
+ * Design template. App styles remain the original family by default.
4
+ */
5
+ :root[data-theme-family="agent-native"],
6
+ :root[data-theme-family="agent-native"][data-appearance] {
7
+ --background: 0 0% 100%;
8
+ --foreground: 0 0% 10%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 0 0% 10%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 0 0% 10%;
13
+ --primary: 0 0% 15%;
14
+ --primary-foreground: 0 0% 100%;
15
+ --secondary: 0 0% 95%;
16
+ --secondary-foreground: 0 0% 15%;
17
+ --muted: 0 0% 95%;
18
+ --muted-foreground: 0 0% 45%;
19
+ --accent: 0 0% 95%;
20
+ --accent-foreground: 0 0% 15%;
21
+ --destructive: 0 84% 60%;
22
+ --destructive-foreground: 0 0% 98%;
23
+ --border: 0 0% 90%;
24
+ --input: 0 0% 90%;
25
+ --ring: 0 0% 40%;
26
+ --radius: 0.5rem;
27
+ --sidebar-background: 0 0% 97%;
28
+ --sidebar-foreground: 0 0% 45%;
29
+ --sidebar-primary: 0 0% 15%;
30
+ --sidebar-primary-foreground: 0 0% 100%;
31
+ --sidebar-accent: 0 0% 95%;
32
+ --sidebar-accent-foreground: 0 0% 15%;
33
+ --sidebar-border: 0 0% 90%;
34
+ --sidebar-ring: 0 0% 40%;
35
+ }
36
+
37
+ .dark[data-theme-family="agent-native"],
38
+ .dark[data-theme-family="agent-native"][data-appearance] {
39
+ --background: 0 0% 13%;
40
+ --foreground: 0 0% 90%;
41
+ --card: 0 0% 15%;
42
+ --card-foreground: 0 0% 90%;
43
+ --popover: 0 0% 15%;
44
+ --popover-foreground: 0 0% 90%;
45
+ --primary: 0 0% 75%;
46
+ --primary-foreground: 0 0% 10%;
47
+ --secondary: 0 0% 18%;
48
+ --secondary-foreground: 0 0% 90%;
49
+ --muted: 0 0% 16%;
50
+ --muted-foreground: 0 0% 60%;
51
+ --accent: 0 0% 18%;
52
+ --accent-foreground: 0 0% 96%;
53
+ --destructive: 0 91% 71%;
54
+ --destructive-foreground: 0 0% 98%;
55
+ --border: 0 0% 24%;
56
+ --input: 0 0% 24%;
57
+ --ring: 0 0% 60%;
58
+ --sidebar-background: 0 0% 10%;
59
+ --sidebar-foreground: 0 0% 60%;
60
+ --sidebar-primary: 0 0% 75%;
61
+ --sidebar-primary-foreground: 0 0% 10%;
62
+ --sidebar-accent: 0 0% 16%;
63
+ --sidebar-accent-foreground: 0 0% 90%;
64
+ --sidebar-border: 0 0% 20%;
65
+ --sidebar-ring: 0 0% 60%;
66
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const css = readFileSync(
6
+ new URL("./theme-families.css", import.meta.url),
7
+ "utf8",
8
+ );
9
+
10
+ describe("theme families", () => {
11
+ it("provides a shared palette for the resolved light and dark modes", () => {
12
+ expect(css).toContain(':root[data-theme-family="agent-native"]');
13
+ expect(css).toContain('.dark[data-theme-family="agent-native"]');
14
+ expect(css).toContain("--background: 0 0% 100%");
15
+ expect(css).toContain("--background: 0 0% 13%");
16
+ expect(css).not.toContain("prefers-color-scheme");
17
+ });
18
+
19
+ it("keeps the selected family palette ahead of appearance presets", () => {
20
+ expect(css).toContain(
21
+ ':root[data-theme-family="agent-native"][data-appearance]',
22
+ );
23
+ expect(css).toContain(
24
+ '.dark[data-theme-family="agent-native"][data-appearance]',
25
+ );
26
+ expect(css).toContain("--background: 0 0% 100%");
27
+ expect(css).toContain("--background: 0 0% 13%");
28
+ });
29
+ });
@@ -0,0 +1,74 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { 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 { ComposerContextBanner } from "./ComposerContextBanner.js";
8
+
9
+ (
10
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
11
+ ).IS_REACT_ACT_ENVIRONMENT = true;
12
+
13
+ let container: HTMLDivElement;
14
+ let root: Root;
15
+
16
+ beforeEach(() => {
17
+ container = document.createElement("div");
18
+ document.body.appendChild(container);
19
+ root = createRoot(container);
20
+ });
21
+
22
+ afterEach(() => {
23
+ act(() => root.unmount());
24
+ container.remove();
25
+ });
26
+
27
+ describe("ComposerContextBanner", () => {
28
+ it("renders localized context actions and calls add and dismiss handlers", async () => {
29
+ const onAddContext = vi.fn();
30
+ const onDismiss = vi.fn();
31
+
32
+ await act(async () =>
33
+ root.render(
34
+ <ComposerContextBanner
35
+ ariaLabel="Context guidance"
36
+ addContextLabel="Add context"
37
+ dismissLabel="Dismiss context guidance"
38
+ onAddContext={onAddContext}
39
+ onDismiss={onDismiss}
40
+ >
41
+ Improve answers by adding data definitions.
42
+ </ComposerContextBanner>,
43
+ ),
44
+ );
45
+
46
+ const banner = container.querySelector<HTMLElement>(
47
+ '[data-agent-composer-slot="context-banner"]',
48
+ )!;
49
+ expect(banner.getAttribute("aria-label")).toBe("Context guidance");
50
+ expect(banner.classList).toContain("gap-1.5");
51
+ expect(banner.classList).toContain("px-2.5");
52
+ expect(banner.classList).toContain("py-2");
53
+ expect(banner.classList).toContain("rounded-lg");
54
+ expect(banner.textContent).toContain(
55
+ "Improve answers by adding data definitions.",
56
+ );
57
+
58
+ await act(async () =>
59
+ Array.from(container.querySelectorAll("button"))
60
+ .find((button) => button.textContent?.includes("Add context"))!
61
+ .click(),
62
+ );
63
+ await act(async () =>
64
+ container
65
+ .querySelector<HTMLButtonElement>(
66
+ 'button[aria-label="Dismiss context guidance"]',
67
+ )!
68
+ .click(),
69
+ );
70
+
71
+ expect(onAddContext).toHaveBeenCalledOnce();
72
+ expect(onDismiss).toHaveBeenCalledOnce();
73
+ });
74
+ });