@devicai/ui 0.37.1 → 0.38.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 (38) hide show
  1. package/README.md +53 -0
  2. package/dist/cjs/api/client.js +44 -0
  3. package/dist/cjs/api/client.js.map +1 -1
  4. package/dist/cjs/api/types.js.map +1 -1
  5. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +32 -2
  6. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  7. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +6 -2
  8. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  9. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +248 -0
  10. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -0
  11. package/dist/cjs/components/theme.js +33 -0
  12. package/dist/cjs/components/theme.js.map +1 -0
  13. package/dist/cjs/index.js +4 -0
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/styles.css +1 -1
  16. package/dist/esm/api/client.d.ts +45 -1
  17. package/dist/esm/api/client.js +44 -0
  18. package/dist/esm/api/client.js.map +1 -1
  19. package/dist/esm/api/types.d.ts +32 -0
  20. package/dist/esm/api/types.js.map +1 -1
  21. package/dist/esm/components/ChatDrawer/ChatDrawer.js +32 -2
  22. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  23. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +14 -0
  24. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.d.ts +8 -1
  25. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +6 -2
  26. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  27. package/dist/esm/components/IntegrationsModal/IntegrationsModal.d.ts +47 -0
  28. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +246 -0
  29. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -0
  30. package/dist/esm/components/IntegrationsModal/index.d.ts +2 -0
  31. package/dist/esm/components/theme.d.ts +33 -0
  32. package/dist/esm/components/theme.js +31 -0
  33. package/dist/esm/components/theme.js.map +1 -0
  34. package/dist/esm/index.d.ts +5 -1
  35. package/dist/esm/index.js +2 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/styles.css +1 -1
  38. package/package.json +1 -1
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+ var reactDom = require('react-dom');
6
+ var client = require('../../api/client.js');
7
+ var DevicContext = require('../../provider/DevicContext.js');
8
+ var theme = require('../theme.js');
9
+
10
+ function PlugIcon() {
11
+ return (jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsxRuntime.jsx("path", { d: "M12 22v-5" }), jsxRuntime.jsx("path", { d: "M9 8V2" }), jsxRuntime.jsx("path", { d: "M15 8V2" }), jsxRuntime.jsx("path", { d: "M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" })] }));
12
+ }
13
+ /** A random value tying an OAuth round trip to the window that started it. */
14
+ function newNonce() {
15
+ const c = typeof crypto !== "undefined" ? crypto : undefined;
16
+ if (c?.randomUUID)
17
+ return c.randomUUID();
18
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
19
+ }
20
+ /** How the card describes an app at a glance. */
21
+ function stateOf(integration) {
22
+ if (integration.connected)
23
+ return { key: "connected", label: "Connected" };
24
+ if (integration.accounts.some((a) => a.needsReconnect)) {
25
+ return { key: "reconnect", label: "Needs reconnection" };
26
+ }
27
+ return { key: "disconnected", label: "Not connected" };
28
+ }
29
+ function accountLabel(account) {
30
+ if (!account.connectedAt)
31
+ return account.status.toLowerCase();
32
+ const when = new Date(account.connectedAt);
33
+ if (Number.isNaN(when.getTime()))
34
+ return account.status.toLowerCase();
35
+ return `connected ${when.toLocaleDateString()}`;
36
+ }
37
+ /**
38
+ * Modal where the END USER of an application manages their *own* third-party
39
+ * accounts: the apps the developer offered to tenants of this assistant, each
40
+ * with the accounts this tenant has connected, and the buttons to add or
41
+ * remove one.
42
+ *
43
+ * Backed by `/api/v1/tenant-integrations`, which resolves the tenant
44
+ * server-side, so what is listed here is only ever this tenant's — never the
45
+ * workspace-wide accounts an admin connected, and never another tenant's.
46
+ *
47
+ * Connecting opens the provider's consent screen in a popup. The popup is
48
+ * opened empty *before* the request that produces its URL, because browsers
49
+ * only honour `window.open` inside the gesture that triggered it: opening it
50
+ * after the round trip is what gets it blocked. When it is blocked anyway, the
51
+ * URL is offered as a link instead.
52
+ */
53
+ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title = "Connected apps", onChange, theme: theme$1, }) {
54
+ const context = DevicContext.useOptionalDevicContext();
55
+ const resolvedApiKey = apiKey || context?.apiKey;
56
+ const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
57
+ const resolvedTenantId = tenantId || context?.tenantId;
58
+ const resolvedSubtenantId = subtenantId || context?.subtenantId;
59
+ const client$1 = React.useMemo(() => resolvedApiKey
60
+ ? new client.DevicApiClient({
61
+ apiKey: resolvedApiKey,
62
+ baseUrl: resolvedBaseUrl,
63
+ })
64
+ : null, [resolvedApiKey, resolvedBaseUrl]);
65
+ const scopeOptions = React.useMemo(() => ({
66
+ assistantId,
67
+ tenantId: resolvedTenantId || undefined,
68
+ subtenantId: resolvedSubtenantId || undefined,
69
+ }), [assistantId, resolvedTenantId, resolvedSubtenantId]);
70
+ const [loading, setLoading] = React.useState(false);
71
+ const [integrations, setIntegrations] = React.useState([]);
72
+ const [error, setError] = React.useState(null);
73
+ /** App slug with a connect/disconnect in flight, so only its card is busy. */
74
+ const [busyApp, setBusyApp] = React.useState(null);
75
+ /** Authorization URL surfaced as a link when the popup was blocked. */
76
+ const [blockedUrl, setBlockedUrl] = React.useState(null);
77
+ /**
78
+ * Apps whose logo failed to load. The URL comes from the provider and is
79
+ * fetched from whatever host it names, so a broken one is a matter of when,
80
+ * not if — and a broken-image icon in someone else's product looks like our
81
+ * bug.
82
+ */
83
+ const [brokenLogos, setBrokenLogos] = React.useState([]);
84
+ const refresh = React.useCallback(async (dropCache = false) => {
85
+ if (!client$1) {
86
+ setError("API key not configured");
87
+ return;
88
+ }
89
+ setLoading(true);
90
+ setError(null);
91
+ try {
92
+ // The server caches a scope's connections briefly; after an OAuth round
93
+ // trip that cache is exactly one step behind, so drop it first.
94
+ if (dropCache) {
95
+ await client$1.refreshIntegrations(scopeOptions).catch(() => undefined);
96
+ }
97
+ const list = await client$1.getIntegrations(scopeOptions);
98
+ setIntegrations(list);
99
+ onChange?.(list);
100
+ }
101
+ catch (err) {
102
+ setError(err instanceof Error ? err.message : String(err));
103
+ }
104
+ finally {
105
+ setLoading(false);
106
+ }
107
+ }, [client$1, scopeOptions, onChange]);
108
+ // Fetch on every open: accounts may have been connected or revoked elsewhere.
109
+ const wasOpenRef = React.useRef(false);
110
+ React.useEffect(() => {
111
+ if (isOpen && !wasOpenRef.current) {
112
+ setBlockedUrl(null);
113
+ void refresh();
114
+ }
115
+ wasOpenRef.current = isOpen;
116
+ }, [isOpen, refresh]);
117
+ // Escape closes
118
+ React.useEffect(() => {
119
+ if (!isOpen)
120
+ return;
121
+ const onKey = (e) => {
122
+ if (e.key === "Escape")
123
+ onClose();
124
+ };
125
+ document.addEventListener("keydown", onKey);
126
+ return () => document.removeEventListener("keydown", onKey);
127
+ }, [isOpen, onClose]);
128
+ /** The round trip currently in flight, if any. */
129
+ const pendingRef = React.useRef(null);
130
+ const popupRef = React.useRef(null);
131
+ const pollRef = React.useRef(null);
132
+ const finishConnect = React.useCallback(() => {
133
+ if (pollRef.current !== null) {
134
+ window.clearInterval(pollRef.current);
135
+ pollRef.current = null;
136
+ }
137
+ pendingRef.current = null;
138
+ popupRef.current = null;
139
+ setBusyApp(null);
140
+ void refresh(true);
141
+ }, [refresh]);
142
+ // The callback page tells us it is done. Treat the message as a nudge, never
143
+ // as the result: what is displayed comes from re-reading the server, so a
144
+ // forged message can at worst cause one redundant fetch.
145
+ React.useEffect(() => {
146
+ if (!isOpen)
147
+ return;
148
+ const onMessage = (event) => {
149
+ const data = event.data;
150
+ if (!data || data.source !== "devic")
151
+ return;
152
+ if (data.type !== "integration-connected")
153
+ return;
154
+ const pending = pendingRef.current;
155
+ if (!pending || data.returnTo !== pending.returnTo)
156
+ return;
157
+ popupRef.current?.close();
158
+ finishConnect();
159
+ };
160
+ window.addEventListener("message", onMessage);
161
+ return () => window.removeEventListener("message", onMessage);
162
+ }, [isOpen, finishConnect]);
163
+ // Stop polling if the modal goes away mid-flow.
164
+ React.useEffect(() => () => {
165
+ if (pollRef.current !== null)
166
+ window.clearInterval(pollRef.current);
167
+ }, []);
168
+ const handleConnect = async (integration) => {
169
+ if (!client$1 || busyApp)
170
+ return;
171
+ setError(null);
172
+ setBlockedUrl(null);
173
+ setBusyApp(integration.app);
174
+ const nonce = newNonce();
175
+ const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;
176
+ // Opened empty inside the click, navigated once the URL is known.
177
+ const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
178
+ try {
179
+ const { authorizationUrl } = await client$1.connectIntegration(integration.app, { ...scopeOptions, returnTo });
180
+ pendingRef.current = { app: integration.app, returnTo };
181
+ if (popup && !popup.closed) {
182
+ popupRef.current = popup;
183
+ popup.location.href = authorizationUrl;
184
+ // The user may close the popup without the callback ever posting back
185
+ // — a cancelled consent screen, or a provider that lands somewhere
186
+ // else. Watching for the close is what keeps the card from staying
187
+ // busy forever.
188
+ pollRef.current = window.setInterval(() => {
189
+ if (popup.closed)
190
+ finishConnect();
191
+ }, 700);
192
+ }
193
+ else {
194
+ // Blocked (Safari, in-app browsers, extensions): hand over the URL.
195
+ setBlockedUrl({ app: integration.app, url: authorizationUrl });
196
+ setBusyApp(null);
197
+ }
198
+ }
199
+ catch (err) {
200
+ popup?.close();
201
+ pendingRef.current = null;
202
+ setError(err instanceof Error ? err.message : String(err));
203
+ setBusyApp(null);
204
+ }
205
+ };
206
+ const handleDisconnect = async (app, account) => {
207
+ if (!client$1 || busyApp)
208
+ return;
209
+ setBusyApp(app);
210
+ setError(null);
211
+ try {
212
+ await client$1.disconnectIntegration(account.id, scopeOptions);
213
+ await refresh(true);
214
+ }
215
+ catch (err) {
216
+ setError(err instanceof Error ? err.message : String(err));
217
+ }
218
+ finally {
219
+ setBusyApp(null);
220
+ }
221
+ };
222
+ if (!isOpen)
223
+ return null;
224
+ return reactDom.createPortal(
225
+ // The variables go on the overlay, not on the modal: the backdrop is part
226
+ // of the dialog, and a portal inherits nothing from the drawer that opened
227
+ // it.
228
+ jsxRuntime.jsx("div", { className: "devic-int-overlay", style: theme.themeVars(theme$1), onClick: onClose, children: jsxRuntime.jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxRuntime.jsxs("div", { className: "devic-int-header", children: [jsxRuntime.jsxs("h3", { className: "devic-int-title", children: [jsxRuntime.jsx(PlugIcon, {}), title] }), jsxRuntime.jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxRuntime.jsxs("div", { className: "devic-int-body", children: [error && jsxRuntime.jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxRuntime.jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsxRuntime.jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
229
+ pendingRef.current = null;
230
+ setBlockedUrl(null);
231
+ }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : (integrations.map((integration) => {
232
+ const state = stateOf(integration);
233
+ const busy = busyApp === integration.app;
234
+ return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": state.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [integration.logo &&
235
+ !brokenLogos.includes(integration.app) ? (jsxRuntime.jsx("img", { className: "devic-int-logo", src: integration.logo, alt: "", "aria-hidden": "true", onError: () => setBrokenLogos((prev) => prev.includes(integration.app)
236
+ ? prev
237
+ : [...prev, integration.app]) })) : (jsxRuntime.jsx("span", { className: "devic-int-logo devic-int-logo-fallback", children: integration.name.charAt(0).toUpperCase() })), jsxRuntime.jsxs("div", { className: "devic-int-card-text", children: [jsxRuntime.jsx("div", { className: "devic-int-name", children: integration.name }), jsxRuntime.jsx("div", { className: "devic-int-state", children: state.label })] }), jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn${state.key === "connected" ? "" : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, children: busy
238
+ ? "Waiting…"
239
+ : state.key === "disconnected"
240
+ ? "Connect"
241
+ : state.key === "reconnect"
242
+ ? "Reconnect"
243
+ : "Add account" })] }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", children: integration.description })), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small devic-int-btn-danger", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, children: "Disconnect" })] }, account.id))) }))] }, integration.app));
244
+ }))] }), jsxRuntime.jsxs("div", { className: "devic-int-footer", children: [jsxRuntime.jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
245
+ }
246
+
247
+ exports.IntegrationsModal = IntegrationsModal;
248
+ //# sourceMappingURL=IntegrationsModal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IntegrationsModal.js","sources":["../../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport { themeVars, type DevicTheme } from \"../theme\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n onChange,\n theme,\n}: IntegrationsModalProps): JSX.Element | null {\n const context = useOptionalDevicContext();\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n\n const client = useMemo(\n () =>\n resolvedApiKey\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n })\n : null,\n [resolvedApiKey, resolvedBaseUrl]\n );\n\n const scopeOptions = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [loading, setLoading] = useState(false);\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [error, setError] = useState<string | null>(null);\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n /**\n * Apps whose logo failed to load. The URL comes from the provider and is\n * fetched from whatever host it names, so a broken one is a matter of when,\n * not if — and a broken-image icon in someone else's product looks like our\n * bug.\n */\n const [brokenLogos, setBrokenLogos] = useState<string[]>([]);\n\n const refresh = useCallback(\n async (dropCache = false) => {\n if (!client) {\n setError(\"API key not configured\");\n return;\n }\n setLoading(true);\n setError(null);\n try {\n // The server caches a scope's connections briefly; after an OAuth round\n // trip that cache is exactly one step behind, so drop it first.\n if (dropCache) {\n await client.refreshIntegrations(scopeOptions).catch(() => undefined);\n }\n const list = await client.getIntegrations(scopeOptions);\n setIntegrations(list);\n onChange?.(list);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setLoading(false);\n }\n },\n [client, scopeOptions, onChange]\n );\n\n // Fetch on every open: accounts may have been connected or revoked elsewhere.\n const wasOpenRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n void refresh();\n }\n wasOpenRef.current = isOpen;\n }, [isOpen, refresh]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const handleConnect = async (integration: Integration) => {\n if (!client || busyApp) return;\n setError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scopeOptions, returnTo }\n );\n pendingRef.current = { app: integration.app, returnTo };\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setError(err instanceof Error ? err.message : String(err));\n setBusyApp(null);\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setError(null);\n try {\n await client.disconnectIntegration(account.id, scopeOptions);\n await refresh(true);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps available here yet.\n </div>\n ) : (\n integrations.map((integration) => {\n const state = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={state.key}\n >\n <div className=\"devic-int-card-head\">\n {integration.logo &&\n !brokenLogos.includes(integration.app) ? (\n <img\n className=\"devic-int-logo\"\n src={integration.logo}\n alt=\"\"\n aria-hidden=\"true\"\n onError={() =>\n setBrokenLogos((prev) =>\n prev.includes(integration.app)\n ? prev\n : [...prev, integration.app]\n )\n }\n />\n ) : (\n <span className=\"devic-int-logo devic-int-logo-fallback\">\n {integration.name.charAt(0).toUpperCase()}\n </span>\n )}\n <div className=\"devic-int-card-text\">\n <div className=\"devic-int-name\">{integration.name}</div>\n <div className=\"devic-int-state\">{state.label}</div>\n </div>\n <button\n type=\"button\"\n className={`devic-int-btn${\n state.key === \"connected\" ? \"\" : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n >\n {busy\n ? \"Waiting…\"\n : state.key === \"disconnected\"\n ? \"Connect\"\n : state.key === \"reconnect\"\n ? \"Reconnect\"\n : \"Add account\"}\n </button>\n </div>\n\n {integration.description && (\n <div className=\"devic-int-description\">\n {integration.description}\n </div>\n )}\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small devic-int-btn-danger\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n >\n Disconnect\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useOptionalDevicContext","client","useMemo","DevicApiClient","useState","useCallback","useRef","useEffect","createPortal","themeVars"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A theme as inline CSS variables.
5
+ *
6
+ * Needed because the modals render through a portal into `document.body`: the
7
+ * drawer sets these variables on its own element, and a portal is not inside
8
+ * it, so nothing cascades down. A white dialog then opens over a dark
9
+ * application, which is what this exists to prevent.
10
+ *
11
+ * Only the values actually given are emitted — an absent one must fall through
12
+ * to the stylesheet default rather than be pinned to `undefined`.
13
+ */
14
+ function themeVars(theme) {
15
+ if (!theme)
16
+ return {};
17
+ const vars = {};
18
+ const set = (name, value) => {
19
+ if (value)
20
+ vars[name] = value;
21
+ };
22
+ set("--devic-primary", theme.color);
23
+ set("--devic-font-family", theme.fontFamily);
24
+ set("--devic-bg", theme.backgroundColor);
25
+ set("--devic-text", theme.textColor);
26
+ set("--devic-bg-secondary", theme.secondaryBackgroundColor);
27
+ set("--devic-border", theme.borderColor);
28
+ set("--devic-text-muted", theme.mutedTextColor);
29
+ return vars;
30
+ }
31
+
32
+ exports.themeVars = themeVars;
33
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sources":["../../../../src/components/theme.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * Colours and font a component should paint itself with.\n *\n * The same names the drawer's options use, so passing them through is a\n * one-liner and an integrator who already themed the drawer has nothing new to\n * learn.\n */\nexport interface DevicTheme {\n /** Accent: primary buttons, focus rings, links. */\n color?: string;\n fontFamily?: string;\n /** Surface the component sits on. */\n backgroundColor?: string;\n textColor?: string;\n /** Secondary surface: rows, chips, hover states. */\n secondaryBackgroundColor?: string;\n borderColor?: string;\n /** Muted text: captions, timestamps, empty states. */\n mutedTextColor?: string;\n}\n\n/**\n * A theme as inline CSS variables.\n *\n * Needed because the modals render through a portal into `document.body`: the\n * drawer sets these variables on its own element, and a portal is not inside\n * it, so nothing cascades down. A white dialog then opens over a dark\n * application, which is what this exists to prevent.\n *\n * Only the values actually given are emitted — an absent one must fall through\n * to the stylesheet default rather than be pinned to `undefined`.\n */\nexport function themeVars(theme?: DevicTheme): CSSProperties {\n if (!theme) return {};\n const vars: Record<string, string> = {};\n const set = (name: string, value?: string) => {\n if (value) vars[name] = value;\n };\n\n set(\"--devic-primary\", theme.color);\n set(\"--devic-font-family\", theme.fontFamily);\n set(\"--devic-bg\", theme.backgroundColor);\n set(\"--devic-text\", theme.textColor);\n set(\"--devic-bg-secondary\", theme.secondaryBackgroundColor);\n set(\"--devic-border\", theme.borderColor);\n set(\"--devic-text-muted\", theme.mutedTextColor);\n\n return vars as CSSProperties;\n}\n"],"names":[],"mappings":";;AAuBA;;;;;;;;;;AAUG;AACG,SAAU,SAAS,CAAC,KAAkB,EAAA;AAC1C,IAAA,IAAI,CAAC,KAAK;AAAE,QAAA,OAAO,EAAE;IACrB,MAAM,IAAI,GAA2B,EAAE;AACvC,IAAA,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,KAAc,KAAI;AAC3C,QAAA,IAAI,KAAK;AAAE,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK;AAC/B,IAAA,CAAC;AAED,IAAA,GAAG,CAAC,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC;AACnC,IAAA,GAAG,CAAC,qBAAqB,EAAE,KAAK,CAAC,UAAU,CAAC;AAC5C,IAAA,GAAG,CAAC,YAAY,EAAE,KAAK,CAAC,eAAe,CAAC;AACxC,IAAA,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC;AACpC,IAAA,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,wBAAwB,CAAC;AAC3D,IAAA,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,WAAW,CAAC;AACxC,IAAA,GAAG,CAAC,oBAAoB,EAAE,KAAK,CAAC,cAAc,CAAC;AAE/C,IAAA,OAAO,IAAqB;AAC9B;;;;"}
package/dist/cjs/index.js CHANGED
@@ -13,6 +13,8 @@ var UsageBar = require('./components/ChatDrawer/UsageBar.js');
13
13
  var LimitBanner = require('./components/ChatDrawer/LimitBanner.js');
14
14
  var RecalledMemoriesWidget = require('./components/ChatDrawer/RecalledMemoriesWidget.js');
15
15
  var CoreMemoryModal = require('./components/CoreMemoryModal/CoreMemoryModal.js');
16
+ var IntegrationsModal = require('./components/IntegrationsModal/IntegrationsModal.js');
17
+ var theme = require('./components/theme.js');
16
18
  var ThreadStateTag = require('./components/ThreadStateTag/ThreadStateTag.js');
17
19
  var AICommandBar = require('./components/AICommandBar/AICommandBar.js');
18
20
  var useAICommandBar = require('./components/AICommandBar/useAICommandBar.js');
@@ -48,6 +50,8 @@ exports.UsageBar = UsageBar.UsageBar;
48
50
  exports.LimitBanner = LimitBanner.LimitBanner;
49
51
  exports.RecalledMemoriesWidget = RecalledMemoriesWidget.RecalledMemoriesWidget;
50
52
  exports.CoreMemoryModal = CoreMemoryModal.CoreMemoryModal;
53
+ exports.IntegrationsModal = IntegrationsModal.IntegrationsModal;
54
+ exports.themeVars = theme.themeVars;
51
55
  exports.ThreadStateTag = ThreadStateTag.ThreadStateTag;
52
56
  exports.AICommandBar = AICommandBar.AICommandBar;
53
57
  exports.formatShortcut = useAICommandBar.formatShortcut;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}