@useatlas/react 0.0.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 (98) hide show
  1. package/README.md +95 -0
  2. package/dist/chunk-2WFDP7G5.js +231 -0
  3. package/dist/chunk-2WFDP7G5.js.map +1 -0
  4. package/dist/chunk-44HBZYKP.js +224 -0
  5. package/dist/chunk-44HBZYKP.js.map +1 -0
  6. package/dist/chunk-5SEVKHS5.cjs +229 -0
  7. package/dist/chunk-5SEVKHS5.cjs.map +1 -0
  8. package/dist/chunk-UIRB6L36.cjs +249 -0
  9. package/dist/chunk-UIRB6L36.cjs.map +1 -0
  10. package/dist/hooks.cjs +251 -0
  11. package/dist/hooks.cjs.map +1 -0
  12. package/dist/hooks.d.cts +132 -0
  13. package/dist/hooks.d.ts +132 -0
  14. package/dist/hooks.js +237 -0
  15. package/dist/hooks.js.map +1 -0
  16. package/dist/index.cjs +2976 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +69 -0
  19. package/dist/index.d.ts +69 -0
  20. package/dist/index.js +2926 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/result-chart-NFAJ4IQ5.js +398 -0
  23. package/dist/result-chart-NFAJ4IQ5.js.map +1 -0
  24. package/dist/result-chart-YLCKBNV4.cjs +400 -0
  25. package/dist/result-chart-YLCKBNV4.cjs.map +1 -0
  26. package/dist/styles.css +59 -0
  27. package/dist/use-dark-mode-rFxawUv1.d.cts +123 -0
  28. package/dist/use-dark-mode-rFxawUv1.d.ts +123 -0
  29. package/dist/widget.css +2 -0
  30. package/dist/widget.js +445 -0
  31. package/package.json +113 -0
  32. package/src/components/__tests__/tool-renderers.test.tsx +239 -0
  33. package/src/components/actions/action-approval-card.tsx +296 -0
  34. package/src/components/actions/action-status-badge.tsx +50 -0
  35. package/src/components/admin/change-password-dialog.tsx +128 -0
  36. package/src/components/atlas-chat.tsx +656 -0
  37. package/src/components/chart/chart-detection.ts +318 -0
  38. package/src/components/chart/result-chart.tsx +590 -0
  39. package/src/components/chat/api-key-bar.tsx +66 -0
  40. package/src/components/chat/copy-button.tsx +25 -0
  41. package/src/components/chat/data-table.tsx +104 -0
  42. package/src/components/chat/error-banner.tsx +32 -0
  43. package/src/components/chat/explore-card.tsx +41 -0
  44. package/src/components/chat/follow-up-chips.tsx +29 -0
  45. package/src/components/chat/loading-card.tsx +10 -0
  46. package/src/components/chat/managed-auth-card.tsx +116 -0
  47. package/src/components/chat/markdown.tsx +146 -0
  48. package/src/components/chat/python-result-card.tsx +245 -0
  49. package/src/components/chat/sql-block.tsx +54 -0
  50. package/src/components/chat/sql-result-card.tsx +163 -0
  51. package/src/components/chat/starter-prompts.ts +6 -0
  52. package/src/components/chat/tool-part.tsx +106 -0
  53. package/src/components/chat/typing-indicator.tsx +22 -0
  54. package/src/components/conversations/conversation-item.tsx +135 -0
  55. package/src/components/conversations/conversation-list.tsx +69 -0
  56. package/src/components/conversations/conversation-sidebar.tsx +113 -0
  57. package/src/components/conversations/delete-confirmation.tsx +27 -0
  58. package/src/components/schema-explorer/schema-explorer.tsx +517 -0
  59. package/src/components/ui/alert-dialog.tsx +196 -0
  60. package/src/components/ui/badge.tsx +48 -0
  61. package/src/components/ui/button.tsx +64 -0
  62. package/src/components/ui/card.tsx +92 -0
  63. package/src/components/ui/dialog.tsx +158 -0
  64. package/src/components/ui/dropdown-menu.tsx +257 -0
  65. package/src/components/ui/input.tsx +21 -0
  66. package/src/components/ui/label.tsx +24 -0
  67. package/src/components/ui/scroll-area.tsx +62 -0
  68. package/src/components/ui/separator.tsx +28 -0
  69. package/src/components/ui/sheet.tsx +143 -0
  70. package/src/components/ui/table.tsx +116 -0
  71. package/src/components/ui/toggle-group.tsx +83 -0
  72. package/src/components/ui/toggle.tsx +47 -0
  73. package/src/context.tsx +85 -0
  74. package/src/env.d.ts +9 -0
  75. package/src/hooks/__tests__/provider.test.tsx +83 -0
  76. package/src/hooks/__tests__/use-atlas-auth.test.tsx +283 -0
  77. package/src/hooks/__tests__/use-atlas-chat.test.tsx +157 -0
  78. package/src/hooks/__tests__/use-atlas-conversations.test.tsx +159 -0
  79. package/src/hooks/__tests__/use-atlas-theme.test.tsx +56 -0
  80. package/src/hooks/index.ts +47 -0
  81. package/src/hooks/provider.tsx +77 -0
  82. package/src/hooks/theme-init-script.ts +17 -0
  83. package/src/hooks/use-atlas-auth.ts +131 -0
  84. package/src/hooks/use-atlas-chat.ts +102 -0
  85. package/src/hooks/use-atlas-conversations.ts +61 -0
  86. package/src/hooks/use-atlas-theme.ts +34 -0
  87. package/src/hooks/use-conversations.ts +189 -0
  88. package/src/hooks/use-dark-mode.ts +150 -0
  89. package/src/index.ts +36 -0
  90. package/src/lib/action-types.ts +11 -0
  91. package/src/lib/helpers.ts +198 -0
  92. package/src/lib/tool-renderer-types.ts +76 -0
  93. package/src/lib/types.ts +29 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/styles.css +59 -0
  96. package/src/test-setup.ts +55 -0
  97. package/src/widget-entry.ts +20 -0
  98. package/src/widget.css +12 -0
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useMemo } from "react";
4
+ import { useChat, type UIMessage } from "@ai-sdk/react";
5
+ import { DefaultChatTransport } from "ai";
6
+ import { useAtlasContext } from "./provider";
7
+
8
+ export type AtlasChatStatus = "submitted" | "streaming" | "ready" | "error";
9
+
10
+ export interface UseAtlasChatOptions {
11
+ /** Conversation ID to associate with this chat session. The server will append messages to this conversation. To load prior messages, use loadConversation() from useAtlasConversations and pass them via setMessages. */
12
+ initialConversationId?: string;
13
+ /** Called when the server assigns or changes the conversation ID. */
14
+ onConversationIdChange?: (id: string) => void;
15
+ }
16
+
17
+ export interface UseAtlasChatReturn {
18
+ messages: UIMessage[];
19
+ /** Replace all messages, or update via callback `(prev) => next`. */
20
+ setMessages: (messages: UIMessage[] | ((prev: UIMessage[]) => UIMessage[])) => void;
21
+ /** Send a text message. Rejects on failure (also surfaces via `error`). */
22
+ sendMessage: (text: string) => Promise<void>;
23
+ /** Current input value (managed by the hook). */
24
+ input: string;
25
+ /** Update the input value. */
26
+ setInput: (input: string) => void;
27
+ /** Chat status from the AI SDK. */
28
+ status: AtlasChatStatus;
29
+ /** Whether the chat is currently loading (streaming or submitted). */
30
+ isLoading: boolean;
31
+ /** Last error, if any. */
32
+ error: Error | null;
33
+ /** Current conversation ID. Initially set from options, updated when the server returns an x-conversation-id header. */
34
+ conversationId: string | null;
35
+ /** Manually set the conversation ID. */
36
+ setConversationId: (id: string | null) => void;
37
+ }
38
+
39
+ export function useAtlasChat(options: UseAtlasChatOptions = {}): UseAtlasChatReturn {
40
+ const { apiUrl, apiKey, isCrossOrigin } = useAtlasContext();
41
+ const [conversationId, setConversationId] = useState<string | null>(
42
+ options.initialConversationId ?? null,
43
+ );
44
+ const conversationIdRef = useRef(conversationId);
45
+ conversationIdRef.current = conversationId;
46
+
47
+ const onChangeRef = useRef(options.onConversationIdChange);
48
+ onChangeRef.current = options.onConversationIdChange;
49
+
50
+ const transport = useMemo(() => {
51
+ const headers: Record<string, string> = {};
52
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
53
+
54
+ return new DefaultChatTransport({
55
+ api: `${apiUrl}/api/chat`,
56
+ headers,
57
+ credentials: isCrossOrigin ? "include" : undefined,
58
+ body: () =>
59
+ conversationIdRef.current
60
+ ? { conversationId: conversationIdRef.current }
61
+ : {},
62
+ fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
63
+ const response = await globalThis.fetch(input, init);
64
+ const convId = response.headers.get("x-conversation-id");
65
+ if (convId && convId !== conversationIdRef.current) {
66
+ setConversationId(convId);
67
+ onChangeRef.current?.(convId);
68
+ }
69
+ return response;
70
+ }) as typeof fetch,
71
+ });
72
+ }, [apiKey, apiUrl, isCrossOrigin]);
73
+
74
+ const { messages, setMessages, sendMessage: rawSend, status, error } =
75
+ useChat({ transport });
76
+
77
+ const [input, setInput] = useState("");
78
+
79
+ const sendMessage = async (text: string) => {
80
+ const previousInput = input;
81
+ setInput("");
82
+ try {
83
+ await rawSend({ text });
84
+ } catch (err) {
85
+ setInput(previousInput);
86
+ throw err;
87
+ }
88
+ };
89
+
90
+ return {
91
+ messages,
92
+ setMessages,
93
+ sendMessage,
94
+ input,
95
+ setInput,
96
+ status: status as AtlasChatStatus,
97
+ isLoading: status === "streaming" || status === "submitted",
98
+ error: error ?? null,
99
+ conversationId,
100
+ setConversationId,
101
+ };
102
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import type { UIMessage } from "@ai-sdk/react";
4
+ import { useAtlasContext } from "./provider";
5
+ import { useConversations } from "./use-conversations";
6
+ import type { Conversation } from "../lib/types";
7
+
8
+ export interface UseAtlasConversationsOptions {
9
+ /** When false, refresh() becomes a no-op. Defaults to true. */
10
+ enabled?: boolean;
11
+ }
12
+
13
+ export interface UseAtlasConversationsReturn {
14
+ conversations: Conversation[];
15
+ total: number;
16
+ isLoading: boolean;
17
+ available: boolean;
18
+ selectedId: string | null;
19
+ setSelectedId: (id: string | null) => void;
20
+ refresh: () => Promise<void>;
21
+ loadConversation: (id: string) => Promise<UIMessage[] | null>;
22
+ deleteConversation: (id: string) => Promise<boolean>;
23
+ starConversation: (id: string, starred: boolean) => Promise<boolean>;
24
+ }
25
+
26
+ /**
27
+ * Manage conversation history with auth automatically wired from AtlasProvider.
28
+ *
29
+ * Wraps the lower-level `useConversations` hook with context-derived
30
+ * API URL and credentials.
31
+ */
32
+ export function useAtlasConversations(
33
+ options: UseAtlasConversationsOptions = {},
34
+ ): UseAtlasConversationsReturn {
35
+ const { apiUrl, apiKey, isCrossOrigin } = useAtlasContext();
36
+ const { enabled = true } = options;
37
+
38
+ const inner = useConversations({
39
+ apiUrl,
40
+ enabled,
41
+ getHeaders: () => {
42
+ const headers: Record<string, string> = {};
43
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
44
+ return headers;
45
+ },
46
+ getCredentials: () => (isCrossOrigin ? "include" : "same-origin"),
47
+ });
48
+
49
+ return {
50
+ conversations: inner.conversations,
51
+ total: inner.total,
52
+ isLoading: inner.loading,
53
+ available: inner.available,
54
+ selectedId: inner.selectedId,
55
+ setSelectedId: inner.setSelectedId,
56
+ refresh: inner.refresh,
57
+ loadConversation: inner.loadConversation,
58
+ deleteConversation: inner.deleteConversation,
59
+ starConversation: inner.starConversation,
60
+ };
61
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import {
4
+ useDarkMode,
5
+ useThemeMode,
6
+ setTheme,
7
+ applyBrandColor,
8
+ type ThemeMode,
9
+ } from "./use-dark-mode";
10
+
11
+ export type { ThemeMode };
12
+
13
+ export interface UseAtlasThemeReturn {
14
+ /** Current theme setting: "light", "dark", or "system". */
15
+ theme: ThemeMode;
16
+ /** Whether the effective (resolved) theme is dark. */
17
+ isDark: boolean;
18
+ /** Set the theme mode. Persists to localStorage. */
19
+ setTheme: (mode: ThemeMode) => void;
20
+ /** Apply a brand color (oklch format, e.g. "oklch(0.759 0.148 167.71)") via CSS custom property --atlas-brand. */
21
+ applyBrandColor: (color: string) => void;
22
+ }
23
+
24
+ export function useAtlasTheme(): UseAtlasThemeReturn {
25
+ const theme = useThemeMode();
26
+ const isDark = useDarkMode();
27
+
28
+ return {
29
+ theme,
30
+ isDark,
31
+ setTheme,
32
+ applyBrandColor,
33
+ };
34
+ }
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef } from "react";
4
+ import type { Conversation, ConversationWithMessages, Message } from "../lib/types";
5
+ import type { UIMessage } from "@ai-sdk/react";
6
+
7
+ export interface UseConversationsOptions {
8
+ apiUrl: string;
9
+ enabled: boolean;
10
+ getHeaders: () => Record<string, string>;
11
+ getCredentials: () => RequestCredentials;
12
+ }
13
+
14
+ export interface UseConversationsReturn {
15
+ conversations: Conversation[];
16
+ total: number;
17
+ loading: boolean;
18
+ available: boolean;
19
+ selectedId: string | null;
20
+ setSelectedId: (id: string | null) => void;
21
+ fetchList: () => Promise<void>;
22
+ loadConversation: (id: string) => Promise<UIMessage[] | null>;
23
+ deleteConversation: (id: string) => Promise<boolean>;
24
+ starConversation: (id: string, starred: boolean) => Promise<boolean>;
25
+ refresh: () => Promise<void>;
26
+ }
27
+
28
+ export function transformMessages(messages: Message[]): UIMessage[] {
29
+ return messages
30
+ .filter((m) => m.role === "user" || m.role === "assistant")
31
+ .map((m) => {
32
+ const content = typeof m.content === "string"
33
+ ? m.content
34
+ : JSON.stringify(m.content);
35
+
36
+ return {
37
+ id: m.id,
38
+ role: m.role as "user" | "assistant",
39
+ parts: [{ type: "text" as const, text: content }],
40
+ };
41
+ });
42
+ }
43
+
44
+ export function useConversations(opts: UseConversationsOptions): UseConversationsReturn {
45
+ const [conversations, setConversations] = useState<Conversation[]>([]);
46
+ const [total, setTotal] = useState(0);
47
+ const [loading, setLoading] = useState(false);
48
+ const [available, setAvailable] = useState(true);
49
+ const [selectedId, setSelectedId] = useState<string | null>(null);
50
+ const fetchedRef = useRef(false);
51
+ const networkFailRef = useRef(0);
52
+
53
+ const fetchList = useCallback(async () => {
54
+ if (!opts.enabled || !available) return;
55
+ setLoading(true);
56
+ try {
57
+ const res = await fetch(`${opts.apiUrl}/api/v1/conversations?limit=50`, {
58
+ headers: opts.getHeaders(),
59
+ credentials: opts.getCredentials(),
60
+ });
61
+
62
+ if (res.status === 404) {
63
+ setAvailable(false);
64
+ return;
65
+ }
66
+
67
+ if (!res.ok) {
68
+ const errorBody = await res.json().catch(() => null);
69
+ if (errorBody?.code === "not_available") {
70
+ setAvailable(false);
71
+ return;
72
+ }
73
+ console.warn(`fetchList: HTTP ${res.status}`, errorBody);
74
+ return;
75
+ }
76
+
77
+ const data = await res.json();
78
+ setConversations(data.conversations ?? []);
79
+ setTotal(data.total ?? 0);
80
+ fetchedRef.current = true;
81
+ } catch (err) {
82
+ console.warn("fetchList error:", err);
83
+ // Network error before any successful fetch — disable temporarily.
84
+ // A subsequent explicit fetchList() call can retry.
85
+ if (!fetchedRef.current) {
86
+ networkFailRef.current += 1;
87
+ if (networkFailRef.current >= 3) setAvailable(false);
88
+ }
89
+ } finally {
90
+ setLoading(false);
91
+ }
92
+ }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available]);
93
+
94
+ const loadConversation = useCallback(async (id: string): Promise<UIMessage[] | null> => {
95
+ try {
96
+ const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
97
+ headers: opts.getHeaders(),
98
+ credentials: opts.getCredentials(),
99
+ });
100
+
101
+ if (!res.ok) {
102
+ console.warn(`loadConversation: HTTP ${res.status} for ${id}`);
103
+ return null;
104
+ }
105
+
106
+ const data: ConversationWithMessages = await res.json();
107
+ return transformMessages(data.messages);
108
+ } catch (err) {
109
+ console.warn("loadConversation error:", err);
110
+ return null;
111
+ }
112
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
113
+
114
+ const deleteConversation = useCallback(async (id: string): Promise<boolean> => {
115
+ try {
116
+ const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
117
+ method: "DELETE",
118
+ headers: opts.getHeaders(),
119
+ credentials: opts.getCredentials(),
120
+ });
121
+
122
+ if (!res.ok) {
123
+ console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);
124
+ return false;
125
+ }
126
+
127
+ setConversations((prev) => prev.filter((c) => c.id !== id));
128
+ setTotal((prev) => Math.max(0, prev - 1));
129
+
130
+ if (selectedId === id) setSelectedId(null);
131
+
132
+ return true;
133
+ } catch (err) {
134
+ console.warn("deleteConversation error:", err);
135
+ return false;
136
+ }
137
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId]);
138
+
139
+ const starConversation = useCallback(async (id: string, starred: boolean): Promise<boolean> => {
140
+ // Optimistic update
141
+ setConversations((prev) =>
142
+ prev.map((c) => (c.id === id ? { ...c, starred } : c)),
143
+ );
144
+ try {
145
+ const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}/star`, {
146
+ method: "PATCH",
147
+ headers: { ...opts.getHeaders(), "Content-Type": "application/json" },
148
+ credentials: opts.getCredentials(),
149
+ body: JSON.stringify({ starred }),
150
+ });
151
+
152
+ if (!res.ok) {
153
+ console.warn(`starConversation: HTTP ${res.status} for ${id}`);
154
+ // Rollback
155
+ setConversations((prev) =>
156
+ prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
157
+ );
158
+ return false;
159
+ }
160
+
161
+ return true;
162
+ } catch (err) {
163
+ console.warn("starConversation error:", err);
164
+ // Rollback
165
+ setConversations((prev) =>
166
+ prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
167
+ );
168
+ return false;
169
+ }
170
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
171
+
172
+ const refresh = useCallback(async () => {
173
+ await fetchList();
174
+ }, [fetchList]);
175
+
176
+ return {
177
+ conversations,
178
+ total,
179
+ loading,
180
+ available,
181
+ selectedId,
182
+ setSelectedId,
183
+ fetchList,
184
+ loadConversation,
185
+ deleteConversation,
186
+ starConversation,
187
+ refresh,
188
+ };
189
+ }
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { createContext, useSyncExternalStore } from "react";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Theme types & constants
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { THEME_STORAGE_KEY } from "./theme-init-script";
10
+
11
+ export type ThemeMode = "light" | "dark" | "system";
12
+
13
+ export { THEME_STORAGE_KEY };
14
+
15
+ /**
16
+ * Default brand color — must match `brand.css` `:root { --atlas-brand }` and
17
+ * the `ATLAS_BRAND_COLOR` default in `packages/api/src/lib/settings.ts`.
18
+ */
19
+ export const DEFAULT_BRAND_COLOR = "oklch(0.759 0.148 167.71)";
20
+
21
+ /** Basic oklch format check — prevents obviously invalid values from breaking the theme. */
22
+ export const OKLCH_RE = /^oklch\(\s*[\d.]+\s+[\d.]+\s+[\d.]+\s*(?:\/\s*[\d.%]+\s*)?\)$/;
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Shared state — single source of truth for the chosen mode.
26
+ // Listeners are notified on change so useSyncExternalStore re-renders.
27
+ // ---------------------------------------------------------------------------
28
+
29
+ let _mode: ThemeMode = "system";
30
+ const _listeners = new Set<() => void>();
31
+
32
+ function notify() {
33
+ for (const fn of _listeners) fn();
34
+ }
35
+
36
+ /** Read stored preference (called once on module load in the browser). */
37
+ function init() {
38
+ try {
39
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
40
+ if (stored === "light" || stored === "dark" || stored === "system") {
41
+ _mode = stored;
42
+ }
43
+ } catch (err) {
44
+ console.warn("Could not read theme preference from localStorage:", err);
45
+ }
46
+ }
47
+
48
+ if (typeof window !== "undefined") init();
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Derived boolean: is the effective theme dark?
52
+ // ---------------------------------------------------------------------------
53
+
54
+ function systemPrefersDark(): boolean {
55
+ return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
56
+ }
57
+
58
+ function resolveIsDark(mode: ThemeMode): boolean {
59
+ if (mode === "dark") return true;
60
+ if (mode === "light") return false;
61
+ return systemPrefersDark();
62
+ }
63
+
64
+ function applyClass(isDark: boolean) {
65
+ if (typeof document === "undefined") return;
66
+ document.documentElement.classList.toggle("dark", isDark);
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // External store for isDark (reacts to both mode changes AND system changes)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function subscribeIsDark(onChange: () => void) {
74
+ _listeners.add(onChange);
75
+
76
+ // Also listen for system preference changes (relevant when mode === "system").
77
+ // Apply dark class immediately on OS change so the DOM stays in sync before React re-renders.
78
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
79
+ const handler = () => {
80
+ applyClass(resolveIsDark(_mode));
81
+ onChange();
82
+ };
83
+ mq.addEventListener("change", handler);
84
+
85
+ return () => {
86
+ _listeners.delete(onChange);
87
+ mq.removeEventListener("change", handler);
88
+ };
89
+ }
90
+
91
+ function getSnapshotIsDark() {
92
+ return resolveIsDark(_mode);
93
+ }
94
+
95
+ function getServerSnapshotIsDark() {
96
+ return false;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // External store for mode
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function subscribeMode(onChange: () => void) {
104
+ _listeners.add(onChange);
105
+ return () => {
106
+ _listeners.delete(onChange);
107
+ };
108
+ }
109
+
110
+ function getSnapshotMode() {
111
+ return _mode;
112
+ }
113
+
114
+ function getServerSnapshotMode(): ThemeMode {
115
+ return "system";
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Public API
120
+ // ---------------------------------------------------------------------------
121
+
122
+ export function setTheme(mode: ThemeMode) {
123
+ _mode = mode;
124
+ const isDark = resolveIsDark(mode);
125
+ applyClass(isDark);
126
+ try {
127
+ localStorage.setItem(THEME_STORAGE_KEY, mode);
128
+ } catch (err) {
129
+ console.warn("Could not persist theme preference to localStorage:", err);
130
+ }
131
+ notify();
132
+ }
133
+
134
+ /** Apply --atlas-brand on :root so theme tokens update without reload. */
135
+ export function applyBrandColor(color: string) {
136
+ if (typeof document === "undefined") return;
137
+ document.documentElement.style.setProperty("--atlas-brand", color);
138
+ }
139
+
140
+ export const DarkModeContext = createContext(false);
141
+
142
+ /** Returns whether the effective theme is dark. */
143
+ export function useDarkMode(): boolean {
144
+ return useSyncExternalStore(subscribeIsDark, getSnapshotIsDark, getServerSnapshotIsDark);
145
+ }
146
+
147
+ /** Returns the current ThemeMode ("light" | "dark" | "system"). */
148
+ export function useThemeMode(): ThemeMode {
149
+ return useSyncExternalStore(subscribeMode, getSnapshotMode, getServerSnapshotMode);
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Core component
2
+ export { AtlasChat } from "./components/atlas-chat";
3
+ export type { AtlasChatProps } from "./components/atlas-chat";
4
+
5
+ // Context & providers
6
+ export { AtlasUIProvider, useAtlasConfig } from "./context";
7
+ export type { AtlasUIConfig, AtlasAuthClient } from "./context";
8
+
9
+ // Theme
10
+ export { setTheme } from "./hooks/use-dark-mode";
11
+ export type { ThemeMode } from "./hooks/use-dark-mode";
12
+ export { buildThemeInitScript, THEME_STORAGE_KEY } from "./hooks/theme-init-script";
13
+
14
+ // Types
15
+ export type {
16
+ AuthMode,
17
+ Conversation,
18
+ Message,
19
+ ConversationWithMessages,
20
+ ChatErrorCode,
21
+ ChatErrorInfo,
22
+ } from "./lib/types";
23
+ export { AUTH_MODES, parseChatError } from "./lib/types";
24
+
25
+ // Tool renderer types
26
+ export type {
27
+ ToolRendererProps,
28
+ ToolRenderers,
29
+ SQLToolResult,
30
+ ExploreToolResult,
31
+ PythonToolResult,
32
+ } from "./lib/tool-renderer-types";
33
+
34
+ // Hooks
35
+ export { useConversations } from "./hooks/use-conversations";
36
+ export type { UseConversationsOptions, UseConversationsReturn } from "./hooks/use-conversations";
@@ -0,0 +1,11 @@
1
+ export {
2
+ ALL_STATUSES,
3
+ RESOLVED_STATUSES,
4
+ isActionToolResult,
5
+ } from "@useatlas/types/action";
6
+ export type {
7
+ ActionStatus,
8
+ ResolvedStatus,
9
+ ActionToolResultShape,
10
+ ActionApprovalResponse,
11
+ } from "@useatlas/types/action";